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、网络、文件系统、用户等等资源。在某种程度上,实现了一个简单的虚拟:让一个主机上可以同时运行多个互不感知的系统。
此外,为了限制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进行隔离。
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)。
- 优先级分配(Prioritization):通过分配的 CPU 时间片数量及硬盘 IO 带宽大小,实际上就相当于控制了进程运行的优先级。
- 资源统计(Accounting): cgroups 可以统计系统的资源使用量,如 CPU 使用时长、内存用量等等,这个功能非常适用于计费。
- 进程控制(Control):cgroups 可以对进程组执行挂起、恢复等操作。
Docker正是使用cgroup进行资源划分,每个容器都作为一个进程运行起来,每个业务容器都会有一个基础的pause容器也就是POD作为基础容器。pause容器提供了划分namespace的内容,并连通同一POD下的所有容器,共享网络资源。查看容器的PID,对应/proc/pid/下是该容器的运行资源,每一个文件保持打开的话,对应的namespace就会一直存在。
在分析 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 相关文件。这些都是资源划分相关文件。 一般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 ~]#
可以看出业务容器实际上重用了 pause 容器的 net 和 ipc 命名空间!我认为上述发现完美的解释了同一个 Pod 中容器具有的能力:
- 能够互相通信,共享网络, 通过
nsenter -t <容器进程号> -n -F -- ip a命令可以看出它们的网络配置完全一样 - 使用 IPC(共享内存,消息队列等)
再来看一个本地的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.
network namespace
参考上面的例子,我们着重关注一下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中执行操作,这样就可以起到执行结果和原(父)进程隔离的效果。
参考文档
- k8s官网
- 《kubernetes网络权威指南》