前言
当人们聊到容器技术,就会躲不开这几个关键字 namespace, cgroup, ufs,但说完这几个关键字之后貌似就不能往下谈了。在这里将通过通俗易懂的方式直接揭开这几个关键字的含义,及进程是如何使用这些技术达到容器化。
- 文章涉及一点点golang 代码,但都做了详细解释,不影响理解;
1、Linux Namespace
Namespace 用于对资源的隔离,包括进程隔离,挂载点隔离,网络隔离,用户隔离,程序组进程间通信隔离; 当一个进程被创建的时候,可以使用clone() 创建这些NS,当然也可使用setns() 来加入已存在的NS,最后还可以使用unshare() 进程移出某个NS
Namespace 类型 | 系统调用参数 | 内核版本 |
---|---|---|
Mount NS | CLONE_NEWNS | 2.4.19 |
UTS NS | CLONE_NEWUTS | 2.6.19 |
IPC NS | CLONE_NEWIPC | 2.6.19 |
PID NS | CLONE_NEWPID | 2.6.24 |
Network NS | CLONE_NEWNET | 2.6.29 |
User NS | CLONE_NEWUSER | 3.8 |
1.1、环境准备
后面通过代码的实现来了解以上的namespace , 前提需要准备一下环境: 1、centos 7 系统, 内核4.4 以上,因为4.4系统调用方式改了;(需支持aufs的内核版本,后面实现需要) 2、golang 1.12以上;
2、 代码实现NS
2.1、UTS Namespace
package main
/*
UTS Namespace 主要用于隔离 node name 和 domain name
*/
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
log.Println("start new namespace")
// 命令
cmd := exec.Command("sh")
// 系统调用clone 进程时,设定新的uts namespace
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)
}
}
- 使用以下命令校验
# 执行当前脚本, 进入容器
$ go run uts.go
# 查看进程树
$ pstree -pl
# echo $$
19915
# 查看进程uts namespace 号;
$ readlink /proc/19915/ns/uts
uts:[40287666333]
// 对比另一个shell的hostname,不同则是已经隔离了 hostname 及 domain
$ hostname -b shadow
shadow
2.2、IPC Namespace
package main
/*
IPC Namespace 用来隔离 system V IPC 和 POSIX message queues
*/
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
log.Println("start new namespace")
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)
}
}
/*
ipcs 命令查看 Message Queues | Shared Memory Segments| Semaphore Arrays
ipcrm 删除
ipcmk 创建
[t1]:
ipcs -q
ipcmk -Q
ipcs -q
[t2]:
ipcs -q
*/
Term1
# 执行当前脚本
# 进入容器
$ go run ipc.go
2020/09/16 01:11:47 start new namespace
sh-4.2# ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
sh-4.2# ipcmk -Q
消息队列 id:0
sh-4.2# ipcs
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0x6a89a1ad 0 root 644 0 0
Term2
$ ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
2.3、PID Namespace
package main
/*
PID Namespace 用来隔离进程ID
*/
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
log.Println("start new namespace")
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)
}
}
/*
[t1]:
pstree -pl
echo $$
*/
Term1
# 进入容器
$ go run pid.go
2020/09/16 01:18:49 start new namespace
# 可以看到当前的pid 为1
sh-4.2# echo $$
1
2.4、Mount Namespace
package main
/*
Mount Namespace 主要用于隔离各个进程看到的挂载点
*/
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
log.Println("start new namespace")
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID |syscall.CLONE_NEWNS | syscall.CLONE_NEWIPC,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
下面可以看出,挂载/proc到自己的namespace 后,我们只会看到我们自己进程信息;
$ go run mount.go
2020/09/16 01:22:14 start new namespace
# 未挂载/proc
sh-4.2# ls /proc
1 20 300 5186 64 6776 79 cpuinfo kallsyms mtrr thread-self
10 21 301 5332 6481 6780 796 crypto kcore net timer_list
1055 22 370 549 65 6783 8 devices keys pagetypeinfo tty
11 23 3925 550 6543 68 80 diskstats key-users partitions uptime
1113 24 3939 553 6570 69 804 dma kmsg sched_debug version
1115 25 396 554 6571 71 850 driver kpagecgroup schedstat vmallocinfo
13 26 397 559 6572 72 9 dynamic_debug kpagecount scsi vmstat
1300 271 4 560 6574 73 93 execdomains kpageflags self zoneinfo
1306 272 419 562 66 74 94 fb loadavg slabinfo
14 274 439 563 6652 75 acpi filesystems locks softirqs
15 275 446 574 67 76 buddyinfo fs mdstat stat
16 276 448 6 6740 77 bus interrupts meminfo swaps
18 283 450 61 6741 780 cgroups iomem misc sys
19 294 452 62 6742 781 cmdline ioports modules sysrq-trigger
2 3 508 63 6745 784 consoles irq mounts sysvipc
# 挂载 /proc 设备后;
sh-4.2# mount -t proc proc /proc
sh-4.2# ps
PID TTY TIME CMD
1 pts/0 00:00:00 sh
5 pts/0 00:00:00 ps
sh-4.2# ls /proc
1 crypto fs kmsg modules self timer_list
6 devices interrupts kpagecgroup mounts slabinfo tty
acpi diskstats iomem kpagecount mtrr softirqs uptime
buddyinfo dma ioports kpageflags net stat version
bus driver irq loadavg pagetypeinfo swaps vmallocinfo
cgroups dynamic_debug kallsyms locks partitions sys vmstat
cmdline execdomains kcore mdstat sched_debug sysrq-trigger zoneinfo
consoles fb keys meminfo schedstat sysvipc
cpuinfo filesystems key-users misc scsi thread-self
2.5、User Namespace
package main
/*
USER Namespace 主要用于隔离各个用户id及用户组id
*/
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
log.Println("start new namespace")
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: 5001,
HostID: syscall.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 5001,
HostID: syscall.Getuid(),
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)
}
}
可以看出当前用户不一致
Term1
# 进入容器
$ go run user.go
2020/09/16 01:33:33 start new namespace
sh-4.2$ id
uid=5001 gid=5001 组=5001
sh-4.2$ exit
exit
Term2 物理机
$ id
uid=0(root) gid=0(root) 组=0(root)
2.6、Network Namespace
package main
/*
Mount Namespace 主要用于隔离网络空间
*/
import (
"log"
"os"
"os/exec"
"syscall"
)
func main() {
log.Println("start new namespace")
cmd := exec.Command("sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID |syscall.CLONE_NEWNS | syscall.CLONE_NEWIPC |
syscall.CLONE_NEWNET,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}
- 进入容器后,看不到有网络配置
$ go run net.go
2020/09/16 01:35:10 start new namespace
sh-4.2# ifconfig
3、Linux Cgroup
什么是Cgroups? Cgroup 提供了对一组进程的资源限制、控制和统计的能力,这些资源包括CPU,内存,存储,网络等。通过Cgroups,可以方便的限制程序占用的资源,并且可以实时的监控进程和统计信息。 Cgroups 中的3个组件
cgroup
是对进程分组管理的一种机制,一个cgroup包含一组进程,并且可以在这个cgroup上增加 linux subsystem的各种参数配置,将一组进程和一组subsystem的系统参数关联起来。subsystem
是一组资源控制的模块,一般包含如下配置:- blkio 谁知对块设备的输入输出访问控制;
- cpu 设置cgroup中进程的cpu 被调度的策略;
- cpuacct 可以统计cgroup中进程的cpu占用;
- cpuset 在多核机器上设置ccgroup中进程可以使用的cpu
- devices 控制cgroup中进程对设备的访问;
- freeze 用去刮起和恢复cgroup 中的进程;
- memory 用于控制cgroup中的进程内存;
- net_cls 用于将cgroup中进程产生的网络包分类,以便linux的tc(traffic controller) 可以根据分类区分出来自某个cgroup中的包做限流或监控;
- net_prio 设置cgroup中的进程产生的网络流量优先级;
- ns 它的作用是使cgroup中的进程在新的namespace中fork新进程(newns)时,创建一个新的cgroup,这个cgroup包含新的namespaace中的进程;
# // 查询系统中支持的subsystems
$ lssubsys -a
cpuset
cpu,cpuacct
blkio
memory
devices
freezer
net_cls,net_prio
perf_event
hugetlb
pids
hierarchy
的功能是把一组cgroup串成一个树状结构,便于继承,相当于默认有一个cgroup根结点,其他cgroup都是该cgroup的子节点;
三个组件的关系
- 系统创建新的hierarchy之后,系统中所有的进程都会加入到这个hierarchy的cgroup根节点中;
- 一个subsystem只能附加到一个hierarchy;
- 一个hierarchy 可以附加多个 subsystem;
- 一个进程可以作为多个cgroup的成员,但是这些cgroup必须在不同的hierarchy中;
- 一个进程fork 出子进程时,子进程是和父进程在同一个cgroup中,也可以根据需要移动到其他的cgroup中;
3.1、呈现Cgroup
1、创建并挂载一个hierarcy;
$ mkdir cgroup-test
$ 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
这些文件是hierachey的根cgroup节点配置
- cgroup.clone_children, cpuset 的subsystem会读取这个配置文件,如这个值是1(default 0),子cgroup才会继承父cgroup的cpuset的配置
- cgroup.procs 是树中当前节点cgroup中的进程ID,现在的位置是在根节点,这个文件中会有现有系统的所有进程组ID;
- notify_on_rellease 和 release_agent 会一起使用。notify_on_releaase 标示当这个cgroup最后一个进程退出的时候是否执行了release_agent; release_agent 则是一个路径,通常用作进程退出之后自动清理掉不再使用的cgroup;
- task 标识该cgroup下面的进程ID, 如果把一个进程ID写到tasks文件中,便会将相应的进程加入到这个cgroup中;
2、扩展两个子的cgroup
$ mkdir cgroup-1 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
3、在cgroup中添加和移动进程
$ pwd
/data/docker_lab/online/cgroup-test
$ cat tasks |grep `echo $$`
6935
$ echo $$ > cgroup-1/tasks
$ cat tasks |grep `echo $$`
$ cat /proc/6935/cgroup
12:name=cgroup-test:/cgroup-1 # 可以看到当前进程加入到了cgroup-test:/cgroup-1
11:pids:/
10:memory:/user.slice
9:cpu,cpuacct:/user.slice
8:devices:/user.slice
7:freezer:/
6:blkio:/user.slice
5:perf_event:/
4:cpuset:/
3:hugetlb:/
2:net_cls,net_prio:/
1:name=systemd:/user.slice/user-0.slice/session-449.scope
4、通过subsystem限制cgroup中的进程资源
- 由于系统早就默认为所有的subsystem创建了一个默认的hierachy,而一个susbsysten只能属于一个hierachy,所以我们只能使用默认的hierachy 来做限制实验;
# // 默认的memory hierachy
$ mount |grep mem
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
# // 系统资源压测命令
$ stress --vm-bytes 500m --vm-keep -m 1
# // 可以看到stress 占用了500m
$ top
# // 新建子cgroup,并进入
$ cd /sys/fs/cgroup/memory ; mkdir test-limit-mem ; cd test-limit-mem
# // 加入限制内存
$ echo 200m > memory.limit_in_bytes
# // 移动当前pid 到cgroup
$ echo $$ > tasks
# // 系统资源压测命令
$ stress --vm-bytes 500m --vm-keep -m 1
# // 可以看到stress 占用了 200m
$ top
3.2、Docker 中使用cgroups
$ docker run -itd -m 128m ubuntu
# // 在里面能找到对应的docker 的唯一ID目录同样在 memory.limit_in_bytes 中设置;
$ ls /sys/fs/cgroup/memory/docker/
4、Union File System
4.1、什么是 Union File System
UFS 是一种把其他文件系统联合到一个联合挂载带你的文件系统服务,它使用branch 把不同文件系统的文件和目录“透明地”覆盖,形成一个单一一致的文件系统。这些branch 或者是read-only 的或这的read-write的,所以当这个虚拟后的联合文件系统进行写操作的时候,系统是真整写到一个新的文件中。看起来这个整个系统是可以对任何文件进行操作,但是并没有改变原来这个文件。因为unionfs 用到一个叫写时复制的技术(COW)。 Copy-on-write, 也叫隐式共享;如果一个资源是重复的,在没有任何修改的前提下,是不需要建立一个新的资源,这个资源可以同时被新旧实例共享;当第一次写操作发生时,会对该资源完整的复制并进行修改;
4.2、AUFS
AUFS 是 UFS 的一种实现,Docker 选用的第一种存储驱动并沿用至今,还有其他同类型驱动,overlay, overlay2, overlyafs 等;AUFS 需要内核支持
查看是否支持aufs
$ cat /proc/filesystems |grep aufs
nodev aufs
# // 如果不支持可以通过切换到支持aufs的内核
$ cd /etc/yum.repos.d/
$ wget https://yum.spaceduck.org/kernel-ml-aufs/kernel-ml-aufs.repo
$ yum install kernel-ml-aufs
- 内核切换,推荐一个我当时参考: www.cnblogs.com/xzkzzz/p/96…
4.3、Docker 如何使用AUFS
Docker aufs存储目录
- /var/lib/docker/aufs/diff
- docker host filesystem 存储在该目录下
- /var/lib/docker/aufs/layers/
- docker 的镜像主要存储位置
- /var/lib/docker/aufs/mnt;
- 运行时修改的文件内容
4.4、手写aufs
以下是全过程
$ pwd
/data/docker_lab/online/aufs
$ ls
changed-ubuntu cn-l iml1 iml2 iml3 mnt
$ cat cn-l/cn-l.txt
I am cn layer
$ cat iml1/iml1.txt
l1
$ cat iml2/iml2.txt
l2
$ cat iml3/iml3.txt
l3
# 挂载aufs
$ mount -t aufs -o dirs=./cn-l/:./iml3:./iml2:./iml1 none ./mnt
$ tree mnt/
mnt/
├── cn-l.txt
├── iml1.txt
├── iml2.txt
└── iml3.txt
# 这是一个mnt的挂载点信息, 只有cn-l 是可读写的
$ cat /sys/fs/aufs/si_d3fb24f591e1278f/*
/data/docker_lab/online/aufs/cn-l=rw
/data/docker_lab/online/aufs/iml3=ro
/data/docker_lab/online/aufs/iml2=ro
/data/docker_lab/online/aufs/iml1=ro
64
65
66
67
/data/docker_lab/online/aufs/cn-l/.aufs.xino
# 追加一个内容试试,看看COW的反应
$ echo "write to mnt's iml1" >> ./mnt/iml3.txt
# 确实追加成功
$ cat ./mnt/iml3.txt
l3
write to mnt's iml1
# 但是 iml3 下的 iml3.txt 并没有增加内容
$ cat iml3/iml3.txt
l3
# 可以看到iml3.txt 是被复制到读写层进行了修改
$ cat cn-l/iml3.txt
l3
write to mnt's iml1
# 删除iml1.txt 看看UFS 的操作
$ rm ./mnt/iml1.txt
# 确实没有了
$ ls ./mnt/
cn-l.txt iml2.txt iml3.txt
# iml1镜像层的iml1.txt 还存在,下面看看读写层
$ ls iml1/iml1.txt
iml1/iml1.txt
# 可以看到出现很多 .wh开头的文件其中 .wh.iml1.txt 会被隐藏的文件,但不会实际去删除对应的read-only层文件,wh文件称为 whiteout 文件;
$ ll ./cn-l/ -a
drwxr-xr-x 4 root root 4096 9月 15 12:46 .
drwxr-xr-x 8 root root 4096 9月 14 01:21 ..
-rw-r--r-- 1 root root 14 9月 15 12:37 cn-l.txt
-rw-r--r-- 1 root root 23 9月 15 12:45 iml3.txt
-r--r--r-- 2 root root 0 9月 15 12:40 .wh.iml1.txt
-r--r--r-- 2 root root 0 9月 15 12:40 .wh..wh.aufs
drwx------ 2 root root 4096 9月 15 12:40 .wh..wh.orph
drwx------ 2 root root 4096 9月 15 12:40 .wh..wh.plnk
结语
- 如果文章对您有帮助请
点赞
,收藏
;
参考
- 推荐书籍: <<自己动手写docker>>