原本是继续阅读《每天5分钟玩转Kubernetes》一书的,发现该书所用的 Kubernetes 版本着实有点老旧( 1.7), 当前版本是 1.18。操作起来有些不同,所以找来了最新的 《Kubernetes in Action》第二版 来看,该书还在写作当中。第二章全是讲 Docker 的内容,本人读书有个不好的习惯,就是不喜欢跳过跳过。看了总会有收获的,这不,就从中稍微理清了 Docker 容器内进程与 Namespace 的关系。
Docker 容器间的进程本质上是宿主主上的一个进程,它能相互隔离靠的是 chroot, namespace 和 cgroup(对 CPU, 内存,磁盘,带宽等的配额)。千万不要认为启动一个 Docker 容器就是启动了一个虚拟机。
其中 namespace 实现了以下几项资源的隔离
- Mount: 挂载点(文件系统)
- PID: 进程 ID
- Network: 网络设备,网络栈,端口等
- IPC: 进程间通信,信号量,消息队列和共享等
- UTS: 主机名和域名
- User ID: 用户和组 ID
我们下载来检验 Docker 容器中的进程是否是宿主机的进程,还可以作个测试让两个容器共享相同的 Network 和 UTS 两个 Namespace. 由此也可以想像一下 Kubernetes 把多个容器放到一个 Pod 中,它们应该也是共享了 Namespace 的。
创建测试 Docker 镜像
在测试之前我们创建一个自己的 Docker 镜像,需两个文件,app.py 和 Dockerfile,它们的内容分别为
app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import sys import socket from http.server import HTTPServer, BaseHTTPRequestHandler class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers(); self.wfile.write(('You hit: ' + socket.gethostname() + '\n').encode()) port = int(sys.argv[1]) httpd = HTTPServer(('', port), SimpleHTTPRequestHandler); httpd.serve_forever() |
Dockerfile
1 2 3 |
FROM python:3.7.7-alpine3.10 ADD app.py /app.py ENTRYPOINT ["python", "app.py"] |
然后运行 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
af2c5eb278792ea6dc3d6071996cfa8ea7223dd243bf5ab359bb4fa79e7f6f19ubuntu# 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 保持一样的。
当然,更多发挥想像力的地方......
链接:
本文链接 https://yanbin.blog/docker-process-namespace/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
[…] 这涉及到了 Docker 与 Namespace 的话题,详情请参考前一篇 Docker 容器内进程与 Namespace。 […]
what replaces "--link"?
--link 不是 namespace, Docker compose 里也有 link, 你研究好告诉我