解析容器化技术中的资源管理


虚拟化与Docker

虚拟化

虚拟化(virtualization)技术是一个通用的概念,在不同领域有不同的理解。在计算领域,一般指的是计算虚拟化(computing virtualization),或通常说的服务器虚拟化。维基百科上的定义如下:


在计算机技术中,虚拟化是一种资源管理技术,是将计算机的各种实体资源(CPU、内存、磁盘空间、网络适配器等),予以抽象、转换后呈现出来并可供分割、组合为一个或多个计算机配置环境。由此,打破实体结构间的不可切割的障碍,使用户可以比原本的配置更好的方式来应用这些计算机硬件资源。
可见,虚拟化的核心是对资源的抽象,目标往往是为了在同一个主机上同时运行多个系统或应用,从而提高系统资源的利用率,并且带来降低成本、方便管理和容错容灾等好处。如果你想和更多 Docker 技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

Docker

Docker 是虚拟化技术的一种,也是目前使用比较多的一种,采用 Go 语言来开发引擎。

Docker 利用"集装箱"(容器)的原理,将系统、开发软件包、依赖环境等统一打包到容器中,将整个容器部署至其他的平台或者服务器上。

Docker 架构

Docker 使用 C/S (客户端/服务器)体系的架构,Docker 客户端与 Docker 守护进程通信,Docker 守护进程负责构建,运行和分发 Docker 容器。Docker 客户端和守护进程可以在同一个系统上运行,也可以将 Docker 客户端连接到远程 Docker 守护进程。Docker 客户端和守护进程使用 REST API 通过UNIX套接字或网络接口进行通信。
01.jpg

Docker Daemon:Docker 的服务端由多个服务配合完成:dockerd,用来监听 Docker API 的请求和管理 Docker 对象,比如镜像、容器、网络和 Volume;containerd 提供 gRPC 接口响应来自 dockerd 的请求,通过 container-shim 管理 runC 镜像和容器环境,runC 真正控制容器生命周期。containerd 是 containerd-shim 的父进程,contaienrd-shim 是容器进程的父进程。

Docker Client:docker、docker client 是我们和 Docker 进行交互的最主要的方式方法,比如我们可以通过 docker run 命令来运行一个容器,然后我们的这个 client 会把命令发送给上面的 dockerd,让他来做真正事情。

Docker Registry:用来存储 Docker 镜像的仓库,Docker Hub 是 Docker 官方提供的一个公共仓库,而且 Docker 默认也是从 Docker Hub 上查找镜像的,当然你也可以很方便的运行一个私有仓库,当我们使用 docker pull 或者 docker run 命令时,就会从我们配置的 Docker 镜像仓库中去拉取镜像,使用 docker push 命令时,会将我们构建的镜像推送到对应的镜像仓库中。

Images:镜像,镜像是一个只读模板,带有创建 Docker 容器的说明,一般来说的,镜像会基于另外的一些基础镜像并加上一些额外的自定义功能。比如,你可以构建一个基于 CentOS 的镜像,然后在这个基础镜像上面安装一个 Nginx 服务器,这样就可以构成一个属于我们自己的镜像了。

Containers:容器,容器是一个镜像的可运行的实例,可以使用 Docker REST API 或者 CLI 来操作容器,容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。

Docker与虚拟机比较

从概念上来看 Docker 和我们传统的虚拟机比较类似,只是更加轻量级,更加方便使用,下图展示了传统虚拟机与容器在系统层次上的不同:
02.jpg

可以看到,传统方式是在硬件层面上实现虚拟化,需要有额外的虚拟机管理应用和虚拟机操作系统层,Docker 容器是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,因此更加轻量级。

Linux命名空间

命名空间(namespace)是 Linux 内核的一个强大特性,为容器虚拟化的实现带来极大的便利。利用这一特性,每个容器都可以拥有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统环境中一样。

命名空间机制保证了容器之间彼此互不影响。

在操作系统中,包括内核、文件系统、网络、进程号(PID)、用户号(UID)、进程间通信(IPC)等资源,所有的资源都是应用进程直接共享的。要想实现虚拟化,除了要实现内存、CPU、网络IO、硬盘IO、存储空间等的限制外,还要实现文件系统、网络、PID、UID、IPC等的互相隔离。

进程命名空间

Linux 通过进程命名空间管理进程号,对于同一进程,在不同的命名空间中,看到的进程号不相同。进程命名空间是一个父子关系的结构,子空间中的进程对于父空间中的进程是可见的。新fork出的一个进程,在父命名空间和子命名空间将分别对应不同的进程号。

举个例子,新建一个Ubuntu容器,执行sleep命令。
$ docker run --name ubuntu -d ubuntu:16.04 sleep 9999
088ce1db029ebeba1af3a36d48e0193441c8abb920abfc0306dc30aab825ff61

根据我们上面的分析,当使用 Docker 客户端执行 run 命令后,会使用 RESTful 接口向dockerd发起创建容器命令,然后 dockerd 会通过 gRPC 方式调用 containerd,让他帮我们创建一个容器。此时,containerd 进程作为父进程,会为每个容器启动一个 containerd-shim 进程,作为该容器内所有进程的根进程。

查看一下宿主机中 containerd 的进程号:
$ ps -ef | grep containerd
root      9165     1  0 3月08 ?       06:24:03 /usr/bin/containerd

然后查看一下进程树:
$ pstree -l -a -A 9165
containerd
|-containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/088ce1db029ebeba1af3a36d48e0193441c8abb920abfc0306dc30aab825ff61 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc
|   |-sleep 9999
|   `-9*[{containerd-shim}]
`-16*[{containerd}] 

我们再进到容器里面看一下进程号:
$ docker exec -it 088ce1 bash -c 'ps -ef'
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 13:16 ?        00:00:00 sleep 9999

可以看到,在容器内的进程空间中,把 containerd-shim 进程作为 0 号进程,并且只能看到 containerd-shim 进程往下的子进程空间,而无法获知宿主机上的进程信息。
03.jpg

网络命名空间

有了进程命名空间后,不同命名空间中的进程号可以互相隔离了,但是网络端口还是共享本地系统的端口。通过网络命名空间,可以实现网络隔离。一个网络命名空间为进程提供了一个完全独立的网络协议栈视图。包括网络设备、IPv4 和 IPv6 协议栈、IP路由表、防火墙规则、sockets 等,这样每个容器的网络就隔离开来。

Docker 采用虚拟网络设备的方式,将不同命名空间的网络设备连接到一起。默认情况下,Docker 会在宿主机上创建一个 docker0 网桥,实际上是 Linux 的一个 bridge,可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。

同时,Docker 随机分配一个本地未占用的私有网段中的一个地址给 docker0 接口。比如典型的 172.18.0.1,掩码为 255.255.0.0。此后启动的容器内的网口也会自动分配一个同一网段(172.18.0.0/16)的地址。

创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包 发送到一个接口时,另外一个接口也可以收到相同的数据包)。这对接口一端在容器内,即 eth0 ;另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 veth82487d6)。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。
04.jpg

例如我们创建一个容器:
$ docker run -d amouat/network-utils sleep 999
fe29bd04a38870d874b92dab68793ce5a834992d34f5293ed339cece8122a893

为了操纵容器的网络命名空间,首先查看容器中进程的PID:
$ docker top fe29bd04a38
UID    PID           PPID       C       STIME     TTY       TIME          CMD
root   58233      58213     0       11:43       ?            00:00:00   sleep 9999

看到是58233。

然后进入到该进程的命名空间目录:
$  ls -l /proc/58233/ns
lrwxrwxrwx 1 root root 0 5月  28 11:45 ipc -> ipc:[4026532201]
lrwxrwxrwx 1 root root 0 5月  28 11:45 mnt -> mnt:[4026532199]
lrwxrwxrwx 1 root root 0 5月  28 11:43 net -> net:[4026532204]
lrwxrwxrwx 1 root root 0 5月  28 11:45 pid -> pid:[4026532202]
lrwxrwxrwx 1 root root 0 5月  28 12:00 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 5月  28 11:45 uts -> uts:[4026532200]

可以看到该进程的网络命名空间编号是 4026532204。为了操作该命名空间,我们可以创建一个软连接:
$ ln -sf /proc/58233/ns/net "/var/run/netns/net_util_ns"

net_util_ns 是我们给该网络命名空间起的名字,可以自定义。之后,我们就可以针对这个命名空间名字操作容器的网络了:
$ ip netns exec net_util_ns ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
   valid_lft forever preferred_lft forever
247: eth0@if248: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
   valid_lft forever preferred_lft forever

可以看到该命名空间中有一个 veth pair 端点 247: eth0@if248,我们再看下宿主机:
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc pfifo_fast state UP qlen 1000
inet 10.xxx.xxx.xxx/x 
124: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:61:fa:42:fe brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 scope global docker0
248: veth82487d6@if247: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP
link/ether 16:41:63:ef:f5:74 brd ff:ff:ff:ff:ff:ff link-netnsid 0

其中包含了一个 veth pair 的端点,248: veth82487d6@if247,与容器中的 veth pair 端点对应。

查看宿主机网桥:
$ brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.024261fa42fe       no              veth82487d6

IPC命名空间

容器中的进程交互还是采用了 Linux 常见的进程间交互方式(Interprocess Communication,IPC),包括信号量、消息队列和共享内存等方式。

PID 命名空间和 IPC 命名空间可以组合起来一起使用,同一个 IPC 命名空间内的进程可以彼此可见,允许进行交互;不同空间的进程则无法交互。

挂载命名空间

类似于 chroot,挂载(Mount,MNT)命名空间可以将一个进程的根文件系统限制到一个特定的目录下。

挂载命名空间允许不同命名空间的进程看到的本地文件位于宿主机中不同路径下,每个命名空间中的进程所看到的文件目录彼此是隔离的。

UTS 命名空间

UTS(UNIX Time-sharing System)命名空间允许每个容器拥有独立的主机名和域名,从而可以虚拟出一个有独立主机名和网络空间的环境,就跟网络上一台独立的主机一样。

如果没有手动指定主机名,Docker 容器的主机名就是返回的容器 ID 的前 6 字节前缀,否则为指定的主机名:
$ docker run --name ubuntu -d ubuntu:16.04 sleep 9999
d5d5522ab224e67d384446469c0442b5edb28fce39d743cec8e3575168b26b78
$ docker inspect -f {{".Config.Hostname"}} ubuntu
d5d5522ab224
$ docker run --hostname container_ubuntu --name ubuntu -d ubuntu:16.04 sleep 9999
4d82294fa2841378cb40cfa1f2a9ed21785a8a9cc0da6c6d18bc357f5544e779
$ docker inspect -f {{".Config.Hostname"}} ubuntu
container_ubuntu
$ docker exec -it 4d82294fa28 bash -c 'hostname'
container_ubuntu

用户命名空间

每个容器可以有不同的用户和组 ID,也就是说,可以在容器内使用特定的内部用户执行程序,而非本地系统上存在的用户。

每个容器内部都可以有最高权限的 root 账号,但跟宿主机不在一个命名空间。通过使用隔离的用户命名空间,可以提高安全性,避免容器内的进程获取到额外的权限;同事通过使用不同用户也可以进一步在容器内控制权限。

控制组

在使用 Docker 运行容器时,一台主机上可能会运行几百个容器,这些容器虽然互相隔离,但是底层却使用着相同的 CPU、内存和磁盘资源。如果不对容器使用的资源进行限制,那么容器之间会互相影响,小的来说会导致容器资源使用不公平;大的来说,可能会导致主机和集群资源耗尽,服务完全不可用。

正如使用内核的 namespace 来做容器之间的隔离,Docker 也是通过内核的 cgroups 来做容器的资源限制。

控制组是 Linux 内核的特性,主要用来对共享资源进行隔离、限制、审计等。

每个控制组是一组对资源的限制,支持层级化结构。我们查看一下系统支持的控制组:
$ cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset                  6                    62                        1
cpu                       7                    250                      1
cpuacct                7                    250                      1
memory                8                    251                      1
devices                10                  251                       1
freezer                 5                    62                        1
net_cls                 4                    62                        1
blkio                     9                    250                      1
perf_event           2                    62                        1
hugetlb                3                    62                        1

cpuset:如果是多核心的 CPU,这个子系统会为 Cgroup 任务分配单独的 CPU 和内存。

CPU:使用调度程序为 Cgroup 任务提供 CPU 的访问。

cpuacct:产生 Cgroup 任务的 CPU 资源报告。

memory:设置每个 Cgroup 的内存限制以及产生内存资源报告。

devices:允许或拒绝 Cgroup 任务对设备的访问。

freezer: 暂停和恢复 Cgroup 任务。

net_cls:标记每个网络包以供 Cgroup 方便使用。

blkio:限制每个块设备的输入输出。例如:磁盘,光盘以及 usb 等等。

perf_event:增加了对每 group 的监测跟踪的能力,即可以监测属于某个特定的 group 的所有线程以及运行在特定 CPU 上的线程。

hugetlb:这个子系统主要针对于 HugeTLB 系统进行限制,这是一个大页文件系统。

下面我们将针对 CPU、内存和 IO 限制进行举例说明。

测试环境:

宿主机配置:
# 系统
$ uname -a
Linux docker 3.10.0-327.el7.x86_64 #1 SMP Thu Nov 19 22:10:57 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
# CPU
$ cat /proc/cpuinfo |grep "model name" && cat /proc/cpuinfo |grep "physical id"
model name      : Intel Core Processor (Broadwell)
model name      : Intel Core Processor (Broadwell)
model name      : Intel Core Processor (Broadwell)
model name      : Intel Core Processor (Broadwell)
physical id     : 0
physical id     : 1
physical id     : 2
physical id     : 3
# 内存
$ head -n 4 /proc/meminfo
MemTotal:        8003404 kB
MemFree:          733876 kB
MemAvailable:    4191768 kB
Buffers:          394644 kB

有 4 个 CPU 核心可用。

Docker 版本:
$ docker version
Client:
Version:           18.09.3
API version:       1.39
Go version:        go1.10.8
Git commit:        774a1f4
Built:             Thu Feb 28 06:33:21 2019
OS/Arch:           linux/amd64
Experimental:      false
Server: Docker Engine - Community
Engine:
Version:          18.09.3
API version:      1.39 (minimum version 1.12)
Go version:       go1.10.8
Git commit:       774a1f4
Built:            Thu Feb 28 06:02:24 2019
OS/Arch:          linux/amd64
Experimental:     false

测试时使用的容器为 polinux/stress,它提供一个压测任务,可以指定消耗的 CPU、内存的参数,基本使用方式如下:
docker run \
-ti \
--rm \
polinux/stress stress \
--cpu 1 \
--io 1 \
--vm 1 \
--vm-bytes 128M \
--timeout 1s \
--verbose

查看资源工具使用了 htop,类似于 top 命令,不过可以在控制台更直观的输出资源占用情况。

还使用了 Docker 客户端提供的 docker stats 命令查看容器的资源使用情况。

Docker 限制 CPU 资源

查看 Docker 创建容器时支持的 CPU 资源限制选项:
$ docker run --help | grep cpu
  --cpu-period int                 Limit CPU CFS (Completely Fair Scheduler) period
  --cpu-quota int                  Limit CPU CFS (Completely Fair Scheduler) quota
-c, --cpu-shares int                 CPU shares (relative weight)
  --cpus decimal                   Number of CPUs
  --cpuset-cpus string             CPUs in which to allow execution (0-3, 0,1) 

  • --cpu-shares 用来设置 CPU 在竞争时的权重比例,资源充足时没有效果
  • --cpuset-cpus 限制容器只能使用某几个核心
  • --cpu-period 和 --cpu-quota 一起使用来限制 CPU 使用上限,1.13 版本后,可以使用 —cpus 来代替


下面分别举例介绍一下。

限制容器 CPU 使用率

首先,我们尝试使用 —-cpus 来限制容器可用的 CPU 个数(个数这个单位可能不准确,因为 CPU 其实是按分片轮转的,这里可以理解为使用 CPU 百分比上限)。

假如不做限制,直接运行容器,配置使用 4 个 CPU,那么将把宿主机的 CPU 资源耗尽:
$ docker run --rm -it polinux/stress stress --cpu 4

使用 htop 工具查看 CPU 使用情况:
05.jpg

接下来使用 —-cpus 参数将容器的 CPU 限制在 1.5 个:
$ docker run --rm -it --cpus 1.5 polinux/stress stress --cpu 4

再次查看一下 CPU 使用情况:
06.jpg

4 个 CPU 核心使用率求和约为 150%,符合预期。

如果在 1.13 版本以前,需要还不支持 —-cpus 参数,那么需要使用 --cpu-period 和 --cpu-quota 配合来限制 CPU 使用率,他们的单位都是微妙,其中 --cpu-period 是调度周期,默认 100ms,--cpu-quota 为每隔 --cpu-period 分配给容器的 CPU 配额,那么上限可以用如下公式得到:
--cpu = --cpu-period / --cpu-quota

具体含义可以参考 CFS Bandwidth Control。

为了实现 150% 限制,可以使用如下方式:
$ docker run --rm -it --cpu-period=100000 --cpu-quota=150000 polinux/stress stress --cpu 4

查看一下 CPU 使用情况:
07.jpg

4 个 CPU 核心使用率求和约为 150%,符合预期。

下面我们查看一下该容器对应的 Cgroup 配置。与 Docker 的 CPU 相关的配置都在宿主机的 /sys/fs/cgroup/cpu/docker 目录下:
$ ls -l /sys/fs/cgroup/cpu/docker
drwxr-xr-x 2 root root 0 5月  28 20:08 1cc14f57a1ef4c04f9174f1d5c6b9d97b1a90a0d45de07371187065d95383071
-rw-r--r-- 1 root root 0 3月   8 17:27 cgroup.clone_children
--w--w--w- 1 root root 0 3月   8 17:27 cgroup.event_control
-rw-r--r-- 1 root root 0 3月   8 17:27 cgroup.procs
-r--r--r-- 1 root root 0 3月   8 17:27 cpuacct.stat
-rw-r--r-- 1 root root 0 3月   8 17:27 cpuacct.usage
-r--r--r-- 1 root root 0 3月   8 17:27 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 3月   8 17:27 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 3月   8 17:27 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 3月   8 17:27 cpu.rt_period_us
-rw-r--r-- 1 root root 0 3月   8 17:27 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 3月   8 17:27 cpu.shares
-r--r--r-- 1 root root 0 3月   8 17:27 cpu.stat
-rw-r--r-- 1 root root 0 3月   8 17:27 notify_on_release
-rw-r--r-- 1 root root 0 3月   8 17:27 tasks

之前我们说过,Cgroup 是有层级,该目录中的配置是 Docker 层级的总体配置,所有容器的限制总和。该目录中有一个 1cc14f57a1ef4c04f9174f1d5c6b9d97b1a90a0d45de07371187065d95383071 子目录,刚好是运行中的容器编号,进去查看一下:
$ ls -l /sys/fs/cgroup/cpu/docker/1cc14f57a1ef4c04f9174f1d5c6b9d97b1a90a0d45de07371187065d95383071
-rw-r--r-- 1 root root 0 5月  28 20:08 cgroup.clone_children
--w--w--w- 1 root root 0 5月  28 20:08 cgroup.event_control
-rw-r--r-- 1 root root 0 5月  28 20:08 cgroup.procs
-r--r--r-- 1 root root 0 5月  28 20:08 cpuacct.stat
-rw-r--r-- 1 root root 0 5月  28 20:08 cpuacct.usage
-r--r--r-- 1 root root 0 5月  28 20:08 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 5月  28 20:08 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 5月  28 20:08 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 5月  28 20:08 cpu.rt_period_us
-rw-r--r-- 1 root root 0 5月  28 20:08 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 5月  28 20:08 cpu.shares
-r--r--r-- 1 root root 0 5月  28 20:08 cpu.stat
-rw-r--r-- 1 root root 0 5月  28 20:08 notify_on_release
-rw-r--r-- 1 root root 0 5月  28 20:08 tasks

这里就是刚才运行的容器相关的 CPU 配置文件,我们关注的是 cpu.cfs_period_us 和 cpu.cfs_quota_us 两个文件:
$ cat /sys/fs/cgroup/cpu/docker/1cc14f57a1ef4c04f9174f1d5c6b9d97b1a90a0d45de07371187065d95383071/cpu.cfs_period_us
100000
$ cat /sys/fs/cgroup/cpu/docker/1cc14f57a1ef4c04f9174f1d5c6b9d97b1a90a0d45de07371187065d95383071/cpu.cfs_quota_us
150000

与我们配置的 --cpu-period 和 --cpu-quota 参数一致。

限制容器使用固定的 CPU

通过 --cpuset-cpus 选项可以让容器始终在一个或某几个 CPU 上运行,这是非常有意义的,因为现在的多核系统中每个核心都有自己的缓存,如果频繁的调度进程在不同的核心上执行势必会带来缓存失效等开销。

下面的命令为容器设置了--cpuset-cpus 选项,指定运行容器的 CPU 编号为 0:
$ docker run --rm -it --cpuset-cpus="0" polinux/stress stress --cpu 4

查看一下 CPU 使用情况:
08.jpg

可以看到,第一个 CPU 核心被占满,但是没有影响其他核的使用,符合预期。

查看对应 Cgroup 配置,目录 /sys/fs/cgroup/cpuset/docker/<docker_id>:
$ cat /sys/fs/cgroup/cpuset/docker/d0e5a00708ec10219195dd80e010c5b18369112ee5bb67f592153f3bf206e685/cpuset.cpus
0


设置使用CPU的权重

默认所有的容器对于 CPU 的利用占比都是一样的,-c 或者 --cpu-shares 可以设置 CPU 利用率权重,默认为 1024,可以设置权重为2或者更高(单个 CPU 为 1024,两个为 2048,以此类推)。如果设置选项为 0,则系统会忽略该选项并且使用默认值 1024。通过以上设置,只会在 CPU 密集(繁忙)型进程时体现出来。当一个 container 空闲时,其它容器都是可以占用CPU的。--cpu-shares 值为一个相对值,实际 CPU 利用率则取决于系统上运行容器的数量。

假如一个 1core 的主机运行 3 个 container,其中一个 --cpu-shares 设置为 1024,而其它--cpu-shares被设置成 512。当 3 个容器中的进程尝试使用 100% CPU 的时候(尝试使用 100% CPU 很重要,此时才可以体现设置值),则设置 1024 的容器会占用 50% 的 CPU 时间。如果又添加一个--cpu-shares为 1024 的 container,那么两个设置为 1024 的容器 CPU 利用占比为 33%,而另外两个则为 16.5%。简单的算法就是,所有设置的值相加,每个容器的占比就是 CPU 的利用率,如果只有一个容器,那么此时它无论设置 512 或者 1024,CPU 利用率都将是 100%。当然,如果主机是 3core,运行 3 个容器,两个 cpu-shares 设置为 512,一个设置为 1024,则此时每个 container 都能占用其中一个 CPU 为 100%,因为 CPU 资源是充足的。

测试主机(4core)当只有 1 个 container 时,可以使用任意的 CPU:
$ docker run --rm -it --cpu-shares 512 polinux/stress stress --cpu 4

查看 CPU 使用情况:
09.jpg

下面我们分别运行两个容器,指定它们都使用 cpu0,并分别设置 --cpu-shares 为 512 和 1024:
$ docker run --rm -it --cpuset-cpus="0" --cpu-shares 512 -d polinux/stress stress --cpu 1
$ docker run --rm -it --cpuset-cpus="0" --cpu-shares 1024 -d polinux/stress stress --cpu 1

查看 CPU 使用情况:
10.jpg

两个容器的 CPU 使用率分别为 75% 和 37.1%,比例大概为 2:1,符合预期。

查看其中一个容器的 Cgroup 配置,目录 /sys/fs/cgroup/cpu/docker/<docker_id>:
$ cat /sys/fs/cgroup/cpu/docker/b280a0f032a174521c198da26b41a06e41002e55ea6c8cd4b0904f8fc405bedb/cpu.shares
512

Docker 限制内存资源

查看 Docker 创建容器时支持的内存资源限制选项:
$ docker run --help | grep memory
  --kernel-memory bytes            Kernel memory limit
-m, --memory bytes                   Memory limit
  --memory-reservation bytes       Memory soft limit
  --memory-swap bytes              Swap limit equal to memory plus swap: '-1' to enable unlimited swap
  --memory-swappiness int          Tune container memory swappiness (0 to 100) (default -1)

限制容器内存使用上限

使用 -m 或者 --memory 选项限制容器使用的最大内存为 300M:
$ docker run --rm -it -m 300M polinux/stress stress --vm 1 --vm-bytes 500M
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [1] (415) <-- worker 7 got signal 9
stress: WARN: [1] (417) now reaping child worker processes
stress: FAIL: [1] (421) kill error: No such process
stress: FAIL: [1] (451) failed run completed in 0s

发现容器直接就被杀死了,这是因为默认情况下,内存不足(OOM)时 Docker 会将容器杀死。

为了防止这种情况,可以在启动时加入 --oom-kill-disable 参数(不建议这样做):
$ docker run --rm -it -m 300M --oom-kill-disable polinux/stress stress --vm 1 --vm-bytes 500M

使用 docker stats 命令查看容器资源占用情况:
11.png

可以看到,我们限制了容器只能使用 300M 内存,全部被用完了。

使用 htop 查看宿主机内存使用情况:
12.png

VIRT 是进程虚拟内存的大小,所以它应该是 500M。RES 为实际分配的物理内存数量,这个值就在 300M 上下浮动。

Docker 限制 IO 资源

对于磁盘来说,考量的参数是容量和读写速度,因此对容器的磁盘限制也应该从这两个维度出发。目前 Docker 支持对磁盘的读写速度进行限制,但是并没有方法能限制容器能使用的磁盘容量(一旦磁盘 mount 到容器里,容器就能够使用磁盘的所有容量)。

默认情况下,Docker 也没有对磁盘的读写速度做限制:
$ docker run -it --rm ubuntu:16.04 bash
root@0e42d93f5cae:/# time $(dd if=/dev/zero of=/tmp/test.data bs=10M count=100 && sync)
100+0 records in
100+0 records out
1048576000 bytes (1.0 GB, 1000 MiB) copied, 1.79154 s, 585 MB/s
real    0m7.519s
user    0m0.001s
sys     0m1.252s

限制磁盘的读写速率

限制磁盘的读写速率,对应的参数有:
  • --device-read-bps:磁盘每秒最多可以读多少比特(bytes)
  • --device-write-bps:磁盘每秒最多可以写多少比特(bytes)


上面两个参数的值都是磁盘以及对应的速率,格式为 <device-path>:<limit>[unit],device-path 表示磁盘所在的位置,限制 limit 为正整数,单位可以是 kb、mb 和 gb。
$ docker run -it --device /dev/vda:/dev/vda --device-read-bps /dev/vda:1mb ubuntu:16.04 bash
root@2f8793726e7e:/# cat /sys/fs/cgroup/blkio/blkio.throttle.read_bps_device
253:0 1048576
root@2f8793726e7e:/# dd iflag=direct,nonblock if=/dev/vda of=/dev/null bs=5M count=10
10+0 records in
10+0 records out
52428800 bytes (52 MB, 50 MiB) copied, 50.0036 s, 1.0 MB/s

从磁盘中读取 50m 花费了 50s 左右,说明磁盘速率限制起了作用。

另外两个参数可以限制磁盘读写频率(每秒能执行多少次读写操作):
  • --device-read-iops:磁盘每秒最多可以执行多少 IO 读操作
  • --device-write-iops:磁盘每秒最多可以执行多少 IO 写操作


上面两个参数的值都是磁盘以及对应的 IO 上限,格式为 <device-path>:<limit>,limit 为正整数,表示磁盘 IO 上限数。

比如,我们可以让磁盘每秒最多读 100 次:
$ docker run -it --device /dev/vda:/dev/vda --device-read-iops /dev/vda:100 ubuntu:16.04 bash
root@3f4041582639:/# dd iflag=direct,nonblock if=/dev/vda of=/dev/null bs=1k count=1000
1000+0 records in
1000+0 records out
1024000 bytes (1.0 MB, 1000 KiB) copied, 9.91894 s, 103 kB/s

从测试中可以看出,容器设置了读操作的 iops 为 100,在容器内部从 block 中读取 1m 数据(每次 1k,一共要读 1000 次),共计耗时约 10s,换算起来就是 100 iops/s,符合预期结果。

查看 Cgroup 配置,目录 /sys/fs/cgroup/blkio/docker/<docker_id>:
$ ls -l /sys/fs/cgroup/blkio/docker/3f4041582639d6c24112d39842eb3fea702e6ec6e843e9946d27f1524a8e53f0
total 0
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_merged
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_merged_recursive
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_queued
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_queued_recursive
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_service_bytes
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_service_bytes_recursive
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_serviced
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_serviced_recursive
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_service_time
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_service_time_recursive
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_wait_time
-r--r--r--. 1 root root 0 May 27 16:23 blkio.io_wait_time_recursive
-rw-r--r--. 1 root root 0 May 27 16:23 blkio.leaf_weight
-rw-r--r--. 1 root root 0 May 27 16:23 blkio.leaf_weight_device
--w-------. 1 root root 0 May 27 16:23 blkio.reset_stats
-r--r--r--. 1 root root 0 May 27 16:23 blkio.sectors
-r--r--r--. 1 root root 0 May 27 16:23 blkio.sectors_recursive
-r--r--r--. 1 root root 0 May 27 16:23 blkio.throttle.io_service_bytes
-r--r--r--. 1 root root 0 May 27 16:23 blkio.throttle.io_serviced
-rw-r--r--. 1 root root 0 May 27 16:23 blkio.throttle.read_bps_device
-rw-r--r--. 1 root root 0 May 27 16:23 blkio.throttle.read_iops_device
-rw-r--r--. 1 root root 0 May 27 16:23 blkio.throttle.write_bps_device
-rw-r--r--. 1 root root 0 May 27 16:23 blkio.throttle.write_iops_device
-r--r--r--. 1 root root 0 May 27 16:23 blkio.time
-r--r--r--. 1 root root 0 May 27 16:23 blkio.time_recursive
-rw-r--r--. 1 root root 0 May 27 16:23 blkio.weight
-rw-r--r--. 1 root root 0 May 27 16:23 blkio.weight_device
-rw-r--r--. 1 root root 0 May 27 16:23 cgroup.clone_children
--w--w--w-. 1 root root 0 May 27 16:23 cgroup.event_control
-rw-r--r--. 1 root root 0 May 27 16:23 cgroup.procs
-rw-r--r--. 1 root root 0 May 27 16:23 notify_on_release
-rw-r--r--. 1 root root 0 May 27 16:23 tasks

其中 blkio.throttle.read_iops_device 对应了设备的读 IOPS,前面一列是设备的编号,可以通过 cat /proc/partitions 查看设备和分区的设备号;后面是 IOPS 上限值:
$ cat /sys/fs/cgroup/blkio/docker/3f4041582639d6c24112d39842eb3fea702e6ec6e843e9946d27f1524a8e53f0/blkio.throttle.read_iops_device 
253:0 100

blkio.throttle.read_bps_device 对应了设备的读速率,格式和 IOPS 类似,只是第二列的值为 bps。

Kubernetes 中的资源管理

Kubernetes 最初源于谷歌内部的 Borg,提供了面向应用的容器集群部署和管理系统。Kubernetes的目标旨在消除编排物理/虚拟计算,网络和存储基础设施的负担,并使应用程序运营商和开发人员完全将重点放在以容器为中心的原语上进行自助运营。Kubernetes 也提供稳定、兼容的基础(平台),用于构建定制化的 workflows 和更高级的自动化任务。 Kubernetes 具备完善的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建负载均衡器、故障发现和自我修复能力、服务滚动升级和在线扩容、可扩展的资源自动调度机制、多粒度的资源配额管理能力。 Kubernetes 还提供完善的管理工具,涵盖开发、部署测试、运维监控等各个环节。

Kubernetes的架构示意图如下:
13.jpg

Kubernetes主要由以下几个核心组件组成:
  • etcd保存了整个集群的状态;
  • API server提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制;
  • Controller manager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
  • Scheduler负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上;
  • kubelet 负责维护容器的生命周期,同时也负责 Volume(CSI)和网络(CNI)的管理;
  • Container runtime 负责镜像管理以及Pod和容器的真正运行(CRI);
  • kube-proxy 负责为 Service 提供 cluster 内部的服务发现和负载均衡。


本文并不会介绍 Kubernetes的 使用细节,只关注它的资源管理。

既然 Kubernetes 是容器集群的管理系统,那么底层肯定也是依赖之前提到的 Linux 命名空间和 Cgroup 技术,实现资源的管理与隔离。

Kubernetes中的资源

目前 Kubernetes 默认带有两类基本资源:
  • CPU
  • memory


其中 CPU,不管底层的机器是通过何种方式提供的(物理机 or 虚拟机),一个单位的 CPU 资源都会被标准化为一个标准的 "Kubernetes Compute Unit" ,大致和 x86 处理器的一个单个超线程核心是相同的。

CPU 资源的基本单位是 millicores,因为 CPU 资源其实准确来讲,指的是 CPU 时间。所以它的基本单位为 millicores,1 个核等于 1000 millicores。也代表了 Kubernetes 可以将单位 CPU 时间细分为 1000 份,分配给某个容器。注意,上面我们提到,Cgroup 将单位 CPU 时间细分为了1024份,这里需要进行单位换算。

memory 资源的基本单位比较好理解,就是字节,与 Cgroup 中的定义一致。

另外,Kubernetes 针对用户的自定制需求,还为用户提供了 device plugin 机制,让用户可以将资源类型进一步扩充。

Kubernetes Pod 资源管理与分配

从上面的架构示意图中看到,Kubernetes 中资源管理与分配的最小分配单元是 Pod 中的容器,针对每个容器,它都可以通过如下两个信息指定它所希望的资源量:
  • request:针对这种资源,这个容器希望能够保证能够获取到的最少的量
  • limit:容器对这个资源使用的上限


根据我们前面对 Cgroup 的分析,对 CPU 来说,容器使用 CPU 过多,内核调度器就会切换,使其使用的量不会超过 limit,而对内存来说,容器使用内存超过 limit,这个容器就会被 OOM kill 掉,从而发生容器的重启。

根据容器指定资源的不同情况,Pod 也被划分为 3 种不同的 QoS (Quality of Service,即服务质量)级别。分别为:
  • Guaranteed
  • Burstable
  • BestEffort


不同的 QoS 级别会在很多方面发挥作用,比如调度,eviction。

Guaranteed 级别的 Pod 主要需要满足两点要求:
  • Pod 中的每一个 container 都必须包含内存资源的 limit、request 信息,并且这两个值必须相等
  • Pod 中的每一个 container 都必须包含 CPU 资源的 limit、request 信息,并且这两个信息的值必须相等


Burstable 级别的 Pod 则需要满足两点要求:
  • 资源申请信息不满足 Guaranteed 级别的要求
  • Pod 中至少有一个 container 指定了 CPU 或者 memory 的 request 信息


BestEffort 级别的 Pod 需要满足:
  • Pod 中任何一个 container 都不能指定 CPU 或者 memory 的 request,limit 信息


所以通过上面的描述也可以看出来:
  • Guaranteed level 的 Pod 是优先级最高的,系统管理员一般对这类 Pod 的资源占用量比较明确;
  • Burstable level 的 Pod 优先级其次,管理员一般知道这个 Pod 的资源需求的最小量,但是当机器资源充足的时候,还是希望他们能够使用更多的资源,所以一般 limit > request;
  • BestEffort level 的 Pod 优先级最低,一般不需要对这个 Pod 指定资源量。所以无论当前资源使用如何,这个 Pod 一定会被调度上去,并且它使用资源的逻辑也是见缝插针。当机器资源充足的时候,它可以充分使用,但是当机器资源被 Guaranteed、Burstable 的 Pod 所抢占的时候,它的资源也会被剥夺,被无限压缩。


下面我们定义一个 Pod,其中运行之前压测使用的 polinux/stress 容器,希望该容器在资源竞争时,也能获得 0.5 单位的 CPU 和 100M 的内存;而且我们限制该容器最多使用 1 单位的 CPU 和 200M 的内存;我们传给容器中的压测进程参数是使用 2 个单位的 CPU,100M 的内存 ,这个 Pod 的 QoS 级别为 Burstable,因为我们指定了容器的 request 和 limit,但是他们不相等:
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
spec:
containers:
- name: resource-demo
image: polinux/stress
resources:
  limits:
    cpu: "1"
    memory: "200Mi"
  requests:
    cpu: "0.5"
    memory: "100Mi"
args:
- stress
- --cpu
- "2"
- --vm
- "1"
- --vm-bytes
- "100M"

使用 Kubernetes 的命令行工具 kubectl 创建 Pod:
$ kubectl create -f resource.yaml

使用 htop 查看一下 CPU 占用:
14.jpg

然后使用 docker stats 查看一下容器资源占用详情:
15.png

从两张图的统计结果看,容器的 CPU 使用率被限制在了 100% 左右,即一个单位;内存上限为 200M,使用了 99.19M,符合预期。

接下来我们看一下 Pod 容器对应的 Cgroup 配置。

先进入宿主机的 cpu cgroup 配置目录:
$ ls -l /sys/fs/cgroup/cpu/
-rw-r--r--   1 root root 0 5月   5 10:18 cgroup.clone_children
--w--w--w-   1 root root 0 5月   5 10:18 cgroup.event_control
-rw-r--r--   1 root root 0 5月   5 10:18 cgroup.procs
-r--r--r--   1 root root 0 5月   5 10:18 cgroup.sane_behavior
-r--r--r--   1 root root 0 5月   5 10:18 cpuacct.stat
-rw-r--r--   1 root root 0 5月   5 10:18 cpuacct.usage
-r--r--r--   1 root root 0 5月   5 10:18 cpuacct.usage_percpu
-rw-r--r--   1 root root 0 5月   5 10:18 cpu.cfs_period_us
-rw-r--r--   1 root root 0 5月   5 10:18 cpu.cfs_quota_us
-rw-r--r--   1 root root 0 5月   5 10:18 cpu.rt_period_us
-rw-r--r--   1 root root 0 5月   5 10:18 cpu.rt_runtime_us
-rw-r--r--   1 root root 0 5月   5 10:18 cpu.shares
-r--r--r--   1 root root 0 5月   5 10:18 cpu.stat
drwxr-xr-x   5 root root 0 5月  29 12:32 kubepods.slice
drwxr-xr-x   2 root root 0 5月   5 10:18 kube-proxy
-rw-r--r--   1 root root 0 5月   5 10:18 notify_on_release
-rw-r--r--   1 root root 0 5月   5 10:18 release_agent
drwxr-xr-x 128 root root 0 5月  29 14:41 system.slice
-rw-r--r--   1 root root 0 5月   5 10:18 tasks
drwxr-xr-x   2 root root 0 5月   5 10:18 user.slice

发现有一个 kubepods.slice 子目录,该目录中存放着 kubelet 管理的 Pod 配置。进入里面看一下:
$ ls -l /sys/fs/cgroup/cpu/kubepods.slice/
-rw-r--r-- 1 root root 0 5月   5 10:19 cgroup.clone_children
--w--w--w- 1 root root 0 5月   5 10:19 cgroup.event_control
-rw-r--r-- 1 root root 0 5月   5 10:19 cgroup.procs
-r--r--r-- 1 root root 0 5月   5 10:19 cpuacct.stat
-rw-r--r-- 1 root root 0 5月   5 10:19 cpuacct.usage
-r--r--r-- 1 root root 0 5月   5 10:19 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 5月   5 10:19 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 5月   5 10:19 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 5月   5 10:19 cpu.rt_period_us
-rw-r--r-- 1 root root 0 5月   5 10:19 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 5月   9 21:55 cpu.shares
-r--r--r-- 1 root root 0 5月   5 10:19 cpu.stat
drwxr-xr-x 3 root root 0 5月  29 12:32 kubepods-besteffort.slice
drwxr-xr-x 3 root root 0 5月  29 14:39 kubepods-burstable.slice
drwxr-xr-x 4 root root 0 5月  29 12:38 kubepods-podd18759a2_81ca_11e9_9c25_fa163e56c6a7.slice
-rw-r--r-- 1 root root 0 5月   5 10:19 notify_on_release
-rw-r--r-- 1 root root 0 5月   5 10:19 tasks

其中有三个子目录,kubepods-besteffort.slice,kubepods-burstable.slice 和 kubepods-podd18759a2_81ca_11e9_9c25_fa163e56c6a7.slice。如果是Guaranteed 级别的 Pod,则会直接出现该层,就像这里的 kubepods-podd18759a2_81ca_11e9_9c25_fa163e56c6a7.slice,而 Burstable 和 BestEffort 级别的 Pod 则会放到对应的字目录下。

我们刚才创建的 Pod 是 Burstable 级别的,我们进去看一下:
$ ls -l /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/
-rw-r--r-- 1 root root 0 5月   5 10:19 cgroup.clone_children
--w--w--w- 1 root root 0 5月   5 10:19 cgroup.event_control
-rw-r--r-- 1 root root 0 5月   5 10:19 cgroup.procs
-r--r--r-- 1 root root 0 5月   5 10:19 cpuacct.stat
-rw-r--r-- 1 root root 0 5月   5 10:19 cpuacct.usage
-r--r--r-- 1 root root 0 5月   5 10:19 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 5月   5 10:19 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 5月   5 10:19 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 5月   5 10:19 cpu.rt_period_us
-rw-r--r-- 1 root root 0 5月   5 10:19 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 5月  29 14:58 cpu.shares
-r--r--r-- 1 root root 0 5月   5 10:19 cpu.stat
drwxr-xr-x 4 root root 0 5月  29 14:41 kubepods-burstable-podca6ccfcb_81dc_11e9_9c25_fa163e56c6a7.slice
-rw-r--r-- 1 root root 0 5月   5 10:19 notify_on_release
-rw-r--r-- 1 root root 0 5月   5 10:19 tasks

可以看到其中有个 Pod 的配置子目录,但是不确实是不是我们刚才创建的 Pod,可以使用 kubectl 查询 Pod 的 uid:
$ kubectl get pod resource-demo -o=jsonpath='{.metadata.uid}{"\n"}'
ca6ccfcb-81dc-11e9-9c25-fa163e56c6a7

发现确实是我们的 Pod,进去看一下具体的配置:
$ ls -l /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podca6ccfcb_81dc_11e9_9c25_fa163e56c6a7.slice
-rw-r--r-- 1 root root 0 5月  29 14:41 cgroup.clone_children
--w--w--w- 1 root root 0 5月  29 14:41 cgroup.event_control
-rw-r--r-- 1 root root 0 5月  29 14:41 cgroup.procs
-r--r--r-- 1 root root 0 5月  29 14:41 cpuacct.stat
-rw-r--r-- 1 root root 0 5月  29 14:41 cpuacct.usage
-r--r--r-- 1 root root 0 5月  29 14:41 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.rt_period_us
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.shares
-r--r--r-- 1 root root 0 5月  29 14:41 cpu.stat
drwxr-xr-x 2 root root 0 5月  29 14:41 docker-5b37f03b3d3fe1efe5328102647c5032a26ab380d26a9486e8ff555672a6db0b.scope
drwxr-xr-x 2 root root 0 5月  29 14:41 docker-972d6a450838dc3bd0e0901731d8c8787c7da404a0278428975e6eaaeaa28ecc.scope
-rw-r--r-- 1 root root 0 5月  29 14:41 notify_on_release
-rw-r--r-- 1 root root 0 5月  29 14:41 tasks

发现有两个 Docker 容器子目录,Kubernetes 在启动 Pod 的时候,除了我们配置的容器,还会额外启动一个 k8s.gcr.io/pause 的容器,我们只需要关心自己配置的容器即可。我们来看一下两个容器具体是什么:
$ docker ps --format "{{.ID}}\t{{.Image}}\t{{.Names}}" | grep 5b37f03
5b37f03b3d3f    polinux/stress  k8s_resource-demo_resource-demo_default_ca6ccfcb-81dc-11e9-9c25-fa163e56c6a7_0
$ docker ps --format "{{.ID}}\t{{.Image}}\t{{.Names}}" | grep 972d6a4
972d6a450838    k8s.gcr.io/pause:3.1    k8s_POD_resource-demo_default_ca6ccfcb-81dc-11e9-9c25-fa163e56c6a7_0

进入 docker-5b37f03b3d3fe1efe5328102647c5032a26ab380d26a9486e8ff555672a6db0b.scope 子目录:
$ ls -l /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podca6ccfcb_81dc_11e9_9c25_fa163e56c6a7.slice/docker-5b37f03b3d3fe1efe5328102647c5032a26ab380d26a9486e8ff555672a6db0b.scope/
-rw-r--r-- 1 root root 0 5月  29 14:41 cgroup.clone_children
--w--w--w- 1 root root 0 5月  29 14:41 cgroup.event_control
-rw-r--r-- 1 root root 0 5月  29 14:41 cgroup.procs
-r--r--r-- 1 root root 0 5月  29 14:41 cpuacct.stat
-rw-r--r-- 1 root root 0 5月  29 14:41 cpuacct.usage
-r--r--r-- 1 root root 0 5月  29 14:41 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.rt_period_us
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 5月  29 14:41 cpu.shares
-r--r--r-- 1 root root 0 5月  29 14:41 cpu.stat
-rw-r--r-- 1 root root 0 5月  29 14:41 notify_on_release
-rw-r--r-- 1 root root 0 5月  29 14:41 tasks

终于找到了我们创建的容器的 CPU 配置,看一下 cpu.shares 配置:
$ cat cpu.shares
512

这样来满足我们 CPU request 设置的 0.5。

在看一下 cpu.cfs_period_us 和 cpu.cfs_quota_us 配置:
$ cat cpu.cfs_period_us
100000
$ cat cpu.cfs_quota_us
100000 

这样就满足了我们设置的 CPU 上限 1。

内存配置与 CPU 目录规则一致,查看一下:
$ cat /sys/fs/cgroup/memory/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podca6ccfcb_81dc_11e9_9c25_fa163e56c6a7.slice/docker-5b37f03b3d3fe1efe5328102647c5032a26ab380d26a9486e8ff555672a6db0b.scope/memory.limit_in_bytes
209715200

209715200 字节刚好等于我们配置的 memory 上限 200M。

最后我们通过一张图整理一下 Kubernetes 使用 Cgroup 管理资源时的层次关系:
16.jpg

Kubernetes 在拿到一个 Pod 的资源申请信息后,针对每一种资源,都会进行如下操作:
  1. 首先,根据 Pod 中所有容器申请的资源计算出 Pod 所需资源的总量,分配一个满足条件的 Node 节点供该 Pod 使用;
  2. 在 Node 节点的 Cgroup 目录中,各类 Kubernetes 管理的容器资源都将被分配到 kubepods 子目录中,目录结构为 /sys/fs/cgroup/{resource_type}/kubepods.slice;
  3. 根据 Pod 申请资源情况判断 Pod 的 QoS 级别,如果是 burstable 或者 besteffort 级别,则会放到对应的 QoS 级别 Cgroup 子目录中,目录结构为 /sys/fs/cgroup/{resource_type}/kubepods.slice(|{/qos_level});如果是 guaranteed 级别,则不需要 QoS 级别的 Cgroup 限制;
  4. 然后为该 Pod 创建 Pod 级别的 Cgroup,它会成为这个 Pod 下面所有容器级别 Cgroup 的父级,目录结构为 /sys/fs/cgroup/{resource_type}/kubepods.slice(|{/qos_level})/kubepods-{pod_id}.slice;
  5. 最后为 Pod 中的每一个容器,创建一个容器级别的 Cgroup,目录结构为 /sys/fs/cgroup/{resource_type}/kubepods.slice(|{/qos_level})/kubepods-{pod_id}.slice/docker-{container_id}.scope/,此处会包含一个默认容器 k8s.gcr.io/pause。


目前 Kubernetes 默认仅支持 CPU 和内存的限制,还不支持 IO、网络资源的限制,有望在后续版本中加入。

总结

本文具体分析了 Docker 和 Kubernetes 如何使用 Linux 系统中的命名空间和控制组两大特性对容器资源进行管理和隔离,在保证容器和系统稳定性的基础上,最大限度地提高资源利用率。

作者:魏东军,网易传媒后端开发工程师,主要负责网易传媒容器化服务管理和通用网关研发相关工作。热衷于探索技术的本源。

参考文献:


原文链接:https://mp.weixin.qq.com/s/jT6m05vy601paKNi-Wh-6A

0 个评论

要回复文章请先登录注册