Docker Cpu资源隔离

概述

docker提供对CPU资源的限制,主要基于cgroup的cpuset来实现;到v1.13版本后,主要演化成以下几个参数。

  • –cpus 指定容器所需的cpu核数;其实现方式只是指定的对应cpu核心数相对的资源总量,容器可能会在多个核心上运行,可能涉及到上下文切换的消耗;
  • –cpu-shares 指定多个容器抢占有限的资源时能够分配到cpu资源的权重;需要搭配着其他两个命令一起使用,单纯使用该命令,或在没有资源抢占的情况下使用该命令没有意义;
  • –cpuset-cpus 指定容器绑定到具体哪些cpu核上运行;资源隔离相对较好,但也可能存在多个容器同时被绑定到同一个核上的情况。 宿主机上有4core:
1
2
# cat /proc/cpuinfo | grep processor | wc -l
4

–cpu-shares参数

该选项用来设置CPU权重,它的默认值为1024。我们可以把它设置为1表示很低的权重,但是设置为0将表示使用默认值1024。它提供对抢占资源时,cpu的分配份额。

在只有一个进程使用资源时,cpu-shares没有意义,资源直接被容器消耗完。

1
2
3
4
5
6
7
8
9
10
11
# docker run --rm --cpu-shares=512 -it progrium/stress -c 4

# top
top - 19:23:51 up 2:25, 7 users, load average: 1.60, 1.46, 0.95
Tasks: 181 total, 6 running, 175 sleeping, 0 stopped, 0 zombie
%Cpu0 : 98.0/1.7 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||]
%Cpu1 : 97.7/2.0 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||]
%Cpu2 : 95.3/4.0 99[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ]
%Cpu3 : 98.0/1.4 99[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ]
KiB Mem : 8009496 total, 4529720 free, 777820 used, 2701956 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 6832780 avail Mem

–cpus参数

指定容器使用的cpu数,在资源够分配时,它对资源使用总量的限制较准确;当资源不够出现超卖时,其无法保障能够获取到对应的CPU资源数。

资源足够时

1
2
3
4
5
6
7
8
9
10
11
# docker run --rm --cpus=1 -it progrium/stress -c 4

# top
top - 19:15:52 up 2:17, 7 users, load average: 0.08, 0.18, 0.45
Tasks: 179 total, 5 running, 174 sleeping, 0 stopped, 0 zombie
%Cpu0 : 24.0/1.7 26[|||||||||||||||||||||||||| ]
%Cpu1 : 23.9/2.0 26[|||||||||||||||||||||||||| ]
%Cpu2 : 24.3/1.7 26[|||||||||||||||||||||||||| ]
%Cpu3 : 23.4/1.4 25[|||||||||||||||||||||||| ]
KiB Mem : 8009496 total, 4532328 free, 776500 used, 2700668 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 6834536 avail Mem

结果显示,此时指定cpus的结果,相当于分配了1个core给stress运行。

超卖资源时

无–cpu-shares

以下命令相当于指定了6 core CPU,此时超过了我们宿主机的cpu core总数4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# docker run --rm --cpus=4 -it progrium/stress -c 4
# docker run --rm --cpus=2 -it progrium/stress -c 4

# top
top - 19:31:26 up 2:33, 7 users, load average: 1.70, 1.42, 1.14
Tasks: 188 total, 9 running, 179 sleeping, 0 stopped, 0 zombie
%Cpu0 : 98.3/1.3 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ]
%Cpu1 : 97.3/2.0 99[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ]
%Cpu2 : 96.6/3.0 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||]
%Cpu3 : 98.6/1.0 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||]
KiB Mem : 8009496 total, 4515312 free, 790772 used, 2703412 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 6819260 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
7615 root 20 0 7304 92 0 R 63.5 0.0 0:03.34 stress
7613 root 20 0 7304 92 0 R 51.5 0.0 0:02.55 stress
7616 root 20 0 7304 92 0 R 48.8 0.0 0:02.48 stress
7614 root 20 0 7304 92 0 R 42.2 0.0 0:02.14 stress

7558 root 20 0 7304 100 0 R 48.5 0.0 0:03.32 stress
7557 root 20 0 7304 100 0 R 47.5 0.0 0:03.26 stress
7560 root 20 0 7304 100 0 R 43.2 0.0 0:03.01 stress
7559 root 20 0 7304 100 0 R 42.5 0.0 0:03.02 stress

有–cpu-shares(貌似效果也不好)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# docker run --rm --cpus=2 --cpu-shares=512 -it progrium/stress -c 4
# docker run --rm --cpus=4 --cpu-shares=1024 -it progrium/stress -c 4

# top
top - 19:34:12 up 2:35, 7 users, load average: 2.81, 1.66, 1.27
Tasks: 185 total, 9 running, 176 sleeping, 0 stopped, 0 zombie
%Cpu0 : 97.3/2.3 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ]
%Cpu1 : 97.7/2.0 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||]
%Cpu2 : 96.3/3.7 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||]
%Cpu3 : 98.3/1.3 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ]
KiB Mem : 8009496 total, 4518760 free, 787184 used, 2703552 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 6822896 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8689 root 20 0 7304 96 0 R 84.4 0.0 0:08.85 stress
8687 root 20 0 7304 96 0 R 70.4 0.0 0:08.37 stress
8686 root 20 0 7304 96 0 R 57.5 0.0 0:08.04 stress
8688 root 20 0 7304 96 0 R 51.5 0.0 0:07.77 stress

8744 root 20 0 7304 100 0 R 42.2 0.0 0:03.20 stress
8745 root 20 0 7304 100 0 R 29.6 0.0 0:02.49 stress
8747 root 20 0 7304 100 0 R 27.9 0.0 0:02.54 stress
8746 root 20 0 7304 100 0 R 26.2 0.0 0:02.35 stress

–cpuset-cpus参数

该参数用来绑定容器使用的CPU core,对资源的限制较精确,但是需要外部维护绑定关系。当有多个容器都绑定到同一个core上时,需要借助–cpu-shares来分配份额。

资源抢占时

当不指定份额时,会平均分配资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# docker run --rm --cpuset-cpus=0  -it progrium/stress -c 1
# docker run --rm --cpuset-cpus=0 -it progrium/stress -c 1

# top
top - 19:40:01 up 2:41, 6 users, load average: 1.86, 1.67, 1.37
Tasks: 181 total, 3 running, 178 sleeping, 0 stopped, 0 zombie
%Cpu0 : 100.0/0.0 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||]
%Cpu1 : 1.0/1.4 2[|| ]
%Cpu2 : 1.4/1.7 3[||| ]
%Cpu3 : 0.3/1.0 1[| ]
KiB Mem : 8009496 total, 4516292 free, 788620 used, 2704584 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 6821116 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
11210 root 20 0 7304 92 0 R 49.8 0.0 0:12.24 stress
11329 root 20 0 7304 96 0 R 49.8 0.0 0:05.64 stress

有–cpu-shares

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# docker run --rm --cpuset-cpus=0 --cpu-shares=512 -it progrium/stress -c 1
# docker run --rm --cpuset-cpus=0 --cpu-shares=1024 -it progrium/stress -c 1

# top
top - 19:38:25 up 2:40, 7 users, load average: 2.89, 1.69, 1.34
Tasks: 182 total, 3 running, 179 sleeping, 0 stopped, 0 zombie
%Cpu0 : 99.5/0.0 100[||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||]
%Cpu1 : 0.7/1.7 2[||| ]
%Cpu2 : 1.0/1.4 2[|| ]
%Cpu3 : 0.7/1.4 2[|| ]
KiB Mem : 8009496 total, 4519984 free, 784588 used, 2704924 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 6824732 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10555 root 20 0 7304 96 0 R 66.8 0.0 0:06.90 stress
10618 root 20 0 7304 96 0 R 33.2 0.0 0:02.32 stress

–cpu-period & –cpu-quota

docker提供–cpu-period、–cpu-quota两个参数控制容器可以分配到的CPU时钟周期。

--cpu-period 用来指定容器对CPU的使用要在多长时间内做一次重新分配;
--cpu-quota 用来指定在这个周期内,最多可以有多少时间用来跑这个容器。跟–cpu-shares不同的是这种配置是指定一个绝对值,而且没有弹性在里面,容器对CPU资源的使用绝对不会超过配置的值。

–cpu-period和–cpu-quota的单位为微秒(μs)。–cpu-period的最小值为1000微秒,最大值为1秒(10^6 μs),默认值为0.1秒(100000 μs)。–cpu-quota的值默认为-1,表示不做控制。

举个例子,如果容器进程需要每1秒使用单个CPU的0.2秒时间,可以将–cpu-period设置为1000000(即1秒),–cpu-quota设置为200000(0.2秒)。当然,在多核情况下,如果允许容器进程需要完全占用两个CPU,则可以将–cpu-period设置为100000(即0.1秒),–cpu-quota设置为200000(0.2秒)。

K8S 代码实现

request 被转化为 —cpu-share 参数

  • 如果request=0 && limit !=0, —cpu-shares=limit;
  • 如果 request != 0, —cpu-shares=request.

limit 被转化为 –cpu-quota 参数

  • –cpu-preiod被强制设置为100毫秒;
  • –cpu-quota = Limit * 100毫秒,但是最小为1毫秒.

在kubelet创建container的时候,kuberuntime_container.go中的类kubeGenericRuntimeManager会调用generateContainerConfig来生成创建container的配置文件(从k8s yaml中指定对resources的配置转化而来)。

绝大多数的参数在不同操作系统之间都是一样的,但是对资源的限制这块,Linux和Windows有自己不同的参数。因此该类会继续调用 applyPlatformSpecificContainerConfig方法, 最终到 generateLinuxContainerConfig来为linux系统转化参数,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// applyPlatformSpecificContainerConfig applies platform specific configurations to runtimeapi.ContainerConfig.
func (m *kubeGenericRuntimeManager) applyPlatformSpecificContainerConfig(config *runtimeapi.ContainerConfig, container *v1.Container, pod *v1.Pod, uid *int64, username string) error {
config.Linux = m.generateLinuxContainerConfig(container, pod, uid, username)
return nil
}

// generateLinuxContainerConfig generates linux container config for kubelet runtime v1.
func (m *kubeGenericRuntimeManager) generateLinuxContainerConfig(container *v1.Container, pod *v1.Pod, uid *int64, username string) *runtimeapi.LinuxContainerConfig {
lc := &runtimeapi.LinuxContainerConfig{
Resources: &runtimeapi.LinuxContainerResources{},
SecurityContext: m.determineEffectiveSecurityContext(pod, container, uid, username),
}

// set linux container resources
var cpuShares int64
cpuRequest := container.Resources.Requests.Cpu()
cpuLimit := container.Resources.Limits.Cpu()
memoryLimit := container.Resources.Limits.Memory().Value()
oomScoreAdj := int64(qos.GetContainerOOMScoreAdjust(pod, container,
int64(m.machineInfo.MemoryCapacity)))
// If request is not specified, but limit is, we want request to default to limit.
// API server does this for new containers, but we repeat this logic in Kubelet
// for containers running on existing Kubernetes clusters.
if cpuRequest.IsZero() && !cpuLimit.IsZero() {
cpuShares = milliCPUToShares(cpuLimit.MilliValue())
} else {
// if cpuRequest.Amount is nil, then milliCPUToShares will return the minimal number
// of CPU shares.
cpuShares = milliCPUToShares(cpuRequest.MilliValue())
}
lc.Resources.CpuShares = cpuShares
if memoryLimit != 0 {
lc.Resources.MemoryLimitInBytes = memoryLimit
}
// Set OOM score of the container based on qos policy. Processes in lower-priority pods should
// be killed first if the system runs out of memory.
lc.Resources.OomScoreAdj = oomScoreAdj

if m.cpuCFSQuota {
// if cpuLimit.Amount is nil, then the appropriate default value is returned
// to allow full usage of cpu resource.
cpuQuota, cpuPeriod := milliCPUToQuota(cpuLimit.MilliValue())
lc.Resources.CpuQuota = cpuQuota
lc.Resources.CpuPeriod = cpuPeriod
}

return lc
}
0%