Linux 容器实现的底层原理是什么?和虚拟化技术一样吗? -- 一个菜鸟的学习笔记分享

2020-11-27 16:13:10 +08:00
 wewin

如今云原生技术可以说是互联网最火的概念之一,作为云原生技术的重要基石 -- “容器” 技术,想必从业者早有学习并大量的运用在工作和日常开发中。

假如有人问你 “什么是容器?”, 你的答案是什么?

容器就是 Docker 吗?它和虚拟化技术有什么区别和联系?它的实现原理又是什么?

这篇文章主要通过 容器和虚拟化技术的对比,容器和 Linux Namespace 的关系,以及 Namespace 的初体验 三个模块让大家更了解什么是容器,以及容器实现的底层技术支撑 -- Liunx Namespace 。

容器和虚拟化

有 Linux 学习经验的同学,想必有使用 VMware 和 VirtualBox 这种软件来安装 Linux 虚拟机的经历。一般是这样的流程,我们先在 windows 机器上安装 VMware 或者 VirtualBox 这类 Hypervisor 虚拟化软件,然后使用这类虚拟化软件将 Linux 的某种发行版镜像安装成为一个 Linux 虚拟机,这样我们就拥有了一个 Linux 操作系统。

这种虚拟化软件是一种运行在基础物理服务器和操作系统之间的中间软件层,可允许多个操作系统和应用共享硬件,通过这种方式我们得到的虚拟机软硬件、内核一应俱全,可以说是功能完备的操作系统,它和宿主机隔离,我们在操作系统里可以放心大胆的做任何的修改,而不用担心对宿主机造成损害,这中资源的隔离型和功能的相似性,和容器表象上可以说是特别的相似。

刚开始接触 Linux 容器的我,习惯性的拿容器和虚拟机对比,认为容器就是一种轻量级的虚拟化技术,只是少了硬件资源的虚拟化,外加运行应用程序所必须的最小的文件系统。不知道有多少人也曾有过和我一样的理解,不得不说这种理解确实对刚接触容器时,理解容器有不少的帮助,但是这种说法却是不妥的,因为容器的实现机制和 Hypervisor 这种虚拟化技术是有本质却别的。

容器和 Namespace

容器本质上只是 Linux 上运行的特殊的进程,之所以说特殊,是因为它和操作系统上的其他进程环境进行了隔离,就像一个集装箱一样,站在容器里面,只能看到它本身。容器技术的基础是 Linux namespacecgroups 内核特性。

隔离的特性确保了不同容器进程互不干扰,拥有各自独立的运行环境。我们知道,在操作系统上 PID 这种东西是唯一的,我们无法拥有一个 PID = 1 的进程 A, 同时拥有一个 PID = 1 的进程 B ;端口号也是唯一的,我们无法让多个进程同时监听同一个端口( fork 子进程的特殊方式除外)。我们也无法让操作系统 hostname 为 server1, 同时让 hostname 为 server2 。但是我们可以在容器 A 里存在一个进程他的 PID = 1, 同时容器 B 里也存在一个进程他的 PID 也等于 1,这就是容器隔离性的体现,它自己就是自己的全世界,开辟任意的空间(自己的文件系统),拥有自己的网络设备,拥有自己的 hostname 。

这种隔离性依靠的就是内核特性 -- Namespace,Namespace 可以让一个进程运行在独立的命名空间中,命名空间里的进程和系统进程相互隔离,不同命名空间中的进程之间相互隔离,所以我们需要了解容器,就需要先了解下 Namespace.

namespace 是内核 2.4 开始有的特性,namespace 一共有如下几种:

|Namespace|隔离的资源| |---|---| |Cgroup|Cgroup root directory| |IPC|System V IPC,POSIX message queues| |Network|网络设备,端口| |Mount|挂载点| |PID|进程 ID| |Time|时钟| |User|用户和用户组 ID| |UTS|主机名、域名|

需要注意的是以上的类型的 namespace 是从内核 2.4 开始逐步加入的,2.4 实现了 mount,2.6 加入了 IPC 、Network 、PID 、和 UTS,User 则是从 2.6 开始出现,但到了 3.8 才宣布完成,Cgroup 则是在 4.6 中才有

上面我们提到的,不同容器中可以拥有相同的 PID,就是因为 PID 这种隔离特性的 namespace, User 和 UTS 这种隔离特性的 namespace 则可以让同一台主机上的不同容器具有相同的 user id 、group id 和 主机名。Namespace 这种内核特性,就是让一组进程运行在一个独立的空间中,让其和操作系统上的进程隔离。

namespace 初体验

相信很多朋友第一次接触 namespace 也是一脸懵逼,没关系,我们可以直接上手对 namespace 来个直观的感受

以下的操作在 ubuntu16.03 ,内核主动升级到了 5.8.12-050812-generic 的系统中完成的

开始之前,需要说明的是 Linux 提供了诸如 clone 、setns 、unshare 、ioctl 这些系统调用 API 来实现对 Namespace 和其进程的操作,不过这是后话,本次初体验我们是使用 Linux 中一个和系统调用函数 unshare 同名的命令行工具(它实际上是调用了系统调用 unshare )

看看它的用法:

# unshare -h

Usage:
 unshare [options] [<program> [<argument>...]]

Run a program with some namespaces unshared from the parent.

Options:
 -m, --mount[=<file>]      unshare mounts namespace
 -u, --uts[=<file>]        unshare UTS namespace (hostname etc)
 -i, --ipc[=<file>]        unshare System V IPC namespace
 -n, --net[=<file>]        unshare network namespace
 -p, --pid[=<file>]        unshare pid namespace
 -U, --user[=<file>]       unshare user namespace
 -C, --cgroup[=<file>]     unshare cgroup namespace
 -f, --fork                fork before launching <program>
     --mount-proc[=<dir>]  mount proc filesystem first (implies --mount)
 -r, --map-root-user       map current user to root (implies --user)
     --propagation slave|shared|private|unchanged
                           modify mount propagation in mount namespace
 -s, --setgroups allow|deny  control the setgroups syscall in user namespaces

 -h, --help                display this help
 -V, --version             display version

UTS Namespace 初体验

UTS Namespace 可以实现 hostname 和 domainname 的隔离

# hostname      # 检查当前的 hostname 是 server0
server0
# unshare -u /bin/sh      # 创建一个新的进程,并处于 UTS namespace 中 
# hostname new-hostname      # 修改 hostname
# hostname      # 当前进程的 hostname 已经改变
new-hostname
# exit      # 退出  UTS namespace
# hostname      # 可以确定原本的 hostname 并未改变
server0

通过上面的例子我们可以看到 UTS Namespace 的隔离特性,它可以让进程拥有自己独立的 hostname,位于 UTS Namespace 中的进程,修改 hostname 不会影响到主机原本的 hostname.

PID Namespace 初体验

PID Namespace 可以实现 PID 的隔离

# ps -aux |wc -l      # 系统原本的进程数
146
# unshare -u /bin/sh     # 没有使用 -p 参数
# ps -aux |wc -l     # UTS Namespace 没有 pid 的隔离
147
# exit
# unshare -fp --mount-proc /bin/bash     # PID Namespace, --mount-proc 选项以确保外壳程序将在新创建的名称空间中获得 PID 1, -f 标志从 unshare 以下位置派生 shell 
# ps -aux |wc -l
4

通过上面的例子,我们特意对比了 UTS 和 PID 两种 namespace,可以更加直观的了解到 PID Namespace 的隔离特性。位于 PID Namespace 中的进程,它可以拥有和主机 PID 重复的进程。

Network Namespace 初体验

Network 可以进行网络的隔离

# ifconfig -a |grep flags  # 原本主机的网卡数量
br-0218842a6d4f: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
br-9a4009fc0074: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
br-ccc197daa6b7: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
cni0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
flannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
vethb960cbb3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
vethe6e2d00a: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1450
# unshare -n /bin/bash # 启动 Network Namespace
# ifconfig -a # 启动 Network Namespace 只有 lo 这种网络设备
lo: flags=8<LOOPBACK>  mtu 65536
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

通过上例我们可以看到,主机的网卡有多个,而位于 Network Namespace 中的进程只有一个 lo 网卡,网络设备和主机的网路设备进行了隔离。

通过以上的例子,相信大家对 Namespace 的隔离特性有了很直观的了解,容器技术的隔离性质正是基于 Namespace 这种内核特性实现,是一种内核级别的特性。

上面通过 unshare 指令我们对 Namespace 有了初步的体验,相信大家对容器的隔离实现也会有了不同的认识。下一个章节,我会用 golang 代码的方式操作 Namespace 。

我们知道容器除了资源的隔离,还有一大特性是资源的限制,其资源的限制则是依赖于 cgroups 这一内核特性,之后的章节我也会对 cgroups 展开做一个说明。

今天就先到这里

3022 次点击
所在节点    Linux
13 条回复
smartyang
2020-11-27 23:37:36 +08:00
还是不知道和虚拟机的区别
SaltyLeo
2020-11-28 00:33:19 +08:00
那到底和虚拟化技术一样吗?

这文不对题啊...
hanguofu
2020-11-28 01:08:33 +08:00
谢谢分享学习笔记。有一个程序必须通过 unix domain socket 和 共享内存与其它程序交互,出于保密的目的,请问哪种 namespace 适合它?
wewin
2020-11-28 08:20:53 +08:00
@smartyang 核心就是虚拟化技术是借助 Hypervisor 这种软件实现的,是一种硬件虚拟化。容器是借助的 Namespace + cgroup 这种 Linux 内核特性,前者做隔离,后者做资源的限制,本质是在隔离环境中运行的一组进程,之后另外写一篇转门说明两者区别的吧。
wewin
2020-11-28 08:23:08 +08:00
@SaltyLeo 容器和虚拟化区别这个话题是挺多内容可以说的,这里说的是比较粗略。重点是想说 Namespace 的,之后可以写一篇转门谈下容器和虚拟化 。
wewin
2020-11-28 08:34:09 +08:00
@hanguofu 我理解的是 unix domain socket 这种的资源的隔离是 Net 这种 Namespace 提供的,共享内存隔离是 IPC 这种 Namespace 提供的,我可能没有特别理解你说的场景,我想的如果你是一组程序之间使用 socket 和 共享内存相互通讯,是不是可以让其都出去 Net 和 IPC 之外的 Namespace 中,就是其他几种都可以考虑开启。场景可以说的更详细下些,一起讨论下。
hanguofu
2020-11-28 12:36:02 +08:00
我希望这个应用程序对于和它交互的程序而言,是完全透明的,不存在的。希望保密的具体内容包括该程序名,该程序的代码,不允许其它程序 dump 该程序在运行时所占据的内存(共享内存除外)。出于保密的目的,namespace 对于该程序而言,是一个好的解决方案吗?
hanguofu
2020-11-28 12:37:30 +08:00
共享内存不需要保密,我也不关心里面的数据。
dream4ever
2020-11-28 23:40:06 +08:00
看到这个话题,想起来今天刚好看到 Stack Overflow 上的一篇讨论: https://stackoverflow.com/questions/16047306/how-is-docker-different-from-a-virtual-machine
kiddingU
2020-11-30 09:45:40 +08:00
容器是宿主机上的一个进程!!!记住这个就行了
julyclyde
2020-11-30 19:54:56 +08:00
@kiddingU 并不一定是一个,甚至有可能都不是一个子树
kiddingU
2020-12-01 09:25:42 +08:00
@julyclyde 容器是宿主机的一个普通进程,这个有啥毛病?万望指点。。
julyclyde
2020-12-01 12:47:29 +08:00
@kiddingU 容器里面可以不止一个进程啊

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/729913

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX