Linux第18篇:Spring Boot 生产部署完全指南——从“能跑”到“敢拍胸脯”

系列导读:这是《Java微服务运维实战》系列第10篇。在上一篇(第9篇)我们学习了网络诊断工具,解决了“怎么连”的问题。本文将解决“怎么部署”的问题——当你的Spring Boot应用准备上线时,你需要的绝不仅仅是java -jar。本文将带你走过从配置分离 → 零停部署 → 健康监控 → 应急排障的完整生命周期。


一、一个让你后背发凉的部署事故

深夜,你信心满满地执行了部署脚本。5分钟后,监控大屏开始报警:

  • 接口响应时间从50ms飙升到3s。
  • 数据库连接池爆满。
  • 更诡异的是,订单表里新生成的创建时间全部变成了UTC时间,比实际时间晚了8小时。

你慌忙回滚,但为时已晚。事故复盘发现:

  1. 配置文件打进了JAR包,导致生产数据库密码随包分发,安全组紧急要求更换所有密码。
  2. 服务器时区是UTC,而代码里用了new Date(),导致时间错乱。
  3. 没有配置优雅停机,回滚时的强制杀进程导致大量正在处理的请求报错,产生了脏数据。

这就是生产部署的残酷现实:“能跑”和“能稳定运行”之间,隔着一整个银河系。

本文将为你建立一套生产级部署的“标准作业程序”,确保你再也不会踩进这些坑里。


二、部署形态抉择: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有一套严谨的配置加载优先级。理解它,是解决“为什么配置没生效”的唯一途径。

优先级(从高到低):

  1. 命令行参数--server.port=8081(最高,用于临时覆盖)
  2. 环境变量SPRING_DATASOURCE_URL=...(K8s/Docker中首选)
  3. /opt/myapp/config/目录下的application-{profile}.yml生产环境推荐方式
  4. /opt/myapp/config/目录下的application.yml
  5. JAR包内部的application-{profile}.yml
  6. 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 蓝绿部署原理

维护两套完全一样的环境(蓝色:当前生产环境;绿色:新版本环境)。

  1. 发布:将新版本部署到绿色环境。
  2. 验证:在绿色环境上跑通所有冒烟测试。
  3. 切换:将负载均衡器(如Nginx)的流量一键切到绿色环境。
  4. 回滚:万一出问题,只需将流量瞬间切回蓝色环境,秒级恢复。

发布后

Nginx

绿色环境 v1.1

蓝色环境 v1.0 待命回滚

发布前

Nginx

蓝色环境 v1.0

绿色环境 闲置

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)。
  • 解决方案(必须双管齐下)
    1. 操作系统sudo timedatectl set-timezone Asia/Shanghai
    2. JVM:在启动参数中强制指定,防止OS时区被其他程序篡改。
      -Duser.timezone=Asia/Shanghai
    3. 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。这很可能是误报。解决办法

  1. 检查/health的详细输出(需开启show-details),看是哪个组件挂了。
  2. 如果是非关键依赖,可以禁用它的健康检查:management.health.redis.enabled=false
  3. 在生产环境,请使用独立的/actuator/health/readiness端点来做服务摘除决策。

❓ Q3:蓝绿部署时,数据库变更怎么办?

这是蓝绿部署最棘手的点。总原则:数据库变更必须向前兼容

  • 可安全操作:新增表、新增列(带默认值)、增加索引。
  • 危险操作(需要分阶段进行):重命名列、修改列类型、删除列。
  • 最佳实践:采用**“扩展-收缩”模式**。先发布一个新版本,它同时兼容新旧两种数据结构(扩展)。等所有实例都升级到新版本后,再执行一个清理任务,删除旧的数据结构(收缩)。

九、核心心法与检查清单

心法:部署即“换路”

把你的部署过程理解为**“将车流(用户流量)从一条老路(旧版本)平稳引导到一条新路(新版本)”**。所有的配置、脚本、监控都是服务于这个目标的。

Spring Boot 生产部署检查清单 ✅

检查项状态
配置分离:生产密码、密钥使用${ENV},并通过EnvironmentFile加载
JAR包不可变:构建过程只产生JAR包,不修改其内容
时区统一:OS、JVM、JDBC连接串全链路时区一致 (Asia/Shanghai)
文件权限:上传目录和应用日志目录属主与运行用户一致
字符编码-Dfile.encoding=UTF-8已设置
优雅停机server.shutdown=gracefulspring.lifecycle.timeout-per-shutdown-phase=30s已配置
健康探针management.endpoint.health.probes.enabled=true已开启
蓝绿部署:部署脚本支持流量切换和秒级回滚
端口隔离:Actuator使用独立端口(9090),与业务端口(8080)分离

如果本文帮你避开了一个部署深坑,请点赞 👍 收藏 ⭐,让这份检查清单帮助更多人!
下一篇我们将深入Linux 防火墙(iptables/firewalld)与云安全组策略,学习如何为你的应用穿上“铠甲”。敬请期待!