如何搞懂容器的核心技术点?
作者 | 阿文
责编 | 郭芮
很多接触过类似KVM、Vmware 等虚拟化产品的开发者一定知道,传统的虚拟机其实是模拟真实计算机硬件,然后需要独立安装一个单独的操作系统。这个操作系统可以是 Linux 或者是 windows,而 Docker 容器则不需要你去安装动则几十个 G 的操作系统——它提供的镜像启动后能够做到很小,例如 busybox 镜像只有几 M 大小:
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 3556258649b2 3 weeks ago 64.2MB
busybox latest db8ee88ad75f 4 weeks ago 1.22MB
而当我们执行:
docker exec -it docker 进程 /bin/bash
进入到 docker 容器中,得到一个和宿主机一样的 shell 终端,和我们连接虚拟机得到的终端几乎没什么不同,我们还可以在终端中执行例如:
apt-get update
亦或是安装各种命令。
等等,这似乎这就是一台虚拟机嘛。可是事实上真的是这样吗?
作为一名云计算行业的从业人员,我见过很多小白用户把容器当初虚拟机来使用,因为在他们看来启动一个容器比启动一个虚拟机要快很多,而且不需要手动去安装操作系统,在容器的终端中执行各种命令也和虚拟机没什么区别?这些初学者,他们会使用 Dockerfile 构建 Docker 镜像,也能够熟练的使用 Docker 的各种命令来管理容器。可是当遇到一些问题时,他们便束手无策,因为他们把容器镜像当成一个小型的操作系统来使用,什么东西都往容器放,导致容器的体积不断变大,导致一个容器镜像就高达到了几个 G,而对此他们便束手无策了,归其原因,还是因为他们并不了解 Docker。
今天我就跟大家聊一聊 Docker 到底是个什么东西。
容器其实是一种特殊进程
容器其实本质上就是一个进程,只不过容器的进程是比较特殊的。
容器技术的核心功能,就是通过约束和修改进程的动态表现,创造出一个“边界”,通过“障眼法”让人觉得它是一个独立的系统。大多数容器都是使用 Cgroups 技术来约束进程,通过 Namespace 技术来修改进程的视图。
Namespace
什么是 Namespace ?
我们通过一个案例来讲解,这里我们使用 Docker 运行一个 busybox 的容器:
# docker run -it busybox /bin/sh
/ # ps
PID USER TIME COMMAND1 root 0:00 /bin/sh6 root 0:00 ps
/ #
我们可以看到在容器中,只有 2 个进程在云信,一个 PID 为 1 的进程,它就是我们的 sh 程序。
我们再重新打开一个窗口执行执行 ps aux | grep docker:
# ps aux | grep docker
root 3490 0.0 5.7 843208 58032 ? Ssl Aug14 0:22 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 14669 0.0 6.3 709900 63612 pts/0 Sl+ 14:36 0:00 docker run -it busybox /bin/sh
可以看到进程 14669 正在执行`docker run -it busybox /bin/sh`,事实上这个 14669 才是这个 Docker 容器的真正 PID。
那它是如何做到在我们 exec 进入容器之后把进程 ID 改成 1 的呢?事实上进程 ID=1 正是操作系统的第一号进程,它是所有进程的父进程。我们可以通过ps aux | grep systemd 查看会发现 systemd 为 1 号进行:
ps aux | grep systemd
root 1 0.0 0.7 225308 7828 ? Ss Jun20 3:12 /lib/systemd/systemd --system --deserialize 39
现在 Docker 把这个/bin/sh 的程序运行在容器中,就需要给这个 ID=14669 的进程做一些手脚,把它自己变成 1 号进程来骗过其他进程。这种机制就是对隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程 ID,可是实际上它还是宿主机的 14669 号进程。
这种技术被称为 Namespace。
Namespace 其实是在创建新进程时候加了一个可选参数,它利用 Linux 的系统调用 clone() 为新创建的进程指定一个 CLONE_NEWPID 的参数,那么新创建的进程就会看到一个全新的进程空间,在这个进程空间里面它的 PID 就是 1。
Namespace 除了可以模拟 PID 之外,还提供了 Mout、UTS、IPC、Network 和 User 等,在不同的进程上下文做隔离操作。
而这些就是 Linux 容器的基本实现原理。
Cgroups
有了 Namespace ,使得容器就像一个沙盒一样看起来在一个独立的系统内运行,与宿主机互不干扰,可是,事实上只是 Namespace 隔离起来并不彻底。因为容器只是运行在宿主机上的一种特殊进程,所有的容器还是要共享宿主机的操作系统内核。并且有一些资源和对象是不能被隔离的,比如时间,如果你修改了容器的时间,那么操作系统的实际也会被修改。这显然不是我们希望看到的。另外使用 Namespace 并不能限制一个容器使用资源的边界,例如我们要限制一个容器使用的 CPU 资源或内存等,这些是 Namespace 无法做到的,它需要 Cgroup 来实现。那么什么是 Cgroup?
Cgroups 是Linux内核提供的一种可以限制单个进程或者多个进程所使用资源的机制,全称是 control groups,可以对 CPU、内存等资源实现精细化的控制。典型的子系统介绍如下:
cpu 子系统,主要限制进程的 CPU 使用率。
cpuacct 子系统,可以统计 Cgroups 中的进程的 CPU 使用报告。
cpuset 子系统,可以为 Cgroups 中的进程分配单独的 CPU 节点或者内存节点。
memory 子系统,可以限制进程的 memory 使用量。
blkio 子系统,可以限制进程的块设备 IO。
devices 子系统,可以控制进程能够访问某些设备。
net_cls 子系统,可以标记 Cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
freezer 子系统,可以挂起或者恢复 Cgroups 中的进程。
ns 子系统,可以使不同 Cgroups 下面的进程使用不同的 Namespace。
在 Linux 系统中,我们可以使用 mount 查看:
# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
会看到输出一系列的系统目录和文件。我们可以看到在 /sys/fs/cgroup 中有我们上述的这些资源的名称所对应的子目录,也被称为子系统。
例如下面就是 CPU 的相关配置文件:
cd /sys/fs/cgroup/cpu
/sys/fs/cgroup/cpu# ls
cgroup.clone_children cpuacct.stat cpuacct.usage_percpu_sys cpu.cfs_period_us docker tasks
cgroup.procs cpuacct.usage cpuacct.usage_percpu_user cpu.cfs_quota_us notify_on_release user.slice
cgroup.sane_behavior cpuacct.usage_all cpuacct.usage_sys cpu.shares release_agent
container cpuacct.usage_percpu cpuacct.usage_user cpu.stat system.slice
例如 cpu.cfs_period_us cpu.cfs_quota_us 这样的文件,它们就是用来限制 CPU 在一定时间内只能分配总量是多少的 CPU 时间。
举个例子,我们执行:
while : ; do : ; done &
[1] 10108
这是一个死循环,它能够把系统的 CPU 资源消耗到 100%,它的进程 ID 我们记住是 10108。
我们可以使用 top 命令来查看 CPU 的使用率已经高达 98.7:
top - 11:28:29 up 57 days, 1:47, 2 users, load average: 0.23, 0.23, 0.21
Tasks: 99 total, 3 running, 63 sleeping, 0 stopped, 0 zombie
%Cpu(s): 98.7 us, 1.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1006788 total, 185284 free, 458532 used, 362972 buff/cache
KiB Swap: 969964 total, 928280 free, 41684 used. 355496 avail Mem
我们接下来在 CPU 的 Cgroup 中创建一个 demo 的文件:
/sys/fs/cgroup/cpu# mkdir demo
/sys/fs/cgroup/cpu# cd demo/
/sys/fs/cgroup/cpu/demo# ls
cgroup.clone_children cpuacct.usage cpuacct.usage_percpu_sys cpuacct.usage_user cpu.shares tasks
cgroup.procs cpuacct.usage_all cpuacct.usage_percpu_user cpu.cfs_period_us cpu.stat
cpuacct.stat cpuacct.usage_percpu cpuacct.usage_sys cpu.cfs_quota_us notify_on_release
查看cfs_quota_us 和 cfs_period_us 的默认值,cfs_period_us 默认是 100ms(100000us):
# cat cpu.cfs_quota_us
-1
/sys/fs/cgroup/cpu/demo# cat cpu.cfs_period_us
100000
我们修改 cfs_quota_us 的值为 10000,设置为 10ms:
# echo 10000 > cpu.cfs_quota_us
/sys/fs/cgroup/cpu/demo# cat cpu.cfs_quota_us
10000
然后我们将进程 ID 写入 task:
/sys/fs/cgroup/cpu/demo# echo 10108 > tasks
此时我们再次执行 top 就可以发现 CPU 利用率瞬间降到了 30%以下,其他限制进程的资源使用的方法也类似。
这就是 Cgroup 技术的魅力所在,但是 Cgroup 并非万能,例如 /proc 目录下存储记录当前内核运行状态的一些文件,例如 top 等命令的的信息就来源于这里,如果此时你在容器中执行 top,会发现它显示的是宿主机的 CPU 个数以及内存大小等信息,这又不符合我们的预期,因为 /proc 文件系统不了解 Cgroup 限制的存在。
此时我们可以利用 LXCFS 来实现,LXCFS,FUSE filesystem for LXC是一个常驻服务,它启动以后会在指定目录中自行维护与上面列出的 /proc 目录中的文件同名的文件,容器从 lxcfs 维护的 /proc 文件中读取数据时,得到的是容器的状态数据,而不是整个宿主机的状态。
rootfs
现在我们知道了,容器最核心的原理实际上就是为用户创建进程,然后启动 Linux 的 Namespace 和配置 Cgroup 参数为用户创建了一个隔离环境。
但是仅仅这样还是不够的,我们进入容器其实会发现我们是在一个完全与宿主机不同的目录结构当作,这其实是容器通过 chroot 切换了进程的根目录来实现的,容器在启动时候重新挂载了它的整个根目录,并且依赖于 Namespace 的 mount,这个挂载点对于宿主机来说是不可见的,因此我们在容器中的任何操作宿主机都无感知,在 rootfs 中包含了操作系统所需要的文件、配置和目录,但是并不包含内核。
事实上,容器无法实现启动自己的独立内核,它只能使用宿主机的内核。同时由于 rootfs 里面打包的至少应用和相关依赖。保证了容器的非常重要的一个特性,即一致性。
镜像
容器的镜像体积一般都很小,但是很多稍微使用过容器的肯定会发现往容器里面写入一些文件,有时候可能这些文件很小,但是它会导致容器的体积变得非常大,这是为什么呢?
上面我们介绍了 rootfs,我们模拟了一个独立的系统根目录环境,那我们每次要更新应用软件,难道都需要按照上面的操作重新做一遍系统镜像吗?这个时候我们希望在制作 rootfs 的时候,能够以增量的方法去实现,即每次修改我只修改我需要修改的地方,而其他部分则保持和原来一样,而不是每次都全部fork 一次之前的操作。
事实上,Docker 公司在实现 Docker 时候就做类似的技术创建,他们引入了层的概念,即用户在制作镜像时的每一步操作都会生成一个层,也就是增量的 rootfs。通过 Union file System 即 UnionFS 可以将多个不同位置的目录联合挂载到同一个目录中,Docker 镜像的层分为只读层、可读写层和 init 层三部分组成。
只读层
这一层包含了整个底层操作系统所必须的一些目录和依赖,例如:
它们的挂载方式都是只读的(ro+wh,即 readonly+whiteout 的方式挂载。
可读写层
在这一层,它的挂载方式是 rw,在没写入文件之前,这个目录是空的。而一旦有了写操作,修改的内容就会以增量的方式出现在这层中。
如果要删除只读层的一个文件,AUFS 会在可读写层创建一个 whiteout 文件,把只读层的文件隐藏起来。
init 层
这一层用来存放一些例如 /etc/hosts、/etc/resolv.conf等信息,因为这部分内容往往是需要用户在启动容器时候写入一些指定的值,比如 hostname。所以就需要在可读写层对他们进行修改,而这些修改一般只对当前容器有效。用户在 commit 时候只会提交可读写层,并不包含 init 层的内容。
最后这些层被合并为一个目录下,组成了一个完整的操作系统供容器使用。
好了,以上就是给大家总结的容器技术的几个核心知识点。
【END】
如何在短时间内成为Python工程师?
https://edu.csdn.net/topic/python115?utm_source=csdn_bw
热 文 推 荐
程序员破解推荐系统瓶颈,带来超百亿收入增量!
☞ 双手无法敲代码的程序员,该如何编程?
Java 8 之后,还有哪些进化的功能?
10 步教你接手同事的代码!
☞亚马逊首席科学家李沐国内首次亲授「深度学习实训营」
☞CSDN & 火星财经, 联手发起Libra超级节点竞选!
☞"学了阿里中台,却依然做不好系统?" 聊聊阿里的项目管理
☞如何写出让同事无法维护的代码?
点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。
如何搞懂容器的核心技术点?相关推荐
- 一张图搞懂容器所有操作 - 每天5分钟玩转 Docker 容器技术(26)
前面我们已经讨论了容器的各种操作,对容器的生命周期有了大致的理解,下面这张状态机很好地总结了容器各种状态之间是如何转换的. 如果掌握了前面的知识,要看懂这张图应该不难.不过有两点还是需要补充一下: 可 ...
- 一文搞懂容器运行时 Containerd
在学习 Containerd 之前我们有必要对 Docker 的发展历史做一个简单的回顾,因为这里面牵涉到的组件实战是有点多,有很多我们会经常听到,但是不清楚这些组件到底是干什么用的,比如 libco ...
- java写一个外网访问的接口_【JAVA基础】一个案例搞懂类、对象、重载、封装、继承、多态、覆盖、抽象和接口概念及区别(中篇)...
0 前言 初学JAVA时,总会对一些概念一知半解,相互混淆,不明其设计的用意,如类.对象.重载.封装.继承.多态.覆盖.抽象类.接口概念.为便于理解和巩固,本文将基于一个案例及其变形,展现各个概念的定 ...
- java开发可重用代码包工具包_[Java教程]彻底搞懂Java开发工具包(JDK)安装及环境变量配置...
[Java教程]彻底搞懂Java开发工具包(JDK)安装及环境变量配置 0 2021-01-04 04:00:04 安装并配置JDK环境变量,不但要知道怎样做,也要知道为什么这样做,知其然知其所以然. ...
- 一文搞懂深度优先搜索、广度优先搜索(dfs、bfs)
前言 你问一个人听过哪些算法,那么深度优先搜索(dfs)和宽度优先搜索(bfs)那肯定在其中,很多小老弟学会dfs和bfs就觉得好像懂算法了,无所不能,确实如此,学会dfs和bfs暴力搜索枚举确实利用 ...
- 一文搞懂 Python 的 import 机制
一.前言 希望能够让读者一文搞懂 Python 的 import 机制 1.什么是 import 机制? 通常来讲,在一段 Python 代码中去执行引用另一个模块中的代码,就需要使用 Python ...
- python语言语句快的标记是什么_一文搞懂Python程序语句
原标题:一文搞懂Python程序语句 程序流 Python 程序中常用的基本数据类型,包括: 内置的数值数据类型 Tuple 容器类型 String 容器类型 List 容器类型 自然的顺序是从页面或 ...
- 一张图搞懂Spring bean的完整生命周期
转载自 一张图搞懂Spring bean的完整生命周期 一张图搞懂Spring bean的生命周期,从Spring容器启动到容器销毁bean的全过程,包括下面一系列的流程,了解这些流程对我们想在其中任 ...
- 搞懂 Java HashMap 源码
HashMap 源码分析 前几篇分析了 ArrayList , LinkedList ,Vector ,Stack List 集合的源码,Java 容器除了包含 List 集合外还包含着 Set 和 ...
最新文章
- 吴恩达深度学习笔记(109)-循环神经网络模型(RNN介绍)
- 华为交换机端口组配置实例
- 河马 webshell扫描器对linux服务器进行安全扫描
- [渝粤教育] 中国地质大学 职业健康管理体系 复习题 (2)
- 计算机操作基础英语,计算机操作基础word练习题参考答案
- ECCV2018 论文简析 Oral_1
- html百分比单位,视窗单位 vs 百分比单位
- java volatitl_Java中的关键字volatitle
- typedef用法小结(转载)
- Python基础学习笔记(一)python发展史与优缺点,岗位与薪资
- 文本框输入限制正则表达式收集
- maven下手动导入ojdbc6.jar
- oracle查看表索引及索引类型
- 时间序列预测的机器学习方法
- 深圳 计算机网络与管理,深圳计算机网络管理员路由与交换班
- 爬虫案例——模拟登录QQ空间
- 如何用python做兼职_python学会后可以干什么副业#做什么兼职副业好
- 【论文笔记】Toward A Thousand Lights Decentralized Deep Reinforcement Learning for Large-Scale TSC...
- oracle请求http接口
- 经典排序算法学习笔记二——快速排序
热门文章
- 代码不是重点, 领悟OO思想(一)
- 项目经理修炼手册,泄露章节 ,项目经理需要注意的习惯
- TDT2 多语言本文 4.0 版 TDT3 多语言本文 2.0 版
- python 图表 web_新手向——制作web图表(基于Python和GooPyCharts)
- tcp报文格式_腾讯面试中的TCP/IP协议简述+经典面试题
- 实验代码复现过程记录
- 【小白的刷题之路】字符统计
- Faster RCNN杂谈
- python ca模块_python学习之模块-模块(五)
- python降噪突出人声_当石墨烯动圈遇上主动式降噪,dyplay 城市旅行者2.0开启纯享音乐...