Docker容器基础入门认知-Namespce
在使用 docker 之前我一般都认为容器的技术应该和虚拟机应该差不多,和虚拟机的技术类似,但是事实上容器和虚拟机根本不是一回事。
虚拟机是将虚拟硬件、内核(即操作系统)以及用户空间打包在新虚拟机当中,虚拟机能够利用“虚拟机管理程序”运行在物理设备之上。虚拟机依赖于 hypervisor,其通常被安装在“裸金属”系统硬件之上,这导致hypervisor在某些方面被认为是一种操作系统。一旦 hypervisor 安装完成, 就可以从系统可用计算资源当中分配虚拟机实例了,每台虚拟机都能够获得唯一的操作系统和负载(应用程序)。简言之,虚拟机先需要虚拟一个物理环境,然后构建一个完整的操作系统,再搭建一层Runtime,然后供应用程序运行。
容器其实就是在系统中,利用 namespace 和 cgroup 原本的 linux 特性,将进程隔离在一个封闭的环境中,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”,再通过 veth 让进程的网络环境相隔离,利用 docker 的网桥来进行容器进程之间的相互通信。
这里要说的是容器并没有使用什么很高深的技术,而是将上面所说的 linux 相关的技术进行融合形成容器技术,所以有句话非常正确:
容器本身没有价值,有价值的是“容器编排”。
接下来会从三个方向说一下容器底层所使用到的技术:隔离,限制还有互通。
隔离-Namespace机制
首先先创建一个简单的容器:
$ docker run -it busybox /bin/sh
/ #
这里是简单起了一个 busybox 的容器,用 -it 交互的方式来进行 shell
此时在容器内 ps
/ # ps PID USER TIME COMMAND 1 root 0:00 /bin/sh 10 root 0:00 ps
可以看到,我们在 Docker 里最开始执行的 /bin/sh,就是这个容器内部的第 1 号进程(PID=1),而这个容器里一共只有两个进程在运行。这就意味着,前面执行的 /bin/sh,以及我们刚刚执行的 ps,已经被 Docker 隔离在了一个跟宿主机完全不同的世界当中。
这里的 pid=1 只是 namespace 机制中的映射,实际在宿主机器中
$ ps -ef | grep docker
root 7067 2417 0 16:02 pts/0 00:00:00 docker run -it busybox /bin/sh
这里的 pid=7067
这种技术,就是 Linux 里面的 Namespace 机制。而 Namespace 的使用方式也非常有意思:它其实只是 Linux 创建新进程的一个可选参数。我们知道,在 Linux 系统中创建进程的系统调用是 clone(),比如:
int pid = clone(main_function, stack_size, SIGCHLD, NULL);
这个系统调用就会为我们创建一个新的进程,并且返回它的进程号 pid。而当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。
之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值,比如 100。当然,我们还可以多次执行上面的 clone() 调用,这样就会创建多个 PID Namespace,而每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。而除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。这,就是 Linux 容器最基本的实现原理了。
userns-remap
在这里还想说一下关于 docker 中使用 userns-remap 做用户命名空间
下面的步骤我是参考 https://blog.csdn.net/zyy247796143/article/details/114386484
使用普通用户运行docker容器,在docker中,在容器内创建的文件在从主机检查时往往具有不可预测的所有权。默认情况下,卷上文件的所有者是root(uid 0),但只要非root用户帐户涉及容器并写入文件系统,所有者就会从主机角度变得或多或少随机 。需要使用调用docker命令的同一用户帐户从主机访问卷数据时,这是一个问题.
典型的解决方法是:
- 在Dockerfiles中创建时强制用户uID(非可移植)
- 将主机用户的UID作为环境变量传递给docker run命令,然后在入口点脚本中的卷上运行一些chown 命令
-
用户命名空间( user namespace)是这个问题的最终解决方案.
通过 user namespace 技术,把宿主机中的一个普通用户(只有普通权限的用户)映射到容器中的 root 用户。在容器中,该用户在自己的 user namespace 中认为自己就是 root,也具有 root 的各种权限,但是对于宿主机上的资源,它只有很有限的访问权限(普通用户)。
Kernel 内核启用 namespace
grubby --args="namespace.unpriv_enable=1 user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)"
echo "user.max_user_namespaces=15076" >> /etc/sysctl.conf
重启系统
reboot
创建用户
useradd -u 5000 dku groupadd -g 500000 dku-root useradd -u 500000 -g dku-root dku-root
在/etc/subuid 和 /etc/subgid 文件中修改自动生成的从属ID范围:
dku:500000:65536
修改 /etc/docker/daemon.json,新增 "userns-remap"
[root@izm5e37rlunev9ij58ixy9z ~]# cat /etc/docker/daemon.json { "registry-mirrors": ["https://registry.docker-cn.com"], "userns-remap": "dku" }
访问数据卷文件
假如,在容器里面创建一个 UID 为1000 的 test 用户,他能读取挂载在容器里面的什么权限的文件呢?
测试文件权限:在系统的 /data/test 目录上,创建几个不同的测试文件
1. 一个属主和属组都为 501000 的文件,权限为600,文件名为:501000.txt 。
3. 一个属主和属组都为 dku-root (UID为500000)的文件,权限为600,文件名为:dku-root.txt 。
4. 一个在系统上权限为 root (UID为0)的文件,权限为600,文件名为:root.txt 。
5. 一个在系统上权限为 root (UID为0)的文件,权限为666,文件名为:test.txt 。
6.启动一个centos 7.9 的容器,并在里面创建一个 UID 为 1000 的test 用户,进入挂载的 /test 目录。
查看权限如下:
结果是只有 501000.txt 和 test.txt 这两个文件可以看到里面内容。剩下两个均没有权限。其中501000.txt 有权限,是因为在系统上这个文件的属主和属组都是 501000,其映射在容器里面的属主和属组就是1000,所以UID 为 1000 的 test 用户可以查看里面的内容。
test.txt 能查看,因为文件的权限是666,other 位的权限是 rw,所以其他用户都有权限查看。
这里的实验步骤要复现的话,还需要考虑linux的内核版本,如果低版本的运行 docker 的用户空间会出现这种问题:
docker: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused "process_linux.go:319: getting the final child's pid from pipe caused \"EOF\"": unknown.
我用 centos7.6 的是肯定会出现的,貌似这个 error 在 github 上有很多 issues
https://github.com/moby/moby/issues/40835