尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot jar包Linux部署实战:从环境准备到systemd与JVM调优

Spring Boot jar包Linux部署实战:从环境准备到systemd与JVM调优 上周帮一个朋友排查线上环境某个 Spring Boot 服务隔几天就挂一次日志里只看到 OOM他第一反应是是不是流量太大了。折腾到凌晨才发现问题就出在当初部署的时候只敲了一行java -jar app.jar没指定堆内存、没配守护进程、连日志都没做切割。这类问题在真实生产环境里太常见了。很多小伙伴习惯了在 IDEA 里直接点运行一说到Linux 环境用 jar 包部署就以为等于java -jar xxx.jar实际上部署这件事从环境准备到启动脚本再到开机自启和调优每一步都有讲究。这篇就把我实际用过、踩过坑的经验一次说清楚。这篇文章主要针对的是后端开发转手做部署、刚接触 Linux 服务器的运维新人以及那些已经部署过几次但总在细节上翻车的朋友。核心内容就是怎么在 Linux 上把一个 Spring Boot或其他 Java 应用的 jar 包从上传到启动再到开机自启完完整整跑起来顺带把资源限制、日志切割、回滚方案这些生产环境必须考虑的点也聊透。1. 部署前的准备别急着敲 java -jar很多人拿到 jar 包第一反应就是java -jar xxx.jar直接跑但生产环境比开发机复杂多了硬件配置、系统环境、权限、网络、文件目录每一项都可能把你埋在坑里。提前花十几分钟把环境理一遍后面能省一整晚。1.1 JDK 版本和安装方式怎么定部署 Java 应用第一步不是装 JDK而是确认你用的 JDK 版本对不对。Spring Boot 3.x 要求 JDK 17 起步Spring Boot 2.x 用 JDK 8/11 都行。如果 JDK 版本不对哪怕 jar 包能启动跑起来也经常会出一些摸不着头脑的 ClassNotFound 或奇怪的报错。安装 JDK 的方式有几种从省事到可控依次是yum/apt 直接装例如yum install -y java-17-openjdk好处是快坏处是如果一台机器上装了好几个版本的 JDK环境变量容易乱。手动下载 JDK tar.gz 包解压通过/etc/profile或~/.bashrc配JAVA_HOME可控性最强推荐生产环境用这种方式。我习惯的做法是固定一个目录专门放 JDK比如/usr/local/java/jdk17然后在/etc/profile.d/java.sh里写export JAVA_HOME/usr/local/java/jdk17 export PATH$PATH:$JAVA_HOME/bin注意/etc/profile.d/下的脚本会在用户登录时自动加载比直接改/etc/profile更干净也不会影响系统其他脚本。改完之后source /etc/profile跑java -version验证一下。关于用 openjdk 还是 oracle jdk这个问题现在基本不用纠结了。开源项目、绝大多数商业项目跑在 OpenJDK 上完全没问题如果没有特殊协议要求选 OpenJDK 就行省心又免费。1.2 系统层面先排雷内存、磁盘、防火墙部署前花两分钟看三样东西能避开很多灵异事件磁盘剩余空间用df -h看至少要留出日志两三个月的增长空间这个经常被忽略。内存大小用free -h看别只看总内存还要看 available。如果你的应用配了 2G 堆内存但机器总共才 2G启动后系统会疯狂 swap性能直接崩。端口占用情况用ss -lntp或netstat -lntp查确认你要用的端口比如 8080没被其他进程占用。另外提醒一句很多云服务器的防火墙安全组默认拦着端口本地怎么测都通不上先查安全组入站规则再查 Linux 的 firewalld/ufw。养成本地先curl http://127.0.0.1:8080/health测一下、再从外部访问的习惯能省掉大量排查时间。1.3 运行用户不要把服务跑在 root 下看到这条你可能觉得小题大做但这是生产环境的常规要求。用 root 跑 jar一旦应用被入侵攻击者直接就是 root 权限代价太大。更实际的问题是哪天你手滑删了 root 用户的某个环境变量机器上的所有服务全都起不来。我的建议是创建一个单独的用户跑应用useradd -m appuser然后把 jar 包和应用目录的属主改成这个用户。用sudo -u appuser java -jar ...启动或者配合后面要讲的 systemd在 service 文件里指定Userappuser。这个习惯养成之后你自然就会注意目录权限、日志写权限这些细节很多为什么服务起不来的坑就自动绕开了。2. jar 包部署的本质弄清楚你在部署什么在敲命令之前最好先搞明白 jar 包里面到底有什么为什么一个 jar 就能把整个应用跑起来。这部分能帮你建立部署思维以后遇到任何启动失败都知道该往哪个方向想。2.1 一个可执行 jar 的内部结构以 Spring Boot 的可执行 jar 为例打开它jar tf app.jar就能看你会发现里面大概长这样BOOT-INF/ classes/ # 你自己的代码和配置文件 lib/ # 项目依赖的所有第三方 jar META-INF/ MANIFEST.MF # 启动入口定义 org/springframework/boot/loader/你看热搜词里有人搜jar包的lib中是什么其实就是这个BOOT-INF/lib。Spring Boot 会把项目依赖的所有第三方库统统打进这个目录这样你部署的时候就只需要传一个 jar不需要像传统 WAR 那样在服务器上准备一堆依赖。这也是 jar 部署最方便的地方一个文件到处运行环境隔离做得干净。MANIFEST.MF里面Main-Class指向的是 Spring Boot 的 Launcher比如org.springframework.boot.loader.launch.JarLauncher它先创建自定义的类加载器去加载BOOT-INF/lib里的依赖再调用你项目里Start-Class指定的那个主类。理解这条链后面如果遇到明明能把进程拉起来但业务代码没生效这类问题你就知道该看哪一层了。2.2 三种常见的部署形态别选错同一个 jar 包在 Linux 上跑起来的方式有好几种各有利弊直接java -jar app.jar最原始窗口一关服务就死。只适合本地临时验证。写一个启动脚本启动后用 nohup 放后台比直接敲命令好一点但脚本质量参差不齐很多项目就是栽在这种半成品脚本上。用 systemd 管起来真正的生产级做法支持开机自启、异常自动拉起、日志统一交给 journald是我们这篇文章最后推荐的方式。后面第 4 部分我会把 systemd 的完整配置写出来这里先有个概念就行。如果你公司还在用脚本 nohup的套路建议你认真考虑切换到 systemd运维体验真的会提升一个档次。2.3 依赖在服务器上怎么处理如果你的项目用上了本地引入的特殊 jar 包比如某些第三方不再提供中央仓库依赖的 SDK这里要注意不要简单地把外部 jar 和 main 方法打在一起也不要手动往本地 Maven 仓库塞一堆东西。最稳的做法是把这些 jar 安装进 Maven 仓库时使用install:install-file命令然后在 pom 里正常引用最后mvn clean package打成一个包含所有依赖的可执行 jar。如果确实需要把外部 jar 和应用的 lib 分离比如为了减小部署包体积可以这样nohup java -cp app.jar:lib/* com.example.MainClass 但这里有个坑如果外部 jar 里的类还依赖了 Spring Boot 的内嵌容器相关类用-jar反而找不到用-cp方式又容易类加载混乱。我的经验是除非非拆不可否则干脆打成一个 fat jar部署时一个文件搞清楚省掉一堆 classpath 的麻烦。3. 从上传到启动一次完整的部署实操理论说了一堆接下来进入正题如何在一台干净的 Linux 服务器上把一个 Spring Boot jar 包安全地跑起来。3.1 目录规划和 jar 包上传先把部署目录规划好别啥都往/root或家目录里丢。推荐一套比较通用的结构/opt/app/appname/ app.jar # 当前运行的版本 logs/ # 日志目录 conf/ # 外部配置文件如 application-prod.yml backups/ # 历史版本备份方便回滚用 scp 上传例如scp target/demo-1.0.0.jar appuserserver:/opt/app/appname/app.jar如果公司服务器有跳板机或者用了统一的发布平台就用平台拉取但上面的目录结构依然适用。上传后建议做两件事第一看一眼 jar 大小因为一个正常的 Spring Boot fat jar 通常几十兆起如果只有几 K大概率是打包配置有问题启动必失败第二用jar tf app.jar | head -20确认包里确实有 BOOT-INF 目录提前发现问题总比启动时报错好。3.2 第一次启动先占住前台观察日志第一次启动千万别直接 nohup 扔后台那样日志刷刷刷翻过去你都来不及截图。老老实实前台跑cd /opt/app/appname sudo -u appuser java -Xms512m -Xmx1024m -jar app.jar关键参数说明-Xms512m初始堆内存建议配置成和-Xmx一致避免运行期再扩容导致性能抖动。-Xmx1024m最大堆内存上限不要超过物理内存的一半得给操作系统和其他进程留余地。-Dserver.port8080如果默认端口不合适用这个覆盖。也可以用外部配置文件--spring.profiles.activeprod加载生产环境配置。看到类似这样的日志出现就说明启动成功了Tomcat started on port(s): 8080 (http) with context path Started DemoApplication in 10.5 seconds (JVM running for 11.2)此时到浏览器访问一下健康检查接口确认能通再进入下一步。3.3 配置写入日志文件并启动后台进程确定没问题后停掉前台进程用日志输出加 nohup 的方式放进后台。nohup 可以让进程忽略挂断信号但我不建议只依赖 nohup更推荐把启动逻辑放到 systemd 里。如果你临时要后台跑一下可以这样先顶着cd /opt/app/appname sudo -u appuser nohup java -Xms512m -Xmx1024m -jar app.jar \ --spring.profiles.activeprod \ logs/app.log 21 echo $! logs/app.pid注意这里把 stdout 和 stderr 都重定向到了logs/app.log不会出现启动报错却看不到任何输出的情况。echo $!把进程号记录下来后面要停就是kill $(cat logs/app.pid)。但这只是应急做法真正要长期稳定运行继续往下看 systemd。3.4 编写一个正规军级别的启动脚本我想强调一下production 级别的服务没有道理让进程靠一个后台 job活着。systemd 是 Linux 上标准的服务管理方式把 jar 部署交给它能做到开机自启、崩溃自动拉起、统一日志管理。写一个/etc/systemd/system/appname.service[Unit] DescriptionDemo Spring Boot Application Afternetwork.target [Service] Typesimple Userappuser WorkingDirectory/opt/app/appname EnvironmentJAVA_HOME/usr/local/java/jdk17 ExecStart/usr/local/java/jdk17/bin/java -Xms512m -Xmx1024m -jar /opt/app/appname/app.jar ExecStop/bin/kill -s TERM $MAINPID SuccessExitStatus143 Restartalways RestartSec10 StartLimitIntervalSec60 StartLimitBurst3 [Install] WantedBymulti-user.target这里有几条值得展开解释一下Typesimple表示 systemd 认为启动命令一执行就算启动完成Spring Boot 这种常驻进程适合这个模式。Restartalways表示只要进程意外退出就自动拉起来配合RestartSec10防止拉起后立刻又挂造成死循环。SuccessExitStatus143是给 Java 用的因为 systemd 停止服务时发送 SIGTERMJVM 默认退出码是 143。如果不写明这个服务每次正常停止后 systemd 都会认为异常退出。Userappuser确保服务以普通用户身份运行权限控制意识要体现在每一层。启用服务并启动systemctl daemon-reload systemctl enable appname --now systemctl status appname用systemctl查看如果显示active (running)恭喜你的应用已经被纳入 systemd 的监管了。以后看日志用journalctl -u appname -f停止用systemctl stop appname重启用systemctl restart appname清爽很多。3.5 日志保留多久、怎么清理日志总会越堆越大就算用 systemd 管如果不处理/var/log/journal也会撑爆。两个办法如果应用自身写文件日志比如 logback 写到logs/app.log建议在 logback 里配maxHistory和totalSizeCap比如只保留 30 天、总大小不超过 2GB。如果依赖 systemd 的 journald改/etc/systemd/journald.conf里的SystemMaxUse500M然后重启 systemd-journaldjournald 就会自动帮你滚动清理。你的应用可能在多台机器上部署日志集中收集走 ELK 或 Loki 是另一个话题但单机层面先把日志不撑爆磁盘这条底线守住了否则某天磁盘满了服务集体挂掉那场面真的很难看。4. 部署过程中最常见的五个坑只要是部署 Java 应用多多少少都会遇到下面这些情况。我把它们按出现频率排个序顺便把排查思路一起给出来。4.1 端口被占用或权限不足现象是应用日志显示端口被占用或者 bind 时直接Permission denied。排查命令优先用ss -lntp | grep 8080看是哪个进程占了端口如果是权限问题大概率是端口小于 1024 且你没有 root 权限比如直接绑 80 端口这种。对策很简单要么换端口要么在用 systemd 配置时加上AmbientCapabilitiesCAP_NET_BIND_SERVICE用高权限绑定低端口顺带还要配CapabilityBoundingSetCAP_NET_BIND_SERVICE限制能力范围不推荐直接给服务开 root。4.2 JDK 版本不匹配Spring Boot 3.x 扔到 JDK 8 环境上启动报UnsupportedClassVersionError这种错一眼就知道。更奇葩的是你系统里默认 java 是 8但你配的 JAVA_HOME 却是 17两个地方指向不同版本。排查时统一认准一套which java java -version echo $JAVA_HOME三个命令结果必须一致。systemd 服务里如果指定了ExecStart用绝对路径的 java那还要确认这个 java 版本是否正确。4.3 启动后进程立刻消失有一种情况很典型systemctl status显示服务一直在 restart日志却只有简单几行。原因基本是三种配置文件里有语法问题导致 Spring 启动失败JVM 参数里-Xmx超过机器可用内存或者服务 jar 包路径写错了。排查方式就一条从 systemd 的journalctl -u appname里看完整日志。很多消息不是没有记录是大家没找到地方看。4.4 内存不足导致 OOMJVM 报java.lang.OutOfMemoryError: Java heap space是生产环境最常见的故障之一。根本原因要么是-Xmx设太小要么是代码里有内存泄漏。但注意真正压垮服务器的往往是系统内存不足导致被杀掉用dmesg | tail看到Out of memory字样多半是进程被内核 OOM killer 杀了。排查顺序先用free -h看物理内存余量再查你的堆配置Java 堆之外还有 Metaspace、线程栈、堆外内存-Xmx不等于实际内存占用上限最后再用jmap -heap pid看 JVM 动态占用。4.5 日志里没有报错但接口访问超时有一种常见情况服务确实起来了systemd 显示 active但访问接口一直超时。优先检查健康检查地址是不是对了很多应用/health不是根路径。防火墙和安全组是否放行端口。最隐蔽的坑服务绑定了localhost而不是0.0.0.0外部自然访问不通。Spring Boot 配置里server.address默认是0.0.0.0但如果你在配置文件里不小心设成了127.0.0.1那外部就完全访问不了。5. 部署完只是开始调优与演进前面把部署流程走完了但既然搞生产部署眼光就得放长远一点部署之后的应用和能跑之间还差着几条街性能调优、优雅停机、灰度回滚每一项都值得投入时间。5.1 调整 JVM 参数让应用真正稳每个应用的内存特点不同我的基本经验是堆内存-Xms和-Xmx保持一致防止 JVM 运行时临时扩容导致延迟抖动。给 Metaspace 设上限-XX:MaxMetaspaceSize256m防止类加载器太多把内存撑爆。看 GC 日志确认你的停顿时间是否在可接受范围必要时换 G1 或 ZGC。启动后可以用jstat -gcutil pid 5000观察 GC 频率和堆使用率连续观察一段时间就能判断参数合不合理。没有最完美的参数只有最适合你业务场景的配置。关键是得去看别设完就不管了。5.2 优雅停机别让 kill 直接腰斩请求Spring Boot 应用如果被直接kill -9正在处理的请求会丢失数据库事务也可能断在半路。所以至少要保证用 systemd 时是systemctl stop来停服务systemd 会默认发 SIGTERM。Spring Boot 在收到 SIGTERM 后会进入优雅停机流程在配置里加上# 应用关闭后等待所有请求处理完再真正退出 spring.lifecycle.timeout-per-shutdown-phase20s server.shutdowngraceful这样你的发布脚本就是先触发优雅停机、再启动新版 jar用户体验会好很多。5.3 发布与回滚策略线上发布最怕出问题。简单但可靠的做法部署前把当前 jar 复制到 backups 目录带个时间戳。杀掉旧进程启动新 jar。验证健康检查接口通过。如果不通过立刻把 backups 里的旧 jar 恢复重启旧版本。这个流程手动做嫌麻烦的话可以写成一个发布脚本。再进阶一些可以用ln -s做软链接指向当前版本发布时切换链接回滚时切回去。这些方法都不依赖复杂的 CI/CD 平台但能有效控制线上风险。5.4 别忘了进程监控systemd 虽然能拉起进程但进程活着不代表应用没问题。实践经验是至少配置一个外部探活每 5 到 10 秒 curl 一下健康检查地址连续失败几次就告警或自动重启。如果你公司有监控系统把systemctl status和日志接入进去如果没有先写个最简单的 cron 脚本跑探活也比裸奔强。6. 部署工具对比该不该上 Docker在 Linux 上用 jar 部署很多人会建议直接 Docker 多香。确实Docker 容器在隔离性、环境一致性上有天然优势但如果你的团队还没上容器化或者服务器资源有限裸机跑 jar 依然是一个极其广泛、合理且高效的方案。它没有镜像构建、仓库管理、容器网络这些额外学习成本一个 JDK 加一个 systemd service 就能稳定跑生产任务。我的建议是单机部署、中小业务、运维能力不强的团队jar 包 systemd 是最合适的方案简单直接排查问题也更直观。当你面临多环境强一致、弹性扩容、微服务数量暴涨这些场景时再引入 Docker 和编排层不迟。毕竟工具是拿来解决问题的不是拿来显得先进的。根据我个人经验部署这种事情的成败往往不是看你会不会java -jar而是看你对环境、进程、日志这些细节有没有敬畏心。我强烈建议从一开始就把 systemd 配置写好、JVM 参数想清楚、日志切割配上目录结构定规范。这些基础工作做得越扎实后面出事故的概率就越低。希望这篇内容能让你少走几趟弯路把部署 jar 包从玄学变成确定性操作。
返回列表