Docker 容器内进程与 Namespace

原本是继续阅读《每天5分钟玩转Kubernetes》一书的,发现该书所用的 Kubernetes 版本着实有点老旧( 1.7), 当前版本是 1.18。操作起来有些不同,所以找来了最新的 《Kubernetes in Action》第二版 来看,该书还在写作当中。第二章全是讲 Docker 的内容,本人读书有个不好的习惯,就是不喜欢跳过跳过。看了总会有收获的,这不,就从中稍微理清了 Docker 容器内进程与 Namespace 的关系。

Docker 容器间的进程本质上是宿主主上的一个进程,它能相互隔离靠的是 chroot, namespace 和 cgroup(对 CPU, 内存,磁盘,带宽等的配额)。千万不要认为启动一个 Docker 容器就是启动了一个虚拟机。

其中 namespace 实现了以下几项资源的隔离

  1. Mount: 挂载点(文件系统)
  2. PID: 进程 ID
  3. Network: 网络设备,网络栈,端口等
  4. IPC: 进程间通信,信号量,消息队列和共享等
  5. UTS: 主机名和域名
  6. User ID: 用户和组 ID

我们下载来检验 Docker 容器中的进程是否是宿主机的进程,还可以作个测试让两个容器共享相同的 Network 和 UTS 两个 Namespace. 由此也可以想像一下 Kubernetes 把多个容器放到一个 Pod 中,它们应该也是共享了 Namespace 的。

创建测试 Docker 镜像

在测试之前我们创建一个自己的 Docker 镜像,需两个文件,app.py 和 Dockerfile,它们的内容分别为

app.py

Dockerfile

然后运行 docker build -t python-web . 创建镜像 python-web

启动容器

现在用刚刚创建的 python-web 启动两个容器,命令为 docker run -d --name web1 -p8000:8000 python-web 8000

查看容器中的进程

容器启动后用 docker exec -it 69 sh 登陆到该容器的 shell 查看容器内的进程

ubuntu# docker exec -it 82 sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 python app.py
6 root 0:00 sh
11 root 0:00 ps aux
/ # netstat -nlp | grep 8000
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 1/python

在容器内我们所看到的 python app.py 进程 ID 为 1

查看宿主机上的进程

这就验证了容器中的进程实质是宿主机上的一个进程,如果在宿主机上的那个进程用 kill 22763 杀了,这时候发现相应的 Docker 容器也被杀掉了,因为 22763  对应的是容器内的 ID 为 1 的进程。

在宿主机上看下这两个进程对应的 namespace

ubuntu# ls -l /proc/22763/ns
total 0
lrwxrwxrwx 1 root root 0 Mar 29 13:44 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 ipc -> 'ipc:[4026532360]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 mnt -> 'mnt:[4026532358]'
lrwxrwxrwx 1 root root 0 Mar 29 13:39 net -> 'net:[4026532363]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 pid -> 'pid:[4026532361]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 pid_for_children -> 'pid:[4026532361]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 uts -> 'uts:[4026532359]'
ubuntu# ls -l /proc/22889/ns
total 0
lrwxrwxrwx 1 root root 0 Mar 29 13:44 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 ipc -> 'ipc:[4026532475]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 mnt -> 'mnt:[4026532473]'
lrwxrwxrwx 1 root root 0 Mar 29 13:40 net -> 'net:[4026532478]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 pid -> 'pid:[4026532476]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 pid_for_children -> 'pid:[4026532476]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Mar 29 13:44 uts -> 'uts:[4026532474]'

每个 python app.py 都有自己的 namespace, 其中 user  共用了相同的 user:[4026531837], 其他的都不同。

注:操作系统是 Linux 下的话,宿主机就是自己,如果是用的 Mac OS X 或 Windows 用的 Dock Desktop, 那么宿主机是中间隔着的那个 Linux 虚拟机,可以如下命令进入那个虚拟机

$ docker run --net=host --ipc=host --uts=host --pid=host -it --security-opt=seccomp=unconfined --privileged --rm -v /:/host alpine chroot /host
/ # uname -a
Linux docker-desktop 4.19.76-linuxkit #1 SMP Thu Oct 17 19:31:58 UTC 2019 x86_64 GNU/Linux

此处运行一个  alpine 容器,充分利用了 Namespace 的好处,把 net, ipc, uts, pid, mnt(-v) 的 Namespace 全部指向到 host(即宿主机), chroot 也更改到了宿主机的主目录,并有完全的系统权限,所以进去看到的是宿主机的内容。

多个容器共用 Network 和 UTS

了解了容器内进程之间的隔离是通过 namespace 达到的,并且所谓的容器内的进程实质上是宿主机上的进程。那么接下来我们让两个容器共用相同的 Network 和 UTS

用下面两个命令来启动容器

# docker run -d --name=web1 -p8000:8000 -p 8001:8001 --uts=host python-web 8000
# docker run -d --name=web2 --network=container:web1 --uts=host python-web 8001

docker run  命令有大量的命令来定制化容器的运行,用 docker run --help 查看帮助

启动 web2 时用  --network=container:web1 指定它用与  web1 相同的网络,所以端口映射必须在启动 web1 时全部指定好。假如启动 web2 同时用  --network 和 -p 或  -P 启动不会成功,它的 --network 用了  web1 的所有的端口映射关系必须会部在 web1  上设定好。 

现在在验证它们是不是真的使用了相同的网络

ubuntu# curl http://localhost:8000
You hit: ubuntu
ubuntu# curl http://localhost:8001
You hit: ubuntu

它们用了宿主机的 UTS

检查它们的 IP 地址

ubuntu# docker exec -t web1 ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:62 errors:0 dropped:0 overruns:0 frame:0
TX packets:32 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:6064 (5.9 KiB) TX bytes:2580 (2.5 KiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
......
ubuntu# docker exec -t web2 ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:62 errors:0 dropped:0 overruns:0 frame:0
TX packets:32 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:6064 (5.9 KiB) TX bytes:2580 (2.5 KiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
......

它们的 IP  地址一样的。会是完全同一个容器吗?当然不是,它们启动服务的端口就不一样,或者下面那样验证

ubuntu# docker exec -t web1 touch /a
ubuntu# docker exec -t web2 ls /a
ls: /a: No such file or directory
ubuntu# docker exec -t web2 ls /

再来看它们的 namespace,在宿主机上找到它们的进程 ID

ubuntu# ps aux|grep 'python app.py'
root 26259 0.1 0.0 18036 15120 ? Ss 14:35 0:00 python app.py 8000
root 26343 0.0 0.0 18036 15152 ? Ss 14:35 0:00 python app.py 8001
root 27272 0.0 0.0 8988 908 pts/0 S+ 14:42 0:00 grep python app.py
ubuntu# ls -l /proc/26259/ns
total 0
lrwxrwxrwx 1 root root 0 Mar 29 14:42 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Mar 29 14:38 ipc -> 'ipc:[4026532359]'
lrwxrwxrwx 1 root root 0 Mar 29 14:38 mnt -> 'mnt:[4026532358]'
lrwxrwxrwx 1 root root 0 Mar 29 14:35 net -> 'net:[4026532362]'
lrwxrwxrwx 1 root root 0 Mar 29 14:38 pid -> 'pid:[4026532360]'
lrwxrwxrwx 1 root root 0 Mar 29 14:42 pid_for_children -> 'pid:[4026532360]'
lrwxrwxrwx 1 root root 0 Mar 29 14:42 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Mar 29 14:42 uts -> 'uts:[4026531838]'
ubuntu# ls -l /proc/26343/ns
total 0
lrwxrwxrwx 1 root root 0 Mar 29 14:43 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Mar 29 14:38 ipc -> 'ipc:[4026532471]'
lrwxrwxrwx 1 root root 0 Mar 29 14:38 mnt -> 'mnt:[4026532470]'
lrwxrwxrwx 1 root root 0 Mar 29 14:38 net -> 'net:[4026532362]'
lrwxrwxrwx 1 root root 0 Mar 29 14:38 pid -> 'pid:[4026532472]'
lrwxrwxrwx 1 root root 0 Mar 29 14:43 pid_for_children -> 'pid:[4026532472]'
lrwxrwxrwx 1 root root 0 Mar 29 14:43 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Mar 29 14:43 uts -> 'uts:[4026531838]'

这也能看到它们使用了相同的 Network namespace.

不指 --uts  再来测试一下

ubuntu# docker run -d --name=web1 -p8000:8000 -p 8001:8001 python-web 8000
046cedf0b7276c8c7aa37191c42a8b9e5fa159ca634d80074dce7470fa61f00e
ubuntu# docker run -d --name=web2 --network=container:web1 python-web 8001
af2c5eb278792ea6dc3d6071996cfa8ea7223dd243bf5ab359bb4fa79e7f6f19

ubuntu# curl http://localhost:8000
You hit: 046cedf0b727
ubuntu# curl http://localhost:8001
You hit: 046cedf0b727

只共享了 Network, 去掉了 --uts 后,看它们的 /proc/<PID>/ns 也能发现 uts namespace 不一样了。

另一个关于端口重定向的发现,docker 是通过  docker-proxy 实现的

理解 Docker Namespace 的好处

明白了 Docker 容器与 Namespace 的关系后,我们能够对  docker run 进行更多的控制,比如共享 Mount 空间能使两个容器看到相同的外部文件系统,共享了 PID 空间的话,两个容器中可以看到相同的进程

还比如像前面的命令:docker run --net=host --ipc=host --uts=host --pid=host -it --security-opt=seccomp=unconfined --privileged --rm -v /:/host alpine chroot /host, 类似 Impersonate 的方式进入另一个容器中。

还有,因为容器中的进程实质是一个宿主机上的进程,也就可以联接相应的宿主机进程进行单步断点调试,或都用 --pid=host 让容器中的进程 ID 与外部的 ID 保持一样的。

当然,更多发挥想像力的地方......

链接:

  1. docker run
  2. Sharing Network Namespaces in Docker
  3. Docker 基础技术之 Linux namespace 详解

本文链接 https://yanbin.blog/docker-process-namespace/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

3 Comments
Inline Feedbacks
View all comments
trackback

[…] 这涉及到了 Docker 与 Namespace 的话题,详情请参考前一篇 Docker 容器内进程与 Namespace。 […]

Yijun Yuan
Yijun Yuan
4 years ago

what replaces "--link"?