讲述Docker底层原理的开篇文章,本文讲述了Linux内核中的Namespace和Docker的关系;
系列文章:
源代码:
Docker原理实战-1:Namespace
前言 - 实验环境
在讲述Docker底层原理之前,先说一下实验的环境吧;
操作系统Ubuntu:
1 2 3 4 5 6
| root@jasonkay:~# lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal
|
Linux内核版本:
1 2
| root@jasonkay:~# uname -a Linux jasonkay 5.4.0-81-generic
|
Docker版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| root@jasonkay:~# docker version Client: Docker Engine - Community Version: 20.10.8 API version: 1.41 Go version: go1.16.6 Git commit: 3967b7d Built: Fri Jul 30 19:54:27 2021 OS/Arch: linux/amd64 Context: default Experimental: true
Server: Docker Engine - Community Engine: Version: 20.10.8 API version: 1.41 (minimum version 1.12) Go version: go1.16.6 Git commit: 75249d8 Built: Fri Jul 30 19:52:33 2021 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.4.9 GitCommit: e25210fe30a0a703442421b0f60afac609f950a3 runc: Version: 1.0.1 GitCommit: v1.0.1-0-g4144b63 docker-init: Version: 0.19.0 GitCommit: de40ad0
|
Golang版本:
1 2
| root@jasonkay:~# go version go version go1.17 linux/amd64
|
基本上都是目前(2021年08月29日)最新的了!
废话不多说,直接进入正题;
本文作为讲述Docker底层原理的开篇,先来讲述 Docker 实现容器隔离的技术:Namespace;
Linux Namespace
Namespace是Linux Kernel中的一个功能,便于隔离一系列的系统资源,如:
- PID;
- User ID;
- Network;
- ……
Namespace和chroot命令有些类似,但是比chroot强大的多!
关于chroot:
用途:
例如:Namespace可以做到UID级别的隔离,即:以UID为n的用户,虚拟化出来一个Namespace,在这个Namespace中,用户具有root权限!(但是在真实的物理机器上,他还是UID为n的用户!)
命令空间建立起系统不同的视图,从用户的角度来看:
每一个命名空间都如同一台单独的Linux计算机一样,有自己的init进程(PID为1),其他进程的PID依次递增!
例如下图:

A和B空间均存在PID为1的init进程,子命名空间的进程映射至父命名空间的进程上;
因此:父命名空间可以知道每个子命名空间的运行状态,而子命名空间之间是相互隔离的!
目前,Linux一共实现了6种不同类型的Namespace:
| Namespace类型 |
系统调用参数 |
内核版本 |
| Mount Namespace |
CLONE_NEWNS |
2.4.19 |
| UTS Namespace |
CLONE_NEWUTS |
2.6.19 |
| IPC Namespace |
CLONE_NEWIPC |
2.6.19 |
| PID Namespace |
CLONE_NEWPID |
2.6.24 |
| Network Namespace |
CLONE_NEWNET |
2.6.29 |
| User Namespace |
CLONE_NEWUSER |
3.8 |
Namespace API主要使用以下三个系统调用:
- clone():创建新进程;根据调用参数来判断哪些类型的Namespace被创建,并且由他创建的子进程会被包含到这些Namespace中!
- unshare():将进程移出某个Namespace;
- setns():将进程加入到Namespace中;
UTS Namespace
UTS Namespace 用来隔离nodename和domainname两个系统标识;
在UTS Namespace中,每个Namespace允许拥有自己的hostname!
下面是代码:
chapter2_basic/namespace/uts_namespace_demo.go
1 2 3 4 5 6 7 8 9 10 11 12 13
| func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } }
|
代码解释:
exec.Command("sh")指定被fork出来的新进程内的初始命令sh;
- 使用
CLONE_NEWUTS创建新的UTS命名空间并将clone()系统调用后的新的子进程加入;
cmd.StdXXX = os.StdXXX:当前进程和子进程输入输出流绑定;
运行代码:
1 2
| root@jasonkay:~/workspace/my_docker/chapter2_basic# go run namespace/uts_namespace_demo.go
|
进入一个sh运行环境中;
查看进程关系:
1 2 3 4 5 6 7 8 9 10 11 12 13
| systemd(1)─┬─VGAuthService(843) ├─sshd(941)───sshd(1095)─┬─bash(4096)───go(15135)─┬─uts_namespace_d(15232)─┬─sh(15237)───pstree(15246) │ │ │ ├─{uts_namespace_d}(15233) │ │ │ ├─{uts_namespace_d}(15234) │ │ │ ├─{uts_namespace_d}(15235) │ │ │ └─{uts_namespace_d}(15236) │ │ ├─{go}(15136) ……
15237
|
验证父子进程不在同一个UTS命名空间中:
1 2 3 4
| uts:[4026532644]
uts:[4026531838]
|
可以看到,的确不在同一个UTS命名空间!
查看hostname:
在宿主机启动另一个Shell,并查看hostname:
1 2
| root@jasonkay:~# hostname jasonkay
|
可以看到:外部的hostname并没有被内部的修改所影响!
IPC Namespace
IPC命名空间用来隔离System V IPC和 POSIX message queues;
chapter2_basic/namespace/ipc_namespace_demo.go
1 2 3 4 5 6 7 8 9 10 11 12 13
| func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } }
|
代码增加了CLONE_NEWIPC,代表同时创建IPC命名空间;
下面进行测试(需要同时在宿主机上打开两个Shell);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| root@jasonkay:~# ipcs -q
------ Message Queues -------- key msqid owner perms used-bytes messages
root@jasonkay:~# ipcmk -Q Message queue id: 0
root@jasonkay:~# ipcs -q
------ Message Queues -------- key msqid owner perms used-bytes messages 0x5336d6db 0 root 644 0 0
|
此时已经存在一个queue了!
使用另外一个shell运行程序:
1 2 3 4 5 6
| root@jasonkay:~/workspace/my_docker/chapter2_basic# go run namespace/ipc_namespace_demo.go
------ Message Queues -------- key msqid owner perms used-bytes messages
|
可以发现,新的命名空间,看不到宿主机的消息队列!
PID Namespace
PID命名空间用来隔离进程ID:
同一个进程在不同的PID命名空间可以拥有不同的PID!
例如:
在Docker容器中使用ps -ef可以发现:容器内前台运行的进程PID为1,但是在容器外却是不同的PID;
代码如下:
chapter2_basic/namespace/pid_namespace_demo.go
1 2 3 4 5 6 7 8 9 10 11 12 13
| func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } }
|
下面进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| root@jasonkay:~/workspace/my_docker/chapter2_basic# go run namespace/pid_namespace_demo.go
root@jasonkay:~# pstree -pl systemd(1)─┬─VGAuthService(843) ├─sshd(941)───sshd(1095)─┬─bash(4096)───go(16024)─┬─pid_namespace_d(16119)─┬─sh(16124) │ │ │ ├─{pid_namespace_d}(16120) │ │ │ ├─{pid_namespace_d}(16121) │ │ │ ├─{pid_namespace_d}(16122) │ │ │ └─{pid_namespace_d}(16123) │ │ ├─{go}(16025) ……
1
|
可以看到,PID被映射到了1上;
注:此时不能使用ps命令查看!
因为ps和top命令使用的是/proc中的内容,而此时我们没有修改挂载(Mount)命名空间!
Mount Namespace
Mount命名空间用来隔离各个进程看到的挂载点视图:
在不同的命名空间中的进程看到的文件系统层次是不同的,并且在新的Mount命名空间中调用mount()和unmount仅仅影响当前命名空间内的文件系统,而对全局的文件系统没有影响!
这个命名空间的功能类似于chroot,但是实现比这个系统调用更加灵活和安全!
Mount命名空间的系统调用参数为NEWNS;
这是因为,Mount命名空间是Linux第一个实现的命名空间类型,当时还没有意识到还有其他更多类型的命名空间出现!
下面是代码:
chapter2_basic/namespace/mount_namespace_demo.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) } }
|
首先运行代码,并查看/proc目录中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| root@jasonkay:~/workspace/my_docker/chapter2_basic# go run namespace/mount_namespace_demo.go
1 130 144 163 2 272 310 331 4096 57 810 9441 iomem pressure 10 1302 145 16385 20 273 311 332 41 58 812 951 ioports sched_debug 10720 13095 146 164 206 274 312 333 413 582 813 957 irq schedstat 1095 13098 147 16434 21 275 313 334 42 59 814 966 kallsyms scsi 11 131 148 165 22 276 314 335 438 6 815 991 kcore self 1100 13369 149 16522 23 277 315 336 44 60 839 acpi keys slabinfo 1102 13370 15 16527 24 278 316 337 45 64 843 asound key-users softirqs 11129 13384 150 16530 258 279 317 338 46 65 845 buddyinfo kmsg stat 117 134 151 166 259 28 318 339 47 66 877 bus kpagecgroup swaps 11713 135 152 167 26 280 319 34 478 67 879 cgroups kpagecount sys 118 137 153 168 260 281 32 35 479 6758 893 cmdline kpageflags sysrq-trigger 119 138 154 169 261 282 320 36 48 68 899 consoles loadavg sysvipc 12 13873 155 17 262 283 321 366 480 69 9 cpuinfo locks thread-self 12563 13874 156 172 263 284 322 38 50 786 901 crypto mdstat timer_list 12570 13875 157 173 264 285 323 384 51 787 907 devices meminfo tty 126 139 158 174 265 286 324 385 514 788 908 diskstats misc uptime 12608 14 159 175 266 287 325 387 52 789 909 dma modules version 127 140 16 176 267 29 326 388 521 790 920 driver mounts version_signature 1275 141 160 178 268 3 327 389 53 799 928 execdomains mpt vmallocinfo 128 14132 16013 179 269 30 328 39 538 800 9284 fb mtrr vmstat 1286 142 161 18 27 304 329 4 54 802 929 filesystems net zoneinfo 129 143 162 190 270 308 33 40 554 803 941 fs pagetypeinfo 13 1432 16252 193 271 309 330 401 56 805 9440 interrupts partitions
|
/proc是一个文件系统,提供额外机制,通过内核和内核模块将信息发送给进程!
此时/proc为宿主机的,因此看到里面会比较乱;
下面将/proc mount 至自己的Namespace下:
1 2 3 4 5 6 7 8 9 10
|
1 consoles fb kcore locks net slabinfo timer_list 5 cpuinfo filesystems keys mdstat pagetypeinfo softirqs tty acpi crypto fs key-users meminfo partitions stat uptime asound devices interrupts kmsg misc pressure swaps version buddyinfo diskstats iomem kpagecgroup modules sched_debug sys version_signature bus dma ioports kpagecount mounts schedstat sysrq-trigger vmallocinfo cgroups driver irq kpageflags mpt scsi sysvipc vmstat cmdline execdomains kallsyms loadavg mtrr self thread-self zoneinfo
|
可以看到少了非常多!
此时再使用ps查看系统进程:
1 2 3 4
| UID PID PPID C STIME TTY TIME CMD root 1 0 0 20:36 pts/1 00:00:00 sh root 6 1 0 20:40 pts/1 00:00:00 ps -ef
|
可以看到,当前命名空间中 sh 进程为PID为 1 的进程,而当前的 Mount命名空间和外部是隔离的!
Docker Volumn也是利用了这个特性!
注:试验结束后,需要在容器中执行umount /proc取消挂载,否则会影响到外部挂载!
(这是受systemd影响的!后面会介绍如何避免受影响!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
1 130 144 163 193 271 309 330 401 56 805 9440 interrupts partitions 10 1302 145 164 2 272 310 331 4096 57 810 9441 iomem pressure 10720 13095 146 16434 20 273 311 332 41 58 812 951 ioports sched_debug 1095 13098 147 165 206 274 312 333 413 582 813 957 irq schedstat 11 131 148 16522 21 275 313 334 42 59 814 966 kallsyms scsi 1100 13369 149 16527 22 276 314 335 438 6 815 991 kcore self 1102 13370 15 166 23 277 315 336 44 60 839 acpi keys slabinfo 11129 13384 150 167 24 278 316 337 45 64 843 asound key-users softirqs 117 134 151 16706 258 279 317 338 46 65 845 buddyinfo kmsg stat 11713 135 152 16716 259 28 318 339 47 66 877 bus kpagecgroup swaps 118 137 153 16755 26 280 319 34 478 67 879 cgroups kpagecount sys 119 138 154 168 260 281 32 35 479 6758 893 cmdline kpageflags sysrq-trigger 12 13873 155 169 261 282 320 36 48 68 899 consoles loadavg sysvipc 12563 13874 156 17 262 283 321 366 480 69 9 cpuinfo locks thread-self 12570 13875 157 172 263 284 322 38 50 786 901 crypto mdstat timer_list 126 139 158 173 264 285 323 384 51 787 907 devices meminfo tty 12608 14 159 174 265 286 324 385 514 788 908 diskstats misc uptime 127 140 16 175 266 287 325 387 52 789 909 dma modules version 1275 141 160 176 267 29 326 388 521 790 920 driver mounts version_signature 128 14132 16013 178 268 3 327 389 53 799 928 execdomains mpt vmallocinfo 1286 142 161 179 269 30 328 39 538 800 9284 fb mtrr vmstat 129 143 162 18 27 304 329 4 54 802 929 filesystems net zoneinfo 13 1432 16252 190 270 308 33 40 554 803 941 fs pagetypeinfo
|
umount后,挂载恢复!
User Namespace
User命名空间用于隔离用户的用户组,即:
一个进程的UserId和GroupId在User命名空间内外可以是不同的!
例如:
在宿主机上以一个非root用户创建一个User命名空间,而在User命名空间里面映射为root!
注:在 Linux Kernel 3.8开始,非root进程也可以创建User命名空间了!
代码如下:
chapter2_basic/namespace/user_namespace_demo.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
UidMappings: []syscall.SysProcIDMap{ { ContainerID: 1, HostID: syscall.Getuid(), Size: 1, }, }, GidMappings: []syscall.SysProcIDMap{ { ContainerID: 1, HostID: syscall.Getgid(), Size: 1, }, }, }
cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) }
os.Exit(-1) }
|
需要注意的是:这里配置了Uid和Gid的映射;
不同的操作系统可能要求是不同的,这里建议使用Ubuntu操作系统!
下面进行测试:
1 2 3
| root@jasonkay:~# id uid=0(root) gid=0(root) groups=0(root)
|
可以看到,此时是root用户;
运行程序:
1 2 3
| root@jasonkay:~/workspace/my_docker/chapter2_basic# go run namespace/user_namespace_demo.go $ id uid=1(daemon) gid=1(daemon) groups=1(daemon)
|
可以看到,他们的UID是不同的!
Network Namespace
Network命名空间用来隔离网络设备、IP地址等;
Network命名空间可以让每个容器拥有自己独立的(虚拟)网络设备,并且容器内的应用可以绑定到自己的端口,并且不会冲突!
代码如下:
chapter2_basic/namespace/network_namespace_demo.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET, UidMappings: []syscall.SysProcIDMap{ { ContainerID: 1, HostID: syscall.Getuid(), Size: 1, }, }, GidMappings: []syscall.SysProcIDMap{ { ContainerID: 1, HostID: syscall.Getgid(), Size: 1, }, }, }
cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { log.Fatal(err) }
os.Exit(-1) }
|
首先,在宿主机查看网络设备:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| root@jasonkay:~# ifconfig ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.24.135 netmask 255.255.255.0 broadcast 192.168.24.255 inet6 fe80::20c:29ff:fe4d:11db prefixlen 64 scopeid 0x20<link> ether 00:0c:29:4d:11:db txqueuelen 1000 (Ethernet) RX packets 980680 bytes 94756175 (94.7 MB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 1697074 bytes 2161080469 (2.1 GB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> loop txqueuelen 1000 (Local Loopback) RX packets 1650 bytes 131668 (131.6 KB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 1650 bytes 131668 (131.6 KB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
|
运行程序,并在容器中查看:
1 2 3
| root@jasonkay:~/workspace/my_docker/chapter2_basic# go run namespace/network_namespace_demo.go $ ifconfig $
|
此时,容器中没有任何设备,即:容器和宿主机直接网络是隔离的!
小结
本篇作为开篇,讲述了Docker中容器隔离所依赖的技术 Namespace,主要包括六个部分:
- UTS Namespace
- IPC Namespace
- PID Namespace
- Mount Namespace
- User Namespace
- Network Namespace
下一篇将会介绍 Docker 和 K8S 中限制容器内硬件资源的技术:Cgroups;
附录
系列文章:
源代码: