文章目录
写在前面
本blog是基于《自己动手写docker》一书的实验笔记,与原书本的生产环境和golang版本都不同,所以会导致各种bug,本文针对这些进行了修复。
系统环境
测试环境
[root@iz2zegp1t778mbekgw2rdrz fs]# lsb_release -a
LSB Version: :core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description: CentOS Linux release 7.3.1611 (Core)
Release: 7.3.1611
Codename: Core
[root@iz2zegp1t778mbekgw2rdrz fs]# uname -r
3.10.0-514.26.2.el7.x86_64
[root@iz2zegp1t778mbekgw2rdrz fs]# go version
go version go1.13.5 linux/amd64
[root@iz2zegp1t778mbekgw2rdrz fs]# docker -v
Docker version 19.03.12, build 48a66213fe
Namespace
概念
是Kernel的一个功能,主要用于资源隔离,当前Linux支持六种Namespace
Namespace版本 | 系统调用参数 | 内核版本 |
---|---|---|
UTS Namespace | CLONE_NEWUTS | 2.6.19 |
IPC Namespace | CLONE_NEWIPC | 2.6.19 |
Mount Namespace | CLONE_NEWNS | 2.4.19 |
User Namespace | CLONE_NEWUTS | 3.8 |
PID Namespace | CLONE_NEWPID | 2.6.24 |
Network Namespace | CLONE_NEWUTS | 2.6.29 |
Namespace 的API主要由下列三个调用
- clone():创建新process,由参数来确定被创建的n、Namespace的类型,并且其子进程也会被包含到Namespace中。
- unshare():将进程移出某个Namespace。
- setns():将进程加入某个Namespace中。
6个Namespace
UTS Namespace
UTS Namespace(UNIX Time-sharing System Namespace)主要用来隔离nodename和domainname这两个系统标识。在每个UTS Namespace中,每个Namespace允许拥有自己的hostname。
代码实现:
func UTSNS() {
cmd := exec.Command("sh")
// 笔者使用的是MACOS,服务器是阿里云。如果使用goland等IDE,IDE自动识别当前系统为Darwin,下面这个struct会报错,但是不用管他。
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)
}
}
运行代码,进入交互式终端,使用pstree -pl
查看当前的进程树
[root@iz2zegp1t778mbekgw2rdrz docker]# go run main.go
sh-4.2# pstree -pl
├─sshd(17408)───bash(17410)───go(17546)─┬─main(17571)─┬─sh(17574)───pstree(17575)
可以看到,父进程为main(17571)
,子进程为sh(17574)
。
查看它们是否在同一个UTS Namespace中
sh-4.2# readlink /proc/17571/ns/uts
uts:[4026531838]
sh-4.2# readlink /proc/17574/ns/uts
uts:[4026532220]
的确不在同一个UTS Namespace中,接下来验证hostname是否可以隔离。
sh-4.2# hostname -b hu5ky
sh-4.2# hostname
hu5ky
可见当前shell中的hostname为hu5ky。重新开启一个shell
[root@iz2zegp1t778mbekgw2rdrz home]# hostname
iz2zegp1t778mbekgw2rdrz
可见当前主机的hostname为iz2zegp1t778mbekgw2rdrz,因此hostname隔离成立。
IPC Namespace
IPC Namespace是用来隔离system V mq和POSIX mq的。每一个IPC Namespace都有自己的system V mq和POSIX mq。
代码实现:
func IPCNS() {
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)
}
}
开启两个shell来验证IPC的资源隔离
首先在宿主机打开一个shell:
[root@iz2zegp1t778mbekgw2rdrz ~]# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
可以看到目前没有ipc,接下来创建一个新的ipc:
[root@iz2zegp1t778mbekgw2rdrz ~]# ipcmk -Q
Message queue id: 0
[root@iz2zegp1t778mbekgw2rdrz ~]# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x7e9ce120 0 root 644 0 0
打开另一个shell:
[root@iz2zegp1t778mbekgw2rdrz docker]# go run main.go
sh-4.2# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
可以看到,在运行了main.go进入sh后,使用ipcs -q
查询的ipc为空,因此宿主机和开启的新终端ipc资源隔离,并且IPC Namespace创建成功。
Mount Namespace
Mount Namespace用来隔离各个进程的挂载点视图。也就是说,在Mount Namespace中使用mount
或者umount
不会影响其他进程的挂载。因为Mount Namespace是第一个支持的Namespace,所以其系统调用参数为NEWNS(New Namespace)。
代码实现:
func MountNS() {
cmd := exec.Command("sh")
syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")
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
作用是实现进程和内核之间通信。首先运行代码,查看/proc
中的资源:
sh-4.2# ls /proc/
1 17729 18481 238 328 460 765 cmdline interrupts locks scsi uptime
10 18 18493 239 338 462 766 consoles iomem mdstat self version
10423 18292 18515 241 356 464 795 cpuinfo ioports meminfo slabinfo vmallocinfo
106 18298 18518 25 37 465 797 crypto irq misc softirqs vmstat
12 18300 18520 253 370 489 798 devices kallsyms modules stat zoneinfo
13 18317 19 258 39 490 8 diskstats kcore mounts swaps
14 18319 1928 259 40 5 9 dma keys mtrr sys
15 18394 2 26 4031 6 908 driver key-users net sysrq-trigger
15494 18396 215 27 41 60 acpi execdomains kmsg pagetypeinfo sysvipc
15526 18431 216 28 430 7 buddyinfo fb kpagecount partitions timer_list
16 18433 236 29 451 700 bus filesystems kpageflags sched_debug timer_stats
17 18479 237 3 454 763 cgroups fs loadavg schedstat tty
可以看见目前由大量的资源信息,因为目前/proc
中还是宿主机的资源信息,我们将其挂载到当前进程:
sh-4.2# mount -t proc proc /proc
sh-4.2# ls /proc/
1 consoles execdomains irq kpageflags mounts scsi sysrq-trigger vmallocinfo
4 cpuinfo fb kallsyms loadavg mtrr self sysvipc vmstat
acpi crypto filesystems kcore locks net slabinfo timer_list zoneinfo
buddyinfo devices fs keys mdstat pagetypeinfo softirqs timer_stats
bus diskstats interrupts key-users meminfo partitions stat tty
cgroups dma iomem kmsg misc sched_debug swaps uptime
cmdline driver ioports kpagecount modules schedstat sys version
可以看到,现在少了很多的资源信息,我们查看一下当前的进程信息:
sh-4.2# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 11:44 pts/1 00:00:00 sh
root 5 1 0 11:47 pts/1 00:00:00 ps -ef
可以看到当前的ps命令查看的信息中,sh进程的PID为1,实现了挂载隔离,docker volume就是借助这个特性实现的。
User Namespace
User Namespace主要作用是用来隔离当前用户的用户组ID,一个进程在User Namespace内外的User ID和Group ID可以是不同的,比较常用的作用是使一个非root用户在一个User Namespace中以root权限去调度资源。Linux Kernel >=3.8
支持非root用户创建一个User Namespace。
代码实现:
func USERNS() {
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: 1234,
HostID: syscall.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 1234,
HostID: syscall.Getgid(),
Size: 1,
},
},
}
//cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(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)
}
首先在运行前看一下当前用户在宿主机的id信息。
[root@iz2zegp1t778mbekgw2rdrz docker]# id
uid=0(root) gid=0(root) groups=0(root)
运行代码查看id信息:
sh-4.2$ id
uid=1234 gid=1234 groups=1234
uid,gid,groups不同,User Namespace生效。
PID Namespace
PID Namespace用来隔离进程的ID,在不同的PID Namespace中同一个进程拥有不同的PID。当进入一个docker container时,使用ps -ef
查看到当前的进程PID为1,但是在宿主机查看时,PID并不为1,这就是PID Namespace的作用。
代码实现:
func PIDNS() {
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)
}
}
我们需要打开两个shell终端来验证。
首先打开宿主机的shell:
[root@iz2zegp1t778mbekgw2rdrz ~]# pstree -pl
├─sshd(18317)───bash(18319)───go(18440)─┬─main(18462)─┬─sh(18465)
可以看到当前的sh的PID为18465,接下来运行main.go的终端来查看PID:
[root@iz2zegp1t778mbekgw2rdrz docker]# go run main.go
sh-4.2# echo $$
1
可以看到当前的PID为1,也就是说宿主机的PID映射到当前的进程是PID为1,PID Namespace创建成功。
**NOTICE:**在sh终端内不能使用ps命令,因为ps,top等命令会用到/proc
资源。因为当前的sh中/proc
资源还是宿主机的资源,需要使用mount
挂在到当前Namespace中才能看到当前的PID。
Network Namespace
Network Namespace是用来隔离网络设备,IP,端口等网络栈的Namespace。Network Namespace可以使当前命名空间拥有自己独立的(虚拟的)网络设备,各个Namespace之间的端口都不会冲突。
代码实现:
func NETNS() {
cmd := exec.Command("sh")
syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")
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: 1234,
HostID: syscall.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 1234,
HostID: syscall.Getgid(),
Size: 1,
},
},
}
//cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(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)
}
首先查看一下宿主机的网卡信息:
[root@iz2zegp1t778mbekgw2rdrz docker]# ifconfig
docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.18.0.1 netmask 255.255.0.0 broadcast 172.18.255.255
ether 02:42:ed:46:5e:70 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.203.111 netmask 255.255.240.0 broadcast 172.17.207.255
ether 00:16:3e:34:01:d3 txqueuelen 1000 (Ethernet)
RX packets 5105 bytes 2556002 (2.4 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 3632 bytes 5609285 (5.3 MiB)
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
loop txqueuelen 1 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
可以看到有三个网络设备,然后运行main.go查看网络设备:
[root@iz2zegp1t778mbekgw2rdrz docker]# go run main.go
sh-4.2$ ifconfig
sh-4.2$
可以看到当前是没有任何的网络设备的,因此Network Namespace生效。