02-基础技术

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 看到的文件系统是不一样的,同时调用mountumount互不影响。

执行 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 main

import (
"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
# 找到 memory 的 hierarchy
❯ 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

# 限制成只能使用 100m
❯ sudo sh -c "echo 100m > memory.limit_in_bytes"
# 将当前 shell 加入 cgroups
❯ sudo sh -c "echo $$ > tasks"
# 使用 stress-ng 创建一个进程获取 200m 的内存,此时在 top 上查看该进程使用的内存数量应该为 100m
❯ 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 main

import (
"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 的实现:

image and 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 )。

AUFS and Docker

  • 每个 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

Overlay

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 layerid变短的。
  • 其余三个文件夹中存在多个文件/文件夹:
    • diff :存储内容的文件夹
    • work:似乎 OverlayFS2 已经不再使用了
    • link :存储了其在l目录下所对应的符号链接名
    • lower :用来索引其下一层的位置,例如l/UOOOMKV7G2J567S4UXQPD6UMV5
    • committed:似乎是个标记位?用来标记该层是否被 commited,因为在运行容器之后,会出现一个没有commited但是有merged文件夹的image layer
    • merged:包含了联合挂载后的内容(即其自身和其下面层合并后的内容),这个在当前系统有容器在运行时会看到