2024-05-01-冒死分析:Ingress 没有准备好成为"统一流量入口"

#blog

分类:: 交付故事
时间:: 2024.05.01
状态:: done

当你手里有一把锤子🔨,看什么都像是钉子

1 开始
...

2023年6月,我们与一位客户展开合作,帮助客户做容器化上云转型,客户拥有10多家已经完成容器化改造的 ISV(Independent Software Vendor),需要一个 PaaS 容器云平台,对供应商软件的资源配额、服务暴露、存储、配置等进行统一管理。

于是我们基于 Kubernetes 1.21.0,交付了一款容器云管理平台的 PaaS 产品,在系统稳定运行3个月后,我们遇到了一次由于使用 Ingress 作为统一流量入口导致的P0级生产事故,并由此开始了一次对 Ingress 架构设计的改造。

希望本文中提到的解决方案,能给从事容器云实施交付的小伙伴,提供些许解决此类问题的灵感。

1.1 我们的 Ingress 设计
...

使用 nginx controller Ingress,作为全局统一流量入口,包括容器云平台流量、十多家供应商的业务流量。

ingress 本身独占两台 8c16g 的 worker 节点做高可用和集群内部的负载均衡,Ingress 上方配置 SLB 做两台机器的负载均衡。平台流量和所有供应商流量共用一套 Ingress 访问入口,共用一个 SLB 的 80/443 端口。

一款非常典型的 Ingress 架构设计。

image-20231204165213556.png

1.2 生产事故是怎样发生的?
...

系统稳定运行3个月后的某一天,好巧不巧,当市里领导视察的时候,平台突然挂了,同一时间,平台无法访问,所有供应商应用无法访问,当时告警功能没配置,最后在手忙脚乱排查2小时后,将接收 UDP 请求的应用下线,平台恢复正常。但是99.9%的 SLA 也没了。

Nginx-Ingress 版本: v0.46.0
Kubernetes 版本: v1.21.0

image-20231204165528658.png

为什么会出现这样的故障?

UDP是一种无连接的协议,意味着在发送数据之前不需要在发送方和接收方之间建立连接。特点就是速度快,可靠性低,一直发发发,而每一个 UDP 请求经过ingress,都需要占用 nginx worker 进程,正常情况下,纳秒级处理完UDP请求 worker 进程就释放,老的 worker 进程过载就再开一个新的 worker 进程处理请求,这是 happy path。

但,天不遂人愿。

供应商处理 UDP 请求的pod,本身是带缺陷上生产的状态,平时是 Running,一处理 UDP 请求,马上就 Crash。

首先出于 kubernetes 的自愈机制,deploy 控制器检测到副本数从 1 -> 0 后,会自动重启 pod,控制副本数从 0 -> 1;

同时 Ingress 本身有一个reload机制,当检测到副本数从 N -> 0 或者 0 -> N 时,Ingress Controller 会同时触发两个 Ingress 的重载,开辟新的worker进程,并关闭老的 worker 进程;

再同时供应商将 UDP 请求设置了一个超时机制,超时时间为 600s,由于 pod 已经 crash,所以连接一直在等待响应,虽然 Ingress 本身也有worker进程的超时时间,为240s,两者取最短,实际上pod crash 状态出现时,每个 UDP 请求最多只等待 240s,老的 worker 进程就会被关闭。但 240s 也是很长的时间了,已经足够发N多UDP请求,足够触发N次副本数 1->0, 0->1 的变化使 Ingress reload 多次,足够发生老的 nginx worker 进程被占用一直处于释放中,新的 worker 已经被用尽到无法创建。

循环几次,最终导致 nginx worker 进程全部处于 shutting down 状态,Ingress 无法提供服务。

再叠加 Ingress 作为统一入口的架构设计,reload 机制是同时刷新 2 个 Ingress 副本,导致 2 个 Ingress 同时挂,此时现场 10+ 家供应商云上业务都无法访问,容器云平台本身也无法访问。

场内供应商们纷纷高呼,平台挂了,平台挂了
远处正在给领导汇报的客户,看着突然一片空白的屏幕,一脸懵逼
L1运维被围起来,手脚并用敲着键盘,额头冒出一层细汗

Buff 叠满!

以至于完成ingress架构改造的1个月以后,当我看着一分为三的 ingress,还是会想起那个令人紧张的,面向多方的故障质询会议,愤怒的客户,严谨的监理,等待解释的上下游供应商,前期承诺的容器的隔离性啊,Ingress 的双副本高可用啊,统统变成巴掌,啪啪打脸

言归正传,nginx-Ingress pod 报错信息如下,实际上,根据这行报错已经可以快速找到解决方式,毕竟github上已经有不少大佬踩过坑在提 issue https://github.com/kubernetes/ingress-nginx/issues/5492
image-20240301110034617.png

2 如何解决?
...

Ingress 独占的 node 机器,CPU 是 8 核,所以 nginx worker 进程数是 8 个,1 分钟内,可以稳定复现这个故障。

一些基于规范流程的解决方式:

  • 不要带缺陷上生产!所有应用在测试环境稳定运行1周后,才可以转生产
  • 启动告警监控,实现故障发生1分钟内,邮件/短信推送给运维人员

2.1 方案一:最极端的隔离方式,一步到位
...

一家供应商,一个ingress,冗余但完全的隔离。

现场十多家供应商,最后得将一个 ingress 入口拆分成 10+ 个ingress入口,每套 ingress CPU 内存资源是2台 8C16G 独立 node 节点,大概得投入 100+C 的机器资源,每套入口使用率肉眼可见的很低,高投入低收益。

是一个偷懒的,头疼医头脚疼医脚的方案,最大的作用是给客户提供一个明显的排除项,以凸显方案二和三的价值。

2.2 方案二:业务维度,合理隔离
...

根据业务 + 协议维度划分成三个 ingress 入口:

  • 平台流量
  • 业务流量(HTTP + HTTPS)
  • 业务流量(UDP)

优点是将来再出现 reload 卡死的现象,平台和业务流量能分开,不至于全挂;使用 HTTP/HTTPS 和 UDP 区分的方式,是针对这次生产事故比较个性化的解决方案;

实际操作时,也可以通过核心业务 / 非核心业务 进行划分,但是呢,这个界限非常主观而模糊,毕竟谁愿意承认自己是边缘业务呢QAQ,所以最后客户还是选择通过协议进行简单粗暴划分。

缺点是作为一个天天 get hands dirty 的苦逼运维,我仍然觉得这样隔离很冗余,增加了运维的工作量,架构应该尽量简洁。

image-20231204165620071.png

操作步骤

  1. 基础设施:申请2套 ingress 机器资源 + 2个 slb + slb 端口开通
  2. PaaS平台: 实施 ingress 隔离
  3. 供应商:在PaaS平台修改ingress,将当前统一ingress 更改为隔离后的 ingress
  4. DNS 同步修改,新增两条解析记录
  5. 验证:修改完成后,访问供应商业务,同时观察新的 ingress pod log 中是否出现请求日志

风险:
切换ingress入口时,可能存在2-3分钟的业务中断,可以在晚上操作,风险可控。

2.3 方案三:用 apisix 代替 nginx
...

apisix 没有 reload 的机制,后面有机会再深入研究研究。

3 绝对的统一流量入口,真的有必要吗?
...

有必要。

作为运维人员来看,使用统一流量入口,可以简化运维,只需要做一套监控体系,监控所有流量;

从架构上看,使用一套流量入口,更简洁,尤其是需要对 ingress 做一些修改,比如需要修改 config map 来显示客户端真实 IP,当我把三套 ingress 改到第三遍的时候,就开始觉得 ingress 拆分真的是一种过度设计。

但是 nginx ingress reload 机制本身的设计缺陷,现阶段确实无法支持 ingress 担起统一入口的大梁,即使设置了2台 ingress 作为高可用,在 reload 现象发生时,依然会成为单点故障,一挂全挂,风险系数高,不是一个稳妥的设计。

ingress 作为统一流量入口,是一个美好的愿景,目前广泛使用 nginx ingress,那只能寄希望 nginx reload机制优化,或者 apisix,但是 apisix 比较小众,生产使用,还有不少坑要踩。