无需Kubernetes,利用Docker Compose实现零停机部署
速览
本文探讨了在不使用Kubernetes的情况下,如何利用Docker Compose实现应用的零停机部署。这种方法简化了部署流程,降低了运维复杂度,特别适合中小型项目或资源受限的环境。通过合理的配置和策略,开发者可以在不影响用户访问的情况下完成应用更新。
AI 深度解读
零停机部署:Docker Compose 与 HAProxy 的实战复盘
在云原生领域,存在一种普遍的错觉:运行严肃的生产级服务必须依赖 Kubernetes。事实并非如此。StatusDude 团队通过实际案例证明,仅使用 Docker Compose 和 HAProxy,即可实现每分钟数千次监控检查、多区域 Worker 运行以及每日多次部署,且做到零请求丢失、零停机,无需在凌晨三点去维护 etcd。
然而,这条路径并非一帆风顺。团队最初尝试了流行的 Traefik,但在短短四小时内便宣告失败,最终转向了 HAProxy。
背景
StatusDude 的服务规模要求极高的可用性:每分钟处理数千次监控检查,运行多区域 Worker,并每天进行多次部署。传统的认知认为,要达到这种级别的稳定性和自动化,必须引入 Kubernetes 这样的复杂编排系统。
为了验证“轻量级方案”的可行性,团队决定在不使用 Kubernetes 的情况下,仅依靠 Docker Compose 配合负载均衡器来实现零停机滚动更新(Rolling Update)。他们首先选择了在 Docker 生态中非常受欢迎的反向代理 Traefik,期望利用其自动发现服务、自带仪表盘和简洁文档的优势来简化配置。
然而,现实很快给了他们一记重击。在尝试使用 Traefik 进行滚动部署时,团队遭遇了严重的路由冲突、状态同步延迟以及致命的请求丢失问题,最终不得不放弃 Traefik,转而寻找更底层的解决方案。
核心内容
1. Traefik 的失败尝试
团队最初尝试通过 Traefik 实现滚动部署,主要遇到了以下三个层面的问题:
服务定义冲突
最初的策略是在过渡期间同时运行 backend(旧版本)和 backend_new(新版本)两个服务。由于两者配置了相同的 Traefik 路由标签(相同的 Host 规则和 Service 定义),Traefik 的 Docker 提供者将每个 Compose 服务视为独立的配置源,直接报错 "Service defined multiple times",导致所有请求返回 404。
Scale-Down 竞态条件
为了解决标签冲突,团队改用 docker compose --scale backend=4 的方式,将副本数扩展到 4(2 旧 + 2 新),然后再缩容回 2。这虽然解决了标签冲突,但引发了新的问题:Traefik 内部的路由表更新速度滞后于 Docker 容器的实际状态。当团队将副本数从 4 缩容到 2 时,Traefik 仍会将流量路由到正在关闭的容器中,导致每隔一个请求就出现 502 错误。这种路由状态与 Docker 现实之间的数秒延迟,足以造成大量流量丢失。
致命的缺陷:无法跨后端重试
这是导致团队彻底放弃 Traefik 的根本原因。在滚动部署中,当旧容器收到 SIGTERM 信号开始优雅关闭时,存在一个时间窗口:此时到达的请求或正在进行的请求会连接到即将死亡的 Backend。
- 现象:连接在传输中途断开,客户端收到空响应、连接重置或部分身体数据。
- Traefik 的行为:Traefik 的重试中间件(Retry Middleware)默认仅在同一个后端上重试。对于正在死亡的容器,重试只会再次失败,请求被直接丢弃,没有任何机制将其重新分发到健康的后端。
尽管团队尝试了被动健康检查、停止前断开网络连接等变通方法,但都无法解决“失败请求无法重定向到健康节点”这一核心缺陷。
2. 零停机部署的本质需求
剥离复杂的技术栈,实现零停机部署实际上只需要三个核心要素:
- 多个后端实例:以便在替换一个实例时,其他实例仍能承载流量。
- 支持跨后端重试的负载均衡器:确保当容器正在关闭时,失败的请求能被重定向到健康的容器,而不是被丢弃。
- 逐个替换实例的部署脚本:实现滚动更新。
3. 基于 Docker Compose 和 HAProxy 的解决方案
团队最终采用 Docker Compose 管理应用实例,HAProxy 作为负载均衡器,实现了稳定可靠的零停机部署。
Step 1: 使用 Docker Compose 管理多副本
利用 Docker Compose 内置的 deploy.replicas 设置,可以轻松启动多个后端容器。这些容器共享同一个 Docker DNS 名称(如 backend)。在 Docker 网络内部解析该名称时,会返回所有容器实例的 IP 列表。
- 无需 Pod 规范、Deployment 对象或 ReplicaSet。
- 一个 Dockerfile,一个镜像,多个容器实例。
Step 2: 配置 HAProxy 实现零停机
HAProxy 的核心优势在于其 option redispatch 功能。以下是关键配置解析:
-
跨后端重试(Redispatch):
retries 3 option redispatch 1 retry-on conn-failure empty-response response-timeout 502 503 504当请求失败(连接拒绝、空响应、502/503/504 错误)时,HAProxy 会进行重试。
option redispatch 1确保每次重试都会尝试不同的后端。如果请求命中了正在关闭的容器,HAProxy 会静默地将请求重定向到另一个健康的副本,客户端完全感知不到错误。 -
三层健康检测机制:
- 每请求重试(毫秒级):针对单个请求失败,立即在不同后端重试。捕获部署期间的瞬时故障。
- 被动观察(Passive Observation):通过
observe layer7监控实际 HTTP 响应。如果后端连续返回 3 次 5xx 错误(error-limit 3),HAProxy 会立即将其从轮询中移除(on-error mark-down),无需等待探测周期。 - 主动健康检查(Active Health Checks):通过
inter 1s fall 1 rise 1每秒探测/health端点。捕获完全崩溃且无流量进入的后端。一次失败即标记为 DOWN,一次成功即恢复轮询。
-
基于 DNS 的服务发现: 使用
server-template backend 1-10 backend:8000 check配置。HAProxy 通过 Docker 内置 DNS 解析器(127.0.0.11:53)解析backend域名,并动态创建服务器条目。hold valid 2s:每 2 秒重新解析 DNS。- 容器死亡时,其 IP 从 DNS 中消失;新容器启动时,其 IP 出现。HAProxy 自动适应变化。
- 无需挂载 Docker Socket,无需解析标签,无需动态生成配置。
关键要点
- Kubernetes 并非必需品:对于许多场景,Docker Compose 配合成熟的负载均衡器足以支撑高可用生产环境,避免了 K8s 的运维复杂性。
- Traefik 在滚动更新中的局限性:Traefik 的 Docker 提供者在处理同名标签服务时会报错;其路由表更新存在延迟;最关键的是,其重试机制无法将失败请求重定向到不同的后端,导致滚动部署期间请求丢失。
- HAProxy 的
redispatch是关键:option redispatch允许 HAProxy 在重试时将请求发送到不同的后端服务器,这是实现零停机部署的核心特性。 - 多层健康检查策略:结合每请求重试、被动流量观察和主动健康检查,可以覆盖瞬时故障、负载下的错误响应以及静默崩溃等多种故障模式。
- 静态配置优于动态生成:通过 Docker DNS 进行服务发现,配合静态 HAProxy 配置文件,避免了挂载 Docker Socket 和解析复杂标签的安全性与复杂性风险。
- 部署逻辑简化:零停机部署的核心在于“多副本 + 智能重试 + 滚动替换”,无需过度工程化。
意义与影响
StatusDude 的实践打破了“只有 Kubernetes 才能生产”的行业迷思,为中小型团队或特定架构提供了更具性价比和可维护
