Linux第18篇:Spring Boot 生产部署完全指南——从“能跑”到“敢拍胸脯”
系列导读:这是《Java微服务运维实战》系列第10篇。在上一篇(第9篇)我们学习了网络诊断工具,解决了“怎么连”的问题。本文将解决“怎么部署”的问题——当你的Spring Boot应用准备上线时,你需要的绝不仅仅是
java -jar。本文将带你走过从配置分离 → 零停部署 → 健康监控 → 应急排障的完整生命周期。
一、一个让你后背发凉的部署事故
深夜,你信心满满地执行了部署脚本。5分钟后,监控大屏开始报警:
- 接口响应时间从50ms飙升到3s。
- 数据库连接池爆满。
- 更诡异的是,订单表里新生成的创建时间全部变成了UTC时间,比实际时间晚了8小时。
你慌忙回滚,但为时已晚。事故复盘发现:
- 配置文件打进了JAR包,导致生产数据库密码随包分发,安全组紧急要求更换所有密码。
- 服务器时区是UTC,而代码里用了
new Date(),导致时间错乱。 - 没有配置优雅停机,回滚时的强制杀进程导致大量正在处理的请求报错,产生了脏数据。
这就是生产部署的残酷现实:“能跑”和“能稳定运行”之间,隔着一整个银河系。
本文将为你建立一套生产级部署的“标准作业程序”,确保你再也不会踩进这些坑里。
二、部署形态抉择:JAR包 vs WAR包
在开始部署前,我们得先选好“打包方式”。Spring Boot应用主要有两种形态:
| 特性 | JAR 包 (推荐) | WAR 包 |
|---|---|---|
| 容器 | 内嵌 Tomcat/Netty | 依赖外部 Tomcat 容器 |
| 部署方式 | java -jar app.jar | 放入webapps/目录,启动 Tomcat |
| 适用场景 | 微服务、云原生、容器化(Docker) | 传统单体、受制于运维规范的老项目 |
| 优点 | 自包含、依赖清晰、启动快、易于自动化 | 可复用现有Tomcat配置、支持热部署(不建议生产用) |
| 缺点 | 对底层容器配置的灵活性稍弱 | 部署复杂、Tomcat版本与应用可能冲突 |
结论:对于新项目,请毫不犹豫地选择JAR包。它是Spring Boot的“原生”形态,与微服务和容器化(Docker/K8s)天生一对。
三、外部化配置:把“命根子”从JAR包里拎出来
铁律:永远不要将生产环境的配置文件(尤其是包含密码的)打包进JAR文件!
3.1 配置文件加载“军规”
Spring Boot有一套严谨的配置加载优先级。理解它,是解决“为什么配置没生效”的唯一途径。
优先级(从高到低):
- 命令行参数:
--server.port=8081(最高,用于临时覆盖) - 环境变量:
SPRING_DATASOURCE_URL=...(K8s/Docker中首选) /opt/myapp/config/目录下的application-{profile}.yml(生产环境推荐方式)/opt/myapp/config/目录下的application.yml- JAR包内部的
application-{profile}.yml - JAR包内部的
application.yml(最低,仅用于默认值)
3.2 生产级目录与配置实践
遵循第2篇的标准化目录规范,推荐的生产目录结构如下:
/opt/myapp/ ├── current/ │ └── myapp.jar# 1. 只有代码,无任何环境配置├── config/ │ ├── application-prod.yml# 2. 生产环境配置(不含密码)│ └── .env# 3. 敏感信息(密钥、密码),权限600!└── scripts/ └── deploy.sh# 4. 部署脚本配置文件内容 (application-prod.yml):
spring:datasource:url:jdbc:mysql://192.168.1.200:3306/myapp?useSSL=true&serverTimezone=Asia/Shanghaiusername:myapp_userpassword:${MYSQL_PASSWORD}# 关键:从环境变量读取!redis:host:192.168.1.201password:${REDIS_PASSWORD}# 关键:从环境变量读取!server:port:8080shutdown:graceful# 优雅停机,必须开启!敏感信息文件 (.env):
# 此文件权限必须为 600MYSQL_PASSWORD="your_strong_password"REDIS_PASSWORD="your_redis_password"系统服务配置 (/etc/systemd/system/myapp.service):
[Service] User=myapp Group=myapp EnvironmentFile=/opt/myapp/config/.env # 加载敏感信息 ExecStart=/usr/bin/java \ -Dspring.profiles.active=prod \ -Dspring.config.location=/opt/myapp/config/application-prod.yml \ -jar /opt/myapp/current/myapp.jar💡 核心思想:整个部署过程,我们只变配置和脚本,JAR包本身是“一次构建,各处运行”的不可变产物。这正是持续交付的精髓。
四、蓝绿部署:实现“用户无感知”的零停机发布
停机发布?那是上个时代的事情了。对于微服务,我们追求零停机。蓝绿部署是实现这一目标的经典且可靠的模式。
4.1 蓝绿部署原理
维护两套完全一样的环境(蓝色:当前生产环境;绿色:新版本环境)。
- 发布:将新版本部署到绿色环境。
- 验证:在绿色环境上跑通所有冒烟测试。
- 切换:将负载均衡器(如Nginx)的流量一键切到绿色环境。
- 回滚:万一出问题,只需将流量瞬间切回蓝色环境,秒级恢复。
4.2 自动化蓝绿部署脚本
下面是一个实战级的部署脚本,它实现了无损切换。关键点在于:
- 健康检查:切换前确保新应用已完全就绪。
- 优雅停机:切换后,给旧应用足够时间处理完正在进行的请求。
#!/bin/bash# 文件名: blue-green-deploy.shset-euopipefailAPP_NAME="myapp"APP_HOME="/opt/${APP_NAME}"NEW_JAR="${1:?"请指定新JAR包路径"}"HEALTH_CHECK_URL="http://localhost:8080/actuator/health/readiness"# 1. 确定当前活跃颜色CURRENT_COLOR=$(cat"${APP_HOME}/.active_color"2>/dev/null||echo"blue")if["$CURRENT_COLOR"="blue"];thenNEW_COLOR="green"NEW_PORT=8081elseNEW_COLOR="blue"NEW_PORT=8080filog(){echo"[$(date'+%Y-%m-%d %H:%M:%S')]$1";}log"🚀 开始蓝绿部署: 当前活跃 [$CURRENT_COLOR:$CURRENT_PORT] → 目标 [$NEW_COLOR:$NEW_PORT]"# 2. 部署新版本cp"${NEW_JAR}""${APP_HOME}/${NEW_COLOR}/app.jar"sudosystemctl start"${APP_NAME}-${NEW_COLOR}"# 3. 【关键】等待新版本就绪(带超时重试)log"⏳ 等待新版本服务就绪..."MAX_RETRY=30foriin$(seq1$MAX_RETRY);doifcurl-sSf"${HEALTH_CHECK_URL}"|grep-q'"status":"UP"';thenlog"✅ 新版本健康检查通过!"breakfiif[$i-eq$MAX_RETRY];thenlog"❌ 新版本启动超时!回滚操作..."sudosystemctl stop"${APP_NAME}-${NEW_COLOR}"exit1fisleep5done# 4. 【关键】原子性地切换流量(Nginx Upstream)log"🔀 切换 Nginx 流量..."sudosed-i"s/localhost:${CURRENT_PORT}/localhost:${NEW_PORT}/g"/etc/nginx/conf.d/${APP_NAME}.confsudonginx-t&&sudosystemctl reload nginx# 5. 【关键】优雅停机旧版本(给旧应用30秒处理完存量请求)log"🕒 等待${CURRENT_COLOR}处理完存量请求..."sleep30log"🛑 停止旧版本${CURRENT_COLOR}..."sudosystemctl stop"${APP_NAME}-${CURRENT_COLOR}"# 6. 记录状态echo"${NEW_COLOR}">"${APP_HOME}/.active_color"log"🎉 部署成功!当前活跃版本:${NEW_COLOR}"五、监控与探针:Spring Boot Actuator 的“三把斧”
部署完只是开始,如何让运维/监控系统知道你“活得好不好”?全靠Spring Boot Actuator。
5.1 核心端点配置
management:endpoints:web:exposure:include:health,info,metrics,prometheus# 只暴露必要的端点base-path:/ops# 自定义路径,增加安全性endpoint:health:show-details:when-authorized# 保护敏感健康信息probes:enabled:true# 开启 K8s 探针支持server:port:9090# 使用独立端口,与业务隔离5.2 理解三个关键“探针”
在云原生(Kubernetes)环境中,这三个探针是应用的生命线:
| 探针 | 端点 | 作用 | 失败后果 |
|---|---|---|---|
| 存活探针 (Liveness) | /actuator/health/liveness | 应用是否活着(非僵死状态)。 | 重启Pod。应用死锁时用它抢救。 |
| 就绪探针 (Readiness) | /actuator/health/readiness | 应用是否准备好接收流量。 | 从服务发现摘除。启动时或依赖服务(如DB)不可用时,不接收流量。 |
| 启动探针 (Startup) | /actuator/health/startup | 应用是否已启动完成。 | 保护应用在启动慢时不被存活探针误杀。 |
💡 生产提示:永远不要用
/actuator/health的“大盘”状态来做决策。比如,Redis挂了并不影响应用处理HTTP请求,此时就不该将应用从Nginx摘除。正确做法是:K8s的就绪探针只检查readiness端点,而该端点只检查HTTP服务自身状态。
六、生产“三连坑”:时区、文件、编码
这三个问题,100%的生产环境都会遇到,你提前掌握,就能在别人抓狂时轻松解决。
6.1 时区(数据混乱的元凶)
- 事故现场:数据库里存的是
2026-06-30 10:00:00,代码查出来变成2026-06-30 02:00:00。 - 根因:服务器默认时区是UTC(协调世界时),而你的业务在中国(UTC+8)。
- 解决方案(必须双管齐下):
- 操作系统:
sudo timedatectl set-timezone Asia/Shanghai - JVM:在启动参数中强制指定,防止OS时区被其他程序篡改。
-Duser.timezone=Asia/Shanghai - JDBC连接串:确保数据库连接也一致。
jdbc:mysql://host/db?serverTimezone=Asia/Shanghai
- 操作系统:
6.2 文件路径(Permission Denied 的元凶)
- 事故现场:上传文件功能报错
java.io.IOException: Permission denied。 - 根因:你的应用是以
myapp用户运行的,但上传目录/opt/myapp/uploads的所有者是root。 - 解决方案:
# 创建目录并赋予正确的用户和权限sudomkdir-p/opt/myapp/uploadssudochown-Rmyapp:myapp /opt/myapp/uploadssudochmod750/opt/myapp/uploads# 属主有全部权限,同组可读执行,其他无权限# 验证权限是否生效sudo-umyapptouch/opt/myapp/uploads/test.txt&&echo"OK"||echo"FAIL"6.3 字符编码(中文乱码的元凶)
- 事故现场:日志、HTTP响应中出现
????或乱码。 - 根因:系统
LANG环境变量不是UTF-8,或者JVM默认使用了系统字符集。 - 解决方案:
# 1. 系统层面echo"LANG=en_US.UTF-8"|sudotee-a/etc/environment# 2. JVM层面(双重保险)-Dfile.encoding=UTF-8七、应急处理:端口被占用的快速排查
当你部署时看到Port 8080 was already in use,别慌,按下面三步走:
# 1. 【查】谁占用了我的端口?ss-tlnp|grep8080# 输出示例:LISTEN 0 128 *:8080 *:* users:(("java",pid=12345,fd=120))# 2. 【看】这个进程是不是之前的残留?ps-p12345-f# 3. 【杀】如果是残留,优雅地干掉它sudosystemctl stop myapp# 优先用systemctl# 或kill-1512345# SIGTERM,触发优雅停机# 最后才考虑kill-912345# 强制杀死,不到万不得已不要用!八、常见问题精选(FAQ)
❓ Q1:spring.profiles.active应该写在哪里?
永远不要写在JAR包内的application.yml里!这会让你的JAR包与环境强绑定。正确做法是在启动命令(systemd或Dockerfile)中通过环境变量SPRING_PROFILES_ACTIVE=prod或JVM参数-Dspring.profiles.active=prod传入。这样才能实现“一次构建,到处部署”。
❓ Q2:Actuator的/health显示DOWN,但应用看起来正常,怎么回事?
/health大盘状态是**所有依赖组件状态逻辑与**的结果。如果Redis连不上,大盘就是DOWN。这很可能是误报。解决办法:
- 检查
/health的详细输出(需开启show-details),看是哪个组件挂了。 - 如果是非关键依赖,可以禁用它的健康检查:
management.health.redis.enabled=false。 - 在生产环境,请使用独立的
/actuator/health/readiness端点来做服务摘除决策。
❓ Q3:蓝绿部署时,数据库变更怎么办?
这是蓝绿部署最棘手的点。总原则:数据库变更必须向前兼容。
- 可安全操作:新增表、新增列(带默认值)、增加索引。
- 危险操作(需要分阶段进行):重命名列、修改列类型、删除列。
- 最佳实践:采用**“扩展-收缩”模式**。先发布一个新版本,它同时兼容新旧两种数据结构(扩展)。等所有实例都升级到新版本后,再执行一个清理任务,删除旧的数据结构(收缩)。
九、核心心法与检查清单
心法:部署即“换路”
把你的部署过程理解为**“将车流(用户流量)从一条老路(旧版本)平稳引导到一条新路(新版本)”**。所有的配置、脚本、监控都是服务于这个目标的。
Spring Boot 生产部署检查清单 ✅
| 检查项 | 状态 |
|---|---|
配置分离:生产密码、密钥使用${ENV},并通过EnvironmentFile加载 | ☐ |
| JAR包不可变:构建过程只产生JAR包,不修改其内容 | ☐ |
时区统一:OS、JVM、JDBC连接串全链路时区一致 (Asia/Shanghai) | ☐ |
| 文件权限:上传目录和应用日志目录属主与运行用户一致 | ☐ |
字符编码:-Dfile.encoding=UTF-8已设置 | ☐ |
优雅停机:server.shutdown=graceful和spring.lifecycle.timeout-per-shutdown-phase=30s已配置 | ☐ |
健康探针:management.endpoint.health.probes.enabled=true已开启 | ☐ |
| 蓝绿部署:部署脚本支持流量切换和秒级回滚 | ☐ |
| 端口隔离:Actuator使用独立端口(9090),与业务端口(8080)分离 | ☐ |
如果本文帮你避开了一个部署深坑,请点赞 👍 收藏 ⭐,让这份检查清单帮助更多人!
下一篇我们将深入Linux 防火墙(iptables/firewalld)与云安全组策略,学习如何为你的应用穿上“铠甲”。敬请期待!