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

资讯详情

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

Jenkins构建成功却启动失败?完整排查思路与解决指南

Jenkins构建成功却启动失败?完整排查思路与解决指南 做持续集成的同学应该都遇到过这种尴尬局面Jenkins任务状态栏显示漂亮的蓝色成功但到服务器上一执行jar包压根没起来或者起来三秒就静默退出连日志都没来得及打印。这个场景在“Jenkins构建、jar包、启动失败”这个组合里特别高频比构建直接报错更让人抓狂——因为构建日志里全是一路绿灯问题全藏在环境差异、打包产物、启动脚本这些容易被忽略的缝隙里。这篇文章从我自己的排障经验出发把这个“构建成功但启动失败”的问题完整拆一遍先讲怎么快速拿到有效信息再分析最常见的几类根源JDK环境、打包插件、依赖冲突、脚本细节最后给出我一直在用的预防方案和一套排查速查表。无论你是刚接触Jenkins的小白还是被这个问题折磨过的老手照着这个思路走一遍大概率能少熬夜。1. 先别急着看日志搞清楚“没起来”到底是哪种没起来1.1 两种典型的“假成功”现象我接手过的启动失败案例里表面上都是“Jenkins构建成功jar包启动不起来”但细看现象其实是完全不同的两类。第一类是进程根本没拉起来。你执行java -jar app.jar命令立刻返回或者几秒钟后进程就消失了。这类问题的特点是日志极少甚至没有任何输出。这种情况十有八九是环境层面的问题——JDK版本对不上、启动脚本权限不对、端口被占导致Spring Boot启动失败然后自杀退出或者干脆是JVM参数写得有问题连虚拟机都起不来。第二类是进程拉起来了但马上死掉。你在ps -ef里能看到Java进程但它存活时间极短或者反复重启。这类问题通常有完整日志可查问题往往出在Spring容器初始化失败、数据源连接不上、配置文件读取不到、依赖类加载冲突等。从我踩过的坑来看第二类占六成以上因为Jenkins构建时环境和运行时环境的差异很容易在启动时集中爆发。判断“第一种还是第二种”有个笨但有效的办法在启动命令后面加一个 sleep 30如果30秒内进程死了说明它在启动过程中主动退出如果进程压根没出现过那就是JVM层面就没起来。这个方法帮我少走了很多弯路。1.2 排查前必须收集的三类信息很多人上来就盯着Jenkins的构建日志看看半天也看不出所以然。我的经验是在动手之前先收集三类信息否则排查就是大海捞针。第一启动时的原始输出。不要在Jenkins里只看“构建成功”那个绿点要找到构建日志里最后执行的启动命令是什么然后手动到服务器上复现这条命令把终端里的完整输出包括报错栈留存下来。这里有个关键点Jenkins通过SSH插件或者Publish Over SSH执行远程命令时它拿到的环境和你手动SSH登录是不一样的所以最好是在Jenkins实际执行的环境里复现。第二jar包的构建时间、大小和内容清单。用ls -l看jar包大小是否正常一个Spring Boot应用如果只有几百KB基本可以断定是打包姿势不对用unzip -l app.jar | grep BOOT-INF/lib | wc -l看依赖数量是否合理。第三运行环境的版本信息。包括JDK版本java -version、操作系统发行版与内核、是否有其他Java进程占着端口。这三类信息凑齐后80%的问题在开始查之前心里已经有数了。2. 环境差异是头号杀手Jenkins构建机和服务器根本不是同一个世界2.1 JDK版本不一致编译版本与运行版本错位这是最经典也最容易忽略的问题。Jenkins构建机上装的是JDK 17代码里用了switch模式匹配这类新语法Maven编译时用的是--release 17编译出来的class文件是61.0版本。结果服务器上装的是JDK 8运行java -jar时JVM直接抛UnsupportedClassVersionError启动立即失败。这个错误其实还算好查因为报错信息很明确。真正坑人的是另一种情况编译时用的是JDK 8但Maven编译器插件没有显式指定release代码里不小心用了JDK 11的API但编译机恰好是JDK 11编译时靠了JRE带的rt.jar运行时到了JDK 8就报NoSuchMethodError。这种错误信息出现在很深的调用链里不熟悉的人很容易以为是业务代码的问题。我的建议是Jenkins全局工具配置里明确给每个任务指定JDK版本不要用“默认”或“自动选择”。Maven的pom.xml里强制加maven-compiler-plugin并指定release或source/target且source和target必须一致不要出现source8但target11的组合。发布到服务器后在启动脚本开头强制校验Java版本不匹配就直接报错退出避免启动到一半挂掉问题还要靠猜。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration release8/release /configuration /plugin注意release是从JDK 9才有的参数JDK 8环境用source1.8/sourcetarget1.8/target也可以但不要迷信JAVA_HOME设置好了就没问题JVM运行时用的是java命令。2.2 环境变量与PATH差异Jenkins executor跑的和你SSH登录的不是一回事这个坑我印象太深了。Jenkins任务里通过“Execute shell”执行#!/bin/bash java -jar target/app.jar /tmp/app.log 21 构建成功但服务器上ps -ef看不到java进程。手动上去跑同样的命令好端端的。后来才发现Jenkins以服务形式安装时它的SSH会话环境变量是精简过的——PATH里没有/usr/local/java/bin或/opt/jdk/bin导致java命令根本找不到而任务里如果是#!/bin/sh -xe的话错误信息被日志的夸张回显淹没了。排查办法在Jenkins的shell步骤第一行加echo $JAVA_HOME which java看它实际调用的Java在哪个路径。启动脚本里使用绝对路径调用Java不要裸写java。比如#!/bin/bash export JAVA_HOME/opt/jdk17 export PATH$JAVA_HOME/bin:$PATH nohup $JAVA_HOME/bin/java -jar /data/app/app.jar --spring.profiles.activeprod /data/app/app.log 21 echo $! /data/app/app.pid除了Java还要注意PATH里是否缺少unzip、tar等基础命令有些精简版系统镜像连ps和lsof都没有排查时还得先yum install或apt install。2.3 工作空间路径与文件权限隐藏的破坏者Jenkins默认工作区路径通常是/var/lib/jenkins/workspace/项目名而这台构建机上如果直接在这个目录里运行jar包可能会遇到两个问题。第一工作区清理策略。Jenkins配置了“清理工作空间”后下一次构建会把整个目录删掉重建。如果你把运行时的日志、pid文件、生成的临时文件都放在工作区里那构建一跑完上一次运行的依赖文件就被删了服务自然起不来。第二权限问题。/var/lib/jenkins/workspace目录是jenkins用户所有但你发布到的目标目录如果是/data/app部署用户是app那jar包拷贝过去后的属主可能还是jenkins一旦read或execute权限不对启动就会失败。我的习惯是发布时用chown或者chmod x显式修复权限不要依赖umask。3. 构建产物本身的问题打包时埋下的雷3.1 spring-boot-maven-plugin 的经典连环坑如果你用的是Spring Bootspring-boot-maven-plugin必须配置好了才能打出可执行jar。否则打出来的jar只是个普通的依赖jar里面没有org.springframework.boot.loader.JarLauncher执行java -jar时报“no main manifest attribute”。这个错一般能马上看到但还有一种更隐蔽的情况pom.xml里声明了打包插件但没有绑定repackagegoal。Maven先执行maven-jar-plugin生成了一个普通jar然后Spring Boot的repackage没生效最终产物里没有启动类信息。你在本地IDEA里点运行没事因为IDEA直接读classpath但到命令行java -jar就扑街。我的写法是确保executions里包含plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version${spring-boot.version}/version executions execution goals goalrepackage/goal /goals /execution /executions /plugin提示构建完成后用unzip -p app.jar META-INF/MANIFEST.MF看一眼Main-Class是不是org.springframework.boot.loader.JarLauncher或org.springframework.boot.loader.launch.JarLauncherSpring Boot 3.x。不是的话基本就是repackage没生效。3.2 依赖冲突启动时不报错才是怪事Jenkins构建机本地仓库里缓存了一堆依赖如果某些传递依赖解析到了不同版本你在本地或者Jenkins上构建时可能“碰巧”能跑到启动但从Jenkins拉取的jar包部署到服务器后因为classpath顺序、fat jar里的依赖布局发生了变化类冲突才暴露出来。典型错误如NoClassDefFoundError类在编译期存在但运行期的fat jar里没有这个类或者类版本不对。NoSuchMethodError同名类存在但方法签名变了通常是依赖版本被覆盖。BeanCreationException多个jar里存在相同的全限定类名Spring自动装配时加载了错误的类。排查手段用mvn dependency:tree查看依赖树重点检查同一个group/artifact出现的不同版本。用mvn dependency:analyze找未声明的依赖。解压fat jar后用find BOOT-INF/lib -name *.jar看有没有重复的artifact但版本不同。启动时加-verbose:class可以打印每个类的加载来源但这个输出量巨大适合最后手段。我见过最离谱的一次是A服务引入了B-client和C-commonB-client内部依赖了旧版commons-lang3C-common依赖了新版commons-lang3两个版本内容都在fat jar里因为加载顺序问题Spring启动时用了旧的StringUtils里面少了新方法直接抛NoSuchMethodError排查了两个小时才定位到。3.3 Jenkins构建缓存与脏包问题Jenkins默认会复用本地Maven仓库~/.m2/repository。如果仓库里某个SNAPSHOT依赖被污染了——比如某个.lastUpdated文件标记为“解析失败”Maven后续构建就总是拿旧的不完整文件打出来的jar包在运行时各种类缺失。我的习惯是在Jenkins任务里加一个“可选”的清理步骤只在必要时清理。真正的重点是这个在构建后、部署前主动校验产物完整性。用unzip -t app.jar检查压缩包是否完整然后检查关键类是否存在再用java -jar app.jar --help如果应用支持做一次冒烟测试。一条命令就能拦住90%的脏包unzip -t target/app.jar /dev/null 21 echo jar is ok || echo jar is corrupt4. 启动脚本与运行时环境的细节都是从不起眼的地方翻车4.1 换行符问题CRLF vs LFWindows上编辑的脚本坑Linux这个坑对用Windows客户端连Jenkins的同学特别常见。你在Windows上写了个deploy.sh提交到Git仓库Jenkins拉下来执行时Shell脚本的每一行末尾都带着\rLinux的Shell解释器直接罢工或执行时报$\r: command not found。很多人的第一反应是“脚本权限不对”加完chmod x后还是不行其实问题在换行符。解决办法在项目工程里统一用Git的autocrlf或.gitattributes强制LF。在Jenkins的执行步骤里对脚本做一次格式化sed -i s/\r$// deploy.sh ./deploy.sh或者干脆写脚本时第一行加#!/bin/bash然后执行时用bash deploy.sh有些环境能容错但别指望。4.2 端口被占用与资源限制启动即退出的一大隐性原因Spring Boot默认端口8080如果服务器上已经有一个旧进程占用了8080新进程启动时会报Port already in use。Spring Boot默认行为是启动失败退出而不是自动换端口。这在我们配合Jenkins做滚动发布时尤其常见——老服务没停干净新服务就急着起。排查命令很简单netstat -tlnp | grep 8080 lsof -i:8080但要注意有些发行版没有lsofnetstat的-p参数需要root权限。我通常用ss -tlnp | grep 8080这个更通用一些。另外JVM参数设置不当也会导致启动失败。比如-Xmx512m配得太大服务器物理内存不足JVM申请内存失败直接Error occurred during initialization of VM / Could not reserve enough space for 512000KB object heap。服务器上如果同时有好几个Java进程这种情况特别容易炸。4.3 配置文件读取路径jar包外部化配置的坑Spring Boot应用经常需要外置application.yml文件启动命令通常是java -jar app.jar --spring.config.location/etc/app/application.yml但Jenkins构建时你并不确定服务器上的这个配置文件是否存在、权限是否正确、格式是否合法YAML语法错误、缩进不对。最常见的问题是在Jenkins构建机上测试时用的配置和服务器上的配置不一样服务器上的某个配置项比如数据库地址改了但没同步启动时数据源初始化失败直接退出。建议在启动脚本里加一个配置文件的校验逻辑if [ ! -f /etc/app/application.yml ]; then echo 配置文件不存在 exit 1 fi部署流程里也应该把“配置文件由配置中心拉取或由部署脚本生成”作为标准化动作不要让配置文件散落在各处、手动维护。4.4 进程残留与重复启动构建越跑越多服务越来越乱还有一类情况不是启动失败而是“启动看起来失败了”——实际上旧进程还在新进程因为端口冲突退出但你看到的是新进程没了所以误以为启动失败。这种情况在Jenkins重复构建、重复部署时很常见。处理方式很朴素但很有效启动前先按应用名停掉旧进程。pkill -f your-app.jar sleep 2 nohup java -jar your-app.jar /data/app/app.log 21 注意pkill -f是按命令行模式匹配的如果系统里有其他用户也在跑同名进程会有误杀风险。更稳妥的写法是用PID文件if [ -f /data/app/app.pid ]; then kill $(cat /data/app/app.pid) || true sleep 2 fi nohup java -jar /data/app/your-app.jar /data/app/app.log 21 echo $! /data/app/app.pid5. 排障工具链与速查表遇到问题时如何高效定位5.1 日志分级定位法一款“起不来的jar”的完整排查实例假设你遇到了最典型的场景Jenkins构建成功jar包拷贝到了服务器执行启动脚本后进程三秒后消失。你应该按什么顺序看我的个人流程是第一步先看JVM是否启动成功。执行java -jar app.jar --spring.main.web-application-typenone这种参数试一下或者直接看是否有Error occurred during initialization of VM输出排除JVM本身的问题。第二步看Spring Boot的启动日志。Spring Boot启动时如果port 8080被占或者数据源连接失败日志会打印APPLICATION FAILED TO START那个Banner下面的Description和Action部分已经说得很清楚了。很多人不看这两部分直接翻堆栈事倍功半。第三步用jps和jstack确认进程状态。如果进程还在但卡住用jstack pid | head -100看主线程在干什么。有一次我发现应用卡在“初始化连接池”上日志没有报错但也没输出“Started”就是数据库网络不通导致的长时间等待。第四步看GC日志和资源限制。如果启动过程中内存不断飙高然后OOMjstat -gc pid能看出来。像-XX:HeapDumpOnOutOfMemoryError这样的参数我建议默认加上真到排查OOM时没有堆转储文件那才叫难受。5.2 Jenkins构建后部署场景的排查速查表下面这表是我整理过多次的实际参考发布在运维文档里看一遍能避开大部分低级错误现象可能原因快速验证方法解决措施启动无任何输出进程瞬间消失JAVA_HOME/PATH不对which java、启动脚本echo JAVA_HOME使用绝对路径调用Java启动报UnsupportedClassVersionError编译JDK与运行JDK不一致编译时指定release服务器java -version统一JDK版本no main manifest attributespring-boot-maven-plugin未生效unzip -p jar META-INF/MANIFEST.MF配置repackagePort already in use旧进程未停止ss -tlnp | grep 8080启动前按PID停旧进程Could not reserve enough spaceJVM参数-Xmx超过物理内存free -m 查看可用内存调低堆内存参数$\r: command not found脚本CRLF换行符file deploy.sh看到CRLFsed -i s/\r$// deploy.shNoClassDefFoundError依赖缺失或冲突mvn dependency:tree对比版本排除或锁定版本APPLOCATION FAILED TO START配置错误或外部依赖不可用详细读Banner下方的Action提示修正配置或启动外部依赖这张表不是让你背下来的而是排查时对照看能快速定位大致方向。5.3 Jenkins Pipeline中嵌入启动验证的实战写法既然都用了Jenkins就不要手动到服务器上执行启动命令。我建议在Jenkinsfile里把“启动验证”作为发布流程的一环防止“构建成功但启动失败”流到生产环境。下面这段是我项目里在用的关键逻辑按需修改即可pipeline { agent any environment { DEPLOY_HOST your-server-ip DEPLOY_DIR /data/app JAR_NAME app.jar } stages { stage(Build) { steps { sh mvn clean package -DskipTests } } stage(Deploy Verify) { steps { sh scp target/${JAR_NAME} ${DEPLOY_HOST}:${DEPLOY_DIR}/${JAR_NAME} ssh ${DEPLOY_HOST} pkill -f ${JAR_NAME} || true sleep 2 cd ${DEPLOY_DIR} nohup java -jar ${JAR_NAME} --spring.profiles.activeprod app.log 21 echo \\$! app.pid # 等待启动完成最多30秒 for i in \$(seq 1 30); do if curl -sf http://127.0.0.1:8080/actuator/health; then echo service started ok exit 0 fi sleep 1 done echo service start failed, see app.log exit 1 } } } }这里面的关键点是使用健康检查接口来判断启动成功而不是只等10秒再看。有些应用启动比较慢10秒不够有些应用启动很快但实际没起来单纯sleep容易漏判。用健康检查做探活问题不大。5.4 一个实操坑Jenkins发布时scp的jar包不完整这个问题很多人可能遇到过scp命令明明显示成功但到服务器上执行时提示jar包损坏或者启动时ZipException。原因大概率是Jenkins任务所在的workspace里多个并行任务同时在写同一个target目录或者前一个构建还没有结束后一个构建就开始打包target/app.jar被覆盖到一半就被scp走了。解决思路每个任务使用独立的工作空间不要并行构建同一个任务。打包完成后先sha256sum生成校验和scp到服务器后再sha256sum -c校验。发布用的jar包不要直接从target里拉先复制到一个独立的“发布目录”再传。6. 从源头预防让Jenkins构建完jar包一定能启动6.1 构建阶段就做一次本地启动冒烟测试很多人把“启动验证”这个动作放在发布到服务器之后才做但发现服务器已经改了回滚又要一轮时间。其实完全可以在构建阶段就做一次“本地启动”冒烟测试Jenkins构建机上如果有Java环境直接执行java -jar target/app.jar并检查健康接口成功了再继续发布。这样可以把大部分环境无关的问题比如打包插件配置错误、依赖冲突、主类找不到在构建机上就能拦住。步骤大致是mvn package构建出jar包。在Jenkins的workspace里nohup java -jar target/app.jar --server.port0启动并让其随机端口。定期检查日志中出现Started关键字或者使用--spring.main.web-application-typenone方式快速验证Spring上下文能加载。验证通过后kill掉进程继续进行发布。不过要注意构建机上不要随便跑占用端口长时间不释放的进程最好用随机端口加超时强制结束。6.2 统一构建环境与运行环境容器化发布一劳永逸如果你已经厌倦了排查环境差异最推荐的方案是把jar包和运行环境一起做成Docker镜像。这样Jenkins构建出的镜像里既包含JDK又包含应用和启动脚本把“构建环境”和“运行环境”彻底对齐。Dockerfile示例FROM openjdk:17-jdk-slim WORKDIR /app COPY target/app.jar app.jar COPY deploy/start.sh start.sh RUN chmod x start.sh EXPOSE 8080 CMD [./start.sh]容器化之后之前那些JDK版本不一致、PATH不对、脚本换行符、端口被占的问题全部归零。唯一要小心的是镜像里的基础镜像JDK版本和应用编译时的版本要匹配。我用openjdk:17-jdk-slim的时候遇到过应用代码用JDK 17编译但某些第三方库在JDK 17的强封装下有反射兼容问题启动直接InaccessibleObjectException这种情况只能换用-Dadd-opens参数或换基础镜像。另外容器化后还要注意时区、字体、中文字符集的问题。部分基础镜像是UTC时间启动后日志时间会差8小时别被误导以为是服务异常。6.3 构建日志里埋好“可观测性”我见过很多团队把Jenkins构建日志和运行日志都当“出了问题再翻”的产物实际上它们应该在日常就提供足够的观测性。比如启动脚本里不要只写nohup java -jar app.jar app.log 21 可以加一行先打印关键环境信息echo JAVA_HOME$JAVA_HOME echo JAVA_VERSION$(java -version 21 | head -1) echo START_TIME$(date %Y-%m-%d %H:%M:%S) echo JAR$JAR_NAME SIZE$(du -h $JAR_NAME | cut -f1)这些信息在排查“为什么启动失败”时往往能直接给出答案。比如你发现JAVA_HOME打印出来是空的那问题就清楚了。这个习惯成本极低收益很高。7. 最后再分享一个小技巧排查过太多启动失败之后我现在有个固定习惯在启动脚本里加上“日志输出到控制台的同时开启自动回声”意思是启动后不仅写日志文件还在进程退出时打印退出码。比如java -jar app.jar app.log 21 PID$! echo $PID app.pid wait $PID echo process exited with code $?这个wait在这里的作用是等进程退出后打印退出码如果退出码是130或143通常是外部kill如果是1才是应用自身抛错退出。很多时候退出码能直接缩小范围别小看这一个数字。如果你按上面的排查顺序走完一遍还是没解决我强烈建议把下面三样东西一次性发到群里问别人启动命令、启动日志前200行、unzip -p app.jar META-INF/MANIFEST.MF的输出。这三样信息足够让经验丰富的人一眼看出问题。实际工作中“信息不全”比“问题难”更浪费时间。Jenkins构建完jar包启动不起来这个问题的本质往往不在Jenkins本身而是环境的割裂和产物的不可控。把这个链路理清楚你以后遇到的绝大多数启动失败都能在日志和退出码里找到答案。
返回列表