背景
压测信息:压测工具是jmeter,压测的并发数都是1并发,压测的URL固定。
资源信息:ACK集群的ECS是32C, ingress pod 无limit限制, 业务pod采用绑核处理8c或12c
链路:客户端 -> nginx(同VPC下的ECS )-> SLB > ingress pod -> 业务pod
问题
客户压测时候, 采用slo-manager对业务pod采用绑核处理,表明业务pod上的ECS设置的limit核数,业务pod独占,不与其他进程共享。客户遇到一个奇怪现象是采用12c绑核的。 可以看到采用12C时候,,CPU使用率达到100%时候,qps、rt和P99指标相对于8c指标反而并没有提高。
压测结果
14:07~14:17 8核16G: 1并发:QPS 36.7,平均响应时间27ms,P99 49ms,CPU使用率100%
14:33~14:43 12核16G: 1并发:QPS 28.4,平均响应时间35ms,P99 78ms,CPU使用率100%
15:14~15:24 8核16G: 1并发:QPS 36.5,平均响应时间27ms,P99 49ms,CPU使用率100%
18:29~18:39 12核16G: 1并发:QPS 28.5,平均响应时间35ms,P99 78ms,CPU使用率100%
分析过程
-
pod镜像并无改变。整个链路业务行为也无变化,唯一变化就是绑核由8c改到12c,相关压测指标下降了。 pod配置都开启的cpu-policy: static-burst和 cpuset-scheduler: true 自动绑核和CPU拓扑感知调度
-
压测结果 显示12核 性能远逊于 8和 15:14~15:24 8核16G: 1并发:QPS 36.5,平均响应时间27ms,P99 49ms,CPU使用率100%18:29~18:39 12核16G: 1并发:QPS 28.5,平均响应时间35ms,P99 78ms,CPU使用率100%
-
8核心pod 部署在 cn-beijing.xxxx节点上, pod绑定的CPU核心是0-7.
4. 12核心pod 部署在 cn-beijing.xxxx节点上, pod绑定的CPU核心是16-31 这16个CPU中
5. 12核 pod查看slo是工作的, cgroup显示动态绑核是成功的
6. 但是通过cpu.stat,可以看到依然存在 CPU throttle
7. promethes监控显示 在同样绑核情况下,8c的pod没有CPU throttled,12c的pod,CPU使用率相比于8C并没有100%趋势下,反而遇到了CPU throttled,明显不符合预期。但是前面的步骤已经验证了绑核是成功的,由此怀疑到应该是系统层面对于cfs调度存在的未知的非预期的行为.
12核
8核
结论
内核版本低于 4.19,如果一个周期内,进程未消耗完CPU时间,则会预留1ms直到一次完整的分配周期结束。1ms虽然不多,但是试想一下,如果是个100c的CPU,那么每个周期就是就有99ms,也就是1C左右的 CPU无法使用,这就可能产生限流。
容器内跑一些压力时(使用 while :; do :; done 模拟 )
复现情况如下:
1、Cenos7.9,对应 3.10 内核,很容易复现
发生 CPU Throttling 比例 ~1%
2、alinux 2,对于 4.19 内核,
发生 CPU Throttling 比例 ~千分之一
拓展
CPU限制
容器的limit声明式限制最终反映在宿主机的cgroup中,根据pod的qos,可以在下面的三个地址中找到limit设置。
-
/sys/fs/cgroup/cpu/kubepods.slice/kubepods-pod{pod ID}.slice/{docker/cri}-{container ID}.scope
-
/sys/fs/cgroup/cpu/kubepods-besteffort.slice/kubepods-pod{pod ID}.slice/{docker/cri}-{container ID}.scope
-
/sys/fs/cgroup/cpu/kubepods-burstable.slice/kubepods-pod{pod ID}.slice/{docker/cri}-{container ID}.scope
-
cpu.cfs_period_us:设定周期时间,必须与cfs_quota_us配合使用。
-
cpu.cfs_quota_us :设定周期内最多可使用的时间。这里的配置指 task 对单个 cpu 的使用上限,若cfs_quota_us是cfs_period_us的两倍,就表示在两个核上完全使用。数值范围为 1000 - 1000,000(微秒)。
-
cpu.stat:统计信息,包含nr_periods(表示经历了几个cfs_period_us周期)、nr_throttled(表示 task 被限制的次数)及throttled_time(表示 task 被限制的总时长)。
-
cpuset.cpus:在这个文件中填写 cgroup 可使用的 CPU 编号,如0-2,16代表 0、1、2 和 16 这 4 个 CPU。
CFS运行逻辑和CPU限流原理
某个单线程容器应用运行时。假设此应用需要200ms的CPU时间才能处理完毕一个请求,如果没有其他约束,此时它的cfs分配应该入下图所示。
现在我们给这个应用程序设置0.4 CPU的CPU限制(CPU quota是0.4)。这意味着应用程序每100ms CPU周期,该程序将要获得40ms的运行时间——即使这些时间CPU是空余的。还是同样的一个200ms的请求,由于0.4c的限制,现在需要440ms才能完成。
这个时候查看该容器路径下的cpu group的cpu.stat,其中 throttled_time会被限制了 240ms(对于每 100 毫秒的周期,应用程序只能运行 40ms,并被限制 60ms。4 个周期,因此 4 * 60 = 240ms。)
低CPU使用率情况下依然遇到限流
容器环境中,一个关键指标是throttling,这表明容器被限制的次数。我们发现很多容器无论 CPU 使用率是否接近极限都会受到限制。如下一个案例:
在动画中可以看到 CPU 限制设置为800m(0.8 个核心,80% 的核心),峰值使用率最高为200m(20% 的核心)。看到之后,我们可能会认为我们有足够的 CPU 让服务在它节流之前运行,但是依然会遇到cpu限流,这就意味着延迟增高和性能下降。
限流原因
如下图,第一个图显示了 cgroup 在一段时间内的全局配额。这从 20ms 的配额开始,这与 0.2 CPU 相关。中间的图表显示分配给每个 CPU 队列的配额,底部的图表显示实际工作线程在其 CPU 上运行的时间。
在 10 毫秒:
-
Worker 1 收到了一个请求,需要5毫秒处理请求
-
一部分配额从全局配额转移到 CPU 1 的每个 CPU 队列。
-
Worker 1 需要 5ms 来处理和响应请求。
在 17 毫秒:
-
Worker 2 收到了一个请求。
-
一部分配额从全局配额转移到 CPU 2 的每个 CPU 队列。
Worker 1 需要精确到需要5 毫秒来响应请求,并完全用完这5毫秒是不现实的。如果很快就完成请求会发生什么呢?
在 30 毫秒:
-
Worker 1 收到了一个请求。
-
Worker 1 只需要 1 毫秒来处理请求,而 CPU 1 的每个 CPU 存储桶上还剩下 4 毫秒。
-
由于每个 CPU 运行队列上还有剩余时间,但 CPU 1 上没有更多可运行线程,因此设置了一个计时器以将 slack 配额返回给全局存储桶。
在 36 毫秒:
-
CPU 1 上设置的 slack 计时器触发并将除 1 ms 之外的所有配额返回到全局配额池(返回的是5-1(消耗)-1(预留)=3ms),此时全局quota还剩下5+3=8ms
-
这会在 CPU 1 上留下 1 毫秒的配额。
在 41 毫秒:
-
Worker 2 收到一个长请求。
-
所有剩余时间都从全局存储桶转移到 CPU 2 的 per-CPU 存储桶,Worker 2 使用所有时间。
在 49 毫秒:
-
CPU 2 上的 Worker 2 现在在未完成请求的情况下受到限制,此处worker 我产生throttled
-
尽管 CPU 1 仍有 1ms 的配额,但仍会发生这种情况。
虽然 1 毫秒可能对双核机器没有太大影响,但这些毫秒在高核数机器上加起来。如果我们在 88 核 (n) 机器上遇到此行为,我们可能会在每个周期内耗费 87 (n-1) 毫秒,因为肯定有1个限制的CPU。那可能无法使用的 87 毫秒或 0.87 CPU。也就意味着假设一个容器分配的CPU越多,那么在一个周期内,其预留的时间累积就越多,而往往一个压测时间并不是100ms,所以在CPU越多,其受到throttled概率也就越多。
linux 内核是如何解决这个问题
当且仅当每个 CPU 的过期时间与全局过期时间匹配时,预补丁代码才会在运行时过期cfs_rq->runtime_expires != cfs_b->runtime_expires。因此,那 1 毫秒永不过期。该补丁将此逻辑从基于时钟时间更改为周期序列计数,解决了内核中长期存在的错误。代码如下:
- if (cfs_rq->runtime_expires != cfs_b->runtime_expires) {
+ if (cfs_rq->expires_seq == cfs_b->expires_seq) {
/* 延长本地期限,漂移以 2 个滴答为界 */
cfs_rq->runtime_expires + = TICK_NSEC;
} else {
/* 全局截止日期提前,过期已过 */
cfs_rq->runtime_remaining = 0;
}
修改问题 5.4+ 主线内核的一部分。它们已被反向移植到许多可用的内核中:
-
Linux-stable: 4.14.154+, 4.19.84+, 5.3.9+
-
Ubuntu: 4.15.0–67+, 5.3.0–24+
-
Redhat Enterprise Linux:
-
RHEL 7: 3.10.0–1062.8.1.el7+
-
RHEL 8: 4.18.0–147.2.1.el8_1+
-
CoreOS: v4.19.84+
该错误https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=763a9ec06c4已被修复并合并到运行 4.19 或更高版本的 Linux 发行版的内核中。
同时k8s社区也有关于此问题的issue:https://github.com/kubernetes/kubernetes/issues/67577
如果使用的 Linux 发行版的内核版本低于 4.19,建议使用aliyun2 最新版或升级到最新版本的内核
ACK另辟蹊径
通过前文,我们已经知道了这是由于内核cpu cgroup在调度cpu时候预留行为会对绑核的容器性能产生影响,解决办法是通过升级内核到4.19以上或者使用aliyunOS。但是更换OS往往对客户来说是一个很大的工程,那么ACK是否有其他途径呢?这时候可以使用koordinator的CPU burst,此feature可以根据对CPU Throttled的动态感知,自动调节CPU burst参数,允许容器在空闲时积累一些CPU时间片,用于满足突发时的资源需求,进而可以提升容器性能、降低延迟指标,临时允许突破limit限制,从而保障业务平稳运行。
-
sched_cfs_bw_burst_enabled 为1 表明CPU Burst功能的全局开关已打开
-
cpu.cfs_burst_us 表明可以突发额外使用最多几个CPU资源。可以看到在aliyun2 系统下,可以额外突发十倍limit的burst资源。
而客户在我们建议下,开启Pod的Annotation alibabacloud.com/cpuBurst: '{"policy": "auto"}' 后,12C下,相关的CPU限流已经被改善。
一句话总结
低于4.19内核版本的,在cgroup cpu调度过程中,由于基于时钟时间的cfs,所以在一个cpu周期内,如果worker未完全消耗完cpu时间,则每次slack回收时候,都会预留1ms/per-cpu,当CPU数量越大,则预留的1ms 'cpu'也就越多,进而产生CPU throttled概率也就越大,从而业务感知上也越加明显。建议:采用4.19以上的内核OS或者使用ack-koordinator的cpu burst功能。