最后更新:2026-11-01
适用环境:Spring Boot 3.5、JDK 17 或 21、内嵌 Tomcat;Linux systemd 管理进程,开源 Nginx 1.24 及以上反向代理
平时接口正常,一更新就短暂出现 502,往往要先检查发布顺序:旧进程已经停了,新进程还没准备好;或者代理切走了,旧请求却在处理到一半时被终止。单纯延长代理超时,补不上这段可用实例的空档。
这篇用同一台机器上的两个端口说明一次受控切换:先让新实例准备好,再把新流量切过去,最后等待旧请求结束。它需要机器有同时运行两份应用的余量;共享依赖、数据库变更和长连接也要符合切换条件。做不到这些条件时,应安排明确的维护窗口,不能把一次单实例重启描述成无中断发布。
文中的端口、服务名、配置文件名都是示例,按实际部署调整。容器、Kubernetes 或云负载均衡应由对应发布机制完成就绪检查与摘流,不能直接照搬本机 systemd 命令。
先把错误放到发布的时间线上
对齐失败请求、Nginx 错误日志与应用启动/关闭日志,记录“旧实例开始退出、新实例开始监听、新实例就绪、代理切流”的时间。
| 发布时的现象 | 优先确认 |
|---|---|
旧进程停后出现 connection refused | upstream 是否还指向已停止的端口,替代实例是否可用 |
| 新进程启动了,但第一批请求失败 | 初始化、缓存预热和必要依赖是否完成,切流是否只看了进程存活 |
upstream prematurely closed connection 恰好发生在退出时 | 旧实例是否被强制结束,在途请求是否来得及完成 |
| 新实例收到请求后长时间不响应 | CPU、线程、连接池与数据库是否承受得住,是否实际为 504 |
这些信息用于缩小方向,不能把某一条日志直接当作根因。状态码和代理链的判断,可先看 Nginx 与 Spring Boot 的 502/504 排查。
优雅停机要和替代实例一起安排
Spring Boot 3.5 的内嵌 Web 服务默认启用优雅停机。正常关闭应用上下文时,Tomcat 会停止在网络层接受新请求,并给已有请求一定的完成时间。下面显式写出相关配置,便于发布人员核对:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
probes:
enabled: true
这里的 30s 是每个关闭阶段的超时配置,不能直接当成整个进程一定在 30 秒内退出。具体行为还受 Web 服务器、持久连接及其他生命周期组件影响,见 Spring Boot 3.5 优雅停机文档。
正常停止需要把关闭信号传到 Java 进程。检查 systemd 单元中的 ExecStart、ExecStop、KillSignal 和 TimeoutStopSec:启动脚本应正确转发信号,停止脚本不应直接强杀。管理器的停止预算要覆盖实际关闭流程;到期强制结束仍可能截断请求,见 systemd 服务停止超时说明。
优雅停机负责给旧请求收尾的机会。要让新请求持续有地方去,还需要另一个已就绪的实例。若 Nginx 只有一个上游,先停止它再启动替代进程,仍然会有服务空档。
准备两个独立实例,先验证新实例
下面假设已经配置好两份独立的 systemd 服务,Nginx 与应用在同一台机器。应用端口只监听本机回环地址,现有 TLS、鉴权和访问控制继续沿用。
| 实例 | systemd 服务示例 | 监听地址 | 当前用途 |
|---|---|---|---|
| blue | java-api-blue.service | 127.0.0.1:8081 | 旧版本,正在承接流量 |
| green | java-api-green.service | 127.0.0.1:8082 | 新版本,验证通过后接流量 |
两份服务使用独立的发布目录和端口,保留旧版本产物。启动前核对 green 的实际 JAR 版本、配置、工作目录和端口,不能只根据服务名判断运行版本。不要覆盖正在运行版本的 JAR 或它可能继续读取的资源文件。发布前还要确认双实例同时运行时的内存、CPU、线程和数据库连接总量;连接预算可对照 连接池耗尽排查。
项目需已引入 Actuator,上面的 probes 配置让非 Kubernetes 环境也能使用就绪端点。示例假设没有修改应用 context path 或 Actuator 基础路径,且现有访问策略允许本机发布探针读取 health;若需要认证,沿用受控探针身份,不放宽公网权限。
在部署机器用 Bash 执行,要求已安装 curl 和 jq:
set -o pipefail
sudo systemctl start java-api-green.service
curl -fsS --max-time 3 \
'http://127.0.0.1:8082/actuator/health/readiness' \
| jq -e '.status == "UP"'
启动命令返回成功,只说明服务管理器完成了相应启动操作。就绪检查失败、超时或返回非 UP 时,先保留 blue 的流量并处理 green,不能继续切换。
就绪状态也有边界:默认 readiness 主要反映应用的可接流量状态,不会自动把数据库、缓存等全部纳入检查。还要按项目约定验证必要的只读业务路径、配置与依赖;异步预热若尚未纳入就绪门槛,就必须单独确认完成。不要用首页返回 200 替代业务验证。Actuator 就绪检查文档说明了探针和外部依赖的取舍。
这里用的是主应用端口。若管理端点另开端口,管理端口正常不能充分证明业务端口能接请求;应从实际业务监听端口验证。Nginx 对公网的路由也应保留管理路径的访问限制。
新实例通过后,再切换 Nginx 上游
先核对有效配置中哪个 upstream 真正服务目标域名。下面假设现有 location 使用 proxy_pass http://java_api;,该 upstream 原来只指向 blue 的 8081 端口。
把当前配置备份到不会被 Nginx include 的目录,再将对应 upstream 的地址改为已验证的 green。不要额外新建第二个同名 upstream,也不要覆盖原有域名、TLS 和路径转发规则。
以下内容属于 Nginx 的 http 上下文,例如已经被包含的 /etc/nginx/conf.d/java-api-upstream.conf:
upstream java_api {
server 127.0.0.1:8082;
}
确认修改指向正确后再执行:
sudo nginx -t && sudo systemctl reload nginx
nginx -t 检查配置语法及相关文件,不能证明新应用业务健康。reload 由新 worker 使用新配置,旧 worker 继续处理已有客户端后退出;提交成功后仍要查看错误日志和实际 upstream 地址,确认新配置确已生效。Nginx 重载说明描述了这个过程。
开源 Nginx 不会因为应用提供了 readiness URL,就自动按这个 URL 控制上述静态切换。这里的就绪检查是发布流程的前置门槛;采用其他健康检查或服务发现方案时,应核对它真正参与了选路。
旧请求排空后,才停止旧实例
reload 返回后,不要立刻停止 blue。 旧 Nginx worker 可能仍用旧配置处理已有请求,这些请求仍会到 8081。此时停掉 blue,会把刚刚避免的中断重新引入。
切流前记录 Nginx worker PID;切流后结合进程、代理与应用侧证据确认排空:
- 新请求确实进入 green;现有访问日志可核对
$upstream_addr、$upstream_status与耗时,注意重试时可能有多组值。 - 本次 reload 前的旧 worker 已结束,旧实例没有剩余在途 HTTP 请求;若有其他代理或直连调用方,也完成了摘流。
- WebSocket、SSE、长轮询和长任务已有单独的等待或重连安排,不能只用普通短请求的观察时间判断。
Linux 上可用下面的命令辅助查看 Nginx 进程,并核对旧服务的停止预算。PID 与进程标题只是证据的一部分,还需结合应用指标或请求日志:
ps -o pid,ppid,args -C nginx
sudo systemctl show java-api-blue.service \
-p KillSignal -p TimeoutStopUSec
如果设置了 worker_shutdown_timeout,到期后 Nginx 会尝试关闭仍打开的连接。这是等待的上限,不能作为“所有请求已经成功结束”的证明,见 该指令文档。长连接需要明确可接受的重连与中断策略。
确认排空后,再正常停止旧服务并检查关闭日志:
sudo systemctl stop java-api-blue.service
sudo journalctl -u java-api-blue.service --since '10 minutes ago' --no-pager
等待多久应由真实请求时长、连接类型和关闭阶段决定。一个固定的 sleep 5 或 sleep 30 不能证明排空成功;关闭日志正常也不能反推刚才没有用户请求失败。
回滚也要先确认旧版本能接流量
green 切流后异常时,若 blue 仍在运行,先重新确认它的就绪状态;若已经停止,先用保留的旧版本服务启动并验证。然后把 upstream 恢复为 8081,执行配置检查与 reload,确认流量回到 blue,再按同样方式排空 green。
回滚条件要在发布前确定。新版本若已做了旧代码不能读取的数据库结构变更、消息格式变化或不可逆写入,仅切回旧 JAR 并不构成完整回滚。优先安排新旧版本能共存的兼容变更,明确数据恢复边界与负责人。
双实例共存还意味着定时任务、消息消费和本地会话行为可能变化。需要单次执行的任务要有既定的互斥或幂等机制;会话、上传文件和缓存不能依赖某个即将停止的进程。HTTP 切流成功不能代替这些检查。
不要为了遮住发布错误随手开启非幂等请求重试。Nginx 的 proxy_next_upstream non_idempotent 会允许原本受限制的此类重试,已经送到上游的 POST 等请求可能重复执行;应先明确业务幂等性及重复提交处理。代理重试文档说明了相关限制。
用一轮实际发布验收,而不是只看进程
在测试环境先按同样顺序走一遍发布与回滚,覆盖普通请求、较慢请求和实际使用的长连接。记录每一步的版本、端口、时间与判断依据。正式发布时观察对应时间段的状态码、延迟、目标实例与业务结果,也检查连接池和后台任务是否异常。
如果仍有短暂 502,沿时间线查:错误发生时还有哪个路由指向旧端口,新实例是否真的就绪,旧连接是否被提前关闭,哪个管理器发出了强制终止信号。若是请求已经进入应用后持续变慢,再回到 Java 后端排障专题 按 CPU、OOM、数据库和外部调用继续定位。
参考资料
- Spring Boot 3.5:优雅停机
- Spring Boot 3.5:Actuator 与就绪探针
- Nginx:配置重载过程
- Nginx:upstream 配置
- Nginx:worker 关闭等待上限
- Nginx:代理重试条件
- systemd:服务停止行为与超时
更新记录
- 2026-11-01:整理双端口发布中的就绪检查、切流、排空与回滚顺序;配置和行为按官方文档核对,未在生产环境执行上述部署或验证零中断效果。
Spring Boot 部署更新如何避免短暂 502
https://java.li/archives/spring-boot-deployment-avoid-502
评论