总览
这篇笔记主要记录了kubelet中cgroup的管理原理,涉及几个方面
-
kubelet主要资源提供者CAdvisor获取资源的途径
-
kubelet怎么维护自己的CgroupRoot=kubepods.slice路径
-
针对QoS的不同POD类型,kubelet怎么维护对应的CgroupRoot路径
CAdvisor获取资源
CAdvisor获取CPU
CAdvisor获取CPU的数量和NUMA的相关概念有关,可以先行了解NUMA的相关概念(https://blog.csdn.net/yk_wing4/article/details/87474172)
获取流程
# 1. 获取NODE的数量,NODE对应的就是一个NUMA NODE
root@ubuntu:~# ls -la /sys/devices/system/node/
total 0
drwxr-xr-x 4 root root 0 8月 2 14:34 node0
drwxr-xr-x 4 root root 0 8月 2 14:34 node1
# 2. 获取每个NODE下的CPU核心数量,对应的是逻辑CPU核心数量
root@ubuntu:~# ls -la /sys/devices/system/node/node0/
total 0
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu0 -> ../../cpu/cpu0
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu1 -> ../../cpu/cpu1
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu12 -> ../../cpu/cpu12
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu13 -> ../../cpu/cpu13
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu14 -> ../../cpu/cpu14
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu15 -> ../../cpu/cpu15
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu16 -> ../../cpu/cpu16
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu17 -> ../../cpu/cpu17
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu2 -> ../../cpu/cpu2
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu3 -> ../../cpu/cpu3
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu4 -> ../../cpu/cpu4
lrwxrwxrwx 1 root root 0 8月 2 14:36 cpu5 -> ../../cpu/cpu5
上述所有NODE下的逻辑CPU核心数量对应的就是CAdvisor中的CPU核心数量了
小插曲:怎么知道每个逻辑CPU核心对应的物理CPU核心呢?
root@ubuntu:~# cat /sys/devices/system/node/node0/cpu0/topology/core_id
0
root@ubuntu:~# cat /sys/devices/system/node/node0/cpu1/topology/core_id
1
通过查看每个逻辑CPU所属的CPU核心,就能知道我们的主机是几核几线程的啦
CAdvisor获取内存
root@ubuntu:~# cat /proc/meminfo
MemTotal: 49422924 kB
直接读取文件 /proc/meminfo,关注它的 MemTotal字段即可,Cadvisor中通常会把它转成Byte单位
CAdvisor获取内存大页HugePages
root@ubuntu:~# ls -la /sys/kernel/mm/hugepages/
total 0
drwxr-xr-x 2 root root 0 8月 2 14:37 hugepages-1048576kB
drwxr-xr-x 2 root root 0 8月 2 14:37 hugepages-2048kB
root@ubuntu:~# cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
0
该目录下对应的就是2M和1G的内存大页,nr_hugepages文件中记录的是对应内存大页的数量
kubelet维护cgroup
kubelet中和cgroup打交道的主要是ContainerManager,我们具体看下kubelet是怎么维护cgroup信息的
func NewContainerManager(mountUtil mount.Interface, cadvisorInterface cadvisor.Interface, nodeConfig NodeConfig, failSwapOn bool, devicePluginEnabled bool, recorder record.EventRecorder) (ContainerManager, error) {
// 读取 /proc/$pid/mountinfo 获取进程的所有挂载信息
// 读取 /proc/$pid/cgroup 获取进程的cgroup信息
// 再mountinfo中找出所有挂载了的 cgroup 路径
subsystems, err := GetCgroupSubsystems()
...
var internalCapacity = v1.ResourceList{}
machineInfo, err := cadvisorInterface.MachineInfo()
// 从CAdvisor中获取到CPU、内存、内存大页容量
capacity := cadvisor.CapacityFromMachineInfo(machineInfo)
for k, v := range capacity {
internalCapacity[k] = v
}
// 额外从文件 /proc/sys/kernel/pid_max 获取PID最大值
pidlimits, err := pidlimit.Stats()
if err == nil && pidlimits != nil && pidlimits.MaxPID != nil {
internalCapacity[pidlimit.PIDs] = *resource.NewQuantity(
int64(*pidlimits.MaxPID),
resource.DecimalSI)
}
// CgroupRoot 由于默认启用了CgroupsPerQOS 默认会设置为 "/"
cgroupRoot := ParseCgroupfsToCgroupName(nodeConfig.CgroupRoot)
cgroupManager := NewCgroupManager(subsystems, nodeConfig.CgroupDriver)
// Check if Cgroup-root actually exists on the node
if nodeConfig.CgroupsPerQOS {
// 检查 /sys/fs/cgroup/memory/ 等各个子系统路径必须存在
if !cgroupManager.Exists(cgroupRoot) {
return nil, fmt.Errorf("invalid configuration: cgroup-root %q doesn't exist", cgroupRoot)
}
// cgroupRoot 默认设置为 kubepods
cgroupRoot = NewCgroupName(cgroupRoot, "kubepods")
}
cm := &containerManagerImpl{
cadvisorInterface: cadvisorInterface,
mountUtil: mountUtil,
NodeConfig: nodeConfig,
subsystems: subsystems,
cgroupManager: cgroupManager,
capacity: capacity,
internalCapacity: internalCapacity,
cgroupRoot: cgroupRoot,
recorder: recorder,
qosContainerManager: qosContainerManager,
}
...
return cm, nil
}
这里创建ContainerManager主要获取当前节点上挂了的所有cgroup子系统,并校验所有cgroup子系统的路径必须存在
我们可以具体看下kubelet对应的pid中的cgroup相关的挂载信息,可以看到常见的cpu, memory等均有挂载
root@ubuntu:~# cat /proc/22507/mountinfo |grep cgroup
28 18 0:24 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755
29 28 0:25 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd
31 28 0:27 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,freezer
32 28 0:28 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,cpu,cpuacct
33 28 0:29 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,memory
34 28 0:30 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,pids
35 28 0:31 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,cpuset
36 28 0:32 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,hugetlb
37 28 0:33 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,net_cls,net_prio
38 28 0:34 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,perf_event
39 28 0:35 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,devices
40 28 0:36 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,blkio
然后再kubelet启动的时候,ContainerManager也会相应的启动,我们看下它启动会做哪些工作
func (cm *containerManagerImpl) Start(node *v1.Node,
activePods ActivePodsFunc,
sourcesReady config.SourcesReady,
podStatusProvider status.PodStatusProvider,
runtimeService internalapi.RuntimeService) error {
// 验证 节点的资源 必须大于 系统保留、kube保留、hard驱逐资源的和
if err := cm.validateNodeAllocatable(); err != nil {
return err
}
// Setup the node
if err := cm.setupNode(activePods); err != nil {
return err
}
// 检查是否存在定时任务,默认会存在docker的一个定时任务,用于检测dockerd的运行cgroup信息
// 还存在一个kubelet的定时任务,用于检测kubelet的运行cgroup信息
if len(cm.periodicTasks) > 0 {
go wait.Until(func() {
for _, task := range cm.periodicTasks {
if task != nil {
task()
}
}
}, 5*time.Minute, wait.NeverStop)
}
}
func (cm *containerManagerImpl) setupNode(activePods ActivePodsFunc) error {
// 默认启用了CgroupsPerQOS ,这里首先确保CgroupRoot路径是存在的,不存在则创建
// 再创建ContainerManager的时候CgroupRoot路径已经被设置为了kubepods
if cm.NodeConfig.CgroupsPerQOS {
if err := cm.createNodeAllocatableCgroups(); err != nil {
return err
}
err = cm.qosContainerManager.Start(cm.getNodeAllocatableAbsolute, activePods)
}
systemContainers := []*systemContainer{}
// 默认是docker
if cm.ContainerRuntime == "docker" {
// 添加一个定时任务,用于定时更新docker运行的cgroup信息
cm.periodicTasks = append(cm.periodicTasks, func() {
klog.V(4).Infof("[ContainerManager]: Adding periodic tasks for docker CRI integration")
cont, err := getContainerNameForProcess(dockerProcessName, dockerPidFile)
cm.RuntimeCgroupsName = cont
})
}
// 默认是空
if cm.KubeletCgroupsName != "" {
...
} else {
// 再添加一个定时任务,用于定时更新kubelet运行的cgroup信息
cm.periodicTasks = append(cm.periodicTasks, func() {
if err := ensureProcessInContainerWithOOMScore(os.Getpid(), qos.KubeletOOMScoreAdj, nil); err != nil {
...
}
cont, err := getContainer(os.Getpid())
cm.KubeletCgroupsName = cont
})
}
// 默认为空
cm.systemContainers = systemContainers
return nil
}
首先是创建CgroupRoot路径
func (cm *containerManagerImpl) createNodeAllocatableCgroups() error {
// 包括 cpu,内存、HugePages、Pid
nodeAllocatable := cm.internalCapacity
nc := cm.NodeConfig.NodeAllocatableConfig
// EnforceNodeAllocatable 默认包括pods
// 排除掉 系统保留、kube保留
if cm.CgroupsPerQOS && nc.EnforceNodeAllocatable.Has("pods") {
nodeAllocatable = cm.getNodeAllocatableInternalAbsolute()
}
// cgroupRoot 上面创建ContainerManager的时候设置成了 kubepods
cgroupConfig := &CgroupConfig{
Name: cm.cgroupRoot,
ResourceParameters: getCgroupConfig(nodeAllocatable),
}
// /sys/fs/cgroup/cpu/kubepods.slice 目录
// /sys/fs/cgroup/memory/kubepods.slice 目录 等等这些cgroup子系统路径是否存在
if cm.cgroupManager.Exists(cgroupConfig.Name) {
return nil
}
// 不存在则需要创建
if err := cm.cgroupManager.Create(cgroupConfig); err != nil {
}
return nil
}
这一步主要就是创建kubelet的CgroupRoot目录
因为kubelet的CgroupDriver在启动参数中会设置为 systemd,因此 kubepods 这个CgroupRoot 会被转换为 systemd 风格
func (cgroupName CgroupName) ToSystemd() string {
if len(cgroupName) == 0 || (len(cgroupName) == 1 && cgroupName[0] == "") {
return "/"
}
newparts := []string{}
for _, part := range cgroupName {
part = escapeSystemdCgroupName(part)
newparts = append(newparts, part)
}
result, err := cgroupsystemd.ExpandSlice(strings.Join(newparts, "-") + ".slice")
return result
}
转成 systemd风格会把 中划线- 转换为 下划线_,然后添加 .slice后缀
在 /sys/fs/cgroup/cpu/ 等Cgroup子系统下新建目录后,会自动创建这些文件,并且默认会自动继承上一层即 /sys/fs/cgroup/cpu/目录下这些文件中的值
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice# ls -la
total 0
-rw-r--r-- 1 root root 0 8月 4 12:09 cgroup.clone_children
-rw-r--r-- 1 root root 0 8月 4 12:09 cgroup.procs
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.stat
-rw-r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_all
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_percpu
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_sys
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_user
-rw-r--r-- 1 root root 0 8月 4 12:09 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 8月 4 12:09 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 8月 2 10:02 cpu.shares
-r--r--r-- 1 root root 0 8月 4 12:09 cpu.stat
-rw-r--r-- 1 root root 0 8月 4 12:09 notify_on_release
-rw-r--r-- 1 root root 0 8月 4 12:09 tasks
这些文件自动生成出来后,就会对其中的资源进行设置,这些主要设置的是这些子系统
func getSupportedSubsystems() map[subsystem]bool {
supportedSubsystems := map[subsystem]bool{
&cgroupfs.MemoryGroup{}: true,
&cgroupfs.CpuGroup{}: true,
&cgroupfs.PidsGroup{}: true,
}
// not all hosts support hugetlb cgroup, and in the absent of hugetlb, we will fail silently by reporting no capacity.
supportedSubsystems[&cgroupfs.HugetlbGroup{}] = false
return supportedSubsystems
}
而这里设置的值就来自于CAdvisor中获取到的CPU容量、内存容量、PID容量、内存大页容量
func setSupportedSubsystemsV1(cgroupConfig *libcontainerconfigs.Cgroup) error {
for sys, required := range getSupportedSubsystems() {
if _, ok := cgroupConfig.Paths[sys.Name()]; !ok {
}
if err := sys.Set(cgroupConfig.Paths[sys.Name()], cgroupConfig); err != nil {
return fmt.Errorf("failed to set config for supported subsystems : %v", err)
}
}
return nil
}
CPU 子系统的设置
func (s *CpuGroup) Set(path string, cgroup *configs.Cgroup) error {
// CPU容量会转换为 CpuShares
if cgroup.Resources.CpuShares != 0 {
shares := cgroup.Resources.CpuShares
if err := fscommon.WriteFile(path, "cpu.shares", strconv.FormatUint(shares, 10)); err != nil {
return err
}
}
// kubelet不会设置这个文件,使用默认值 100ms
if cgroup.Resources.CpuPeriod != 0 {
if err := fscommon.WriteFile(path, "cpu.cfs_period_us", strconv.FormatUint(cgroup.Resources.CpuPeriod, 10)); err != nil {
return err
}
}
// kubelet不会设置这个文件,使用默认值 -1
if cgroup.Resources.CpuQuota != 0 {
if err := fscommon.WriteFile(path, "cpu.cfs_quota_us", strconv.FormatInt(cgroup.Resources.CpuQuota, 10)); err != nil {
return err
}
}
return s.SetRtSched(path, cgroup)
}
我们可以对比下 kubepods.slice/cpu.shares中的值和上一级的的值是不一样的,其中的值就来自于CAdvisor中获取到的CPU核数 减去kube保留的、减去系统保留的,(24000-500-500)*1024/1000=23552,我这里是24核CPU,1024/1000是CPU核数转换为CpuShares的一个比例参数
root@ubuntu:/sys/fs/cgroup/cpu# cat cpu.shares
1024
root@ubuntu:/sys/fs/cgroup/cpu# cat kubepods.slice/cpu.shares
23552
内存子系统的设置
func (s *MemoryGroup) Set(path string, cgroup *configs.Cgroup) error {
// CAdvisor中获取的内存容量会转换为Memory,而swap我们一般都会关闭
if cgroup.Resources.Memory == -1 && cgroup.Resources.MemorySwap == 0 {
...
}
// When memory and swap memory are both set, we need to handle the cases
// for updating container.
if cgroup.Resources.Memory != 0 && cgroup.Resources.MemorySwap != 0 {
...
} else {
if cgroup.Resources.Memory != 0 {
if err := fscommon.WriteFile(path, "memory.limit_in_bytes", strconv.FormatInt(cgroup.Resources.Memory, 10)); err != nil {
return err
}
}
if cgroup.Resources.MemorySwap != 0 {
...
}
}
if cgroup.Resources.KernelMemory != 0 {
...
}
if cgroup.Resources.MemoryReservation != 0 {
...
}
if cgroup.Resources.KernelMemoryTCP != 0 {
...
}
if cgroup.Resources.OomKillDisable {
...
}
if cgroup.Resources.MemorySwappiness == nil || int64(*cgroup.Resources.MemorySwappiness) == -1 {
return nil
} else if *cgroup.Resources.MemorySwappiness <= 100 {
...
} else {
...
}
return nil
}
内存资源一般只会设置memory.limit_in_bytes,我们也可以对比下kubepods.slice/memory.limit_in_bytes和上一级的值是不一样的,其值就来自CAdvisor中拿到的节点的内存值,(49422924-512*1024-512*1024)*1024=49535332352
root@ubuntu:/sys/fs/cgroup/memory# cat memory.limit_in_bytes
9223372036854771712
root@ubuntu:/sys/fs/cgroup/memory# cat kubepods.slice/memory.limit_in_bytes
49535332352
PID 子系统设置
func (s *PidsGroup) Set(path string, cgroup *configs.Cgroup) error {
if cgroup.Resources.PidsLimit != 0 {
// "max" is the fallback value.
limit := "max"
if cgroup.Resources.PidsLimit > 0 {
limit = strconv.FormatInt(cgroup.Resources.PidsLimit, 10)
}
if err := fscommon.WriteFile(path, "pids.max", limit); err != nil {
return err
}
}
return nil
}
PID 子系统中会设置kubepods.slice/pids.max的值,其值来自/proc/sys/kernel/pid_max文件,而上一级目录是没有这个文件的
root@ubuntu:/sys/fs/cgroup/pids# ls -la
total 0
-rw-r--r-- 1 root root 0 8月 2 14:19 cgroup.clone_children
-rw-r--r-- 1 root root 0 8月 2 14:19 cgroup.procs
-r--r--r-- 1 root root 0 8月 2 14:19 cgroup.sane_behavior
drwxr-xr-x 2 root root 0 8月 2 14:19 init.scope
-rw-r--r-- 1 root root 0 8月 2 14:19 notify_on_release
-rw-r--r-- 1 root root 0 8月 2 14:19 release_agent
drwxr-xr-x 215 root root 0 8月 7 17:05 system.slice
-rw-r--r-- 1 root root 0 8月 2 14:19 tasks
drwxr-xr-x 8 root root 0 8月 7 16:32 user.slice
root@ubuntu:/sys/fs/cgroup/pids# cat kubepods.slice/pids.max
4194304
root@ubuntu:/sys/fs/cgroup/pids# cat /proc/sys/kernel/pid_max
4194304
内存大页子系统设置
func (s *HugetlbGroup) Set(path string, cgroup *configs.Cgroup) error {
for _, hugetlb := range cgroup.Resources.HugetlbLimit {
if err := fscommon.WriteFile(path, strings.Join([]string{"hugetlb", hugetlb.Pagesize, "limit_in_bytes"}, "."), strconv.FormatUint(hugetlb.Limit, 10)); err != nil {
return err
}
}
return nil
}
内存大页主要包括2MB和1GB的内存大页,这里会分别设置对应的limit值,由于我这里没有,所以对应的值都是0,和上一级目录中的值是不一样的
root@ubuntu:/sys/fs/cgroup/hugetlb# cat hugetlb.2MB.limit_in_bytes
9223372036854771712
root@ubuntu:/sys/fs/cgroup/hugetlb# cat kubepods.slice/hugetlb.2MB.limit_in_bytes
0
root@ubuntu:/sys/fs/cgroup/hugetlb# cat hugetlb.1GB.limit_in_bytes
9223372036854771712
root@ubuntu:/sys/fs/cgroup/hugetlb# cat kubepods.slice/hugetlb.1GB.limit_in_bytes
0
上述子系统中现在就都存在kubepods.slice这个目录了,并且这个目录下的limit值也都根据CAdvisor获取到的容量进行了设置
再后续就是添加了两个定时任务,每五分钟执行一次
其中一个是获取docker运行的cgroup信息,优先读取/var/run/docker.pid文件获取dockerd的PID,然后获取这个PID的cgroup信息即可/proc/$PID/cgroup
如果这个文件没有,则会读取/proc下的所有文件,然后读取每一个PID下的/proc/#PID/cmdline文件,获取该PID的运行命令,然后匹配名称dockerd,从而确定dockerd的运行PID,然后再获取这个PID的cgroup信息即可/proc/$PID/cgroup
通常对应的cgroup信息都是/system.slice/docker.service
func getContainerNameForProcess(name, pidFile string) (string, error) {
pids, err := getPidsForProcess(name, pidFile)
...
cont, err := getContainer(pids[0])
return cont, nil
}
另一个定时任务是获取kubelet的运行cgroup信息,首先会对oomscore值进行调整,通过对比kubelet进程和机器1号进程的namespace链接/proc/1/ns/pid是否一致,来判断kubelet是否运行再host namespace中
如果运行再hostnamespace中,那么需要确保kubelet进程的/proc/$PID/oom_score_adj的值固定为-999
最后就是读取kubelet进程的/proc/$PID/cgroup即可获取kubelet运行的cgroup信息,一般为/system.slice/kubelet.service
func ensureProcessInContainerWithOOMScore(pid int, oomScoreAdj int, manager cgroups.Manager) error {
if runningInHost, err := isProcessRunningInHost(pid); err != nil {
...
} else if !runningInHost {
...
return nil
}
...
oomAdjuster := oom.NewOOMAdjuster()
klog.V(5).Infof("attempting to apply oom_score_adj of %d to pid %d", oomScoreAdj, pid)
if err := oomAdjuster.ApplyOOMScoreAdj(pid, oomScoreAdj); err != nil {
}
return utilerrors.NewAggregate(errs)
}
总结一下ContainerManager的工作流程
-
初始化创建的时候会设置
CgroupRoot为kubepods, 并且检测挂载了的cgroup子系统目录必须存在,对应的就是/sys/fs/cgroup/cpu等子系统的目录 -
启动后会检测
CgroupRoot目录是否存在,如果不存在则会为每个挂载了的cgroup子系统都创建kubepods.slice目录,会携带.slice后缀是因为会转换为systemd风格 -
kubepods.slice目录创建后,内核会自动再改目录下生成对应的默认文件,其中的值完全继承自上一级目录 -
然后kubelet会再
kubepods.slice目录,设置CPU、内存、内存大页、PID这些子系统的limit上限,上限值就来自CAdvisor中获取到的主机的容量值
QoS情况下的kubelet的Cgroup管理
在上面创建ContainerManager的时候,会同步创建QoSContainerManager
func NewQOSContainerManager(subsystems *CgroupSubsystems, cgroupRoot CgroupName, nodeConfig NodeConfig, cgroupManager CgroupManager) (QOSContainerManager, error) {
// 默认情况下是启用了 CgroupsPerQOS
if !nodeConfig.CgroupsPerQOS {
return &qosContainerManagerNoop{
cgroupRoot: cgroupRoot,
}, nil
}
return &qosContainerManagerImpl{
// 这里是挂载了的cgroup子系统
subsystems: subsystems,
cgroupManager: cgroupManager,
// 这里再ContainerManager中已经设置为了`kubepods`
cgroupRoot: cgroupRoot,
qosReserved: nodeConfig.QOSReserved,
}, nil
}
再ContainerManager创建了kubepods目录后,并且设置了对应的limit,然后就会来启动QoSContainerManager,启动这个会干啥呢
func (m *qosContainerManagerImpl) Start(getNodeAllocatable func() v1.ResourceList, activePods ActivePodsFunc) error {
cm := m.cgroupManager
// 这里再ContainerManager中已经初始化为了kubepods,并且完成了创建,所以这里检测存在的时候是已经存在了的
rootContainer := m.cgroupRoot
if !cm.Exists(rootContainer) {
return fmt.Errorf("root container %v doesn't exist", rootContainer)
}
// 为Burstable和BestEffort类型的QoS分别创建新的CgroupRoot
qosClasses := map[v1.PodQOSClass]CgroupName{
"Burstable": NewCgroupName(rootContainer, strings.ToLower(string("Burstable"))),
"BestEffort": NewCgroupName(rootContainer, strings.ToLower(string("BestEffort"))),
}
// Create containers for both qos classes
for qosClass, containerName := range qosClasses {
resourceParameters := &ResourceConfig{}
// BestEffort 类型的 CpuShares 设置为 最小值 2
if qosClass == v1.PodQOSBestEffort {
minShares := uint64(2)
resourceParameters.CpuShares = &minShares
}
// containerConfig object stores the cgroup specifications
containerConfig := &CgroupConfig{
Name: containerName,
ResourceParameters: resourceParameters,
}
// 同样的设置内存大页,这里这是的是无无限,int64的最大值
m.setHugePagesUnbounded(containerConfig)
// 检查kubepods.slice/kubepods-besteffort.slice 路径是否存在
// 检查kubepods.slice/kubepods-burstable.slice 路径是否存在
if !cm.Exists(containerName) {
// 不存在的需要创建对应的Cgroup路径
if err := cm.Create(containerConfig); err != nil {
}
} else {
...
}
}
// 分别设置三种QoS类型的CgroupRoot路径
m.qosContainersInfo = QOSContainersInfo{
Guaranteed: rootContainer,
Burstable: qosClasses[v1.PodQOSBurstable],
BestEffort: qosClasses[v1.PodQOSBestEffort],
}
m.getNodeAllocatable = getNodeAllocatable
m.activePods = activePods
// update qos cgroup tiers on startup and in periodic intervals
// to ensure desired state is in sync with actual state.
go wait.Until(func() {
err := m.UpdateCgroups()
}, 1min, wait.NeverStop)
return nil
}
可以看到QoSContainerManager的工作原理和ContainerManager基本一致,只是它会维护不同QoS对应不同的CgroupRoot路径
先简单回顾下kubelet是怎么定义QoS的呢?
-
如果每个容器都没设置limit和request,那么对应的QoS为
BestEffort -
如果所有容器的CPU limit之和 与所有容器的CPU request之和相等、内存也相等,那么对应的QoS为
Guaranteed -
其它情况为
Burstable
QoSContainerManager会为besteffort和burstable这两种类型的QoS创建额外的CgroupRoot路径,Guaranteed类型的QoS则不会创建,以CPU子系统为例,我们通常可以看到如下的目录划分
besteffort和burstable这两种类型的QoS对应的POD会分别创建到对应的目录下,而Guaranteed类型的QoS的POD会直接创建在kubepods.slice目录下
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice# ls -la
total 0
-rw-r--r-- 1 root root 0 8月 4 12:09 cgroup.clone_children
-rw-r--r-- 1 root root 0 8月 4 12:09 cgroup.procs
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.stat
-rw-r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_all
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_percpu
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_sys
-r--r--r-- 1 root root 0 8月 4 12:09 cpuacct.usage_user
-rw-r--r-- 1 root root 0 8月 4 12:09 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 8月 4 12:09 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 8月 2 10:02 cpu.shares
-r--r--r-- 1 root root 0 8月 4 12:09 cpu.stat
drwxr-xr-x 6 root root 0 8月 4 12:09 kubepods-besteffort.slice
drwxr-xr-x 19 root root 0 8月 4 12:09 kubepods-burstable.slice
drwxr-xr-x 4 root root 0 8月 4 12:09 kubepods-pod3001e7c4_5ad8_4bc5_831f_12ecdc006a09.slice
drwxr-xr-x 5 root root 0 8月 4 12:09 kubepods-pod36e2ba78_cd77_4527_89fd_f50e294e12db.slice
drwxr-xr-x 4 root root 0 8月 4 12:09 kubepods-pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa.slice
-rw-r--r-- 1 root root 0 8月 4 12:09 notify_on_release
-rw-r--r-- 1 root root 0 8月 4 12:09 tasks
对应的CgroupRoot路径创建完成后,会进行资源的初始化工作,besteffort类型的CpuShares 会设置为2,besteffort和burstable类型的内存大页都会设置为无上限,即int64的最大值
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice# cat kubepods-besteffort.slice/cpu.shares
2
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice# cat ../../hugetlb/kubepods.slice/kubepods-besteffort.slice/hugetlb.2MB.limit_in_bytes
4611686018427387904
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice# cat ../../hugetlb/kubepods.slice/kubepods-besteffort.slice/hugetlb.1GB.limit_in_bytes
4611686018427387904
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice# cat ../../hugetlb/kubepods.slice/kubepods-burstable.slice/hugetlb.1GB.limit_in_bytes
4611686018427387904
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice# cat ../../hugetlb/kubepods.slice/kubepods-burstable.slice/hugetlb.2MB.limit_in_bytes
4611686018427387904
最后会启动一个定时任务对Cgroup路径下的资源进行动态更新,更新周期为1min
func (m *qosContainerManagerImpl) UpdateCgroups() error {
m.Lock()
defer m.Unlock()
qosConfigs := map[v1.PodQOSClass]*CgroupConfig{
v1.PodQOSBurstable: {
Name: m.qosContainersInfo.Burstable,
ResourceParameters: &ResourceConfig{},
},
v1.PodQOSBestEffort: {
Name: m.qosContainersInfo.BestEffort,
ResourceParameters: &ResourceConfig{},
},
}
// 获取节点上所有Burstable 类型POD的CPU request资源总和 作为 `kubepods-burstable.slice`中的CpuShares
// `kubepods-besteffort.slice` 中的CpuShares一直维持在 2
if err := m.setCPUCgroupConfig(qosConfigs); err != nil {
}
// 维持内存大页为int64最大值
if err := m.setHugePagesConfig(qosConfigs); err != nil {
}
// 执行更新
for _, config := range qosConfigs {
err := m.cgroupManager.Update(config)
}
return nil
}
总结一下QosContainerManager的工作流程
-
在
ContainerManager创建完成kubepods.slice目录并且设置了limit后,会启动QosContainerManager -
QosContainerManager会为besteffort和burstable这两种类型的QoS创建额外的CgroupRoot路径,分别为kubepods-besteffort.slice和kubepods-burstable.slice -
Guaranteed这种QoS类型的POD会直接创建在kubepods.slice目录下 -
besteffort这种QoS类型的CpuShares会维持在最小值2,内存大页会维持在int64的最大值,依赖定时任务每分钟进行更新 -
burstable这种QoS类型的CpuShares会维持在所有burstable类型POD的request值总和,内存大页会维持在int64的最大值,依赖定时任务每分钟进行更新
POD创建时的Cgroup维护
在上一篇kubelet创建POD(https://www.jianshu.com/p/16f7dbccfadd)文中,在最后的syncPod()处理中,对POD的status完成生成和PATCH之后,就会为POD创建Cgroup路径了
我们具体看下是怎么创建的
// 先创建`PodContainerManager`
pcm := kl.containerManager.NewPodContainerManager()
// 非终止状态的POD
if !kl.podIsTerminated(pod) {
// 如果是第一次出现的POD,第一次出现的POD不会有Running状态
firstSync := true
for _, containerStatus := range apiPodStatus.ContainerStatuses {
if containerStatus.State.Running != nil {
firstSync = false
break
}
}
// 针对kubelet重启的情况,已经存在Cgroup路径并且Running状态的容器需要立即重启
podKilled := false
if !pcm.Exists(pod) && !firstSync {
if err := kl.killPod(pod, nil, podStatus, nil); err == nil {
podKilled = true
}
}
// 没被重启的容器并且不是运行一次的容器,需要维持其cgroup路径
if !(podKilled && pod.Spec.RestartPolicy == v1.RestartPolicyNever) {
// 检查POD的cgroup路径是否存在
if !pcm.Exists(pod) {
// 不存在的需要先更新一次`besteffort`和`burstable`这两种QoS类型的资源统计
if err := kl.containerManager.UpdateQOSCgroups(); err != nil {
}
// 再创建这个POD的cgroup路径,并进行资源限制
if err := pcm.EnsureExists(pod); err != nil {
}
}
}
}
我们先看下创建PodContainerManager
func (cm *containerManagerImpl) NewPodContainerManager() PodContainerManager {
// CgroupsPerQOS 默认就是true
if cm.NodeConfig.CgroupsPerQOS {
return &podContainerManagerImpl{
qosContainersInfo: cm.GetQOSContainersInfo(),
subsystems: cm.subsystems,
cgroupManager: cm.cgroupManager,
// 默认值 -1
podPidsLimit: cm.ExperimentalPodPidsLimit,
// 默认值 true
enforceCPULimits: cm.EnforceCPULimits,
// 默认值 100ms
cpuCFSQuotaPeriod: uint64(cm.CPUCFSQuotaPeriod / time.Microsecond),
}
}
return &podContainerManagerNoop{
cgroupRoot: cm.cgroupRoot,
}
}
可以看到本质和前面的ContainerManager或者QosContainerManager是一样的
再看下检测POD的cgroup路径
func (m *podContainerManagerImpl) Exists(pod *v1.Pod) bool {
// 获取POD的cgroup名称
podContainerName, _ := m.GetPodContainerName(pod)
// 和前面一样的检测方式,检测这个cgroup名称对应的路径是否存在
return m.cgroupManager.Exists(podContainerName)
}
func (m *podContainerManagerImpl) GetPodContainerName(pod *v1.Pod) (CgroupName, string) {
// 获取POD的QoS
podQOS := v1qos.GetPodQOS(pod)
// 不同QoS对应不同的CgroupRoot
var parentContainer CgroupName
switch podQOS {
case v1.PodQOSGuaranteed:
parentContainer = m.qosContainersInfo.Guaranteed
case v1.PodQOSBurstable:
parentContainer = m.qosContainersInfo.Burstable
case v1.PodQOSBestEffort:
parentContainer = m.qosContainersInfo.BestEffort
}
// 获取POD的UID,然后拼接前缀pod
podContainer := GetPodCgroupNameSuffix(pod.UID)
// 将名称 `pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa`也拼接到CgroupRoot上去
cgroupName := NewCgroupName(parentContainer, podContainer)
// Get the literal cgroupfs name
cgroupfsName := m.cgroupManager.Name(cgroupName)
return cgroupName, cgroupfsName
}
可以看到就是再QoS的CgroupRoot的基础上扩展了pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa这一串和POD的UID相关的路径
这样处理后,不同QoS的POD对应的路径就不同了
-
Guaranteed这种QoS类型的POD,对应的CgroupRoot是kubepods,在这里处理后CgroupRoot变成了[kubepods, pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa],然后转换为systemd风格后,路径变为kubepods.slice/kubepods-pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa.slice -
Burstable这种QoS类型的POD,对应的CgroupRoot是[kubepods, kubepods-burstable],在这里处理后CgroupRoot变成了[kubepods, ,kubepods-burstable, pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa],然后转换为systemd风格后,路径变为kubepods.slice/kubepods-burstable.slice/kubepods-pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa.slice -
BestEffort这种QoS类型的POD,对应的CgroupRoot是[kubepods, kubepods-besteffort],在这里处理后CgroupRoot变成了[kubepods, kubepods-besteffort, pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa],然后转换为systemd风格后,路径变为kubepods.slice/kubepods-besteffort.slice/kubepods-pod9fc575d3_a7d2_4054_bf55_02b58ac99bfa.slice
然后就是创建POD的CgroupRoot路径了
func (m *podContainerManagerImpl) EnsureExists(pod *v1.Pod) error {
// 获取POD对应的CgroupRoot
podContainerName, _ := m.GetPodContainerName(pod)
// 检查路径是否存在
alreadyExists := m.Exists(pod)
if !alreadyExists {
// Create the pod container
containerConfig := &CgroupConfig{
Name: podContainerName,
ResourceParameters: ResourceConfigForPod(pod, m.enforceCPULimits, m.cpuCFSQuotaPeriod),
}
// 默认值是-1
if m.podPidsLimit > 0 {
containerConfig.ResourceParameters.PidsLimit = &m.podPidsLimit
}
// 创建这个CgroupRoot路径,并设置资源限额
if err := m.cgroupManager.Create(containerConfig); err != nil {
return fmt.Errorf("failed to create container for %v : %v", podContainerName, err)
}
}
return nil
}
其中路径的创建和设置资源限额和前面QoSContainerManager的处理是完全一样的
我们重点看下POD资源是怎么转换为cgroup中的资源的
func ResourceConfigForPod(pod *v1.Pod, enforceCPULimits bool, cpuPeriod uint64) *ResourceConfig {
// POD中所有容器的request和limit资源求和
reqs, limits := resource.PodRequestsAndLimits(pod)
cpuRequests := int64(0)
cpuLimits := int64(0)
memoryLimits := int64(0)
if request, found := reqs[v1.ResourceCPU]; found {
cpuRequests = request.MilliValue()
}
if limit, found := limits[v1.ResourceCPU]; found {
cpuLimits = limit.MilliValue()
}
if limit, found := limits[v1.ResourceMemory]; found {
memoryLimits = limit.Value()
}
// CPU request转换为 CpuShares
cpuShares := MilliCPUToShares(cpuRequests)
// CPU limit转换为 CpuQuota
cpuQuota := MilliCPUToQuota(cpuLimits, int64(cpuPeriod))
// 是否存在容器没有设置limit,获取内存大页的request资源
memoryLimitsDeclared := true
cpuLimitsDeclared := true
hugePageLimits := map[int64]int64{}
for _, container := range pod.Spec.Containers {
if container.Resources.Limits.Cpu().IsZero() {
cpuLimitsDeclared = false
}
if container.Resources.Limits.Memory().IsZero() {
memoryLimitsDeclared = false
}
containerHugePageLimits := HugePageLimits(container.Resources.Requests)
for k, v := range containerHugePageLimits {
if value, exists := hugePageLimits[k]; exists {
hugePageLimits[k] = value + v
} else {
hugePageLimits[k] = v
}
}
}
// determine the qos class
qosClass := v1qos.GetPodQOS(pod)
// 根据QoS返回资源限额
result := &ResourceConfig{}
if qosClass == v1.PodQOSGuaranteed {
result.CpuShares = &cpuShares
result.CpuQuota = &cpuQuota
result.CpuPeriod = &cpuPeriod
result.Memory = &memoryLimits
} else if qosClass == v1.PodQOSBurstable {
result.CpuShares = &cpuShares
if cpuLimitsDeclared {
result.CpuQuota = &cpuQuota
result.CpuPeriod = &cpuPeriod
}
if memoryLimitsDeclared {
result.Memory = &memoryLimits
}
} else {
shares := uint64(MinShares)
result.CpuShares = &shares
}
result.HugePageLimit = hugePageLimits
return result
}
POD的资源限额和QoS类型有关
-
Guaranteed类型的POD,cpu request转换为CpuShares、cpu limit 转换为 CpuQuota、cpu时钟周期固定为100ms、内存limit转换为 Memory -
Burstable类型的POD,cpu request转换为CpuShares、如果所有容器都设置了cpu和内存 limit,则也会设置相应的值 -
BestEffort类型的POD,固定CpuShares为最小值2
我们以一个例子总结说明下
假设我这有个如下的POD资源限额
resources:
limits:
cpu: 100m
memory: 50Mi
requests:
cpu: 100m
memory: 50Mi
可以看到这是个Guaranteed类型的POD,那么会有下面的资源设置
#1. cgroup路径应该为这个
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-pod3001e7c4_5ad8_4bc5_831f_12ecdc006a09.slice#
#2. CpuShares应该为102,100*1024/1000=102 1024/1000前面已经提过了是转换比例
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-pod3001e7c4_5ad8_4bc5_831f_12ecdc006a09.slice# cat cpu.shares
102
#3. CpuQuota应该为10000, 100*100000/1000=10000 100000/1000 是转换比例,对应的是CPU的100ms周期内容器只能使用10ms的CPU时间
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-pod3001e7c4_5ad8_4bc5_831f_12ecdc006a09.slice# cat cpu.cfs_quota_us
10000
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-pod3001e7c4_5ad8_4bc5_831f_12ecdc006a09.slice# cat cpu.cfs_period_us
100000
#4. 内存limit应该为52428800byte,50*1024*1024=52428800 byte
root@ubuntu:/sys/fs/cgroup/memory/kubepods.slice/kubepods-pod3001e7c4_5ad8_4bc5_831f_12ecdc006a09.slice# cat memory.limit_in_bytes
52428800
#5. 没有内存大页限制
root@ubuntu:/sys/fs/cgroup/hugetlb/kubepods.slice/kubepods-pod3001e7c4_5ad8_4bc5_831f_12ecdc006a09.slice# cat hugetlb.2MB.limit_in_bytes
0
root@ubuntu:/sys/fs/cgroup/hugetlb/kubepods.slice/kubepods-pod3001e7c4_5ad8_4bc5_831f_12ecdc006a09.slice# cat hugetlb.1GB.limit_in_bytes
0
POD的资源限额和QoS类型有关
-
Guaranteed类型的POD,cpu request转换为CpuShares、cpu limit 转换为 CpuQuota、cpu时钟周期固定为100ms、内存limit转换为 Memory -
Burstable类型的POD,cpu request转换为CpuShares、如果所有容器都设置了cpu和内存 limit,则也会设置相应的值 -
BestEffort类型的POD,固定CpuShares为最小值2
我们以一个例子总结说明下
假设我这有个如下的POD资源限额
resources:
limits:
cpu: 200m
memory: 200Mi
requests:
cpu: 100m
memory: 100Mi
可以看到这是个Burstable类型的POD,那么会有下面的资源设置
#1. cgroup路径应该为这个
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod1c6e95d0_1af7_4665_80f6_59e8fe5d04f6.slice#
#2. CpuShares应该为102,100*1024/1000=102 1024/1000前面已经提过了是转换比例
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod1c6e95d0_1af7_4665_80f6_59e8fe5d04f6.slice# cat cpu.shares
102
#3. CpuQuota应该为20000, 200*100000/1000=10000 100000/1000 是转换比例,对应的是CPU的100ms周期内容器只能使用20ms的CPU时间
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod1c6e95d0_1af7_4665_80f6_59e8fe5d04f6.slice# cat cpu.cfs_quota_us
20000
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod1c6e95d0_1af7_4665_80f6_59e8fe5d04f6.slice# cat cpu.cfs_period_us
100000
#4. 内存limit应该为209715200byte,200*1024*1024=52428800 byte
root@ubuntu:/sys/fs/cgroup/memory/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod1c6e95d0_1af7_4665_80f6_59e8fe5d04f6.slice# cat memory.limit_in_bytes
209715200
假设我这有个如下的POD资源限额,即request和limit都没设置
resources:
可以看到这是个BestEffort类型的POD,那么会有下面的资源设置
#1. cgroup路径应该为这个
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod26fdf191_388d_45dc_a948_4d6aae3fe21c.slice#
#2. CpuShares应该为102,100*1024/1000=102 1024/1000前面已经提过了是转换比例
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod26fdf191_388d_45dc_a948_4d6aae3fe21c.slice# cat cpu.shares
2
#3. CpuQuota不限制
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod26fdf191_388d_45dc_a948_4d6aae3fe21c.slice# cat cpu.cfs_quota_us
-1
root@ubuntu:/sys/fs/cgroup/cpu/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod26fdf191_388d_45dc_a948_4d6aae3fe21c.slice# cat cpu.cfs_period_us
100000
#4. 内存limit不限制
root@ubuntu:/sys/fs/cgroup/memory/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod26fdf191_388d_45dc_a948_4d6aae3fe21c.slice# cat memory.limit_in_bytes
9223372036854771712
附录
上面设置的CPU资源可能会有点难理解,我们这里加多下解释:
-
cpu.shares对应的是CPU软限制,它的值相当于是权重,在CPU较忙时会按照这个权重分配CPU时间片,CPU不忙时它也可以使用超过该权重的CPU时间 -
BestEffort类型的POD设置的权重都是2,权重很低,调度优先级就低 -
cpu.cfs_quota_us对应的是CPU硬限制,它的值是相对于cpu.cfs_period_us的,即在CPU的调度周期内能够执行多长时间,在进程使用较多CPU时,会被强行抑制,到下个周期才能再分配时间片
上面有个逻辑去维护/proc/$PID/oom_score_adj的值一直为-999;
这个作用是啥呢?其实就是防止kubelet进程被OOM干掉,设置的值越小越不会被OOM kill;参考(https://learning-kernel.readthedocs.io/en/latest/mem-management.html)









网友评论