# 前言

时光匆匆,转眼2020年也只剩下2个多月了,感慨之。

今天来介绍下docker中用到的一个核心技术Namespace,由于个人能力有限,不会深入到具体的细节。

私认为一个基础的docker需具备以下功能:

1.资源隔离。即各个容器都是独立的,只能使用本容器的资源。比如每个容器只能看到自己的进程和文件,而看不到服务器上其他的进程和文件。每个容器的CPU和内存资源也是需要隔离的,不能出现某个容器把CPU占满,导致其他容器无法工作。

2.镜像功能。一处构建,到处执行。用户在安装好docker后,直接拉取镜像即可运行使用。

为了实现这些,docker用到了Namespace,CGroupsUnionFS

Namespace:解决进程、网络、文件系统的资源隔离问题。

CGroups:解决内存、CPU等物理资源隔离问题。

UnionFS:解决镜像问题。

CGroups和UnionFS之前有简单介绍过,这次来看看Namespace。

# Namespace简介

Namespace 是Linux提供的一种内核级别隔离机制,目前提供的能力如下:

名称 隔离资源
Cgroup CLONE_NEWCGROUP cgroup根目录
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(主机名与NIS域名)

以进程隔离为例,我们在启动docker时,会调用createSpec方法,创建docker的namespace。

func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
	s := oci.DefaultSpec()

	// ...
	if err := setNamespaces(daemon, &s, c); err != nil {
		return nil, fmt.Errorf("linux spec namespaces: %v", err)
	}

	return &s, nil
}

setNamespaces方法会设置容器的进程、用户、网络、IPC 以及 UTS 相关的命名空间:

func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error {
	// user
	// network
	// ipc
	// uts

	// pid
	if c.HostConfig.PidMode.IsContainer() {
		ns := specs.LinuxNamespace{Type: "pid"}
		pc, err := daemon.getPidContainer(c)
		if err != nil {
			return err
		}
		ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())
		setNamespace(s, ns)
	} else if c.HostConfig.PidMode.IsHost() {
		oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))
	} else {
		ns := specs.LinuxNamespace{Type: "pid"}
		setNamespace(s, ns)
	}

	return nil
}

通过docker exec 进入容器,你会发现所有pid是从1开始计数的,且看不到系统上其他的进程。

/ # ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 npm
   17 root      0:00 node /node_modules/.bin/egg-scripts start --title=egg-serv
   28 root      0:13 node --require /node_modules/source-map-support/register.j
   35 root      2:32 /usr/local/bin/node --require /node_modules/source-map-sup
   46 root      2:21 /usr/local/bin/node --require /node_modules/source-map-sup
   52 root      2:24 /usr/local/bin/node --require /node_modules/source-map-sup
   75 root      0:00 sh
   81 root      0:00 ps aux

在系统上找一下这个docker进程:

[root@iZwz9eyfvpfa7klu4pa9odZ ~]# ps -ef | grep npm
root     1076972  431810  0 16:12 pts/0    00:00:00 grep --color=auto npm
root     2421826 2421807  0 9月18 ?       00:00:00 npm

可以看到这个docker进程的pid是2421826,父进程是2421807。看下父进程2421807:

[root@iZwz9eyfvpfa7klu4pa9odZ ~]# ps -ef | grep 2421807
root     1030542 2421807  0 15:47 pts/0    00:00:00 sh
root     1088496  431810  0 16:18 pts/0    00:00:00 grep --color=auto 2421807
root     2421807    1731  0 9月18 ?       00:00:56 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/80e012862a8fe4b8cc636928f8b6022d7414ba176b752edadca426a1f34fe815 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc -systemd-cgroup
root     2421826 2421807  0 9月18 ?       00:00:00 npm

可以看到是containerd-shim,是docker的核心进程,它的父进程是1731。我们再找几个父进程是1731的其他containerd-shim进程,对比下他们的namespace:

[root@iZwz9eyfvpfa7klu4pa9odZ ~]# ps -ef | grep 1731
root        1731       1  0 9月14 ?       01:58:44 /usr/bin/containerd
root        2875    1731  0 9月14 ?       00:01:08 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/77c2595e8a78cd7f4c405b6ee445789414707e669ac050a655bc0baa90dc6ba0 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc -systemd-cgroup
root        2887    1731  0 9月14 ?       00:01:04 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/1ae5ccfa0f656a3ce7ad1bf87e5d7c6302776676d76e0277463c9f43cf15e383 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc -systemd-cgroup
root        2968    1731  0 9月14 ?       00:01:12 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/ebe45fdd5fcd8ab585954cc285ec8122f37107f05feb9447dadb6120365a1af9 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc -systemd-cgroup

以pid:2968为例,2968下有个/pause进程,pid是2988:

[root@iZwz9eyfvpfa7klu4pa9odZ ~]# ps -ef | grep 2968
root        2968    1731  0 9月14 ?       00:01:12 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/ebe45fdd5fcd8ab585954cc285ec8122f37107f05feb9447dadb6120365a1af9 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc -systemd-cgroup
root        2988    2968  0 9月14 ?       00:00:00 /pause

我们对比下2988和2421807的namespace:

[root@iZwz9eyfvpfa7klu4pa9odZ ~]# ll /proc/2988/ns
总用量 0
lrwxrwxrwx 1 root root 0 10月 10 16:29 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 10月 10 16:29 ipc -> ipc:[4026532239]
lrwxrwxrwx 1 root root 0 10月 10 16:29 mnt -> mnt:[4026532237]
lrwxrwxrwx 1 root root 0 10月 10 16:29 net -> net:[4026531992]
lrwxrwxrwx 1 root root 0 10月 10 16:29 pid -> pid:[4026532240]
lrwxrwxrwx 1 root root 0 10月 10 16:29 pid_for_children -> pid:[4026532240]
lrwxrwxrwx 1 root root 0 10月 10 16:29 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 10月 10 16:29 uts -> uts:[4026532238]

2421807如下:

[root@iZwz9eyfvpfa7klu4pa9odZ ~]# ll /proc/2421807/ns
总用量 0
lrwxrwxrwx 1 root root 0 10月 10 16:25 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 10月 10 16:25 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 10月 10 16:25 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 10月 10 16:25 net -> net:[4026531992]
lrwxrwxrwx 1 root root 0 10月 10 16:25 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 10月 10 16:25 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 10月 10 16:25 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 10月 10 16:25 uts -> uts:[4026531838]

可以发现他们的ipc,mnt,pid,pid_for_children不同。

# docker网络互通

通过namespace把资源隔离后,网络也隔离了,此时需要解决容器间的网络互通问题。docker目前有bridgehostnoneoverlaymaclanNetwork plugins等网络模式,在启动docker时通过network参数设置具体使用那一种模式。

以bridge模式为例,docker会给每个容器创建一对虚拟网卡,其中一个会加入到 docker0 网桥中,容器间的通信通过docker0来完成。容器和外部的通信使用NAT的方式,通过系统的iptables来实现。

值得一提的是,docker的这些功能都是通过Libnetwork来实现的。Libnetwork是从docker1.6开始,从docker项目中的网络部分抽离出来形成的容器网络模型,也被称为Container Network Model,简称CNM,由Sandbox, Endpoint, Network 三种组件组成。具体的细节可以查阅相关资料,这里就不多做介绍了。

值得二提的是,k8s用的是CNI模型,并非docker的CNM模型。在刚才的例子中,我们对比了两个容器的namespace,其中一个容器是pause,pause在k8s中提供了网络方面的功能,接管了pod里面其他容器的网络。在刚才的例子中也可以发现两者的net namespace是一致的。说到这里,索性就介绍下k8s下的docker网络通信吧:

1.同一个pod里的容器通过localhost来通信。
2.同一个node中pod间的容器通过docker0来通信。
3.不同的node的pod间的容器通过Flannel、 Calico、 Romana、 Weave-net等网络插件来通信。

# 参考

Docker 核心技术与实现原理

Docker 的网络模式