Docker底层基石namespace与cgroup

Docker是使用容器container的平台,容器其实只是一个隔离的进程,除此之外啥都没有。这个进程包含一些封装特性,以便和主机还有其他的容器隔离开。一个容器依赖最多的是它的文件系统也就是image,image提供了容器运行的一切包括 code or binary, runtimes, dependencies, and 其他 filesystem 需要的对象。

容器在Linux上本地运行,并与其他容器共享主机的内核。它运行一个独立的进程,占用的内存不比其他的filesystem多,因此它是轻量级的。相比之下,虚拟机(VM)运行一个成熟的“guest”操作系统,通过hypervisor对主机资源进行虚拟访问。一般来说,vm会产生大量开销,超出应用程序逻辑所消耗的开销。

container vm

容器本质上是把系统中为同一个业务目标服务的相关进程合成一组,放在一个叫做namespace的空间中,同一个namespace中的进程能够互相通信,但看不见其他namespace中的进程。每个namespace可以拥有自己独立的主机名、进程ID系统、IPC、网络、文件系统、用户等等资源。在某种程度上,实现了一个简单的虚拟:让一个主机上可以同时运行多个互不感知的系统。

Alt text

此外,为了限制namespace对物理资源的使用,对进程能使用的CPU、内存等资源需要做一定的限制。这就是Cgroup技术,Cgroup是Control group的意思。比如我们常说的4c4g的容器,实际上是限制这个容器namespace中所用的进程,最多能够使用4核的计算资源和4GB的内存。
简而言之,Linux内核提供namespace完成隔离,Cgroup完成资源限制。namespace+Cgroup构成了容器的底层技术(rootfs是容器文件系统层技术)。

namespace

一个namespace把一些全局系统资源封装成一个抽象体,该抽象体对于本namespace中的进程来说有它们自己的隔离的全局资源实例。改变这些全局资源对于该namespace中的进程是可见的,而对其他进程来说是不可见的。
Linux 提供一下几种 namespaces:

  Namespace   Constant                           Isolates
  -  IPC            CLONE_NEWIPC            System V IPC, POSIX message queues     进程间通信隔离
  -  Network     CLONE_NEWNET           Network devices, stacks, ports, etc.       隔离网络资源
  -  Mount        CLONE_NEWNS             Mount points                             隔离文件系统
  -  PID            CLONE_NEWPID            Process IDs                            进程隔离
  -  User          CLONE_NEWUSER         User and group IDs                        隔离用户权限
  -  UTS          CLONE_NEWUTS           Hostname and NIS domain name              (UNIX Time Sharing)用来隔离系统的hostname以及NIS domain name

为了在分布式的环境下进行通信和定位,容器必然需要一个独立的IP、端口、路由等等,自然就想到了网络的隔离。同时,你的容器还需要一个独立的主机名以便在网络中标识自己。想到网络,顺其自然就想到通信,也就想到了进程间通信的隔离。可能你也想到了权限的问题,对用户和用户组的隔离就实现了用户权限的隔离。最后,运行在容器中的应用需要有自己的PID,自然也需要与宿主机中的PID进行隔离。
Linux内核为以上6种namespace隔离提供了系统调用,具体就是namespacede API包括clone()、setns()以及unshare(),还有/proc下的部分文件。为了确定隔离的到底是哪6项namespace,在使用这些API时,通常需要指定CLONE_NEWNS、CLONE_IPC、CLONE_NEWNET、CLONE_NEWPID、CLONE_USER和CLONE_UTS. 其中CLONE_NEWNS是mount,因为它是第一个linux namespace所以标识位比较特殊。
使用clone()来创建一个独立的namespace进程,是最常见的做法,也是Docker使用namespace最基本的方法,它的调用方式举例如下:

 int child_pid = clone(child_main, child_stack+STACK_SIZE, 
                        CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | SIGCHLD , NULL);

network namespace

Linux Programmer’s Manual里对 Network Namespace 有一个段简短的描述,在里面就列出了最主要的几部分资源,它们都是通过 Network Namespace 隔离的。

  • 第一种,网络设备,这里指的是 lo,eth0 等网络设备。你可以通过 ip link命令看到它们。
  • 第二种是 IPv4 和 IPv6 协议栈。从这里我们可以知道,IP 层以及上面的 TCP 和 UDP 协议栈也是每个 Namespace 独立工作的。所以 IP、TCP、UDP 的很多协议,它们的相关参数也是每个 Namespace 独立的,这些参数大多数都在 /proc/sys/net/ 目录下面,同时也包括了 TCP 和 UDP 的 port 资源。
  • 第三种,IP 路由表,这个资源也是比较好理解的,你可以在不同的 Network Namespace 运行 ip route 命令,就能看到不同的路由表了。
  • 第四种是防火墙规则,其实这里说的就是 iptables 规则了,每个 Namespace 里都可以独立配置 iptables 规则。
    参考上面的例子,我们着重关注一下network namespace. 从Linux内核3.8版本开始,/proc/PID/ns 目录下的文件都是一个特殊的符号链接文件,可以看出这些符号链接的其中一个用途是确定某两个进程是否属于同一个namespace。如果两个进程在同一个namespace中,那么这两个进程/proc/PID/ns目录下对应符号链接文件的inode数字会是一样的。
    除此之外,/proc/PID/ns目录下的文件还有一个作用——当我们打开这些文件时,只要文件描述符保持open状态,对应的namespace就会一直存在,哪怕这个namespace里的所有进程都终止运行了。这是什么意思呢?之前版本的Linux内核,要想保持namespace存在,需要在namespace里放一个进程(当然,不一定是运行中的),这种做法在一些场景下有些笨重(虽然kubernetes就是这么做的)。因此,Linux内核提供的黑科技允许:只要打开文件描述符,不需要进程存在也能保持namespace存在!怎么操作?请看下面的命令:
touch /my/net #新建一个文件
mount --bind /proc/$$/ns/net /my/net

如上所示,把/proc/PID/ns目录下的文件挂载起来就能起到打开文件描述符的作用,而且这个network namespace会一直存在,直到/proc/$$/ns/net被卸载。那么接下来,如何向这个namespace里“扔”进程呢?Linux系统调用setns()int setns(int fd, int nstype)就是用来做这个工作的,其主要功能就是把一个进程加入一个已经存在的namespace中。ip netns exec这个子命令,也可以轻松进入一个network namespace,然后执行一些操作。
与namespace相关的最后一个系统调用是unshare(),该函
数声明为int unshare(int flags);,用于帮助进程“逃离”namespace。unshare()系统调用的工作机制是:先通过制定的flags创建相应的namespace,再把这个进程挪到这些新创建的namespace中,于是也就完成了进程从原先namespace的撤离。unshare()提供的功能很像clone(),区别在于unshare()作用在一个已经存在的进程上,而clone()会创建一个新的进程。该系统调用的应用场景是在当前shell所在的namespace外执行一条命令unshare [options] program [args] Linux会为需要执行的命令启动一个新进程,然后在另外一个namespace中执行操作,这样就可以起到执行结果和原(父)进程隔离的效果。

如何通过PID找到容器名称

/proc/PID/ 下面存放的是容器的几乎所有的信息,其中
-cgroup 放的是cgroup的目录名称;
-cmdline 是容器启动的命令;
-comm 是容器命令??;
-cpuset 是容器的cpu cgroup;
-mountinfo 是容器mount信息;
-cwd 一个符号连接, 指向进程当前的工作目录,即容器运行时的工作目录,其中cwd/etc/hostname中存放的是容器名称;
-net 存放的是容器的网络信息,包括IP配置、路由、收发包状况、iptables、ipvs等等。其中fib_trie存放了当前的IP配置;
-ns 存放的是namespace链接;

cgroups

Cgroups是control groups的缩写,最初由google的工程师提出,后来被整合进Linux内核。Cgroups是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如:CPU、内存、IO等)的机制。对开发者来说,cgroups 有如下四个有趣的特点:

  • cgroups 的 API 以一个伪文件系统的方式实现,即用户可以通过文件操作实现 cgroups 的组织管理。
  • cgroups 的组织管理操作单元可以细粒度到线程级别,用户态代码也可以针对系统分配的资源创建和销毁 cgroups,从而实现资源再分配和管理。
  • 所有资源管理的功能都以“subsystem(子系统)”的方式实现,接口统一。
  • 子进程创建之初与其父进程处于同一个 cgroups 的控制组。

本质上来说,cgroups 是内核附加在程序上的一系列钩子(hooks),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。实现 cgroups 的主要目的是为不同用户层面的资源管理,提供一个统一化的接口。从单个进程的资源控制到操作系统层面的虚拟化。Cgroups 提供了以下四大功能:

  • 资源限制(Resource Limitation):cgroups 可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出 OOM(Out of Memory)。
    资源限制最重要的是CPU限制,CPU Cgroup 一般在 Linux 发行版里会放在 /sys/fs/cgroup/cpu 这个目录下。在这个子系统的目录下,每个控制组(Control Group) 都是一个子目录,各个控制组之间的关系就是一个树状的层级关系(hierarchy)。例如,我们到docker run命令启动的容器对应的CPU cgroup文件目录/sys/fs/cgroup/cpu/docker/下,看到其中三个文件cpu.cfs_period_us,cpu.cfs_quota_us和 cpu.shares.其中cpu.cfs_period_us表示CFS 算法(Completely Fair Scheduler,即完全公平调度器,linux中普遍的进程调度算法)的一个调度周期;cfs_quota_us表示 CFS 算法中,在一个调度周期里这个控制组被允许的运行时间。这两个量的单位是纳秒,都是绝对量值。而cpu.shares是 CPU Cgroup 对于控制组之间的 CPU 分配比例,它的缺省值是 1024。


    举例说明cpu.cfs_period_us,cpu.cfs_quota_us的意义:
    第一个参数是 cpu.cfs_period_us,它是 CFS 算法的一个调度周期,一般它的值是 100000,以 microseconds 为单位,也就 100ms。第二个参数是 cpu.cfs_quota_us,它“表示 CFS 算法中,在一个调度周期里这个控制组被允许的运行时间,比如这个值为 50000 时,就是 50ms。如果用这个值去除以调度周期(也就是 cpu.cfs_period_us),50ms/100ms = 0.5,这样这个控制组被允许使用的 CPU 最大配额就是 0.5 个 CPU。从这里能够看出,cpu.cfs_quota_us 是一个绝对值。如果这个值是 200000,也就是 200ms,那么它除以 period,也就是 200ms/100ms=2。你看,结果超过了 1 个 CPU,这就意味着这时控制组需要 2 个 CPU 的资源配额。


    cpu.shares。这个值是 CPU Cgroup 对于控制组之间的 CPU 分配比例,它的缺省值是 1024。假设我们前面创建的 group3 中的 cpu.shares 是 1024,而 group4 中的 cpu.shares 是 3072,那么 group3:group4=1:3。因为 cpu.shares 是几个控制组之间的 CPU 分配比例,而且一定要到整个节点中所有的 CPU 都跑满的时候,它才能发挥作用。

以上为配置Limit CPU 就是容器所在 Cgroup 控制组中的 CPU 上限值,Request CPU 的值就是控制组中的 cpu.shares 的值,提供了功能基础。 Limit CPU和Request CPU数值一般为CPU的个数,允许是分数。“500m”表示的是500millicores(单位是milli即一个CPU的千分之一)。

  • 优先级分配(Prioritization):通过分配的 CPU 时间片数量及硬盘 IO 带宽大小,实际上就相当于控制了进程运行的优先级。
  • 资源统计(Accounting): cgroups 可以统计系统的资源使用量,如 CPU 使用时长、内存用量等等,这个功能非常适用于计费。
  • 进程控制(Control):cgroups 可以对进程组执行挂起、恢复等操作。
    Docker正是使用cgroup进行资源划分,每个容器都作为一个进程运行起来,每个业务容器都会有一个基础的pause容器也就是POD作为基础容器。pause容器提供了划分namespace的内容,并连通同一POD下的所有容器,共享网络资源。查看容器的PID,对应/proc/pid/下是该容器的运行资源,每一个文件保持打开的话,对应的namespace就会一直存在。
    例如最大进程数限制,系统的最大进程数可以通过查看文件/proc/sys/kernel/pid_max来看,当然还受限于最大打开文件数、最大用户空间进程数(ulimit -a). 对应容器里最大的进程数,以Docker run命令启动的容器为例,可以通过查看文件 /sys/fs/cgroup/pids/docker//pids.max 来确认,如果太小会导致容器有很多defunct进程(僵尸)。
    在分析 K8s、Docker 等 cgroup 相关操作时。比如 docker run xxx 时,可以看到 /sys/fs/cgroup/cpuset/docker/xxx/cpuset.cpus、/sys/fs/cgroup/cpuset/docker/xxx/cpuset.mems 等 cgroup 文件被打开,也可以查看 kube-proxy 在周期性刷新 cgroup 相关文件。这些都是资源划分相关文件。 可以通过cat /proc/mount 来查看 cgroups 挂载的目录,一般在 /sys/fs/cgroup。

分析一个k8s Pod

这里我以一个default命名空间的Pod bizagent-599bb4bcbf-l2gvk为例,来分析它的namespace和cgroup,这个Pod是用calico网络插件启动创建起来的。本机IP地址是192.168.1.2,calico给Pod分配的IP地址是10.8.188.4.

[root@test ~]# docker ps |grep bizagent-599bb4bcbf-l2gvk
4c251b76dbcb   891d874693ed                                             "/docker-entrypoint.…"   5 days ago     Up 5 days                                                                                                                                                    k8s_nginx_bizagent-599bb4bcbf-l2gvk_default_1e2cf7c4-2bfc-4b50-8f20-1203c2c9b08e_2
f82774c4ae42   k8s.gcr.io/pause:3.2                                     "/pause"                 5 days ago     Up 5 days                                                                                                                                                    k8s_POD_bizagent-599bb4bcbf-l2gvk_default_1e2cf7c4-2bfc-4b50-8f20-1203c2c9b08e_3
[root@tes ~]# docker inspect f82774c4ae42|grep Pid
            "Pid": 20560,
            "PidMode": "",
            "PidsLimit": null,
[root@test ~]# docker inspect 4c251b76dbcb|grep Pid
            "Pid": 25180,
            "PidMode": "",
            "PidsLimit": null,
[root@test ~]# 

通过以上命令得到pause容器的pid是20560, 业务容器(我也不太清楚另外一个容器该怎么叫,暂且叫它业务容器吧)的pid是25180, 然后列出它们所有的ns:

[root@test ~]# lsns |grep 20560
4026537282 uts        1   20560 root      /pause
4026537284 mnt        1   20560 root      /pause
4026537285 ipc        3   20560 root      /pause
4026537286 pid        1   20560 root      /pause
4026537288 net        3   20560 root      /pause
[root@test ~]# lsns |grep 25180
4026537534 mnt        2   25180 root      nginx: master process nginx -g daemon off
4026537535 uts        2   25180 root      nginx: master process nginx -g daemon off
4026537536 pid        2   25180 root      nginx: master process nginx -g daemon off
[root@test ~]#

可以看到业务容器比pause容器少了两个隔离资源,分别是ipc和net,事实证明,lsns 不是检查进程名称空间的最佳工具。相反,要检查某个进程使用的命名空间,可以参考 /proc/${pid}/ns 位置:

[root@test ~]#  ls -l /proc/20560/ns
total 0
lrwxrwxrwx 1 root root 0 Apr 12 10:47 ipc -> ipc:[4026537285]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 mnt -> mnt:[4026537284]
lrwxrwxrwx 1 root root 0 Apr 12 10:47 net -> net:[4026537288]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 pid -> pid:[4026537286]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 uts -> uts:[4026537282]
[root@test ~]# 
[root@test ~]#  ls -l /proc/25180/ns
total 0
lrwxrwxrwx 1 root root 0 Apr 13 15:54 ipc -> ipc:[4026537285]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 mnt -> mnt:[4026537534]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 net -> net:[4026537288]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 pid -> pid:[4026537536]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 uts -> uts:[4026537535]
[root@test ~]# 

如果两个进程指向的namespace编号相同,就说明它们在同一个namespace下,否则便在不同namespace里面。上面的示例可以看出业务容器实际上重用了 pause 容器的 net 和 ipc 命名空间!我认为上述发现完美的解释了同一个 Pod 中容器具有的能力:

  • 能够互相通信,共享网络, 通过nsenter -t <容器进程号> -n -F -- ip a命令可以看出它们的网络配置完全一样
  • 使用 IPC(共享内存,消息队列等)
    /proc/[pid]/ns里设置这些link的另外一个作用是,一旦上述link文件被打开,只有打开的文件描述符(fd)存在,那么就算该namespace下的所有进程都已经结束,这个namespace也会一直存在。后续进程也可以加进来。在Docker中,通过文件描述符定位和加入一个存在的namespace是最基本的方式。例如,docker exec 命令在已经运行着的容器中执行一个新的命令,就使用了setns()加入一个已经存在的namespace.通常为了不影响进程的调用者,也为了使新加入的pid namespace生效,会在setns()函数执行后使用clone()创建子进程继续执行命令,让原先的进程结束运行。

再来看一个本地的host网络模式的容器kube-proxy-kfsm6

[root@test ~]# docker ps |grep kube-proxy-kfsm6
bcace94d9a6b   358e7e6ecf20                                             "/usr/local/bin/kube…"   6 days ago     Up 6 days                                                                                                                                                    k8s_kube-proxy_kube-proxy-kfsm6_kube-system_4aa6ff92-3b26-49db-9e52-9c0170adaf41_1
7801c9d080fe   k8s.gcr.io/pause:3.2                                     "/pause"                 6 days ago     Up 6 days                                                                                                                                                    k8s_POD_kube-proxy-kfsm6_kube-system_4aa6ff92-3b26-49db-9e52-9c0170adaf41_1
[root@test ~]# docker inspect bcace94d9a6b|grep Pid
            "Pid": 12711,
            "PidMode": "",
            "PidsLimit": null,
[root@test ~]# docker inspect 7801c9d080fe|grep Pid
            "Pid": 12414,
            "PidMode": "",
            "PidsLimit": null,
[root@test ~]# 
[root@test ~]# lsns |grep 12414
4026534907 mnt        1   12414 root      /pause
4026534908 uts        1   12414 root      /pause
4026534909 ipc        2   12414 root      /pause
4026534910 pid        1   12414 root      /pause
[root@test ~]# 
[root@test ~]# lsns |grep 12711
4026534924 mnt        1   12711 root      /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=test
4026534925 pid        1   12711 root      /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=test
[root@test ~]# 
[root@test ~]# ls -l /proc/12414/ns
total 0
lrwxrwxrwx 1 root root 0 Apr 12 10:47 ipc -> ipc:[4026534909]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 mnt -> mnt:[4026534907]
lrwxrwxrwx 1 root root 0 Apr 12 10:47 net -> net:[4026532004]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 pid -> pid:[4026534910]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 uts -> uts:[4026534908]
[root@test ~]# 
[root@test ~]# 
[root@test ~]# ls -l /proc/12711/ns
total 0
lrwxrwxrwx 1 root root 0 Apr 13 15:54 ipc -> ipc:[4026534909]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 mnt -> mnt:[4026534924]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 net -> net:[4026532004]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 pid -> pid:[4026534925]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 uts -> uts:[4026531838]
[root@test ~]# 
[root@test ~]# ls -l /proc/1/ns
total 0
lrwxrwxrwx 1 root root 0 Apr 13 15:54 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 net -> net:[4026532004]
lrwxrwxrwx 1 root root 0 Apr 12 10:46 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Apr 13 15:54 uts -> uts:[4026531838]
[root@test ~]# 

最后,我查看了1号进程,可以看出kube-proxy的pause容器和业务容器都共享了主机的net、user, 但是为啥业务容器共享了主机的uts而pause容器却隔离了uts呢?欢迎大家讨论。

另外,Pod 的 cgroups 是什么样的?systemd-cgls 可以很好地可视化 cgroups 层次结构:

[root@test ~]# systemd-cgls
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─kubepods
│ ├─besteffort
│ │ ├─pod4aa6ff92-3b26-49db-9e52-9c0170adaf41
│ │ │ ├─bcace94d9a6bc2046e3109d0c6cf608c33c9074fe8d7c1934f90e58c0db783eb
│ │ │ │ └─12711 /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=test-ko-mix-master-1
│ │ │ └─7801c9d080fec92eaff6f7df7ddbb887ccd2a198c1b63b91b228acf96b882f54
│ │ │   └─12414 /pause

其中,Pod 中的容器没有设置内存和 CPU 限制或请求,则就是 BestEffort. k8s拉起的BestEffort容器 cgroup 配置放在了 /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-besteffort.slice/ ,这个文件夹下面文件如下所示:

这里每个 kubepods-besteffort-pod表示一个POD,每个POD里面可以包含多个 docker 容器. 我们以第一个为例,进到里面文件夹如下所示:

注意,这里的文件大小都是 0,即内容为空,他们是用他们的存在以及 stat 属性来表示 cgroup 的。如下所示:

# stat docker-017b45b0440376470bc8d1d1c8179541e6ca8c942b6a993c4024895d652ea2b4.scope
  File: docker-017b45b0440376470bc8d1d1c8179541e6ca8c942b6a993c4024895d652ea2b4.scope
  Size: 0         	Blocks: 0          IO Block: 4096   directory
Device: 2bh/43d	Inode: 1300        Links: 2
Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-06-06 07:21:26.043183651 +0000
Modify: 2022-06-06 07:21:12.859174044 +0000
Change: 2022-06-06 07:21:12.859174044 +0000
 Birth: -

这里面的Inode: 1300 就是cgroupid. 每个容器ID 对应 一个 cgroupID, 同样的,一个 cgroupID 也对应一个 containerID. 在有些应用中,可以应用这种关系通过 cgroupID 来找到 containerID.

关于内存限制的几个知识点

  • Memory Cgroup 中每一个控制组可以为一组进程限制内存使用量,一旦所有进程使用内存的总量达到限制值,缺省情况下,就会触发 OOM Killer。这样一来,控制组里的“某个进程”就会被杀死。
  • 如果容器使用的物理内存超过了 Memory Cgroup 里的 memory.limit_in_bytes (/sys/fs/cgroup/memory/目录下)值,那么容器中的进程会被 OOM Killer 杀死。
  • 在发生 OOM 的时候,Linux 到底是根据 oom_badness() 函数来选择被杀的进程:用系统总的可用页面数,去乘以 OOM 校准值 oom_score_adj(/proc//oom_score_adj ),再加上进程已经使用的物理页面数,计算出来的值越大,那么这个进程被 OOM Kill 的几率也就越大。
  • 每个容器的 Memory Cgroup 在统计每个控制组的内存使用时包含了两部分,RSS 和 Page Cache。
  • RSS 是每个进程实际占用的物理内存,它包括了进程的代码段内存,进程运行时需要的堆和栈的内存,这部分内存是进程运行所必须的。Page Cache 是进程在运行中读写磁盘文件后,作为 Cache 而继续保留在内存中的,它的目的是为了提高磁盘文件的读写性能。有时会看到这样一种情况:容器里的应用有很多文件读写,你会发现整个容器的内存使用量已经很接近 Memory Cgroup 的上限值了,但是在容器中我们接着再申请内存,还是可以申请出来,并且没有发生 OOM。那是因为容器中有部分是PageCache,当容器需要更多内存时,释放了PageCache,所以总大小并没有变化。

问答

  1. 协议栈在内核中实现,容器共享内核,那为什么容器又有自己的协议栈呢?
    答:在容器中,虽然容器和宿主机共享同一个内核,但容器内部仍然会有自己的网络协议栈。这是因为容器通过网络命名空间机制实现了网络资源的隔离,使得容器内部的网络协议栈与宿主机的网络协议栈是相互独立的。
    具体来说,当一个容器启动时,Docker会创建一个新的网络命名空间,并将容器的网络接口和IP地址等信息添加到该命名空间中。此时,容器内部的网络协议栈就可以通过网络命名空间机制实现与宿主机内核的隔离,不同容器之间的网络协议栈也可以互相隔离,从而实现容器级别的网络隔离。
    需要注意的是,虽然容器内部有自己的网络协议栈,但它们实际上是共享宿主机的协议栈代码和底层网络设备的驱动程序的。容器内部的网络协议栈只是在宿主机内核提供的网络协议栈上添加了一层抽象,它们共享了宿主机的协议栈代码和底层网络设备的驱动程序,从而减少了内存占用和运行开销。
    综上所述,虽然容器和宿主机共享同一个内核,但容器仍然可以通过网络命名空间机制实现网络资源的隔离,从而在容器内部创建自己的网络协议栈,实现了容器级别的网络隔离。

  2. 容器内的进程的进程号怎么对应主机的进程号?
    答:因为PID隔离,所以在容器内查看进程的进程号和在主机上查看该进程的进程号不一样。可以通过查看/sys/fs/cgroup/kubepods/burstable/podxxx/b7xxx/cgroup.procs 和 /sys/fs/cgroup/kubepods/burstable/podxxx/b7xxx/cgroup.threads 来查看。这里面只记录了在主机空间的进程号,并没有对应关系。

  3. 如何判断cgroup是哪种类型?
    答:df -T /sys/fs/cgroup/,1. 如果输出的类型为cgroup2,那么就是cgroup v2类型; 2. 如果输出是tmpfs,再df -T 查看、/sys/fs/cgroup/unified 文件属性,(1)如果该文件的属性也是cgroup2那么就是混合模式(hybrid)(2)如果没有unified文件则是cgroup v1(传统模式).

参考文档

  1. k8s官网
  2. 《kubernetes网络权威指南》
  3. 《Docker容器与容器云》中国工信出版集团
posted @ 2019-07-31 11:02  JaneySJ  阅读(2709)  评论(2编辑  收藏  举报