最近压测发现有服务的线程池执行线程数异常的低,只有单核。在排查日志,测试容器内调用Runtime.getRuntime().availableProcessors() 方法以及jvm后,确认该问题。
因为jdk版本已经升级到11,Runtime.getRuntime().availableProcessors()方法拿到的是k8s分配后的cpu核数。
在容器中使用命令查看cpu核数的命令还是宿主机的cpu核数。
linux的cpu cgroups介绍
CGroups为每种可以控制的资源定义了一个子系统,支持的子系统包含: cpuset,cpu,cpuacct,memory,devices,freezer,net_cls,blkio,perf_event,hugetlb,pids,net_prio。其中cpu子系统用限制cgroup中进程的CPU使用份额
在使用cgroups时需要先挂载,例如在centos下被挂载到了/sys/fs/cgroup。cpu cgroup子系统被挂载到了/sys/fs/cgroup/cpu,在这个目录下是各个控制组control group目录,每个控制组目录下还可以有子目录,因此各个控制组形成了一个树状的层级关系。
例如在/sys/fs/cgroup/cpu下创建一个名为foo的控制组目录,控制组目录中有如下内容:
|
重点关注下面3个参数 cpu.cfs_period_us、cpu.cfs_quota_us和cpu.shares。cfs表示Completely Fair Scheduler完全公平调度器,是Linux内核的一部分,负责进程调度。
cpu.cfs_period_us: 用来设置一个CFS调度时间周期长度,默认值是100000us(100ms),一般cpu.cfs_period_us作为系统默认值我们不会去修改它。
cpu.cfs_quota_us: 用来设置在一个CFS调度时间周期(cfs_period_us)内,允许此控制组执行的时间。默认值为-1表示限制时间。
cpu.shares: 用来设置cpu cgroup子系统对于控制组之间的cpu分配比例。默认值是1000。
使用 cfs_quota_us/ cfs_period_us,例如20000us/100000us=0.2,表示允许这个控制组使用的CPU最大是0.2个CPU,即限制使用20%CPU。 如果 cfs_quota_us/cfs_period_us=2,就表示允许控制组使用的CPU资源配置是2个。
对于cpu分配比例的使用,例如有两个cpu控制组foo和bar,foo的cpu.shares是1024,bar的cpu.shares是3072,它们的比例就是1:3。 在一台8个CPU的主机上,如果foo和bar设置的都是需要4个CPU的话( cfs_quota_us/cfs_period_us=4),根据它们的CPU分配比例,控制组foo得到的是2个,而bar控制组得到的是6个。需要注意 cpu.shares是在多个cpu控制组之间的分配比例,且只有到整个主机的所有CPU都打满时才会起作用。 例如刚才这个例子,如果是在一个4CPU的机器上,foo和bar的比例是1:3,如果foo控制组的程序满负载跑,而bar控制组程序没有运行或空负载,此时foo仍然能获得4个CPU,但如果foo和bar都满负载跑,因为它们都设置的需要4个CPU,而主机的CPU不够,只能按比例给它们分配1:3,则foo分配得到1个,而bar得到3个。
也就是说cpu.cfs_quota_us/cpu.cfs_period_us决定cpu控制组中所有进程所能使用CPU资源的最大值,而cpu.shares决定了cpu控制组间可用CPU的相对比例,这个比例只有当主机上的CPU完全被打满时才会起作用。
有了上面对cpu.cfs_quota_us/cpu.cfs_period_us和cpu.shares,可以按照第6节《重学容器06: 容器资源限制背后的技术cgroups》中手动体验cgroup的方式具体测试一下,测试过程这里省略了。
k8s如何使用cpu cgroup限制容器cpu的猜想和验证
最后根据结合Kubernetes中对容器的资源限制requests.cpu和limits.cpu,k8s中的容器是如何实现的呢,大胆的猜想一下:
limits.cpu是容器所能使用cpu的上限,是由cpu.cfs_quota_us/cpu.cfs_period_us来决定的
requests.cpu是为容器保留cpu数,如果当前已经调度到k8s节点上的requests.cpu总和超过节点cpu数,将不会在有新的容器调度到此节点上。因为cpu.share默认值是1024,可以认为是一个CPU的比例,则假设我们要设置某个容器的requests.cpu是2的话,就是把它的cpu控制组的cpu.shares设置成2*1024即2048。例如一个只有4个CPU的节点,上面跑3个容器request的CPU分别是1,1,2,则cpu.shares分别要设置成1024,1024,2048。也就是说如果节点CPU被打满的话,则他们都将获得他们请求的1,1,2个cpu。
下面我们到一个K8S集群中去验证我们的猜想,这个k8s集群的版本是1.21,容器运行时使用的是containerd。 在这个集群中部署了Kubernetes的Dashboard,设置了dashboard容器的资源限制情况如下:
|
即limits.cpu为2,requests.cpu为0.1。按照我们的猜想,则k8s dashboard容器cpu控制组的cpu.cfs_quota_us/cpu.cfs_period_us应该是2,cpu.shares应该是0.1*1024=102.4。
首先确认k8s dashboard的pod在哪个Node上:
|
其在node2节点上,而且这个pod的QoS Class是Burstable,可以到node2的/sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice目录去寻找。
|
可以看到在这个目录下找到了4个pod目录,那么哪个是k8s dashboard的pod呢,实际上目录名里有pod的metadata.uuid,这里k8s dashboard pod的uuid是97a6d223-6382-452b-8591-ee62c9ab1dd8, 因此定位到了kubepods-burstable-pod97a6d223_6382_452b_8591_ee62c9ab1dd8.slice目录。
|
pod的目录里有上面3个cri-containerd目录,哪个是dashboard容器的目录呢。使用crictl查看一下容器id:
|
找到了容器id是6035d1032b8f9,确定了目录是cri-containerd-6035d1032b8f9ca2728eb2fc12632c3bf6608fb6099be891559a98a33aa23483.scope,再进入到这个目录中:
|
查看cpu.cfs_quota_us、cpu.cfs_period_us, cpu.shares的值:
|
即容器cpu控制组中设置是cpu.cfs_quota_us/cpu.cfs_period_us=200000/100000=2, cpu.shares=102,与我们猜想的2和102.4是一致的。
参考
Kubernetes in Action
摘自:https://blog.frognew.com/2021/07/relearning-container-29.html