Docker核心技术-namespace
在我们使用容器的时候,可以体会到在同一个虚拟机上运行多个容器,不同容器之间是互相不受影响的,各个容器可以很好的进行独立运行。那么容器是如果做到互相不受影响的呢?而且我们还可以根据需要给不同容器分配不同的资源配额,那么又是如果做到的呢?还有为什么在容器中随便操作而操作系统本身不会受影响?
容器是怎么做资源隔离和资源限制管理的呢,其实主要是依赖两大核心技术,即Namespace(资源隔离)和Cgroup(资源限制)。带着这些问题我们来看一下docker所用到的核心技术:namespace,cgroup,文件系统。
namespace
docker使用linux namespace做运行环境隔离,目前linux提供了6类系统资源隔离机制。
| namespace | 描述 | clone() flag参数 | 作用 |
|---|---|---|---|
| UTS | UTS namespace | CLONE_NEWUTS | 隔离主机名和域名信息 |
| IPC | IPC namespace | CLONE_NEWIPC | 隔离信号量,消息队列,进程间通信 |
| PID | PID namespace | CLONE_NEWPID | 隔离进程ID |
| Network | Network namespace | CLONE_NEWNET | 隔离网络资源,协议栈,端口 |
| Mount | Mount namespace | CLONE_NEWNS | 隔离文件系统 |
| User | User namespace | CLONE_NEWUSER | 隔离用户和用户组 |
namespace讲解
namespace的操作可以通过clone(),setns(),unshare()来进行,接下来简单来了解下几个接口的用法。
- clone: 通过clone来创建一个独立的Namespace进程
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
参数解释:
child_func: 表示子进程运行的主函数;
child_stack: 子进程使用的栈空间;
flags: 表示使用哪些CLONE标志位;
args: 用户参数
- setus: 使某个进程加入到某个namespace;
- unshare: 使某个进程脱离某个namespace; 查看进程的ns
查看pid=1和pid=13的进程对应的ns,因为pid=13的进程是pid=1的一个子进程,所以会沿用父进程的ns,通过一下结果可以看到两个进程对应的ns都是一样的。
[root@i-l7l18s0l proc]# ls -al /proc/1/ns
总用量 0
dr-x--x--x 2 root root 0 1月 27 10:38 .
dr-xr-xr-x 9 root root 0 12月 29 11:11 ..
lrwxrwxrwx 1 root root 0 1月 27 10:38 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 1月 27 10:38 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 1月 27 10:38 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 1月 27 10:38 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 1月 27 10:38 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 1月 27 10:38 uts -> uts:[4026531838]
clone()调用示例 接下来以UTS隔离为例,创建一个新的拥有自己UTS的进程看下效果,创建newns.c
#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <stdio.h>
#define STACK_SIZE (1024 * 1024)
#define FLAGS SIGCHLD|CLONE_NEWUTS
static char stack[STACK_SIZE];
static char * const child_args[] = {"/bin/bash", NULL };
static int child(void *arg)
{
execv("/bin/bash", child_args);
return 0;
}
int main(int argc, char *argv[])
{
pid_t pid;
pid = clone(child, stack+STACK_SIZE, FLAGS, NULL);
waitpid(pid, NULL, 0);
return 0;
}
对newns.c进行编译,这段代码运行后会启动一个新的bash交互窗口
[root@docker ns]# ls
newns.c
[root@docker ns]# gcc newns.c -o newns
[root@docker ns]# ls
newns newns.c
[root@docker ns]#
运行编译好的文件,然后进行修改hostname的操作,接下来我们在newns启动的bash中修改hostname
[root@docker ns]# ./newns
[root@docker ns]# hostname
docker
[root@docker ns]# hostname docker-new
[root@docker ns]# hostname
docker-new
[root@docker ns]#
可以看到bash中的hostname已经改成了新的docker-new,那么我们再打开一个新的linux shell查看下hostname,可以看到新打开的终端中hostname还是和之前的一样。
[root@docker ~]# hostname
docker
[root@docker ~]#
这时候我们也可以通过查看/proc/$pid/ns和/proc/1/ns的Id对比来看是否和我们想象的一致,UTC NS是不一样的值。
[root@docker ~]# ps -ef | grep new
root 4926 4837 0 16:13 pts/8 00:00:00 ./newns
root 4967 4945 0 16:13 pts/7 00:00:00 grep --color=auto new
[root@docker ~]# ll /proc/1/ns
总用量 0
lrwxrwxrwx 1 root root 0 1月 28 18:13 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 1月 28 18:13 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 1月 28 14:59 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 1月 28 18:13 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 1月 28 18:13 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 1月 28 18:13 uts -> uts:[4026531838]
[root@docker ~]# ll /proc/4926/ns
总用量 0
lrwxrwxrwx 1 root root 0 1月 29 16:13 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 1月 29 16:13 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 1月 29 16:13 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 1月 29 16:13 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 1月 29 16:13 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 1月 29 16:13 uts -> uts:[4026531838]
我们发现uts的pid两个进程是一样的?那这个是为什么呢,说好的新UTS NS呢?我们先继续带着这个问题给下看。
到这里我们已经实现了在新的bash中修改了hostname并且生效,同时修改的hostname并没有影响到原先的主机hostname,达到了hostname隔离的效果。但是大家肯定会有一个疑问,为什么已经实现了UTC隔离之后,我们新启动的bash进程和主进程的namespace id都是一样的呢?这是因为我们虽然实现了UTC隔离,但是我们的bash也是启动在主进程下的子进程的,我们上面已经讲过了子进程是会使用父进程的NS的。我们可以通过在新UTC的bash终端中运行一个新的进程了比较看UTC NS是否会发生改变。
在我们启动的bash中运行下面的shell, shell运行后查看进程的pid,然后比较/proc/$pid/ns和/proc/1/ns下ns的差别。 新建loop.sh
#!/bin/bash
for a in {1..200}
do
sleep 1
echo $a
done
在/newns bash下执行脚本启动一个新的进程。
[root@docker ~]# cd ns
[root@docker ns]# ./newns
[root@docker ns]# ./loop.sh
比较loop进程和主进程的NS,我们发现UTS对应的值不一样,这是因为loop进程是经过UTS隔离的bash下的进程,父进程不再是pid=1的进程,在新的bash中我们已经进行了UTS隔离,所以会用新的UTS NS id。
[root@docker ~]# ps -ef | grep loop
root 4991 4927 0 16:13 pts/8 00:00:00 /bin/bash ./loop.sh
root 5010 4945 0 16:13 pts/7 00:00:00 grep --color=auto loop
[root@docker ~]# ll /proc/1/ns
总用量 0
lrwxrwxrwx 1 root root 0 1月 28 18:13 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 1月 28 18:13 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 1月 28 14:59 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 1月 28 18:13 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 1月 28 18:13 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 1月 28 18:13 uts -> uts:[4026531838]
[root@docker ~]# ll /proc/4991/ns
总用量 0
lrwxrwxrwx 1 root root 0 1月 29 16:14 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 1月 29 16:14 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 1月 29 16:14 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 1月 29 16:14 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 1月 29 16:14 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 1月 29 16:14 uts -> uts:[4026532237]
[root@docker ~]#
到这里我们了解到了linux通过namespace隔离的类型和如果创建一个新的ns隔离类型。
docker容器隔离
通过上面演示的UTS namespace隔离的例子,我们可以知道在不同的namespace下去修改对应的资源,不会对其他namespace下的资源造成影响,那么就可以根据这个特点在操作系统中隔离出很多个互相独立的空间,各自操作各自空间的资源而不影响其他的,这就是docker容器之间互现独立的原理。
接下来通过lsns命令查看进程pid的ns,因为主机上的进程都是属于主进程下的子进程,我们在机器上启动了一个容器,可以看到两个进程的pid ns是不一样的。
[root@docker ns]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
583d44c9ac18 13227829078/gogin:v1.0 "./go-gin" 5 minutes ago Up 5 minutes 8888/tcp relaxed_meninsky
[root@docker ns]#
[root@docker ns]# lsns -t pid
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 96 1 root /usr/lib/systemd/systemd --system --deserialize 24
4026532178 pid 1 3673 root ./go-gin
我们也可以通过ps -ef -o ppid,pid,pidns,args来查看进程和ns
[root@docker ~]# ps -ef -o ppid,pid,pidns,args
PPID PID PIDNS COMMAND
31825 31827 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=117.35.173.64 6476 2
31362 31364 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=117.35.173.64 5950 2
5837 5840 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=117.35.173.64 6412 2
5840 5920 4026531836 \_ ps -ef -o ppid,pid,pidns,args XDG_SESSION_ID=218 HOSTNAME=docker TERM=xterm-256color SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=117.35.173.64 6412 22 SSH_TTY=/dev/pts/0 USER=roo
5456 5458 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=117.35.173.64 6115 2
4943 4945 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=117.35.173.64 5857 2
4834 4837 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=117.35.173.64 5827 2
4837 4926 4026531836 \_ ./newns XDG_SESSION_ID=214 HOSTNAME=docker TERM=xterm-256color SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=117.35.173.64 5827 22 SSH_TTY=/dev/pts/8 USER=root LS_COLORS=rs=0:di=38
4926 4927 4026531836 \_ /bin/bash XDG_SESSION_ID=214 HOSTNAME=docker TERM=xterm-256color SHELL=/bin/bash HISTSIZE=1000 SSH_CLIENT=117.35.173.64 5827 22 SSH_TTY=/dev/pts/8 USER=root LS_COLORS=rs=0
4276 4280 4026531836 -bash LANG=zh_CN.UTF-8 USER=root LOGNAME=root HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root SHELL=/bin/bash SSH_CLIENT=117.35.173.64 5318 2
3657 3673 4026532178 ./go-gin PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=583d44c9ac18 TERM=xterm GOLANG_VERSION=1.14.15 GOPATH=/go GOPROXY=htt
1106 6985 4026531836 -bash HOME=/root USER=root SHELL=/bin/bash TERM=linux PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin MAIL=/var/mail/root LOGNAME=root XDG_SESSION_ID=45 XDG_RUNT
1 1105 4026531836 /sbin/agetty --keep-baud 115200,38400,9600 ttyS0 vt220 LANG=en_US.UTF-8 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin TERM=vt220
[root@docker ~]#
通过NS来反推容器
上面介绍了ns,以及我们可以通过启动的容器pid来查看对应的ns,这些其实都是我们通过容器和ns的关系来进行顺推发现的,那么我们既然知道这些,我们接下来模拟一下通过ns来操作容器,使用nsenter命令来模拟进入到docker容器中。
启动一个容器并且通过docker exec进入容器查看容器app/目录下的文件。
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
583d44c9ac18 13227829078/gogin:v1.0 "./go-gin" 49 minutes ago Up 49 minutes 8888/tcp relaxed_meninsky
[root@docker ~]# docker exec -it 583d44c9ac18 /bin/sh
/app # ls
Dockerfile go-gin go-gin.go go.mod go.sum index.html
/app #
查找到容器对应的进程pid,在使用命令nsenter --target 3673 --mount --uts --ipc --net --pid -- /bin/su - root登陆进入pid对应的容器,查看app/目录下发现是我们的容器目录。关于nsenter登陆容器有时候失败参考链接
[root@docker ~]# lsns -t pid
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 97 1 root /usr/lib/systemd/systemd --system --deserialize 24
4026532178 pid 2 3673 root ./go-gin
[root@docker ~]# nsenter --target 3673 --mount --uts --ipc --net --pid -- /bin/su - root
583d44c9ac18:~# ls /app
Dockerfile go-gin go-gin.go go.mod go.sum index.html
583d44c9ac18:~#
到这里我们已经知道在linux中我们可以通过namespace来进行资源隔离。