订单服务做滚动发布,Deployment 显示更新成功,Pod 没有 CrashLoop,发布平台也全绿。半小时后对账却发现:23 个创建订单请求在发布窗口没有形成明确结果,其中 7 个被客户端重试后生成了重复支付意图。
组件都没有明显“坏掉”,事故发生在 Pod 下线、流量摘除、长请求执行和客户端超时之间的竞态里。

Kubernetes 管理生命周期,不理解你的业务提交点
Pod 被删除时会进入终止流程,端点状态变化、容器生命周期钩子和终止信号共同参与优雅关闭。但应用是否还能接受请求、一个请求何时产生不可逆副作用、失败后能否恢复,只有业务代码知道。
如果收到终止信号后立刻退出,正在支付的请求会被切断;如果继续对外保持 ready,上游在收敛期间还会送来新请求;如果只 sleep 20 秒,却没有统计在途任务,团队只是把问题推迟了 20 秒。

正确顺序是:摘流量、停入口、排在途、再退出
应用收到终止信号后,应先进入 draining 状态,让 readiness 返回失败;HTTP 入口拒绝新的高成本请求,消息消费者停止拉取新任务,定时任务不再领取租约;已经开始的请求继续执行,直到完成或进入可恢复状态。
下面是一个简化的 Python 状态控制器,核心在于显式跟踪在途请求,而不是固定等待:
import asyncio
from contextlib import asynccontextmanager
class DrainState:
def __init__(self):
self.draining = False
self.inflight = 0
self.done = asyncio.Event()
self.done.set()
@asynccontextmanager
async def request(self):
if self.draining:
raise RuntimeError("instance_draining")
self.inflight += 1
self.done.clear()
try:
yield
finally:
self.inflight -= 1
if self.inflight == 0:
self.done.set()
async def drain(self, timeout_seconds=35):
self.draining = True
await asyncio.wait_for(self.done.wait(), timeout=timeout_seconds)
真实服务还要防止“检查 draining 后、inflight 加一前”出现竞争,可用同一个异步锁保护状态切换。网关或服务网格的连接排空也要与应用宽限期对齐。
订单类请求必须能回答“到底提交了吗”
最棘手的时序是数据库已提交,响应还没返回,Pod 就被终止。客户端只看到超时,无法知道订单是否存在,随后重试可能生成第二单。
因此创建订单要使用业务幂等键,查询接口能按该键返回既有结果;支付、库存等后续动作也要有独立状态与恢复机制。优雅下线只能减少中断,不能替代副作用幂等。
CREATE UNIQUE INDEX uq_order_request
ON orders(tenant_id, client_request_id);
-- 重试时先按 tenant_id + client_request_id 查询已有终态
配置要为应用的最坏完成时间留空间
如果业务请求 P99 为 12 秒,支付超时上限 20 秒,terminationGracePeriodSeconds 却只有 15 秒,那么“正常最坏路径”在发布时必然被杀死。宽限期应覆盖停止接流量、上游传播和在途请求完成,并留有安全余量。
配置示例:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: order-api
lifecycle:
preStop:
httpGet:
path: /internal/drain
port: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 2
failureThreshold: 1
preStop 接口应幂等,并立即切换 draining;不要把固定 sleep 当作唯一动作。还要确保探针失败后,应用不会继续从其他入口领取新任务。

零损发布应该怎样测试
持续按真实到达率发送创建、查询、取消三类请求,在每次发布的随机时刻删除 Pod。把请求按“终止前开始、终止中到达、终止后重试”分桶,最后对账数据库、支付和客户端结果。
验收不只看 5xx:每个 client_request_id 最多一个订单;每个已扣款请求都有可查询订单;每个客户端超时都有明确恢复路径;终止中的新请求快速失败并带可重试语义;Pod 在宽限期内完成退出。再主动让下游支付延迟到 25 秒,验证最坏路径。
生产监控增加 draining 实例数、终止时在途请求、强制结束次数、终止窗口的未知结果数和幂等命中数。它们比 Deployment 成功状态更接近用户事实。
滚动发布真正的“成功”,不是新 Pod 都 Ready,而是旧 Pod 离开的过程中,每一个业务请求都能说明自己完成、拒绝,还是可恢复。