小Cの已经记不起来的博客

一次线上 蓝绿部署 故障的复盘记录

故障现场:明明健康检查全绿,切流量后订单直接开始翻车

那天是周三下午,线上订单服务 v2.3.7 要发 v2.3.8。我们用的是 ingress-nginx + K8s Service 切 selector 的蓝绿方式,蓝组叫 order-service-blue,绿组叫 order-service-green。部署文档写得很漂亮:先起 green,等 /actuator/health/readiness 返回 200,再把 ingress 的 service 从 blue 切到 green,观察10分钟,没问题就缩容 blue。

我当时心里还挺稳,之前发版都这么搞,结果全量切过去不到三分钟,监控就开始飘红。订单创建成功率从 99.8% 掉到 60% 多,Nginx ingress 日志里一片 502,应用日志里则是 Communications link failure 和一堆 connection timed out。业务那边已经开始问是不是数据库炸了。

排查过程:先看 DB,再看网络,最后发现是健康检查骗了我

一开始我怀疑是这次发版加了订单扩展字段,导致老数据读取报错,但是 Kibana 里翻了一圈,没有 Unknown column 或者 SQLSyntaxErrorException,反而全是 JDBC 连接超时。然后去查 MySQL,SHOW PROCESSLIST 里大量 Waiting for table metadata lock,看起来像是慢查询把连接池占满了。

但是这里有个很反常的地方:蓝组还活着,数据库没动,为什么 blue 没事,green 一接流量就挂?

我把 ingress 先切回 blue,故障立刻恢复。然后开始看 green 的启动日志。第一次启动时 readiness 确实返回 200,我当时用 curl 也验证过,kubectl exec 进去也正常,甚至 green 单独压了 20 QPS 都没问题。

后来我拿 jstackss -s 看了下,发现 green 接全量流量后,Hikari 连接池瞬间被打满,connectionTimeout 3 秒,大量请求排队等连接,排队又导致上游线程堆积,最后 Nginx 超时。

那为什么 blue 不会?继续对比启动参数和环境变量,发现这次 green 的 Deployment 里少挂了一个 ConfigMap 里的 cache-prewarm-enabled: true。blue 是之前长期运行实例,启动时已经预热过商品、库存和地址缓存;green 虽然启动成功,但它依赖的本地缓存是懒加载,一上来所有订单请求都去查 DB。

说白了,这次蓝绿部署失败,表面是 DB 连接超时,实际是 readiness 健康检查只检查了“服务活着”,没检查“服务能不能接活”。这个健康检查真的太坑了,200 看着挺好看,结果业务还没准备好,直接就被流量教育了。

临时修复:先让 green 活着,再谈切换

确认原因后,我没有直接重新切全量,而是先做两件事。

第一,把 green 的 ConfigMap 补上,重新启动:

kubectl rollout restart deployment order-service-green -n prod
kubectl rollout status deployment order-service-green -n prod

第二,为了避免再次全量砸进去,我先把 ingress 权重切成 5%:

nginx.ingress.kubernetes.io/service-weight: 5

如果你们用的不是 annotation,也可以直接在 ingress backend 里加 service 权重,或者用两个 ingress 做灰度。切完 5% 后看十分钟,成功率回到 99.9%,DB 连接数也稳住了,我才继续加到 20%、50%,最后全量切 green。

切的时候还顺手观察了这几个指标,真的建议每次都看一眼:

mysql_threads_connected
hikaricp_connections_active
hikaricp_connections_pending
http_requests_success_ratio{job="order-service"}

尤其 hikaricp_connections_pending,这个一飙升基本就是连接池不够或者 DB 慢了,比看 CPU 直观。

根因复盘:蓝绿部署不能只盯着“能不能启动”

这次故障根因不复杂,就是新版本实例缺少启动预热开关,导致冷实例直接接全量流量,把数据库连接池打爆。更深层的问题是,我们的发布流程把“部署成功”和“发布成功”混在一起了。

以前我以为 readiness 返回 200,服务就 ready 了。后来复盘会上大家吵了一下,最后结论很清楚:

  1. readiness 必须包含真实依赖,比如数据库、Redis、配置中心、本地缓存预热状态。
  2. 蓝绿切换要支持权重,不要一上来就 100%
  3. 启动脚本里不要默认打开懒加载,尤其是订单、支付这种高并发服务。
  4. 回滚要快,但回滚前一定先把指标截图和日志留存,不然复盘全靠脑补。

我把 readiness 端点也改了,加了一个 CacheReadyIndicator,只有本地缓存命中率达到阈值才返回 UP

@Component
public class CacheReadyIndicator implements HealthIndicator {
    @Override
    public Health health() {
        long hitRate = cacheMetrics.hitRate();
        if (hitRate < 90) {
            return Health.down().withDetail("hitRate", hitRate).build();
        }
        return Health.up().withDetail("hitRate", hitRate).build();
    }
}

这个代码不算完美,比如阈值可能不适合所有机器,但起码比单纯 /actuator/health 靠谱点。

后续改进:发布 checklist 里加几条真能救命的

复盘之后,我把线上蓝绿部署 checklist 更新了一下,现在发版前必须确认这几项:

最后一点真的别笑,这次我一开始还在群里问“之前回滚命令是什么”,业务在催的时候那几分钟真的挺难受。后来我把回滚命令写进发布单,复制粘贴就行:

kubectl rollout undo deployment/order-service-green -n prod

或者更稳一点,用 blue/green selector 切回:

kubectl patch ingress order-service -n prod -p '{"spec":{"rules":[{"http":{"paths":[{"backend":{"service":{"name":"order-service-blue","port":{"number":80}}},"path":"/api/orders","pathType":"Prefix"}]}}]}}'

这种 patch 命令最好提前 --dry-run=client 看一眼,不然一个括号错了,线上就是二次事故。

一点个人感受:蓝绿部署不是按钮点两下就完事

以前我觉得蓝绿部署最大的优势是不停机,后来发现真正的难点不是停机,而是“怎么让新版本实例像老版本一样活着”。健康检查、缓存预热、连接池、数据库锁、配置中心,这些东西只要有一个没准备好,切流量那一刻就会把你教育得明明白白。

这次故障没有丢单,业务影响大概十几分钟,但复盘记录还是要写。线上问题最烦的就是“看着都正常”,等出事才知道自己只看了自己想看的那几个指标。下次发版前,我可能会先多问一句:这个健康检查到底检的是什么?别又是 200 把我骗了。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放