Docker 升级 Web 应用为什么会短暂停机?商业项目是如何做到无感发布的?
一、问题背景
很多人在使用 Docker 部署 Web 应用时,都会遇到一个很常见的问题:
每次升级 Docker 容器,服务都会短暂不可用。
比如我们有一个 Spring Boot、Node.js、Go 或其他 Web 应用,部署结构可能是这样的:
用户请求
↓
Nginx
↓
Web 容器
升级时通常会执行:
docker compose down
docker compose pull
docker compose up -d
或者:
docker stop app
docker rm app
docker run -d ...
这种方式的问题很明显:旧容器被停掉之后,新容器还没有完全启动完成,中间会出现几秒甚至几十秒的空档期。
这段时间内,用户访问服务可能会看到:
对于个人项目来说,这可能只是几秒钟的问题;但对于商业项目来说,短暂停机也可能影响用户体验,甚至造成订单失败、支付异常、数据提交失败等问题。
所以商业项目通常不会直接粗暴地停掉旧容器,而是使用更加平滑的发布方式。
二、为什么 Docker 升级会导致服务中断?
本质原因其实很简单:
线上只有一个可用实例,升级时这个实例被停掉了。
假设当前只有一个 Web 容器:
用户请求
↓
Nginx
↓
app:v1
升级时流程一般是:
问题就在于,从第 1 步到第 5 步之间,没有任何实例可以处理请求。
尤其是 Java 应用,启动过程可能还要做很多事情:
所以,即使 Docker 容器状态已经是 running,也不代表 Web 服务已经真正可用。
三、商业项目的核心思路:多实例 + 负载均衡
商业项目解决这个问题的核心思路是:
不要让用户流量只依赖一个容器,而是部署多个实例,然后通过负载均衡分发请求。
结构通常是这样:
用户请求
↓
负载均衡 / Nginx / API Gateway
↓
┌──────────────┬──────────────┬──────────────┐
│ Web 实例 1 │ Web 实例 2 │ Web 实例 3 │
└──────────────┴──────────────┴──────────────┘
这样即使其中一个实例在升级,其他实例仍然可以继续提供服务。
核心不是 Docker 本身能不能做到无停机,而是你的部署架构是否支持多个实例同时运行。
四、滚动发布:最常见的升级方式
商业项目中最常见的发布方式是 滚动发布,英文叫 Rolling Update。
它的核心思想是:
不一次性停掉所有旧实例,而是一个一个替换。
假设现在有 4 个旧版本实例:
v1 v1 v1 v1
升级时,不是直接全部停掉,而是逐个替换:
v2 v1 v1 v1
v2 v2 v1 v1
v2 v2 v2 v1
v2 v2 v2 v2
整个过程中始终有实例在处理用户请求。
滚动发布流程
滚动发布通常需要满足几个条件:
在 Kubernetes 中,Deployment 默认支持滚动更新。例如:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
其中:
也就是说,Kubernetes 可以先启动一个新版本 Pod,等它健康检查通过之后,再删除一个旧版本 Pod。
这样就避免了“先停服务,再启动服务”的问题。
五、蓝绿发布:准备两套环境,一键切换
除了滚动发布,商业项目中也经常使用 蓝绿发布。
蓝绿发布的核心思想是:
同时准备两套环境,一套对外提供服务,一套部署新版本。
比如:
蓝色环境:当前线上版本 v1
绿色环境:新版本 v2
发布前:
用户流量 → 蓝色环境 v1
然后先把新版本部署到绿色环境:
蓝色环境:v1,正在服务用户
绿色环境:v2,部署完成,暂不接收用户流量
测试通过后,直接切换流量:
用户流量 → 绿色环境 v2
如果新版本有问题,也可以快速切回:
用户流量 → 蓝色环境 v1
蓝绿发布优缺点
蓝绿发布比较适合对稳定性要求较高的系统。它的优势不是最省资源,而是切换和回滚都非常干脆。
六、灰度发布:先让一小部分用户使用新版本
灰度发布也叫 金丝雀发布,英文是 Canary Release。
它的思路是:
不要一次性让所有用户访问新版本,而是先放一小部分流量过去。
例如:
灰度发布的好处是风险可控。
如果新版本有问题,只会影响一小部分用户,而不是全量用户。大型商业项目通常会配合监控系统、日志系统和告警系统一起使用灰度发布。
灰度发布关注的不只是“服务有没有启动成功”,还会关注技术指标和业务指标。
如果这些指标正常,再逐步扩大新版本流量。
七、几种发布方式对比
常见发布方式可以简单对比如下:
可以简单理解:
小项目可以先做蓝绿发布;
一般商业项目常用滚动发布;
大型核心系统更适合灰度发布;
对稳定性要求极高的系统经常会组合使用蓝绿和灰度。
八、健康检查:容器启动不等于服务可用
无停机发布中,健康检查非常重要。
很多人容易误以为:
Docker 容器启动了,应用就能用了。
其实不一定。
容器状态是 running,只能说明进程启动了,但应用可能还没有真正准备好。
比如一个 Spring Boot 应用,可能还在初始化:
所以商业项目通常会提供健康检查接口,例如:
/health
/actuator/health
/readiness
健康检查一般分为两类:
在 Kubernetes 中,通常对应:
livenessProbe
readinessProbe
其中 readinessProbe 特别关键。
只有就绪检查通过之后,Kubernetes Service 才会把流量转发给这个 Pod。
这就避免了新版本还没准备好,就提前接收用户流量的问题。
九、优雅停机:不要直接杀掉正在处理请求的容器
除了启动新实例要谨慎,关闭旧实例也不能太粗暴。
如果旧实例正在处理请求,比如:
用户正在提交订单
用户正在支付
用户正在上传文件
用户正在保存表单
这时候直接杀掉容器,可能导致请求中断。
所以商业项目会使用 优雅停机。
优雅停机的大致流程是:
对于 Spring Boot 项目,可以开启优雅停机:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
这样应用收到关闭信号后,不会立刻退出,而是会等待一段时间,让正在处理的请求尽量执行完成。
当然,优雅停机不是万能的。
如果接口本身耗时很长,比如文件处理、视频转码、复杂报表生成,就需要结合异步任务、消息队列、任务补偿等机制来处理。
十、数据库升级比应用升级更麻烦
Web 应用本身做滚动发布并不难,真正容易出问题的是数据库变更。
比如新版本代码需要新增字段:
ALTER TABLE user ADD COLUMN avatar_url varchar(255);
如果数据库字段还没加,新代码已经上线了,就可能报错。
反过来,如果数据库字段已经删了,但旧代码还在运行,也可能报错。
所以商业项目中有一个很重要的原则:
数据库变更要兼容旧代码,也要兼容新代码。
也就是说,数据库升级不能太激进。
错误做法
如果要把 name 字段改成 username,不要直接这样做:
ALTER TABLE user RENAME COLUMN name TO username;
因为旧代码可能还在读写 name 字段。一旦字段被改名,旧代码就会报错。
更稳妥的做法
对应 SQL 可能是:
ALTER TABLE user ADD COLUMN username varchar(255);
UPDATE user SET username = name WHERE username IS NULL;
ALTER TABLE user DROP COLUMN name;
这个过程虽然麻烦,但它能保证新旧版本代码可以共存。
这对于滚动发布、蓝绿发布、灰度发布都非常重要。
十一、小型 Docker 项目如何改造?
如果是个人项目或小型商业项目,不一定一开始就上 Kubernetes。
可以先从简单方案开始优化。
1. 不要使用 docker compose down
很多人升级时习惯执行:
docker compose down
docker compose pull
docker compose up -d
这个命令的问题是,down 会把整个服务都停掉。
更好的做法是:
docker compose pull
docker compose up -d
这样 Docker Compose 会尽量只重建发生变化的服务,而不是先把所有服务停掉。
不过要注意,如果 Web 应用只有一个实例,重建时仍然可能出现短暂不可用。
2. 前面加 Nginx
先把结构改成:
用户 → Nginx → Web 容器
有了 Nginx 之后,后面就可以逐步扩展成多个后端实例。
3. 同一个应用跑两个实例
比如:
用户
↓
Nginx
↓
┌──────────────┬──────────────┐
│ app-1 │ app-2 │
└──────────────┴──────────────┘
升级时先升级 app-1,app-2 继续提供服务。
app-1 升级完成并通过健康检查后,再升级 app-2。
这样就比单实例部署稳定很多。
4. 用蓝绿方式切换
也可以使用蓝绿发布的简化版:
当前:
Nginx → app-blue
升级:
启动 app-green
测试 app-green
Nginx 切到 app-green
保留 app-blue 作为回滚
如果 app-green 有问题,直接把 Nginx 切回 app-blue。
这种方式非常适合单机 Docker 项目。
十二、小型项目改造路线
对于小型项目,可以按下面的路线逐步演进:
如果只是个人博客、后台管理系统、小型 API 服务,通常做到第四或第五阶段就已经够用了。
如果服务数量越来越多、发布越来越频繁,再考虑 Kubernetes 会更合适。
十三、商业项目完整发布链路
商业项目的发布通常不是简单执行几条 Docker 命令,而是一整套流程。
典型链路如下:
开发提交代码
↓
CI 构建
↓
自动化测试
↓
构建 Docker 镜像
↓
推送镜像仓库
↓
部署到测试环境
↓
测试人员验证
↓
部署到预发环境
↓
灰度发布
↓
观察监控和日志
↓
全量发布
↓
保留旧版本用于回滚
可以整理成表格:
商业项目的发布重点不是“新版本能不能启动”,而是整个发布过程是否:
十四、一个简单的无停机发布架构
对于小型项目,可以先使用下面这种架构:
用户请求
↓
Nginx
↓
┌──────────────┬──────────────┐
│ app-blue │ app-green │
└──────────────┴──────────────┘
↓
数据库 / Redis / MQ
发布流程可以这样设计:
这种方式不需要 Kubernetes,也可以实现比较平滑的发布。
Nginx reload 通常是平滑重载,不会直接中断已有连接,所以比较适合作为小型项目的第一步改造方案。
十五、什么时候需要 Kubernetes?
如果项目规模比较小,服务数量也不多,Docker Compose + Nginx 已经可以满足大部分需求。
但是当系统变复杂之后,手动维护会越来越麻烦。
比如:
Kubernetes 提供了很多商业项目常用能力:
对于中大型项目来说,Kubernetes 可以把很多发布和运维动作标准化。
但是 Kubernetes 本身也有学习成本和维护成本,所以不是所有项目都必须一开始就上 Kubernetes。
十六、总结
Docker 升级 Web 应用时会短暂停机,根本原因通常是:
线上只有一个实例,升级时这个实例被停掉了。
商业项目解决这个问题的核心方案是:
对于小型项目,可以先从这些方向优化:
对于中大型项目,可以使用:
Kubernetes Deployment + Service + Ingress + RollingUpdate
简单来说,商业项目发布的核心不是“把新版本启动起来”,而是:
在用户无感知的情况下,把流量安全地从旧版本迁移到新版本,并且出现问题时可以快速回滚。
这才是无停机发布的关键。
Docker 升级 Web 应用为什么会短暂停机?商业项目是如何做到无感发布的?
https://lautung.com/archives/DI18wK6y