支持Docker运行的三大基础技术
支持Docker运行的三大基础技术
- Linux Namespace
- Linux Cgroups
- Union File System
Linux Namespace
这是Kernel的一个功能,可以用来隔离一系列的系统资源,比如PID、User ID、Network等。
类型 | 系统调用参数 |
---|---|
Mount namespaces | CLONE_NEWNS |
UTS namespaces | CLONE_NEWUTS |
IPC namespaces | CLONE_NEWIPC |
PID namespaces | CLONE_NEWPID |
Network namespaces | CLONE_NEWNET |
User namespaces | CLONE_NEWUSER |
主要的三种系统调用方式
- clone()– 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。
- unshare() – 使某进程脱离某个namespace
- setns()– 把某进程加入到某个namespace
以下将通过Clone调用分别介绍各个Namespace启用后的效果(本来代码实现是用的Go,目的是实现一个简化版的Docker。但是大家没学过,所以我去博客上找了C的代码)
原始程序
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg)
{
printf("Container - inside the container!\n");
/* 直接执行一个shell,以便我们观察这个进程空间里的资源是否被隔离了 */
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
/* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD, NULL);
/* 等待子进程结束 */
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
UST Namespaces
提供了主机名和域名的隔离,这样每个容器就拥有独立的主机名和域名了,在网络上就可以被视为一个独立的节点,在容器中对 hostname 的命名不会对宿主机造成任何影响。
修改代码
int container_main(void* arg)
{
printf("Container - inside the container!\n");
sethostname("container",10); /* 设置hostname */
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWUTS | SIGCHLD, NULL); /*启用CLONE_NEWUTS Namespace隔离 */
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
效果如下:
arbus@ubuntu:~$ sudo ./uts
Parent - start a container!
Container - inside the container!
root@container:~# hostname
container
root@container:~# uname -n
container
IPC Namespcace
IPC全称 Inter-Process Communication,是Unix/Linux下进程间通信的一种方式,IPC有共享内存、信号量、消息队列等方法。所以,为了隔离,我们也需要把IPC给隔离开来,这样,只有在同一个Namespace下的进程才能相互通信。一组IPC有一个全局的ID,我们的目的是隔离这个ID。
首先创建IPC队列
arbus@ubuntu:~$ ipcmk -Q
Message queue id: 0
arbus@ubuntu:~$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0xd0d56eb2 0 arbus 644 0 0
如果没有隔离,在container中,还会看到这个全局ID
arbus@ubuntu:~$ sudo ./uts
Parent - start a container!
Container - inside the container!
root@container:~# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0xd0d56eb2 0 arbus 644 0 0
修改代码:
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);
再次运行程序
root@ubuntu:~$ sudo./ipc
Parent - start a container!
Container - inside the container!
root@container:~/linux_namespace# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
PID Namespace
用来隔离进程ID。同一个进程在不同的PID Namespace中可以拥有不同的PID。使用后,在container内外查看进程,会发现容器外的进程ID号被映射成其他ID号。
├─sshd(23448)─┬─sshd(30442)───zsh(30444)───go(31498)─┬─main(31519)─┬─sh(31522)
│ │ │ ├─{main}(31520)
│ │ │ └─{main}(31521)
│ │ ├─{go}(31499)
│ │ ├─{go}(31500)
│ │ ├─{go}(31501)
│ │ ├─{go}(31502)
│ │ ├─{go}(31515)
│ │ └─{go}(31523)
│ ├─sshd(30870)───zsh(30872)
│ └─sshd(30932)───zsh(30934)───pstree(31534)
├─systemd-journal(325)
├─systemd-logind(475)
├─systemd-udevd(29645)
├─tmux: server(25827)─┬─zsh(25828)
│ ├─zsh(25917)
│ ├─zsh(25952)
│ └─zsh(26185)
└─tuned(19273)─┬─{tuned}(19279)
├─{tuned}(19280)
├─{tuned}(19281)
└─{tuned}(19309)
增加PID Namespace后,在容器内执行echo $$
(当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID)。
sh-4.2# echo $$
1
(PID为1的进程又叫超级进程,也叫根进程。它负责产生其他所有用户进程。所有的进程都会被挂在这个进程下,如果这个进程退出了,那么所有的进程都被 kill 。)
但是,在子进程的shell里输入ps,top等命令,我们还是可以看得到所有进程。说明并没有完全隔离。这是因为,像ps, top这些命令会去读/proc文件系统,所以,因为/proc文件系统在父进程和子进程都是一样的,所以这些命令显示的东西都是一样的。
Mount Namespace
Mount Namespace用来隔离各个进程上看到的挂载点视图,从而达到对文件系统的隔离。也就是说,在不同的Namespace中看到的文件系统是不一样的。
示例:启用mount namespace并在子进程中重新mount /proc文件系统。
➜ ~ ls /proc
1 15825 19087 227 2528 26 30872 39 59 9815 bus driver kallsyms mdstat sched_debug sysrq-trigger zoneinfo
10 16 19160 230 2529 26185 30932 390 6035 9817 cgroups execdomains kcore meminfo schedstat sysvipc
104 16931 19226 233 2542 26881 30934 397 7 9818 cmdline fb keys misc scsi timer_list
12 17 19273 23448 255 28 31498 40 7662 9819 consoles filesystems key-users modules self timer_stats
13 18 19453 235 256 29645 31519 435 794 9820 cpuinfo fs kmsg mounts slabinfo tty
14 18827 19456 237 25827 3 31522 469 796 9821 crypto interrupts kpagecount mtrr softirqs uptime
15 18854 2 243 25828 30442 325 470 8 9875 devices iomem kpageflags net stat version
15819 19 22276 25 25917 30444 36 475 9 acpi diskstats ioports loadavg pagetypeinfo swaps vmallocinfo
15824 19016 22373 250 25952 30870 38 5 9812 buddyinfo dma irq locks partitions sys vmstat
增加CLONE_NEWNS参数。
sh-4.2# ls /proc
1 cgroups devices fb ioports key-users locks mounts sched_debug softirqs sysvipc version
4 cmdline diskstats filesystems irq kmsg mdstat mtrr schedstat stat timer_list vmallocinfo
acpi consoles dma fs kallsyms kpagecount meminfo net scsi swaps timer_stats vmstat
buddyinfo cpuinfo driver interrupts kcore kpageflags misc pagetypeinfo self sys tty zoneinfo
bus crypto execdomains iomem keys loadavg modules partitions slabinfo sysrq-trigger uptime
User Namespace
User Namespace主要是隔离用户的用户组ID,也就是说,一个进程所属User ID在不同User Namespace中可以不同。比较常见的是,在宿主机上一个普通用户创建一个User Namespace,在其中映射成Root用户。这样做可以很好的限制权限。
(代码略长,主要操作也是加入系统调用参数)
加入前:
arbus@ubuntu:~$ id
uid=1000(arbus) gid=1000(arbus) groups=1000(arbus)
加入后
root@container:~# id #<----我们可以看到容器里的用户和命令行提示符是root用户了
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
虽然容器里是root,但其实这个容器的/bin/bash进程是以一个普通用户来运行的。这样一来,我们容器的安全性会得到提高。
Network Namespace
Network namespace 实现了网络资源的隔离,包括网络设备、IPv4 和 IPv6 协议栈,IP 路由表,防火墙,/proc/net 目录,/sys/class/net 目录,套接字等。
Network namespace可以让每个容器都拥有独立的虚拟 网络设备,容器内的应用可以绑定到自己的端口而不会与其他Network namespace冲突。只要在宿主机上搭建网桥,就可以方便的通信。
(例子略)
Linux Cgroup
Linux Cgroup全称Linux Control Group,它提供了对一组进程及将来子进程的资源限制、控制和统计能力,这些资源包括CPU、内存、储存、网络等。通过这个功能,可以方便的限制资源的占用、并且可以实时监控。
三个组件:
- cgroup: 一组按照某种标准划分的进程。Cgroups中的资源控制都是以控制组为单位实现。一个进程可以加入到某个控制组。
- subsystem: 一个子系统就是一个资源控制器。子系统必须附加到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制
- hierarchy: 把一组cgroup串成一个树状结构。通过这种结构,cgroup可以实现继承,控制组树上的子节点继承父结点的属性。
例子:
有一个很占用内存的demo:
int main(void)
{
int i = 0;
for(;;) i++;
return 0;
}
以超级用户权限执行,查看一下资源占用:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3529 root 20 0 4196 736 656 R 99.6 0.1 0:23.13 demo
首先在/sys/fs/cgroup/cpu下创建了一个haoel的group。设置一下这个group的cpu利用的限制:
arbus@ubuntu:~# cat /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us
-1
root@ubuntu:~# echo 20000 > /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us
这个进程的PID是3529,我们把这个进程加到这个cgroup中:
# echo 3529 >> /sys/fs/cgroup/cpu/haoel/tasks
重新查看,发现已经降至19.9:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3529 root 20 0 4196 736 656 R 19.9 0.1 8:06.11 demo
AUFS
先介绍一下什么是UnionFS。它是为类Unix系统设计的,把其他文件系统联合到一个联合挂载点的文件系统服务。
Branch – 就是各个要被union起来的目录。Branch根据被union的顺序形成一个stack,一般来说最上面的是可写的,下面的都是只读的。当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。看起来这个虚拟后的联合文件系统是可以对任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为 unionfs用到了一个重要的资源管理技术,叫写时复制。
写时复制(copy-on–Write,下文简称CoW)也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。它的思想是,如果一个资源是重复的,但没有任何修改,这时并不需要立即创建一个新的资源,这个资源可以被新旧实例共享。创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改时增加小部分的开销。
AUFS有所有Union FS的特性,支持将不同目录挂载到同一个虚拟文件系统下的文件系统。这种文件系统可以一层一层地叠加修改文件。无论底下有多少层都是只读的,只有最上层的文件系统是可写的。当需要修改一个文件时,AUFS创建该文件的一个副本,使用CoW将文件从只读层复制到可写层进行修改,结果也保存在可写层。
$ tree
.
├── fruits
│ ├── apple
│ └── tomato
└── vegetables
├── carrots
└── tomato
# 创建一个mount目录
$ mkdir mnt
# 把水果目录和蔬菜目录union mount到 ./mnt目录中
$ sudo mount -t aufs -o dirs=./fruits:./vegetables none ./mnt
# 查看./mnt目录
$ tree ./mnt
./mnt
├── apple
├── carrots
└── tomato
# 修改
$ echo mnt > ./mnt/apple
$ cat ./mnt/apple
mnt
$ cat ./fruits/apple
mnt
# 再次修改
$ echo mnt_carrots > ./mnt/carrots
$ cat ./vegetables/carrots
$ cat ./fruits/carrots
mnt_carrots