Introduction
Linux Namespace
Linux Cgroups
Namespace 作用:隔离系统资源。
分类:
Mount Namespace
UTS Namespace
IPC Namespace
PID Namespace
Network Namespace
User Namespace
相关的系统调用:
clone:通过配置clone的参数可以创建哪些 Namespace,同时将子进程纳入其中
unshare:将进程移除某个 Namespace
setns:将进程加入到 Namespace
UTS Namespace
UTS Namespace 主要用来隔离 hostname 和 domainname。
通过readlink /proc/<PID>/ns/uts可以验证子进程和父进程是否再同一个 UTS Namespace 中。
IPC Namespace
IPC Namespace 用来隔离 System V IPC 和 POSIX message queues。
通过下面三条命令来验证子进程和父进程是否再同一个 IPC Namespace 中。
ipcs -q:查看当前 IPC 消息队列
ipcmk -Q:创建新的 IPC 消息队列
ipcrm -q <message_id>:通过 Message ID 删除 IPC 消息队列
PID Namespace
PID Namespace 是用来隔离进程 ID 的。
通过echo $$输出当前 PID 可以验证子进程和父进程是否再同一个 PID Namespace 中。
这时用ps或者top验证都是有问题的,因为这两个工具是通过查看/proc来确定PID。
Mount Namespace
Mount Namespace 用来隔离各个进程看到的 Mount 视图。不同 Mount Namespace 看到的文件系统是不一样的,同时调用mount和umount互不影响。
执行 mount -t proc proc /proc 重新挂载 proc,这个时候就可以通过ps或者top看到容器内只有一个少数进程了。
User Namespace
User Namespace 隔离用户的用户组ID,即一个进程的 User ID 和 Group ID 在 User Namespace 内外可以是不同的。
如将一个非 root 用户创建一个 User Namespace,然后在 User Namespace 上映射成 root 用户。
可以使用id命令来查看 uid,gid 和 group id。
Network Namespace
Network Namespace 是用来隔离网络设备、IP 地址端口等网络栈的 Namespace。
Network Namespace 可以让每个容器拥有自己的网络设备,而不同容器间的端口号不会冲突。在宿主机上搭建网桥可以方便实现容器间的通信。
通过ip addr或者ifconfig可以看到当前的网络设备。
Golang 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "log" "os" "os/exec" "syscall" ) func main () { cmd := exec.Command("bash" ) cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } }
Linux Cgroups
Linux Cgroups 可以实现对一组进程的资源限制、控制和统计的能力,这些资源包括 CPU、内存、存储、网络等。
Linux Cgroups 包含以下三个组件:
cgroups:对进程分组管理的一种机制,一个 cgroups 包含一组进程,通过可以 cgroups 可以将一组进程和一组 subsystem 系统参数关联起来
subsystem:资源控制模块:
blkio:对块设备输入输出的访问控制
cpu:cpu调度策略
cpuacct:统计 CPU 占用
cpuset:设置可用的 CPU 和内存(内存只适用于 NUMA 架构)
devices:设备访问控制
freezer:用于挂起和恢复进程
memory:内存占用控制
net_cls:网络包分类,以便更具分类实现限流和监控
net_prio:进程网络流量优先级
ns:使 cgroup 中的进程在新的 Namespace 中 fork 新进程 CNEWNS )时,创建出 一 个新的 cgroup ,这个 cgroup 包含新的 Namespace 中的进程 。
hierachy:将一组 cgroups 串成一个树状结构,通过这个结构可以实现 cgroups 的继承
三者的关系:
一个 subsystem 只能附加到一个 hierarchy 上
一个 hierarchy 可以附加多个 subsystem
一个进程可以作为多个 cgroups 的成员,但是这些 cgroups 必须在不同的 hierarchy
一个进程 fork 出子进程时,子进程默认和父进程同一个 cgroups。
Kernel 接口 1 2 3 4 > mkdir cgroup-test > sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test ❯ ls cgroup-test cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
当前挂载的是这个 hierarchy 的 cgroup 根节点配置项:
cgroups.clone_children:子 cgroup 是否会继承父 cgroup 的 cpuset 配置。
cgroups.procs:树中当前节点 cgroup 中的进程组 ID
cgroup.sane_behavior:貌似是用来区分 cgroups 版本的,参考:https://lwn.net/Articles/547332/
notify_on_release:标识该 cgroups 的最后一个进程退出时是否会调用 release_agent
release_agent:一个路径,通常用于进程退出时自动清楚不再使用的cgroups
tasks:当前 cgroups 包含的进程 ID
创建新的 cgroups:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 > cd cgroup-test > sudo mkdir cgroup-1 > sudo mkdir cgroup-2 ❯ 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
将某个进程移入 cgroups:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 > cd crgoup-1 > sudo sh -c "echo $$ >> tasks" > echo $$ 43934 ❯ cat /proc/43934/cgroup 13:name=cgroup-test:/cgroup-1 12:cpuset:/ 11:blkio:/user.slice 10:cpu,cpuacct:/user.slice 9:memory:/user.slice/user-1002.slice/session-237201.scope 8:hugetlb:/ 7:pids:/user.slice/user-1002.slice/session-237201.scope 6:freezer:/ 5:perf_event:/ 4:net_cls,net_prio:/ 3:rdma:/ 2:devices:/user.slice 1:name=systemd:/user.slice/user-1002.slice/session-237201.scope 0::/user.slice/user-1002.slice/session-237201.scope
通过 subsystem 限制 cgroups 中进程的 Memory 资源:
系统默认为每个 subsystem 创建了一个 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 ❯ mount | grep memory cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory) ❯ cd /sys/fs/cgroup/memory ❯ sudo mkdir test-limit-memory ❯ cd test-limit-memory ❯ ls cgroup.clone_children memory.kmem.tcp.failcnt memory.oom_control cgroup.event_control memory.kmem.tcp.limit_in_bytes memory.pressure_level cgroup.procs memory.kmem.tcp.max_usage_in_bytes memory.soft_limit_in_bytes memory.failcnt memory.kmem.tcp.usage_in_bytes memory.stat memory.force_empty memory.kmem.usage_in_bytes memory.swappiness memory.kmem.failcnt memory.limit_in_bytes memory.usage_in_bytes memory.kmem.limit_in_bytes memory.max_usage_in_bytes memory.use_hierarchy memory.kmem.max_usage_in_bytes memory.move_charge_at_immigrate notify_on_release memory.kmem.slabinfo memory.numa_stat tasks ❯ cat memory.limit_in_bytes 9223372036854771712 ❯ sudo sh -c "echo 100m > memory.limit_in_bytes" ❯ sudo sh -c "echo $$ > tasks" ❯ stress-ng --vm-bytes 200m --vm-keep -m 1 stress-ng: info: [67494] Working directory /sys/fs/cgroup/memory/test-limit-memory is not read /writeable, some I/O tests may fail stress-ng: info: [67494] defaulting to a 86400 second (1 day, 0.00 secs) run per stressor stress-ng: info: [67494] dispatching hogs: 1 vm
Cgroups in docker 1 2 3 4 5 6 7 8 9 10 11 ❯ docker run -itd -m 128m ubuntu:20.04 WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap. b594ea6614b26c86bb02f60efe60d5074851886c675b3d964d0d19ecaffd5cb5 ❯ cd /sys/fs/cgroup/memory/docker/b594ea6614b26c86bb02f60efe60d5074851886c675b3d964d0d19ecaffd5cb5/ ❯ cat memory.limit_in_bytes 134217728 ❯ cat memory.usage_in_bytes 1433600
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 41 42 43 44 45 46 47 48 49 50 package mainimport ( "fmt" "io/ioutil" "os" "os/exec" "path" "strconv" "syscall" ) const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory" func main () { if os.Args[0 ] == "/proc/self/exe" { fmt.Printf("current pid %d\n" , syscall.Getpid()) cmd := exec.Command("bash" , "-c" , "stress-ng --vm-bytes 200m --vm-keep -m 1" ) cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Println(err) os.Exit(1 ) } } cmd := exec.Command("/proc/self/exe" ) cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { fmt.Println("ERROR" , err) os.Exit(1 ) } else { fmt.Printf("%v" , cmd.Process.Pid) os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit" ), 0755 ) ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit" , "tasks" ), []byte (strconv.Itoa(cmd.Process.Pid)), 0644 ) ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit" , "memory.limit_in_bytes" ), []byte ("100m" ), 0644 ) } cmd.Process.Wait() }
Union File System
Union File System,简称 UnionFS,一种把其他文件系统联合到一个挂载点的文件系统服务。
使用 branch 将不同文件系统的文件和目录“透明地”覆盖,形成一个单一一致的文件系统。这些 branch 可以是 read-only 或 read-write。
写时复制
Docker 利用 UnionFS 对 Container 的实现:
从上图可以看出,Docker 使用 UnionFS 将多个文件系统合并起来,并将 Image layer 设置成 Read-only 的,并给每个 Container 提供一个 R/W Layer 来复用 Image Layer,似乎可以将 Image layer 理解成磁盘中的可执行文件,而 Container 则是跑在系统中的程序。
AUFS
AUFS, Advanced Multi-Layered Unification Filesystem, 是 Docker 选用的一种存储驱动。
官方文档:https://docs.docker.com/storage/storagedriver/aufs-driver/
由于现在大多使用的是 Overlay 和 Overlay2 来,所以这部分跳过实践(主要 Ubuntu 20.04 已结在使用 Overlay2 )。
每个 Docker Image 都由一系列的 read-only layer 组成
image layer 存储在 /var/lib/docker/aufs/diff
Docker 利用 AUFS 的写时复制来实现共享 Image layer 和减少磁盘空间。
Docker 会为其创建一个 read-only 的 init layer,用来存储与这 个容器内环境相关的内容: Docker 还会为其创建一个 read-write 的 layer 来执行所有写操作。
container layer 的 mount 目录是/var/lib/docker/aufs/mnt
Overlay2
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 > cd /var/lib/docker/overlay2 > ls l > docker pull ubuntu:20.04 20.04: Pulling from library/ubuntu 345e3491a907: Pull complete 57671312ef6f: Pull complete 5e9250ddb7d0: Pull complete Digest: sha256:cf31af331f38d1d7158470e095b132acd126a7180a54f263d386da88eb681d93 Status: Downloaded newer image for ubuntu:20.04 docker.io/library/ubuntu:20.04 > ls 0c5b737570f2d291b4f05d64fe3bdbcf368db9fb8cbf6d9748a867d7b1a6e269 b52a12fc1404816fdf7dca9a0d7dec3fb73e816f4941668a279dc356079a1dec e3f503fee46e1f7a7b8fb99224c3d51233bd2f091d52d9495120d5ea3039ef3d l > tree -L 2 . ├── 0c5b737570f2d291b4f05d64fe3bdbcf368db9fb8cbf6d9748a867d7b1a6e269 │ ├── diff │ ├── link │ ├── lower │ └── work ├── b52a12fc1404816fdf7dca9a0d7dec3fb73e816f4941668a279dc356079a1dec │ ├── committed │ ├── diff │ └── link ├── e3f503fee46e1f7a7b8fb99224c3d51233bd2f091d52d9495120d5ea3039ef3d │ ├── committed │ ├── diff │ ├── link │ ├── lower │ └── work └── l ├── 2G7ALYFVGBBOJWLR2W6HAFMTMR -> ../e3f503fee46e1f7a7b8fb99224c3d51233bd2f091d52d9495120d5ea3039ef3d/diff ├── MVPK5KGEY6K2C6ABYDLAIGRKCV -> ../0c5b737570f2d291b4f05d64fe3bdbcf368db9fb8cbf6d9748a867d7b1a6e269/diff └── UOOOMKV7G2J567S4UXQPD6UMV5 -> ../b52a12fc1404816fdf7dca9a0d7dec3fb73e816f4941668a279dc356079a1dec/diff 12 directories, 7 files
在l目录下保存了一些符号链接,看上去是用来将image layer的id变短的。
其余三个文件夹中存在多个文件/文件夹:
diff :存储内容的文件夹
work:似乎 OverlayFS2 已经不再使用了
link :存储了其在l目录下所对应的符号链接名
lower :用来索引其下一层的位置,例如l/UOOOMKV7G2J567S4UXQPD6UMV5
committed:似乎是个标记位?用来标记该层是否被 commited,因为在运行容器之后,会出现一个没有commited但是有merged文件夹的image layer
merged:包含了联合挂载后的内容(即其自身和其下面层合并后的内容),这个在当前系统有容器在运行时会看到