一、问题背景

很多人在使用 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 ...

这种方式的问题很明显:旧容器被停掉之后,新容器还没有完全启动完成,中间会出现几秒甚至几十秒的空档期。

这段时间内,用户访问服务可能会看到:

现象

常见原因

502 Bad Gateway

Nginx 找不到可用后端服务

503 Service Unavailable

服务暂时不可用

Connection refused

应用端口还没有监听

请求超时

应用启动慢或初始化未完成

页面白屏

前端资源或接口暂时不可访问

对于个人项目来说,这可能只是几秒钟的问题;但对于商业项目来说,短暂停机也可能影响用户体验,甚至造成订单失败、支付异常、数据提交失败等问题。

所以商业项目通常不会直接粗暴地停掉旧容器,而是使用更加平滑的发布方式。


二、为什么 Docker 升级会导致服务中断?

本质原因其实很简单:

线上只有一个可用实例,升级时这个实例被停掉了。

假设当前只有一个 Web 容器:

用户请求
   ↓
Nginx
   ↓
app:v1

升级时流程一般是:

步骤

操作

是否能提供服务

1

停止旧容器 app:v1

不能

2

删除旧容器

不能

3

拉取新镜像

不能

4

启动新容器 app:v2

不一定

5

应用初始化完成

可以

6

Nginx 转发请求

可以

问题就在于,从第 1 步到第 5 步之间,没有任何实例可以处理请求。

尤其是 Java 应用,启动过程可能还要做很多事情:

初始化内容

说明

初始化 Spring 容器

加载 Bean、配置、依赖关系

连接数据库

建立数据库连接池

连接 Redis

初始化缓存连接

加载配置

读取配置中心或本地配置

注册服务

向注册中心注册实例

启动 HTTP 服务

监听 Web 端口

JVM 预热

刚启动时性能可能还没稳定

所以,即使 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

整个过程中始终有实例在处理用户请求。

滚动发布流程

阶段

操作

目的

1

启动一个新版本实例

先增加新服务能力

2

对新实例做健康检查

确认新实例可用

3

将部分流量转发到新实例

开始接收请求

4

下线一个旧版本实例

逐步替换旧版本

5

重复以上步骤

直到全部升级完成

6

观察监控和日志

确认发布成功

滚动发布通常需要满足几个条件:

条件

说明

多实例

至少要有两个以上应用实例

负载均衡

需要把请求分发到不同实例

健康检查

新实例准备好后才能接流量

优雅停机

旧实例不能被直接杀掉

快速回滚

新版本异常时可以退回旧版本

在 Kubernetes 中,Deployment 默认支持滚动更新。例如:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

其中:

配置

含义

maxUnavailable: 0

升级过程中不能减少可用实例数量

maxSurge: 1

升级过程中允许临时多启动 1 个新实例

也就是说,Kubernetes 可以先启动一个新版本 Pod,等它健康检查通过之后,再删除一个旧版本 Pod。

这样就避免了“先停服务,再启动服务”的问题。


五、蓝绿发布:准备两套环境,一键切换

除了滚动发布,商业项目中也经常使用 蓝绿发布

蓝绿发布的核心思想是:

同时准备两套环境,一套对外提供服务,一套部署新版本。

比如:

蓝色环境:当前线上版本 v1
绿色环境:新版本 v2

发布前:

用户流量 → 蓝色环境 v1

然后先把新版本部署到绿色环境:

蓝色环境:v1,正在服务用户
绿色环境:v2,部署完成,暂不接收用户流量

测试通过后,直接切换流量:

用户流量 → 绿色环境 v2

如果新版本有问题,也可以快速切回:

用户流量 → 蓝色环境 v1

蓝绿发布优缺点

方面

说明

优点

回滚快,新旧环境隔离清晰

缺点

资源消耗高,需要两套环境

适合场景

金融、电商、支付、SaaS 等稳定性要求高的系统

不适合场景

资源紧张、环境复杂、成本敏感的小项目

蓝绿发布比较适合对稳定性要求较高的系统。它的优势不是最省资源,而是切换和回滚都非常干脆。


六、灰度发布:先让一小部分用户使用新版本

灰度发布也叫 金丝雀发布,英文是 Canary Release

它的思路是:

不要一次性让所有用户访问新版本,而是先放一小部分流量过去。

例如:

阶段

旧版本 v1 流量

新版本 v2 流量

初始阶段

100%

0%

第一阶段

95%

5%

第二阶段

80%

20%

第三阶段

50%

50%

最终阶段

0%

100%

灰度发布的好处是风险可控。

如果新版本有问题,只会影响一小部分用户,而不是全量用户。大型商业项目通常会配合监控系统、日志系统和告警系统一起使用灰度发布。

灰度发布关注的不只是“服务有没有启动成功”,还会关注技术指标和业务指标。

指标类型

具体指标

技术指标

错误率、响应时间、CPU、内存、数据库连接数

日志指标

异常日志数量、错误堆栈、超时日志

业务指标

订单量、支付成功率、注册成功率、转化率

用户反馈

投诉量、客服反馈、页面异常反馈

如果这些指标正常,再逐步扩大新版本流量。


七、几种发布方式对比

常见发布方式可以简单对比如下:

发布方式

核心思路

优点

缺点

适合场景

停机发布

停掉旧版本,再启动新版本

简单

会中断服务

测试环境、个人项目

滚动发布

一批一批替换实例

资源利用率高,用户基本无感

新旧版本会短暂共存

大多数商业项目

蓝绿发布

准备两套环境,切换流量

回滚快,环境隔离清楚

资源成本高

稳定性要求高的系统

灰度发布

小比例用户先用新版本

风险最低,可观测性强

实现复杂度高

大型项目、核心业务

金丝雀发布

灰度发布的一种形式

可以小范围试错

需要流量控制能力

高并发、复杂业务系统

可以简单理解:

  • 小项目可以先做蓝绿发布;

  • 一般商业项目常用滚动发布;

  • 大型核心系统更适合灰度发布;

  • 对稳定性要求极高的系统经常会组合使用蓝绿和灰度。


八、健康检查:容器启动不等于服务可用

无停机发布中,健康检查非常重要。

很多人容易误以为:

Docker 容器启动了,应用就能用了。

其实不一定。

容器状态是 running,只能说明进程启动了,但应用可能还没有真正准备好。

比如一个 Spring Boot 应用,可能还在初始化:

容器状态

应用状态

是否应该接收流量

created

容器刚创建

running

进程已启动

不一定

running + health check failed

应用未准备好

running + health check success

应用已准备好

unhealthy

应用异常

所以商业项目通常会提供健康检查接口,例如:

/health
/actuator/health
/readiness

健康检查一般分为两类:

检查类型

作用

举例

存活检查

判断应用进程是否还活着

livenessProbe

就绪检查

判断应用是否可以接收请求

readinessProbe

在 Kubernetes 中,通常对应:

livenessProbe
readinessProbe

其中 readinessProbe 特别关键。

只有就绪检查通过之后,Kubernetes Service 才会把流量转发给这个 Pod。

这就避免了新版本还没准备好,就提前接收用户流量的问题。


九、优雅停机:不要直接杀掉正在处理请求的容器

除了启动新实例要谨慎,关闭旧实例也不能太粗暴。

如果旧实例正在处理请求,比如:

用户正在提交订单
用户正在支付
用户正在上传文件
用户正在保存表单

这时候直接杀掉容器,可能导致请求中断。

所以商业项目会使用 优雅停机

优雅停机的大致流程是:

步骤

操作

目的

1

从负载均衡摘除旧实例

不再接收新请求

2

等待已有请求处理完成

避免请求中断

3

关闭业务线程

停止业务处理

4

释放连接资源

关闭数据库、Redis、MQ 连接

5

退出进程

安全关闭应用

6

停止容器

完成旧实例下线

对于 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 字段。一旦字段被改名,旧代码就会报错。

更稳妥的做法

阶段

操作

说明

1

新增 username 字段

不影响旧代码

2

代码同时写入 nameusername

保持新旧字段都有数据

3

迁移历史数据

把旧字段数据同步到新字段

4

新代码改为读取 username

逐步切换读取逻辑

5

观察一段时间

确认没有旧代码依赖 name

6

删除 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 项目。


十二、小型项目改造路线

对于小型项目,可以按下面的路线逐步演进:

阶段

架构

能力

复杂度

第一阶段

单 Docker 容器

能跑起来

第二阶段

Docker Compose

多服务编排

第三阶段

Nginx + 单应用容器

统一入口

中低

第四阶段

Nginx + 双应用容器

可以轮流升级

第五阶段

Nginx + 蓝绿发布

支持快速回滚

第六阶段

Kubernetes

自动滚动发布、扩缩容、健康检查

如果只是个人博客、后台管理系统、小型 API 服务,通常做到第四或第五阶段就已经够用了。

如果服务数量越来越多、发布越来越频繁,再考虑 Kubernetes 会更合适。


十三、商业项目完整发布链路

商业项目的发布通常不是简单执行几条 Docker 命令,而是一整套流程。

典型链路如下:

开发提交代码
   ↓
CI 构建
   ↓
自动化测试
   ↓
构建 Docker 镜像
   ↓
推送镜像仓库
   ↓
部署到测试环境
   ↓
测试人员验证
   ↓
部署到预发环境
   ↓
灰度发布
   ↓
观察监控和日志
   ↓
全量发布
   ↓
保留旧版本用于回滚

可以整理成表格:

阶段

主要工作

目的

代码提交

提交 Git 仓库

触发发布流程

CI 构建

编译、测试、打包

确认代码可构建

镜像构建

构建 Docker 镜像

形成标准交付物

镜像推送

推送到镜像仓库

方便环境部署

测试环境

自动化测试、接口测试

提前发现问题

预发环境

模拟生产环境验证

降低上线风险

灰度发布

小流量验证新版本

控制影响范围

全量发布

所有流量切到新版本

完成升级

监控观察

看日志、指标、告警

判断发布是否成功

回滚准备

保留旧版本

出问题时快速恢复

商业项目的发布重点不是“新版本能不能启动”,而是整个发布过程是否:

能力

说明

可控

可以控制发布节奏和影响范围

可观察

可以通过日志、监控、告警判断状态

可回滚

出问题时能快速退回旧版本

可追踪

能知道谁在什么时候发布了什么版本

可重复

发布流程标准化,不依赖人工经验


十四、一个简单的无停机发布架构

对于小型项目,可以先使用下面这种架构:

用户请求
   ↓
Nginx
   ↓
┌──────────────┬──────────────┐
│ app-blue     │ app-green    │
└──────────────┴──────────────┘
   ↓
数据库 / Redis / MQ

发布流程可以这样设计:

步骤

操作

说明

1

当前 Nginx 指向 app-blue

blue 是当前线上版本

2

部署新版本到 app-green

green 暂时不接收线上流量

3

调用 app-green 健康检查接口

确认新版本可用

4

修改 Nginx upstream

准备切换流量

5

reload Nginx

平滑重载配置

6

用户流量切到 app-green

新版本正式上线

7

保留 app-blue

方便快速回滚

8

观察一段时间

确认新版本稳定

9

清理旧版本

释放资源

这种方式不需要 Kubernetes,也可以实现比较平滑的发布。

Nginx reload 通常是平滑重载,不会直接中断已有连接,所以比较适合作为小型项目的第一步改造方案。


十五、什么时候需要 Kubernetes?

如果项目规模比较小,服务数量也不多,Docker Compose + Nginx 已经可以满足大部分需求。

但是当系统变复杂之后,手动维护会越来越麻烦。

比如:

问题

Kubernetes 能力

服务数量越来越多

统一管理 Deployment、Service

实例数量越来越多

自动维护副本数

发布越来越频繁

支持滚动更新和回滚

需要自动扩缩容

HPA 自动扩缩容

需要服务发现

Service 提供服务发现

配置越来越复杂

ConfigMap、Secret

需要健康检查

livenessProbe、readinessProbe

需要统一入口

Ingress

需要环境隔离

Namespace

Kubernetes 提供了很多商业项目常用能力:

Kubernetes 资源

作用

Deployment

管理应用副本和滚动更新

Service

提供服务发现和负载均衡

Ingress

管理外部访问入口

ConfigMap

管理普通配置

Secret

管理敏感信息

HPA

自动扩缩容

Probe

健康检查

Namespace

环境隔离

对于中大型项目来说,Kubernetes 可以把很多发布和运维动作标准化。

但是 Kubernetes 本身也有学习成本和维护成本,所以不是所有项目都必须一开始就上 Kubernetes。


十六、总结

Docker 升级 Web 应用时会短暂停机,根本原因通常是:

线上只有一个实例,升级时这个实例被停掉了。

商业项目解决这个问题的核心方案是:

方案

作用

多实例部署

避免服务只依赖一个容器

负载均衡

把请求分发到不同实例

滚动发布

一个一个替换实例

蓝绿发布

两套环境快速切换

灰度发布

小范围验证新版本

健康检查

确认新实例真正可用

优雅停机

避免中断正在处理的请求

快速回滚

新版本异常时恢复旧版本

数据库兼容升级

避免新旧代码同时运行时报错

对于小型项目,可以先从这些方向优化:

优化方向

说明

不要 docker compose down

避免主动停掉所有服务

使用 Nginx 作为入口

方便后续做流量切换

同一个应用部署两个实例

支持轮流升级

增加健康检查接口

确认服务可用后再切流量

使用蓝绿方式切换

简单实现无感发布

保留旧版本

方便快速回滚

对于中大型项目,可以使用:

Kubernetes Deployment + Service + Ingress + RollingUpdate

简单来说,商业项目发布的核心不是“把新版本启动起来”,而是:

在用户无感知的情况下,把流量安全地从旧版本迁移到新版本,并且出现问题时可以快速回滚。

这才是无停机发布的关键。