基础技术
经常听到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 mainimport ( "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/utsuts:[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 mainimport ( "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
验证方法:我用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 mainimport ( "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 /proc1 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 /proc1 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 mainimport ( "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 ```