# 前言
时光匆匆,转眼2020年也只剩下2个多月了,感慨之。
今天来介绍下docker中用到的一个核心技术Namespace,由于个人能力有限,不会深入到具体的细节。
私认为一个基础的docker需具备以下功能:
1.资源隔离。即各个容器都是独立的,只能使用本容器的资源。比如每个容器只能看到自己的进程和文件,而看不到服务器上其他的进程和文件。每个容器的CPU和内存资源也是需要隔离的,不能出现某个容器把CPU占满,导致其他容器无法工作。
2.镜像功能。一处构建,到处执行。用户在安装好docker后,直接拉取镜像即可运行使用。
为了实现这些,docker用到了Namespace
,CGroups
和UnionFS
。
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目前有bridge
、host
、none
、overlay
、maclan
和 Network 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等网络插件来通信。