深入剖析容器技术基础

2022-12-05 09:05:31 浏览数 (1)

大家好,我是码农小余。

好久不见。这阵子没日没夜疯狂地输入,看了一些容器和云原生相关的书籍和课程。

按照费曼学习法中的输入、回顾、输出、传授过程,从这篇文章开始,会将容器、k8s 等相关的知识通过一系列文章跟大家分享。若理解有偏差,还请给个及时的 Feedback。看了有收获的麻烦点个关注哦!

本文先来讲讲容器化技术依赖的 Linux 底层三大件——Namespace、CgroupsUnionFS

安装环境

工欲善其事,必先利其器。先给大伙介绍一个极速安装环境的组合——VagrantFile 组合 Parallels Desktop(Mac)或 VirtualBox(windows)。傻瓜式“下一步”安装完前面的软件之后,使用 M1 系统的童鞋还需要安装一个 PD 插件[1]才能直接使用 Vagrantfile 去启动虚拟机。

准备就绪后,执行以下命令:

代码语言:javascript复制
vagrant init jdputsch/centos-9-aarch64

备注:window 操作系统的可以去 Vagrant Cloud[2] 找对应的 box。

可以看到生成了 VagrantFile 文件,接着执行

代码语言:javascript复制
vagrant up

便可以拉取镜像和启动虚拟机了。

代码语言:javascript复制
vagrant status

执行上述命令可以查询虚拟机状态,我的实验环境截图如下:

最后再执行 vagrant ssh 就可以连接到虚拟机了,默认是 vagrant 账号进入的。

以上几步操作可以说是 vagrant 最基础的使用了,Vagrantfile 支持很多配置,系列后面慢慢都会接触到,包括如何通过 Vagrantfile 去快速用 kubeadm 搭建 k8s 集群。

进入虚拟机,就可以安装 docker 了。执行以下几条命令快速安装:

代码语言:javascript复制
# 安装一些基础工具
sudo yum install -y vim telnet bind-utils wget

# 获取 docker 安装脚本
curl -fsSL get.docker.com -o get-docker.sh

# 执行脚本
sudo chmod  x get-docker.sh
sh get-docker.sh

上述脚本执行完之后,执行 su 切换到 root 用户(密码默认是 vagrant),最后启动 docker

代码语言:javascript复制
# 启动docker服务
systemctl start docker

# 设置开机启动
systemctl enable docker

显示以下截图便可以开始我们的练习了。

跑一个容器

执行大名鼎鼎的 docker run 命令:

代码语言:javascript复制
docker run -idt --cpus=".5" centos

-i 表示以可交互的形式执行,-t 表示分配一个 TTY,-d 表示可以在后台 daemon 执行,--cpus=".5" 表示分配 50% 配置的 CPU 给这个容器。这些命令后续会挨个详细分享,现在可以先忽略。按下回车之后,docker 就会去镜像仓库(没有配置 registry 就会去 dockerhub[3] 拉取),最终在 TTY 看到一个容器 id 就表示容器已经运行起来了,可以执行 docker ps 查看:

代码语言:javascript复制
[root@docker-learning cpudemo]# docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED       STATUS       PORTS     NAMES
78ed33c53307   centos    "/bin/bash"   9 hours ago   Up 9 hours             recursing_shaw

执行 docker inspect xx 查看容器的详细信息,grep 一下容器的 Pid,所以执行以下命令:

代码语言:javascript复制
docker inspect 78ed33c53307 | grep Pid

执行结果如下:

代码语言:javascript复制
[root@docker-learning cpudemo]# docker inspect 78ed33c53307 | grep Pid
            "Pid": 10262,
            "PidMode": "",
            "PidsLimit": null,

你们实验的时候肯定不是这个 Pid,到时候就取各自 Pid 练习即可,拿着这个进程 ID 查看容器内的网络信息,执行:

代码语言:javascript复制
# nsenter 进入进程 10262 执行 ip addr 命令
nsenter -t 10262 -n ip addr

然后在宿主机执行 ip adrr 查看宿主机的网络信息,结果如下:

这就是进程网络隔离效果。

细心的读者可能注意到了,我们在上面执行 docker run 的时候,加了 --cpus=".5" 的参数,那么这个参数是如何生效的?进入到容器内部,我们通过 Cgroups 查看一下这个参数:

代码语言:javascript复制
docker exec -it 78ed33c53307 /bin/bash

进入容器的 TTY 之后,进入 /sys/fs/cgroup 目录查看 cpu.max 文件。结果如下:

代码语言:javascript复制
[root@78ed33c53307 cgroup]# cat cpu.max
50000 100000

cpu.max 表示 cpu 的最大配额,100000 和 50000 表示每 100000ms 的时间中,我要使用 50000ms,即 50%。

以上就是资源限制的表象,下文会通过在宿主机中演练这个现象。

只有进程隔离跟资源限制和技术,容器还不足以撼动虚拟机的地位,镜像才是威震江山的优势。容器 image 是分层(layer)且 readonly 和共享的,共享怎么理解呢?镜像被多个容器使用,也不会在内存或者硬盘上做多份拷贝。在需要对镜像提供的文件进行修改时,该文件会从镜像的文件系统被复制到容器的可写层的文件系统进行修改,而镜像里面的文件不会改变。不同容器对文件的修改都相互独立、互不影响。这就是 Copy-on-Write(写时复制机制)。这也是为什么容器可以快速复制、迁移的原因,比虚拟机有优势的地方。

先来查看一下我们起的 centos 容器信息:

代码语言:javascript复制
docker inspect 78ed33c53307 

注意看到 Data 部分信息:

我们这个容器存储驱动类型是 overlay2,这也是一种 UnionFS,属于文件级的存储驱动。

结合上图,可以看到容器跟 overlayFS 的映射关系。注意我框起来的三个字段:

  • lowerDir 下层目录,可以理解成是镜像层;
  • UpperDir 上层目录,可以理解成是容器层;
  • MergedDir 合并目录,就是上 下目录合并的结果;

我们打印 MergedDir 可以看到容器内的 rootfs:

想了解更多的童鞋可以前往 docker 存储驱动的文档[4]

上面我们通过一个 centos 容器上的配置以及现象,看到容器核心技术的本质。接下来就抛开 docker 容器,我们直接在 Linux 虚拟机上来实践这些核心技术。让我们对容器化技术有更加深刻的认识。

进程隔离

Namespace 技术是用来修改进程视图的方法。什么意思呢?系统可以为进程分配不同的 Namespace,并保证不同 Namespace 资源独立分配,进程彼此隔离。

上面在讲网络隔离的时候使用到了 nsenter 命令,这是进入某个空间执行某条命令的用法,除此之外,还有以下常用命令:

代码语言:javascript复制
# 查看当前系统的 namespace
lsns -t <type>

# 查看进程的 namespace
ls -la /proc/<pid>/ns/

我们实际 ls 一下,看看有哪些资源可以做进程之间的隔离:

挑几个认识一下:

  • ipc 表示 System IPC 和 POSIX 消息队列的隔离;
  • net(Network)网络设备、网络协议栈端口的隔离;
  • pid 进程隔离;
  • mnt 挂载点隔离;
  • user 用户和用户组隔离;
  • uts 主机名和域名的隔离;

通过隔离(蒙蔽双眼)技术,容器内会进入一个全新的进程空间。我们可以进入 centos 容器并执行 ps -ef 看看结果:

可以看到,我们执行的 bash 命令已经成为容器内的 1 号进程了,而宿主机的 1 号进程是 systemd 系统启动进程。

我们通过 unshare 命令也可以实现上述容器隔离的效果,执行:

代码语言:javascript复制
unshare --fork --pid --mount-proc bash

unshare 命令可以在一个新的命名空间中运行程序。详细用法这里就不展开介绍了,感兴趣的可以通过 man unshare 查看。执行 unshare 之后再查看进程列表,我们可以看到,隔离的效果跟在容器内查看进程列表是一致的。

通过障眼法,来达到轻量和高性能是容器的最大优势。但劣势也很明显,容器毕竟是共享一个宿主机操作系统内核的,所以对于 Namespace 无法隔离的资源比如系统时间是容器的痛点,一旦某个容器修改了系统时间,影响的是整个宿主机以及运行在上面的容器,这也是为什么容器安全是用户必须考虑的一个问题。

资源限制

Cgroups 全称是 Control Group,主要作用就是用来限制进程的资源使用,包括 CPU、内存、硬盘、网络等等。前面在讲容器的 CPU 限制的时候,我们是进入到容器内查看 /sys/fs/cgroup,新建一个 cpu_testing 目录,然后 ls 一下:

系统自动帮我们在 cpu_testing 下创建了一系列资源限制的文件。按照上述容器中的 --cpu 参数的效果,我们聚焦在 cpu.maxcgroup.procs 两个文件,前一个是做 CPU 配额限制的文件,后一个是限制具体任务进程的文件。接下来我们就演练一下:

  1. 在宿主机新建一个占满 CPU 资源的进程 PIDX;
  2. top 实时监控资源使用情况,确认 PIDX 进程的 CPU 已经疯狂飙升;
  3. 将 PIDX 写入 cgroup.procs 并更新 cpu.max
  4. 再回到 top 面板,查看 PIDX 进程的 CPU 使用占比;

实践 1、2 步我们执行了一个死循环 shell 脚本,然后通过 top 看到 17002 这个进程的 CPU 使用率是达到了 100% 的。接下来我们将 17002 写入到 cgroup.procs,执行:

代码语言:javascript复制
echo 17002 > /sys/fs/cgroup/cpu_testing/cgroup.procs

然后通过 vim 修改 cpu.max 文件,修改结果如下:

代码语言:javascript复制
[root@docker-learning cpu_testing]# cat ./cpu.max
50000 100000

保存之后再看到 top 监控面板,我们可以很神奇的发现,CPU 占用已经降到 50% 了,见下图:

以上就是容器限制资源的简单原理。当然不仅仅是 CPU 资源,内存、输入输出设备、网络等都可以限制使用。

文件系统

上面讲容器的文件系统时,我们知道 MergedDir 目录就是容器的根文件系统(rootfs),也知道 MergedDir 是 lowerDir 和 upperDir 合并之后的结果。通过 docker info 可以看到我们电脑的 UnionFS 驱动类型。

代码语言:javascript复制
[root@docker-learning overlayfs_test]# docker info | grep Storage
 Storage Driver: overlay2

所以在这一小节,我们来实践一下 overlay2 的挂载,模拟容器的文件系统实现方式。执行以下几步:

创建四个目录,分别叫作 lower、upper、merged、work(跟容器的挂载目录保持一致),然后用下面视图去创建文件:

然后调用 mount 命令挂载文件系统;

代码语言:javascript复制
# 使用 overlay 类型将lower和upper挂载到 merged 目录
[root@docker-learning overlayfs_test]# mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged

查看 merged 目录下的文件,我们可以看到 merged 目录下的文件都是来自 lower 和 upper 中的文件:

文件内容也都跟 lower 和 upper 中的一样,有一个值得注意的是,in_both.txt 文件内容是 upper 目录下的 in_both.txt 内容:

我们尝试修改一下 lower/in_lower.txt 和 upper/in_upper.txt 内容,结果都可以正确反映到 merged/in_lower.txt 和 merged/in_upper.txt 中:

最后我们先修改 lower/in_both.txt,并查看 merged/in_both.txt 结果;然后再修改 upper/in_both.txt,最后再查看 merged/in_both.txt 内容:

发现修改 lower/in_both.txt 并不会映射到 merged/in_both.txt,而修改 upper/in_both.txt 就能够正确映射。

以上就是 UnionFS 的简单实践和原理。再看到 docker 官网的这张图,是否更加清晰了呢?

总结

本文先是介绍了安装环境的利器 Vagrant,学习容器(docker)和 k8s 必不可少的需要在 Linux 环境上敲一敲,了解资源的使用和现象,快速搭建学习环境能够让我们更加集中在具体内容的学习上。

然后通过 docker 跑了一个 centos 容器,我们调用了很多命令,通过容器的表现去理解技术的核心。命令没有其他的掌握渠道,还得是多看文档、多敲、多使用。

最后,通过在宿主机上,我们实践了容器技术的三大核心技术——Namespace,Cgroups以及 UnionFS。掌握了核心技术,下篇文章我们就通过 Dockerfile 制作镜像,并更加深刻地理解 layer 这个概念。

参考资料

[1]

PD 插件: https://parallels.github.io/vagrant-parallels/docs/installation/

[2]

Vagrant Cloud: https://app.vagrantup.com/boxes/search

[3]

dockerhub: https://hub.docker.com/

[4]

docker 存储驱动的文档: https://docs.docker.com/storage/storagedriver/

0 人点赞