如今云原生技术可以说是互联网最火的概念之一,作为云原生技术的重要基石 -- “容器” 技术,想必从业者早有学习并大量的运用在工作和日常开发中。
假如有人问你 “什么是容器?”, 你的答案是什么?
容器就是 Docker 吗?它和虚拟化技术有什么区别和联系?它的实现原理又是什么?
这篇文章主要通过 容器和虚拟化技术的对比,容器和 Linux Namespace 的关系,以及 Namespace 的初体验 三个模块让大家更了解什么是容器,以及容器实现的底层技术支撑 -- Liunx Namespace 。
有 Linux 学习经验的同学,想必有使用 VMware 和 VirtualBox 这种软件来安装 Linux 虚拟机的经历。一般是这样的流程,我们先在 windows 机器上安装 VMware 或者 VirtualBox 这类 Hypervisor 虚拟化软件,然后使用这类虚拟化软件将 Linux 的某种发行版镜像安装成为一个 Linux 虚拟机,这样我们就拥有了一个 Linux 操作系统。
这种虚拟化软件是一种运行在基础物理服务器和操作系统之间的中间软件层,可允许多个操作系统和应用共享硬件,通过这种方式我们得到的虚拟机软硬件、内核一应俱全,可以说是功能完备的操作系统,它和宿主机隔离,我们在操作系统里可以放心大胆的做任何的修改,而不用担心对宿主机造成损害,这中资源的隔离型和功能的相似性,和容器表象上可以说是特别的相似。
刚开始接触 Linux 容器的我,习惯性的拿容器和虚拟机对比,认为容器就是一种轻量级的虚拟化技术,只是少了硬件资源的虚拟化,外加运行应用程序所必须的最小的文件系统。不知道有多少人也曾有过和我一样的理解,不得不说这种理解确实对刚接触容器时,理解容器有不少的帮助,但是这种说法却是不妥的,因为容器的实现机制和 Hypervisor 这种虚拟化技术是有本质却别的。
容器本质上只是 Linux 上运行的特殊的进程,之所以说特殊,是因为它和操作系统上的其他进程环境进行了隔离,就像一个集装箱一样,站在容器里面,只能看到它本身。容器技术的基础是 Linux namespace 和 cgroups 内核特性。
隔离的特性确保了不同容器进程互不干扰,拥有各自独立的运行环境。我们知道,在操作系统上 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 来个直观的感受
以下的操作在 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 可以实现 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 的隔离
# 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 可以进行网络的隔离
# 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 展开做一个说明。
今天就先到这里
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.