Kubernetes 零损发布实战:Pod 全绿背后的 23 个丢失请求

举报
霍格沃兹测试学社 发表于 2026/09/10 17:05:23 2026/09/10
【摘要】 订单服务滚动发布看似成功,实则因Pod优雅终止与业务请求时序错配,导致23笔订单状态不明、7笔重复支付。根本在于未协同流量摘除、在途请求排空与幂等设计,零损发布需以业务语义而非K8s状态为验收标准。

订单服务做滚动发布,Deployment 显示更新成功,Pod 没有 CrashLoop,发布平台也全绿。半小时后对账却发现:23 个创建订单请求在发布窗口没有形成明确结果,其中 7 个被客户端重试后生成了重复支付意图。

组件都没有明显“坏掉”,事故发生在 Pod 下线、流量摘除、长请求执行和客户端超时之间的竞态里。

image.png

Kubernetes 管理生命周期,不理解你的业务提交点

Pod 被删除时会进入终止流程,端点状态变化、容器生命周期钩子和终止信号共同参与优雅关闭。但应用是否还能接受请求、一个请求何时产生不可逆副作用、失败后能否恢复,只有业务代码知道。

如果收到终止信号后立刻退出,正在支付的请求会被切断;如果继续对外保持 ready,上游在收敛期间还会送来新请求;如果只 sleep 20 秒,却没有统计在途任务,团队只是把问题推迟了 20 秒。

image.png

正确顺序是:摘流量、停入口、排在途、再退出

应用收到终止信号后,应先进入 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 当作唯一动作。还要确保探针失败后,应用不会继续从其他入口领取新任务。

image.png

零损发布应该怎样测试

持续按真实到达率发送创建、查询、取消三类请求,在每次发布的随机时刻删除 Pod。把请求按“终止前开始、终止中到达、终止后重试”分桶,最后对账数据库、支付和客户端结果。

验收不只看 5xx:每个 client_request_id 最多一个订单;每个已扣款请求都有可查询订单;每个客户端超时都有明确恢复路径;终止中的新请求快速失败并带可重试语义;Pod 在宽限期内完成退出。再主动让下游支付延迟到 25 秒,验证最坏路径。

生产监控增加 draining 实例数、终止时在途请求、强制结束次数、终止窗口的未知结果数和幂等命中数。它们比 Deployment 成功状态更接近用户事实。

滚动发布真正的“成功”,不是新 Pod 都 Ready,而是旧 Pod 离开的过程中,每一个业务请求都能说明自己完成、拒绝,还是可恢复。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。