前段时间在负责将公司一个非常重要的服务(代号:S)从专供服务器(Snowflake Server)迁移到Kubernetes的集群上。不料在压力测试的时候,我们发现S的请求响应速度明显比专供服务器的服务慢,有时候可能会夸张到慢1秒或者2秒。
由于我们请求的每一跳(hop)都有监控,所以很快排除了应用本身以外的所有问题。但问题又来了,代码完全相同,集成测试也没有发现任何错误,就连CPU使用率,JVM数据,磁盘性能都没有任何区别,那到底是什么拖慢了请求呢?然而在一次非常偶然的情况下,我们尝试将应用的CPU配额在K8s里从4核调到8核,结果奇迹发生了,请求相应速度瞬间降低并能与专供服务器持平!(专供服务器有32核,除了S以外还运行着几个其他应用。我们最初分配4核给S的原因是观察到CPU使用图表显示历史最高为3.5左右)于是我们开始深入反思到底CPU出了什么问题。
CPU节流与响应时间
结论:CPU分配不足会产生CPU节流,导致请求响应变慢。
我们在Kubernetes中使用的是cgroups来管理资源分配与隔离,调度程序则选取了完全公平调度(CFS)中的强制上限(Ceiling Enforcement)。我们以单线程的情况举例:假使cpu.cfs_period_us设定为100毫秒(这是全局统一的设置),cpu.cfs_quota_us设定为20毫秒(这是每个应用在K8s自己设定的,当前的配置等同于给Pod分配200millicore)。这意味着CFS会每隔100毫秒会重新分配应用的CPU使用权,而在每个100毫秒内,应用可以占用CPU20毫秒。在专供服务器的情况下,由于我们没有使用cgroups,应用可以用尽空闲的CPU。但是假如一个请求在专供服务器需要花100毫秒,在刚刚设定的K8s环境下则会发生如下现象:
运行20毫秒 -> 挂起并等待80毫秒(节流) -> 运行20毫秒 -> 挂起并等待80毫秒-> 运行20毫秒 -> 挂起并等待80毫秒-> 运行20毫秒 -> 挂起并等待80毫秒-> 运行20毫秒 -> 请求完成并返回
原本只需100毫秒的请求,现在却花费了420毫秒,整整慢了4倍多!
于是我们加入了新的监控指标:cpu.stat.nr_throttled / cpu.stat.nr_periods (CPU节流率,越高越不好)。对比请求响应时间的数据,完全吻合。
在补充一句在多核多线程情况下,假设cpu.cfs_quota_us是200毫秒,如果有10个线程需要运行,每个线程只能分到20毫秒运行CPU时间。
CPU节流与CPU使用率
但问题又来了,明明CPU节流率已经快到百分之百了,为什么CPU使用率的不是100%呢,甚至在我们的另一些测试中低于20%的情况都有出现。
对于这个问题我们还没有定论,目前的猜测是在多核多线程的情况下,很多线程占着CPU配额却因为IO等原因挂起了,导致配额耗尽但却不计入CPU使用时间(IO不算CPU使用时间),所以看起来CPU使用率很低。
最后想总结一下这次问题排查的心得:
- CPU使用率低并不代表应用性能没有受CPU节流的影响
- 目前认为使用CPU节流率来判断CPU使用状况更合适
- Kubernetes不是黑魔法,迁移一定会有坑,有效的数据监控能够极大得帮助发现问题原因