昨天下午,服务高峰期的时候,有个小功能上线。结果服务就此宕机,半小时后才恢复,今天复盘。
具体情况是,阿里云 K8s 的服务,旧 Pod 下掉了,新 Pod 刚起来就重启,中途报过几次 OOM,疑似性能不够,增加节点和配置后,缓慢恢复。
查看当时日志,没有任何报错。服务本地启动也是正常的,排除代码原因。可是为什么新 Pod 刚起来,就直接挂了呢?当时怀疑可能是流量的问题,是否流量分配过多导致。
我们使用的阿里云 K8s,容器内部各个服务用内网域名调用,内网用的 Service,没有配置额外的 Ingress 注解,默认走的轮询,如果有 5 台老 Pod 和 1 台新 Pod,新 Pod 只会接收 1/6 的流量,理论上不会有突发流量压力。
可在切换的实际场景中,有大量请求报错 502,似乎 5 台老 Pod 都挂了,仔细检查配置后,发现了第一个问题点。
在 kubernetes Deployment 中,我们的滚动策略是:
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
这是 Deployment 的默认配置,maxSurge 和 maxUnavailable 参数用来控制服务滚动的速率,太快服务不稳定,太慢发版时间长。
maxSurge 和 maxUnavailable 的配置,可以是百分比也可以是数字,百分比表示配置的 Pod 数 * 百分比,maxSurge 向上取整,maxUnavailable 向下取整。maxSurge 表示最多可以超出期望的数量/百分比,maxUnavailable 表示最多不可用 Pod 的数量。
上面的描述很抽象,我也不太懂为啥文档不喜欢说人话。说说我自己的理解,maxSurge 表示一次性最多可以启动多少个新 Pod,maxUnavailable 表示发布的过程中允许最多多少台服务不可用。
举个例子,以 25% 为例,如果配置了 5 台 Pod,那么 5 * 25% = 1.25,maxSurge 向上取整就是 2,也就是一次性启动两台新 Pod,而 maxUnavailable 向下取整是 1,也就是允许在更新过程中有 1 台服务不可用。所以在发布的一瞬间,会有两个新 Pod 启动和一个旧 Pod 销毁,整个过程最低 4 台服务提供服务。
查询论坛和网上资料,在生产中我们尽最大可能的追求稳定,所以推荐的配置是:
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
这套配置的意思是,每次只启动一个新 Pod,待新 Pod 接入流量后,销毁一个旧 Pod 同时再启一个新 Pod,直到完成所有更新。
更改配置后发现,更新过程中平滑度有所提升,但还有服务挂掉的情况。所以问题并没有查找完全。
之前的定位点是瞬间流量太大,为了一探究竟。专门找到了阿里云的相关文档。查阅文档后,的确没有对流量做特殊配置,不存在流量一次性都打到新 Pod 的可能,为此专门在 SLS 里把内网 IP 打印出来。
之后在今天的一次发布中,查询日志发现,服务启动时间需要 83.691s!!!
没有任何流量打入,启动一个服务居然要 1 分多钟,要知道我们在本地启动这个服务,只需要 4~5s。至此问题定位更清晰了,很有可能配置不够用。
我们服务的配置是 request 0.5CPU 和 512M,为了排除可能存在的 jvm 运行参数问题,我们把参数复制到本地启动,时间不超过 10s。
-server -Xms512m -Xmx1024m -XX:NewRatio=4 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=512M -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelInitialMarkEnabled -XX:+CMSParallelRemarkEnabled -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai -Djava.security.egd=file:/dev/./urandom
定位到是 CPU 不够用的问题,于是改成 request 1CPU 和 1024M,limit 2CPU 和 2048M,启动时间缩短了一倍。
查询历史发布容器 CPU 使用监控,新 Pod 启动的时候,CPU 占用接近 limit,持续 35 分钟,之后才缓缓下降,正常运行后服务稳定在 0.10.3CPU。
到现在为止,服务宕机真正出问题的原因找到了。
我们就绪检查的间隔是 60s,每隔 10s 检查一次,有一次成功就打入流量,最多检查三次。服务启动需要 80s+,在第三次检测——90s 的时候才会成功,这时 CPU 的压力还没降下来,流量打入,CPU 压力很快提升到 limit,导致健康检查失败,因此主动重启了服务。
所以解决方案是:
- 更改滚动更新策略,每次 1 上 1 下。
- 给予更多的 limit CPU,保证服务快速启动成功。可以不用更改 request CPU。
- 就绪间隔拉长,保证服务启动稳定后,再放入流量。
可是还有个心结没有解决,为什么我们的服务启动时需要这么持续的 CPU 占用?理论上启动完成后 CPU 会骤降。
网上找了很多资料,只表示服务在启动的时候 jvm 将 .class 文件写入内存需要大量计算,所以占用 CPU。可是依然不能解决我的困惑。
有类似经历的小伙伴欢迎在评论区指点指点,一起交流。