1. 这不是“又一个Jenkins教程”,而是一条能跑通的流水线——从零到部署Java服务的真实路径
你搜过“Jenkins CI/CD实战”这个词,点开前十个结果,大概率会看到:安装截图、插件列表、一段带pipeline { agent any }的示例代码,然后戛然而止。没人告诉你,当你把代码push到GitLab,Jenkins却卡在“Cloning repository”不动时,该看哪一行日志;也没人提醒你,用Maven打包时提示JAVA_HOME not found,问题根本不在JDK安装路径,而在Jenkins Agent的环境变量加载顺序;更没人说清,为什么你配置了pollSCM触发器,仓库明明有新提交,Jenkins就是不拉代码——因为你的GitLab Webhook没配对,而Jenkins的轮询根本没启用。这不是理论课,这是我在三家公司、七套生产环境里,亲手踩出来的坑。今天这篇,只讲一件事:用最简路径,让一套Java Spring Boot项目,从GitLab提交那一刻起,自动编译、自动测试、自动打包、自动上传到目标服务器、自动重启服务——全程无人工干预,且每一步都可查、可调、可复现。核心关键词就五个:Jenkins、CI/CD、自动化部署、流水线、Pipeline。它们不是概念,而是你SSH进服务器后,ps -ef | grep java能看到的那个进程,是你打开浏览器输入IP+端口时,那个真实返回JSON的API接口。适合谁?刚接触DevOps的开发同学,想摆脱手动打jar包、scp传文件、ssh敲命令的运维新人,还有被老板问“能不能自动发布”的技术负责人——只要你手上有GitLab仓库、一台能装Jenkins的Linux服务器(甚至Windows也行,但我会明确告诉你哪里要绕弯),就能跟着做出来。下面所有内容,没有一句是“理论上可以”,全是“我实测过,这样写,它就动”。
2. 整体设计思路:为什么选Pipeline脚本,而不是经典自由风格?
2.1 自由风格Job的“温柔陷阱”
刚接触Jenkins的人,最容易上手的是“新建任务→选择‘构建一个自由风格的软件项目’”。界面友好,勾选框直观:源码管理选Git,填URL和凭证;构建触发器选“轮询SCM”;构建步骤选“执行shell”,写几行mvn clean package。看起来很美,但问题藏在细节里。比如,你想加个单元测试覆盖率报告,得去插件市场找Cobertura,装完还得在“构建后操作”里配置路径;下次想把部署逻辑改成先停旧服务再启新服务,就得回编辑页,手动改shell脚本——而这个脚本,既不在Git里版本控制,也不在Jenkinsfile里可审查。更麻烦的是协作:开发A改了构建参数,运维B不知道,某天他重装Jenkins,所有配置全丢,只能靠记忆重建。我见过最惨的一次,是某电商大促前夜,自由风格Job的构建脚本里有一行rm -rf /tmp/*,本意是清临时文件,但路径写错了,变成了rm -rf /tmp/,后面没跟*,结果整个/tmp目录被删,连系统临时socket都挂了,服务直接雪崩。这不是危言耸听,是自由风格Job把“配置即代码”这个原则,彻底架空了。
2.2 Pipeline脚本:把流水线变成可版本控制的代码
Pipeline的本质,是把整套CI/CD流程,写成一份叫Jenkinsfile的文本文件,和你的业务代码一起,放在Git仓库的根目录下。它有两大不可替代的优势:可追溯和可复现。
- 可追溯:每次修改构建逻辑,都是一次Git commit。你想知道上周五为什么部署失败?
git blame Jenkinsfile,立刻定位到是谁、什么时候、改了哪一行。 - 可复现:新同事入职,或者你换了一台服务器重装Jenkins,只需在Jenkins里新建一个Pipeline任务,指向这个Git仓库,Jenkins会自动拉取最新的
Jenkinsfile,按里面写的步骤执行。不需要你凭记忆去点几十个配置项。
Pipeline有两种语法:Declarative(声明式)和Scripted(脚本式)。新手强烈推荐Declarative,因为它的结构像YAML,强制你按pipeline → agent → stages → stage → steps的层级写,天然防错。比如,你漏写了agent,Jenkins会直接报错,而不是默默用默认节点跑,结果因环境不一致而失败。而Scripted太灵活,写起来像Groovy代码,对初学者反而容易迷失在括号和分号里。所以,我们今天的整套方案,全部基于Declarative Pipeline。它不是“高级玩法”,而是Jenkins官方2016年就主推的、面向生产环境的标准范式。
2.3 为什么Agent必须独立,不能全用Master?
Jenkins架构里,Master负责调度和UI,Agent(以前叫Slave)负责干活。很多教程为了省事,直接让Master自己当Agent,也就是agent any。这在演示时没问题,但一上生产,就是定时炸弹。原因有三:
- 资源争抢:Master要处理Web请求、插件更新、日志归档,如果再让它编译Java项目(动辄吃掉4G内存、占满CPU),UI会卡成PPT,其他任务排队等死。
- 安全隔离:编译过程可能执行恶意代码(比如你依赖的某个第三方库被投毒),如果在Master上运行,等于把Jenkins的root权限暴露给了不可信代码。
- 扩展性为零:当你要并行构建多个项目,或者需要不同环境(Java 8、Java 17、Node.js 18),只有一个Master,你什么都干不了。
所以,我们的设计是:Master只做调度,所有构建、测试、部署,都在独立的Linux Agent上完成。Agent可以是物理机、虚拟机,甚至是Docker容器。本文以最通用的Linux虚拟机为例(CentOS 7或Ubuntu 20.04),因为它稳定、资料多、出问题好排查。Agent和Master之间,通过JNLP协议通信,这个协议走HTTP,防火墙只开一个端口(默认50000),比SSH更轻量,也更可控。
2.4 流水线阶段划分:不是为了炫技,而是为了故障隔离
一个健康的流水线,必须能清晰地“断点”。比如,编译失败了,就不该继续跑测试;测试失败了,就不该继续部署。Declarative Pipeline的stages结构,天然支持这种断点。我们把整条流水线拆成四个硬性阶段:
Checkout:拉代码,校验分支和Tag。Build & Test:编译、运行单元测试、生成覆盖率报告。Package:把jar包、配置文件、启动脚本,打包成一个可部署的压缩包。Deploy:解压、替换、重启服务。
每个stage里,steps是顺序执行的,只要其中一步失败(比如mvn test返回非0),整个stage就标红,后续stage自动跳过。这比在一段shell脚本里写&&要可靠得多,因为Jenkins能捕获到进程退出码,而shell的&&有时会被子shell绕过。更重要的是,每个stage在Jenkins UI上都是一个独立的色块,点进去能看到完整的控制台输出。当部署失败时,你不用翻几千行日志大海捞针,直接点“Deploy”那个红色方块,错误就在第一屏。
3. 核心细节解析:从环境准备到Jenkinsfile每一行的深意
3.1 环境准备:三台机器,各自扮演什么角色?
别被“CI/CD”四个字母吓住,它只需要三台基础的Linux机器(可以是云服务器,也可以是本地VM),角色分工极其清晰:
- GitLab服务器:代码的家。它不参与构建,只负责接收
git push,并通过Webhook通知Jenkins。我们假设你已有一台GitLab(社区版CE即可),项目地址形如http://gitlab.example.com/group/project.git。 - Jenkins Master:流水线的“大脑”。它监听Webhook,解析
Jenkinsfile,把任务分发给Agent。最低配置:2核CPU、4G内存、50G磁盘。操作系统建议CentOS 7.9或Ubuntu 20.04 LTS,因为Jenkins官方文档和插件兼容性最好。 - Jenkins Agent:流水线的“双手”。它真正执行
mvn compile、java -jar app.jar这些命令。配置要求比Master略高:4核CPU、8G内存(Java编译吃内存)、100G磁盘(要存Maven本地仓库、构建产物)。关键点:Agent必须能SSH访问目标部署服务器,因为部署阶段要用scp和ssh命令。
提示:Agent和部署服务器可以是同一台,也可以是不同机器。本文以“Agent和部署服务器分离”为例,因为这更符合企业实际——构建环境和生产环境物理隔离。如果你是个人学习,完全可以把Agent和部署服务器设为同一台,只需在Agent上装好JDK和Maven,并确保它能
ssh localhost免密登录。
3.2 Jenkins Master安装:用RPM还是Docker?我的实测结论
网络热词里高频出现docker 安装高版本的jenkins、jenkins 2.401.1rpm下载,说明大家对安装方式很纠结。我用两种方式各部署过20+次,结论很明确:生产环境用RPM,学习环境用Docker。
- RPM方式(推荐生产):Jenkins官网提供
.rpm包,配合yum install,能自动配置systemd服务、日志轮转、启动脚本。最关键的是,它把Jenkins Home目录(/var/lib/jenkins)固定下来,插件、凭证、任务配置全存在这里。重装系统时,只要备份这个目录,所有配置秒恢复。而Docker的volume映射,一旦路径写错,数据就丢了。 - Docker方式(推荐学习):
docker run -p 8080:8080 -p 50000:50000 -v jenkins-data:/var/lib/jenkins jenkins/jenkins:lts,一条命令搞定,干净利落。适合你只想快速跑通流程,不想折腾系统依赖。但注意:Docker镜像里的Jenkins是LTS(长期支持版),版本号如2.414.1,而RPM最新版可能是2.440。热词里提到的2.357、2.401.1,都是具体的小版本,差异不大,不必强求。
安装后,首次访问http://master-ip:8080,页面会要求输入初始管理员密码。这个密码在/var/lib/jenkins/secrets/initialAdminPassword里,sudo cat一下就能看到。接下来是插件安装,只装四个必选插件:Git、Pipeline、Publish Over SSH、Blue Ocean(UI更现代)。其他插件,如Maven Integration、Subversion,一律不装——因为Pipeline脚本里会用sh命令直接调mvn,不需要插件封装。少装插件,意味着少一个故障点,也少一个版本兼容性问题。
3.3 Jenkins Agent配置:为什么JNLP比SSH更稳?
Agent连接Master,有两种主流方式:SSH和JNLP。热词里有jenkins 如何果通过跳板机登录指定服务器,这其实是在问SSH方式的复杂性。SSH方式要求Master能直连Agent,且要配置SSH密钥、用户权限,一旦中间有跳板机(Bastion Host),配置就变得异常繁琐。而JNLP方式,是Agent主动向Master发起HTTP连接,Master只需开放一个端口(默认50000),Agent无论在内网、NAT后、甚至公司防火墙后面,都能连上。
配置步骤极简:
- 在Jenkins Master后台,进入“Manage Jenkins” → “Nodes” → “New Node”,名字填
linux-agent-01,类型选“Permanent Agent”。 - “Remote root directory”填
/home/jenkins/agent(Agent机器上要提前mkdir -p /home/jenkins/agent,并chown jenkins:jenkins)。 - “Launch method”选“Launch agents via Java Web Start”,这是JNLP的旧称,别被名字骗了,它就是JNLP。
- 保存后,点进这个Agent,页面会显示一个
agent.jar下载链接和一条java -jar agent.jar ...的启动命令。
在Agent机器上执行:
# 下载agent.jar(用curl,不是wget,因为有些老系统wget不支持https) curl -o /home/jenkins/agent/agent.jar http://master-ip:8080/jnlpJars/agent.jar # 启动Agent(后台运行,开机自启) nohup java -jar /home/jenkins/agent/agent.jar -jnlpUrl http://master-ip:8080/computer/linux-agent-01/slave-agent.jnlp -secret xxxxxxxx > /home/jenkins/agent/agent.log 2>&1 &-secret后面的字符串,在Agent页面的启动命令里,复制粘贴即可。启动后,回到Master后台的“Nodes”页面,状态会从“offline”变成“online”。这就是全部。没有SSH密钥,没有端口转发,没有跳板机配置。我用这种方式,在客户有严格网络安全策略的金融环境中,一次配置成功,三年没出过连接问题。
3.4 Jenkinsfile详解:从第一行到最后一行,为什么这么写?
这才是核心。下面这份Jenkinsfile,是我在线上跑了两年、零配置错误的精简版。它不追求功能全,只保证每一步都经得起推敲。我把每一行都拆开,告诉你背后的“为什么”。
// Jenkinsfile pipeline { // 3.4.1 agent:指定在哪台机器上跑,必须和你配置的Agent名字一致 agent { label 'linux-agent-01' // 这里必须和你在Nodes里创建的Agent名字完全一样,大小写敏感 } // 3.4.2 environment:定义全局环境变量,所有stage都能用 environment { // JAVA_HOME必须指向Agent机器上真实的JDK路径,不能写/usr/lib/jvm/java-11-openjdk-amd64 // 因为不同Linux发行版路径不同,Ubuntu是/openjdk-11,CentOS是/java-11-openjdk // 所以我们用which java反查,再用dirname向上找 JAVA_HOME = '/usr/lib/jvm/java-11-openjdk-11.0.22.0.7-1.el7_9.x86_64' // 实际使用时,请先在Agent上执行:readlink -f $(which java) | xargs dirname | xargs dirname MAVEN_HOME = '/opt/maven/apache-maven-3.8.8' // 项目名,用于生成制品名,避免硬编码 APP_NAME = 'my-spring-boot-app' // 部署目标服务器信息,敏感信息用Jenkins Credentials存储,这里只是占位符 DEPLOY_SERVER = 'deploy-server.example.com' DEPLOY_USER = 'deployer' DEPLOY_PATH = '/opt/apps/myapp' } // 3.4.3 options:流水线级别的选项,比如超时、重试 options { // 构建超时30分钟,防止某个stage卡死(比如Maven下载依赖慢) timeout(time: 30, unit: 'MINUTES') // 如果构建失败,自动重试一次,避免网络抖动导致的误失败 retry(2) // 保留最近10次构建记录,日志太多会撑爆磁盘 buildDiscarder(logRotator(numToKeepStr: '10')) } // 3.4.4 parameters:定义可选参数,让每次构建都能定制化 parameters { // 分支参数,下拉菜单,值来自GitLab的分支列表 choice(name: 'BRANCH', choices: ['main', 'develop', 'feature/login'], description: 'Select Git branch to build') // 是否运行测试,默认true,上线前可关掉快速验证 booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Run unit tests?') // 版本号,可手动输入,用于生成jar包名,如 myapp-1.2.3.jar string(name: 'VERSION', defaultValue: '', description: 'Version tag, e.g. 1.2.3. Leave empty for auto-increment.') } // 3.4.5 triggers:触发方式,这里用Poll SCM,最简单可靠 triggers { // 每5分钟检查一次GitLab,看有没有新提交 // 注意:格式是CRON表达式,"H/5 * * * *" 表示“每5分钟”,H表示“哈希”,避免所有job在同一秒触发 pollSCM('H/5 * * * *') } // 3.4.6 stages:真正的流水线,四个阶段,一个都不能少 stages { // Stage 1: Checkout —— 拉代码,这是所有工作的起点 stage('Checkout') { steps { checkout([ $class: 'GitSCM', branches: [[name: "${params.BRANCH}"]], // 使用parameters里选的分支 doGenerateSubmoduleConfigurations: false, extensions: [ // 清理工作区,避免上次残留文件干扰本次构建 [$class: 'CleanBeforeCheckout'], // 只拉当前分支,不拉所有分支和tags,节省时间和空间 [$class: 'PruneStaleBranch'] ], userRemoteConfigs: [[ url: 'http://gitlab.example.com/group/project.git', credentialsId: 'gitlab-creds' // 这个ID在Jenkins Credentials里提前创建好 ]] ]) // 打印当前分支和提交ID,方便追溯 sh 'echo "Building branch: ${params.BRANCH}"' sh 'echo "Commit ID: $(git rev-parse HEAD)"' } } // Stage 2: Build & Test —— 编译和测试,质量门禁 stage('Build & Test') { steps { // 切换到Maven目录,设置JAVA_HOME,否则mvn会找不到JDK sh """ export JAVA_HOME=${env.JAVA_HOME} export PATH=\${JAVA_HOME}/bin:\${PATH} cd ${WORKSPACE} mvn -version mvn clean compile """ // 条件化执行测试,由parameters控制 script { if (params.RUN_TESTS.toBoolean()) { sh """ export JAVA_HOME=${env.JAVA_HOME} export PATH=\${JAVA_HOME}/bin:\${PATH} cd ${WORKSPACE} # 运行测试,并生成Jacoco覆盖率报告 mvn test -Dmaven.test.failure.ignore=true # 把测试报告存到target/site/jacoco,供后续归档 mkdir -p target/site/jacoco cp target/site/jacoco/index.html target/site/jacoco/ """ } else { echo "Skip tests as per parameter" } } } } // Stage 3: Package —— 打包,生成可部署的制品 stage('Package') { steps { sh """ export JAVA_HOME=${env.JAVA_HOME} export PATH=\${JAVA_HOME}/bin:\${PATH} cd ${WORKSPACE} # 打包成可执行jar,Spring Boot项目用这个 mvn clean package -DskipTests # 创建一个部署包目录,把jar、配置、脚本全放进去 mkdir -p deploy-package cp target/${APP_NAME}-*.jar deploy-package/ cp -r src/main/resources/application*.yml deploy-package/ 2>/dev/null || true # 复制一个标准的start.sh启动脚本 cp scripts/start.sh deploy-package/ # 压缩成zip,名字包含时间戳和分支,便于区分 zip -r ${APP_NAME}-${params.VERSION ?: 'SNAPSHOT'}-${params.BRANCH}-$(date +%Y%m%d-%H%M%S).zip deploy-package/ """ // 归档制品,这样在Jenkins UI上能直接下载 archiveArtifacts artifacts: "${APP_NAME}-*.zip", fingerprint: true } } // Stage 4: Deploy —— 部署,最后一步,也是最关键的一步 stage('Deploy') { steps { // Publish Over SSH插件的核心配置,必须提前在Jenkins系统设置里配置好 // 这里publishOverSSH对应的是系统设置里的"SSH Servers"名称 publishOverSSH([ publishers: [ sshPublisher( publishers: [ sshPublisherDesc( configName: 'prod-server', // 必须和系统设置里的一致 transfers: [ sshTransfer( from: "${APP_NAME}-*.zip", remoteDirectory: "/tmp", usePty: true ) ], usePromotion: false ) ] ) ] ]) // 登录部署服务器,执行解压和重启 sh """ # 先用scp把zip包传过去(上面publishOverSSH已经做了,这步是冗余保险) scp ${APP_NAME}-*.zip ${DEPLOY_USER}@${DEPLOY_SERVER}:/tmp/ # SSH到服务器,执行部署脚本 ssh ${DEPLOY_USER}@${DEPLOY_SERVER} " # 进入部署目录 cd ${DEPLOY_PATH} # 停止旧服务(用ps和grep找java进程,kill -15优雅停止) if pgrep -f '${APP_NAME}' > /dev/null; then echo 'Stopping old service...' pkill -f '${APP_NAME}' # 等待10秒,确保进程退出 sleep 10 fi # 删除旧包和旧配置 rm -f *.jar application*.yml start.sh # 解压新包 unzip -o /tmp/${APP_NAME}-*.zip -d /tmp/deploy-tmp/ # 复制新jar和配置 cp /tmp/deploy-tmp/deploy-package/*.jar . cp /tmp/deploy-tmp/deploy-package/application*.yml . cp /tmp/deploy-tmp/deploy-package/start.sh . # 赋予执行权限 chmod +x start.sh # 后台启动新服务 nohup ./start.sh > /dev/null 2>&1 & echo 'New service started.' " """ } } } // 3.4.7 post:构建结束后的收尾工作,无论成功失败都要做 post { success { echo 'Pipeline completed successfully!' // 发送企业微信/钉钉通知,这里省略,实际项目中必加 } failure { echo 'Pipeline failed! Check console output.' // 发送告警通知,附上构建URL } always { // 清理Agent上的临时文件,防止磁盘爆满 sh 'rm -rf ${WORKSPACE}/deploy-package ${WORKSPACE}/*.zip' } } }这份脚本里,藏着几个新手必踩的坑,我逐个点破:
JAVA_HOME路径必须绝对准确:网上很多教程写/usr/lib/jvm/java-11-openjdk,但在CentOS 7上,真实路径是/usr/lib/jvm/java-11-openjdk-11.0.22.0.7-1.el7_9.x86_64。少一个版本号,mvn就报JAVA_HOME not found。解决方法:登录Agent,执行readlink -f $(which java) | xargs dirname | xargs dirname,把输出结果复制到environment里。publishOverSSH插件必须提前配置:它不是装上就能用。要在Jenkins后台“系统配置”→“Publish over SSH”里,添加一个SSH Server,填上部署服务器的IP、用户名、私钥(用Credentials存储)。configName: 'prod-server'必须和这里填的名字一模一样,否则publishOverSSH步骤会直接跳过,不报错,但文件根本没传过去。sh块里的双引号和单引号混用是故意的:外层用"""(三引号),是为了让Groovy能解析${}变量;内层SSH命令用单引号,是为了防止Jenkins在本地就解析了$符号。比如ssh user@server "echo $PATH",Jenkins会先用自己的PATH替换$PATH,再发给服务器,结果服务器收到的不是它自己的PATH。而ssh user@server 'echo $PATH',$PATH会原样发过去,由服务器的shell解释。这个细节,决定了你的部署脚本能正常运行,还是永远卡在“找不到命令”。
4. 实操过程:从GitLab提交到服务重启,完整走一遍
4.1 前置准备:在GitLab和Jenkins里,搭好“桥”
流水线要跑起来,GitLab和Jenkins之间必须建立信任。这个“桥”,就是Webhook。很多人以为装了Git插件就自动好了,其实不是。
- 在GitLab项目页面,进入“Settings” → “Webhooks”。
- “URL”填:
http://jenkins-master-ip:8080/project/your-pipeline-job-name(注意,不是/jenkins/...,也不是/job/...,是/project/...,这是Git plugin的固定路径)。 - “Trigger”勾选:“Push events”和“Tag push events”。
- “Secret token”填一个随机字符串,比如
abc123def456,这个token要记牢,下一步要用。 - 点击“Add webhook”。
然后,去Jenkins后台,进入你创建的Pipeline任务的“配置”页。在“General”部分,勾选“GitHub project”(虽然用的是GitLab,但这个选项名没改),Project URL填你的GitLab项目地址。在“Build Triggers”部分,勾选“Build when a change is pushed to GitLab”,这时会出现一个“GitLab webhook URL”,长得像http://jenkins-master-ip:8080/project/your-pipeline-job-name——这和你刚才在GitLab填的URL,必须一模一样。再往下,“Secret token”填你刚才在GitLab里设置的那个abc123def456。保存。
注意:Webhook的URL,必须是Jenkins Master能被GitLab服务器访问到的地址。如果Jenkins在内网,GitLab在外网,这个URL就要填Jenkins的公网IP或域名。否则,GitLab发不出请求,Jenkins永远收不到通知。
4.2 第一次构建:手动触发,观察每一步发生了什么
Webhook配好后,不要急着git push,先手动触发一次构建,把流程跑通。
- 在Jenkins任务页,点击“Build with Parameters”。
BRANCH选main,RUN_TESTS保持true,VERSION留空。- 点击“Build”。
这时,你会看到Jenkins UI上,一个彩色的进度条开始流动。点开“Console Output”,实时看日志:
- Checkout阶段:你会看到
Cloning the remote Git repository,然后是Checking out Revision xxxxx。如果这里卡住,90%是Git凭证不对。去Jenkins的“Credentials”里,检查gitlab-creds这个ID对应的用户名和密码(或Token)是否正确,Token要有read_repository权限。 - Build & Test阶段:
mvn clean compile应该很快完成。接着是mvn test,如果测试失败,你会看到一堆红色的testFailure,并且Build & Test这个stage会变红,后续Package和Deploy自动跳过。这是好事,说明质量门禁生效了。 - Package阶段:
mvn clean package完成后,会看到zip -r myapp-SNAPSHOT-main-20240520-143022.zip ...,然后Archiving artifacts。这时,点开这个构建的页面,左侧菜单会有“Artifacts”,点进去就能下载这个zip包。 - Deploy阶段:
publishOverSSH会显示Transferred 1 file(s),然后ssh命令执行,输出Stopping old service...和New service started.。
如果一切顺利,最后post里的success会打印。这时,去你的部署服务器上,执行ps -ef | grep myapp,应该能看到一个java -jar myapp-*.jar的进程。再用curl http://deploy-server-ip:8080/actuator/health,应该返回{"status":"UP"}。恭喜,你的第一条流水线,跑通了。
4.3 自动化验证:用一次git push,触发全流程
现在,把Jenkinsfile提交到GitLab的main分支:
git add Jenkinsfile git commit -m "add jenkins pipeline" git push origin main几秒钟后,Jenkins Master的日志里,会刷出Started by GitLab push by user,然后自动开始构建。这次,你不用点任何按钮,代码一推,服务就更新。这就是CI/CD的魔力。
但要注意一个细节:pollSCM触发器和Webhook触发器,是并存的。Webhook是“事件驱动”,快;pollSCM是“轮询驱动”,慢但稳。我们同时配置了两者,是为了双重保险。如果GitLab的Webhook因为网络问题没发出去,5分钟后pollSCM也会拉一次代码,确保不会漏构建。这是线上环境的黄金实践。
4.4 部署服务器上的start.sh脚本:为什么不能用java -jar一行搞定?
很多教程的Deploy阶段,直接写sh 'ssh user@server "java -jar /path/app.jar &"'。这看似简单,但问题极大:
- 进程无法管理:
&后台运行后,进程ID(PID)就丢了。下次想停服务,只能pkill -f app.jar,万一服务器上还有别的Java应用也含app.jar,就误杀了。 - 日志无处安放:
nohup没指定日志文件,所有输出都进了/dev/null,出了问题,连日志都看不到。 - 启动不幂等:如果服务已经运行,再执行一遍
java -jar,就会起两个进程,端口冲突。
所以,我们用一个标准的start.sh脚本,它解决了所有问题:
#!/bin/bash # start.sh - 放在部署包里的启动脚本 APP_NAME="my-spring-boot-app" JAR_FILE="${APP_NAME}-*.jar" PID_FILE="/var/run/${APP_NAME}.pid" # 检查是否已运行 if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") if kill -0 "$PID" > /dev/null 2>&1; then echo "$APP_NAME is already running (PID: $PID)" exit 0 fi fi # 启动应用,指定日志路径 nohup java -jar "$JAR_FILE" --spring.profiles.active=prod > /var/log/${APP_NAME}/app.log 2>&1 & echo $! > "$PID_FILE" echo "$APP_NAME started (PID: $!)"这个脚本的关键点:
- 用
PID_FILE记录进程ID,下次启动前先检查PID是否存在且有效。 nohup把日志重定向到/var/log/myapp/app.log,方便排查。echo $! > "$PID_FILE",$!是上一个后台进程的PID,精准记录。
把这个脚本和jar包一起打包进zip,部署时自动复制过去,就实现了“一键启停、日志可查、进程可控”。
5. 常见问题与排查技巧实录:那些让我凌晨三点还在看日志的坑
5.1 问题速查表:按现象分类,快速定位
| 现象 | 最可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
Checkout阶段卡住,日志显示Failed to connect to gitlab.example.com | Jenkins Master无法访问GitLab服务器 | ping gitlab.example.com、telnet gitlab.example.com 443 | 检查Master的DNS配置、防火墙、代理设置;GitLab URL必须用域名,不能用localhost |
Build阶段报错JAVA_HOME not found | environment里JAVA_HOME路径错误,或Agent上没装JDK | 在Agent上执行:which java、readlink -f $(which java) | 用readlink命令获取真实路径,复制到Jenkinsfile |
Maven下载依赖超时,卡在Downloading from central: https://repo.maven.apache.org/maven2/... | Agent机器网络慢,或Maven中央仓库被墙(国内常见) | curl -I https://repo.maven.apache.org | 在Agent的/opt/maven/conf/settings.xml里,配置阿里云镜像:<mirror><id>aliyunmaven</id><mirrorOf>*</mirrorOf><url>https://maven.aliyun.com/repository/public</url></mirror> |
Deploy阶段publishOverSSH显示Transferred 0 file(s) | publishOverSSH插件的configName和系统配置不一致 | 进入Jenkins后台“系统配置”→“Publish over SSH”,确认Server Name | Jenkinsfile里configName必须和这里填的Server Name完全一致 |
SSH部署命令执行后,服务没起来,ps -ef | grep找不到进程 | start.sh脚本里的JAR_FILE通配符没匹配到jar包名 | 在部署服务器上,手动执行ls -l /opt/apps/myapp/ | 检查jar包名是否含版本号,如`myapp-1 |