自己动手写docker--docker原理理解与入门

基础技术

经常听到docker是一个使用了Linux Namespace 和Cgroups的虚拟化工具,本文章将探究什么是Namespace和Cgroups,以及容器是如何使用他们的。此博客适合,有一定go语言基础,对docker还没入门的新手阅读,此博客所有代码都经过了验证,可放心复制运行。

Namespace介绍

Namespace类型 系统调用参数 内核版本
Mount Namespace CLONE_NEWNS 2.4.19
UTS Namespace CLONE_NEWUTS 2.6.19
IPC Namespace CLONE_NEWIPC 2.6.19
PID Nanmespace CLONE_NEWPID 2.6.24
Network Namespace CLONE_NEWNET 2.6.29
User Namespace CLONE_NEWUSER 3.8

UTS Namespace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"os/exec"
"syscall"
"os"
"log"
)

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)
}
}

运行代码后,查看进程的PID以及UTS Namespace,然后修改hostname后退出进程,验证是否有影响进程外的hostname

1
2
3
4
5
6
7
8
9
10
11
simon@simon:~/Documents/go$ sudo go run uts.go
# echo $$
2508
# readlink /proc/2508/ns/uts
uts:[4026532345]
# hostname -b bird
# hostname
bird
# exit
simon@simon:~/Documents/go$ hostname
simon

IPC Namespace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"os/exec"
"syscall"
"os"
"log"
)

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)
}
}

验证方法:在新进程打打开查看消息队列,看到没有mq(message queue,消息队列),然后新建一条队列,查看,多了一条qid为0的mq。

1
2
3
4
5
6
7
8
9
10
11
12
13
# ipcs -q

------ Message Queues --------
key msqid owner perms used-bytes messages

# ipcmk -Q
Message queue id: 0
# ipcs -q

------ Message Queues --------
key msqid owner perms used-bytes messages
0xb6b35576 0 root 644 0 0
#

在进程外查看队列,发现没有,确认IPC已经被隔离

1
2
3
4
5
6
simon@simon:~/Documents/go$ ipcs -q

------ Message Queues --------
key msqid owner perms used-bytes messages

simon@simon:~/Documents/go$

PID Namespace

1

验证方法:我用ps -ef命令查看了某个进程的具体PID,但发现进程内外的PID的值一样,原因是ps和top等命令会读取/proc的内容,还不能用ps查看,但是我们输出进程内的pid,发现是1,而事实上,所有linux系统,只有init进程pid为1,然后我们可以使用pstree命令查看,当前go程序main函数的pid(进程树太长,没有截图,各位可自己查看),是一个不为1的值,验证成功。

1
2
3
4
5
6
7
8
9
10
simon@simon:~/Documents/go$ ps -ef | grep big_writes
simon 1426 1258 0 08:40 ? 00:00:00 /usr/lib/gvfs/gvfsd-fuse /run/user/1000/gvfs -f -o big_writes
simon 10420 10243 0 09:26 pts/19 00:00:00 grep --color=auto big_writes
simon@simon:~/Documents/go$ sudo go run pid.go
# ps -ef | grep big_writes
simon 1426 1258 0 08:40 ? 00:00:00 /usr/lib/gvfs/gvfsd-fuse /run/user/1000/gvfs -f -o big_writes
root 10453 10448 0 09:26 pts/19 00:00:00 grep big_writes
# echo $$
1
# pstree -pl

Mount Namespace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"os/exec"
"syscall"
"os"
"log"
)

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)
}
}

验证,我们首先ls \proc,看一下宿主机的进程情况,因为是宿主机的,所以比较乱,下面我们将/proc/挂在自己的Namespace中,然后再查看,
瞬间少了很多文件,这样就可以使用ps看隔离后的进程号了,可以发现,已经查看不到宿主机的进程了。

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
simon@simon:~/Documents/go$ sudo go run mount.go 
# ls /proc
1 10723 1331 15 1709 19 22 31 3742 56 948 interrupts sched_debug
10 10731 1339 1507 1721 190 221 32 38 6 962 iomem schedstat
1002 10761 1351 1558 1734 191 222 33 39 603 98 ioports scsi
1008 10765 1375 1559 1738 1917 2220 3368 392 608 9831 irq self
10096 10766 1379 1560 1742 192 2235 3389 4 7 99 kallsyms slabinfo
10150 10783 1391 1570 1748 1926 2250 3390 40 8 994 kcore softirqs
10151 10787 1393 1571 1756 193 2276 34 407 846 acpi keys stat
10170 10788 1399 1572 1762 1932 2299 3425 41 847 asound key-users swaps
10175 1093 14 1573 1776 1936 2304 3438 42 849 buddyinfo kmsg sys
1018 11 1403 1577 1788 1943 2354 3442 43 855 bus kpagecgroup sysrq-trigger
10200 114 1407 1593 1798 1968 2367 3535 44 857 cgroups kpagecount sysvipc
10243 1160 1416 16 18 1977 24 3605 45 858 cmdline kpageflags thread-self
104 1178 1421 1612 1800 1979 248 3612 450 861 consoles loadavg timer_list
10468 1181 1426 1619 1807 199 25 3613 452 862 cpuinfo locks tty
10483 12 1444 1632 181 2 256 3616 46 863 crypto mdstat uptime
10527 1209 1446 1655 182 20 2567 3617 47 868 devices meminfo version
10578 1213 1459 1674 183 2012 2578 3620 476 871 diskstats misc version_signature
10618 1250 1472 1675 1839 2019 26 3648 479 873 dma modules vmallocinfo
10620 1251 1477 1677 184 2022 27 3675 48 9 driver mounts vmstat
10631 1256 1479 1678 185 2056 271 3685 49 919 execdomains mtrr zoneinfo
10638 1258 1486 1679 1854 21 273 37 50 924 fb net
10675 13 1488 1686 186 2103 28 3714 52 925 filesystems pagetypeinfo
10722 131 1494 1702 189 2186 30 3740 55 929 fs partitions
# mount -t proc proc /proc
# ls /proc
1 consoles fb kcore locks pagetypeinfo stat uptime
4 cpuinfo filesystems keys mdstat partitions swaps version
acpi crypto fs key-users meminfo sched_debug sys version_signature
asound devices interrupts kmsg misc schedstat sysrq-trigger vmallocinfo
buddyinfo diskstats iomem kpagecgroup modules scsi sysvipc vmstat
bus dma ioports kpagecount mounts self thread-self zoneinfo
cgroups driver irq kpageflags mtrr slabinfo timer_list
cmdline execdomains kallsyms loadavg net softirqs tty
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:52 pts/19 00:00:00 sh
root 5 1 0 09:53 pts/19 00:00:00 ps -ef
#

User Namespcae

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vpackage main

import (
"os/exec"
"syscall"
"os"
"log"
)

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,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}

验证:运行程序前,查看uid和gid,运行后再查看发现uid,gid都不同,user Namespace常见的用法是宿主机非root用户运行,docker内为root用户。

1
2
3
4
5
6
7
8
9
# id
uid=0(root) gid=0(root) groups=0(root)
# su simon
simon@simon:~/Documents/go$ id
uid=1000(simon) gid=1000(simon) groups=1000(simon),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
simon@simon:~/Documents/go$ go run user.go
$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
$

Newwork Namespace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"os/exec"
"syscall"
"os"
"log"
)

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,
}
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
19
20
21
22
23
24
25
26
27
28
29
30
simon@simon:~/Documents/go$ ifconfig
enp0s25 Link encap:Ethernet HWaddr 50:7b:9d:0b:bf:66
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
Interrupt:20 Memory:e1200000-e1220000

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:4132 errors:0 dropped:0 overruns:0 frame:0
TX packets:4132 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:342986 (342.9 KB) TX bytes:342986 (342.9 KB)

wlp3s0 Link encap:Ethernet HWaddr 00:15:00:e5:4e:d0
inet addr:192.168.43.139 Bcast:192.168.43.255 Mask:255.255.255.0
inet6 addr: fe80::1abc:d4bd:1a5b:649c/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:13713 errors:0 dropped:0 overruns:0 frame:0
TX packets:9979 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:15235189 (15.2 MB) TX bytes:2019739 (2.0 MB)

simon@simon:~/Documents/go$ go run net.go
$ ifconfig
$

Linux Cgroups

Namespace是docker帮助进程隔离出“空间”的技术,而Cgroups就是docker隔离(分配)资源的技术,保证进程之间在限定CPU、内存、存储、网络等资源
下运行,不会争抢,并且可以实时监控和统计。

Cgroups的三个组件

cgroup

cgroup是对进程进行分组管理的技术,一个cgroup包含一组进程,并可以在这个cgroup上增加Linux subsystem的各种参数配置,将一组进程和一组subsystem
的系统参数关联起来。

subsystem

subsystem是一组资源控制的模块,一般包含一下几项

模块 功能
blkio 对块设备输入输出的访问控制
cpu 设置cgroup中进程cpu被调度的策略
cpuacct 统计cgroup中进程的cpu占用
cpuset 多核机器上设置进程可以使用的cpu和内存
devices 控制进程的设备访问
freezer 挂起(suspend)和恢复(resume)进程
memory 控制内存占用
net_cls 对进程的网络包分类,一遍tc(traffic controller)根据区分出自哪个cgroup的进程以进行监控和限流
net_prio 设置网络流量的优先级
ns 这个subsystem比较特殊,它的作用是使cgroup中的进程在新的Namespace中fork新进程(NEWUS)时,创建一个新的cgroup,这个新的cgroup包含新的Namespace的进程

apt install cgroup-bin,安装这个命令行工具后,可以lssubsys -a查看系统支持的subsystem,

1
2
3
4
5
6
7
8
9
10
11
12
13
simon@simon:~/Documents/go$ lssubsys -a
cpuset
cpu,cpuacct
blkio
memory
devices
freezer
net_cls,net_prio
perf_event
hugetlb
pids
rdma
simon@simon:~/Documents/go$
hierarchy

hierarchy的功能是把一组cgroup串成一个树形结构,(类似于进程的父子关系),这样子就可以做到继承,例如cgroup1限制cpu。hierarchy是一课多叉树,cgroup是树上的节点。
然后cgroup2需要限制IO,这样子cgroup2可以继承cgroup1,cgroup2就同时限制了IO和cpu(否则的话cgroup2的进程没有被限制cpu,
影响cgroup1的cpu使用)

三个组件的相互关系
  • 系统在创建新的hierarchy之后,系统的所有进程都会加入这个hierarchy的cgroup根节点,这个cgroup根节点是hierarchy默认创建的,
  • 一个subsystem只能附加到一个hierarchy上面。
  • 一个hierarchy可以附加多个subsystem
  • 一个进程可以作为多个cgroup的成员,但是这些cgroup必须在不同的hierarchy(同一个节点可以属于不同树)
  • 一个进程fork出子进程时,父子进程处于同一个cgroup,之后可以根据需要移动到其他cgrouup

    这几句话,现在不理解没有关系,后面的实际使用会逐渐了解他们的关系 ————《自己动手写docker》

kernel接口

下面通过kernel接口演示配置cgroup

创建一个hierarchy
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
simon@simon:~/Documents/go$ ~ mkdir cgroup-test
bash: /home/simon: Is a directory
simon@simon:~/Documents/go$ mkdir cgroup-test
simon@simon:~/Documents/go$ sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test
[sudo] password for simon:
simon@simon:~/Documents/go$ ls ./cgroup-test/
cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
simon@simon:~/Documents/go$ cat cgroup-test/cgroup.clone_children
0
simon@simon:~/Documents/go$ cat cgroup-test/cgroup.procs
1
7
8
1023
simon@simon:~/Documents/go$ cat cgroup-test/notify_on_release
0
simon@simon:~/Documents/go$ cat cgroup-test/release_agent

simon@simon:~/Documents/go$ cat cgroup-test/tasks
1
7
8
1042
simon@simon:~/Documents/go$ pstree -la
sh
└─su simon
└─bash
└─pstree -la
simon@simon:~/Documents/go$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:52 pts/19 00:00:00 sh
root 7 1 0 09:55 pts/19 00:00:00 su simon
simon 8 7 0 09:55 pts/19 00:00:00 bash
simon 1048 8 0 11:02 pts/19 00:00:00 ps -ef

可以看到hierarchy默认的文件,这些就是hierarchy根节点cgroup的默认配置文件,下面解读一下这些文件的含义
| 文件|作用|
| cgroup.clone_children | cpuset的subsystem会读取这个配置文件,如果值为0(默认),则子cgroup会继承父cgroup的cpuset配置 |
| cgroup.procs | 记录树中当前cgroup节点的进程组ID,因为当前是根节点,所以会有系统中所有进程组
的ID |
| notify_on_release| 和release_agent一起使用,标记当这个cgroup最后一个进程退出的时候是否执行了
release_agent,release_agent则是一个路径,用作进程退出之后清理不在使用的cgroup |
|tasks | 标识该cgroup下面的进程ID,如果将一个进程ID写进该文件,则便会将相应的进程加入到这个cgroup |

从cgroup下创建两个子cgroup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
simon@simon:~/Documents/go/cgroup-test$ sudo mkdir cgroup-1
simon@simon:~/Documents/go/cgroup-test$ sudo mkdir cgroup-2
simon@simon:~/Documents/go/cgroup-test$ tree
.
├── cgroup-1
│   ├── cgroup.clone_children
│   ├── cgroup.procs
│   ├── notify_on_release
│   └── tasks
├── cgroup-2
│   ├── cgroup.clone_children
│   ├── cgroup.procs
│   ├── notify_on_release
│   └── tasks
├── cgroup.clone_children
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks

2 directories, 14 files
simon@simon:~/Documents/go/cgroup-test$

可以看到,在一个cgroup下创建文件夹的时候,kernel会自动把文件夹标记为这个cgroup的子cgroup,他们会继承父cgroup的属性

在cgroup中添加和移动进程

如下,使用sudo权限,往cgroup-test/tasks写进当前进程号,则可看见当前进程加入cgroup-test,然后再把当前进程号写进cgroup-test/cgroup-1/tasks,
(记得前面说过,一个进程在1个hierarchy中只能当一个节点,不能重复),所以当前进程被移动到了cgroup-1中。

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
simon@simon:~/Documents/go/cgroup-test$ echo $$
2085
simon@simon:~/Documents/go/cgroup-test$ sudo sh -c "echo $$ >> tasks"simon@simon:~/Documents/go/cgroup-test$ cat /proc/2085/cgroup 13:name=cgroup-test:/
12:blkio:/
11:cpuset:/
10:pids:/user.slice/user-1000.slice
9:devices:/user.slice
8:cpu,cpuacct:/
7:rdma:/
6:perf_event:/
5:memory:/
4:freezer:/
3:net_cls,net_prio:/
2:hugetlb:/
1:name=systemd:/user.slice/user-1000.slice/session-c2.scope
simon@simon:~/Documents/go/cgroup-test$ sudo sh -c "echo $$ >> ./cgroup-1/tasks"simon@simon:~/Documents/go/cgroup-test$ cat /proc/2085/cgroup
13:name=cgroup-test:/cgroup-1
12:blkio:/
11:cpuset:/
10:pids:/user.slice/user-1000.slice
9:devices:/user.slice
8:cpu,cpuacct:/
7:rdma:/
6:perf_event:/
5:memory:/
4:freezer:/
3:net_cls,net_prio:/
2:hugetlb:/
1:name=systemd:/user.slice/user-1000.slice/session-c2.scope
通过subsystem限制cgroup资源

下面的命令,可以看到sys/fs/cgroup/memory目录挂在了memory subsystem的hierarchy上,我们通过stress命令以及,在这个hierarchy上创建cgroup,修改memory限制来测试

1
2
3
4
5
6
7
simon@simon:/sys/fs/cgroup/memory$ mount | grep memory
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
simon@simon:/sys/fs/cgroup/memory$ sudo mkdir test-memory-limit
simon@simon:/sys/fs/cgroup/memory$ cd test-memory-limit/
simon@simon:/sys/fs/cgroup/memory/test-memory-limit$ sudo sh -c "echo "800m" > memory.limit_in_bytes"
simon@simon:/sys/fs/cgroup/memory/test-memory-limit$ sudo sh -c "echo $$ > tasks"
simon@simon:/sys/fs/cgroup/memory/test-memory-limit$ stress --vm-bytes 1600m --vm-keep -m 1

我把内存限制,设为了800m(我的电脑内存16G),下面可以看到stress命令,只用了5%的内存,没有能跑到1600m

1
2
3
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                     
5887 simon 20 0 1645884 792200 276 R 5.0 4.9 0:04.44 stress
4077 simon 20 0 925992 52312 38596 S 1.0 0.3 0:08.78 tilda

下面我把800m改为1600m再观察

1
2
3
4
simon@simon:/sys/fs/cgroup/memory/test-memory-limit$ sudo sh -c "echo "1600m" > memory.limit_in_bytes"
simon@simon:/sys/fs/cgroup/memory/test-memory-limit$ cat memory.limit_in_bytes
1677721600
simon@simon:/sys/fs/cgroup/memory/test-memory-limit$ stress --vm-bytes 1600m --vm-keep -m 1

可以看到,这次的stree命令可以占用10%的内存了,限制成功

1
2
3
4
5
6
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM    TIME+   COMMAND
6028 simon 20 0 1645884 1.555g 276 D 5.0 10.1 0:01.12 stress
2931 simon 20 0 1597172 149540 91460 S 1.0 0.9 1:11.85 code ```



Author

simonisacoder

Posted on

2019-03-01

Licensed under

Comments