Docker 原理剖析(一)隔离

引子

我们之前说过,容器是一种沙盒技术,通俗地说,容器可以像一个集装箱一样把我们的应用装起来。这样,应用与应用之间便有了边界,不会互相干扰,被放进集装箱的应用也可以被我们搬来搬去,完美!那么,你可知道这功能是如何实现的呢?今天,我们便来聊聊边界的实现手段。

手段

我们先下结论,容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个边界。

对于一个进程来说,它的静态表现就是程序,平时就呆在磁盘上。当它运行起来了,就变成了计算机里的数据和状态的总和,包括计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合,这就是它的动态表现。

对于 Docker 来说,Cgroups 是制造约束的主要手段,而 Namespace 是用来修改进程视图的主要方法。

测试

我们执行以下指令:

docker run -it centos /bin/bash

该命令以交互形式启动了一个容器,并在容器中执行了 /bin/bash,这样,我的机器变成了一台宿主机,而运行着 /bin/bash 的容器在我的宿主机里面。

我们可以在容器中执行 ps 指令
在这里插入图片描述
易见,我们在 Docker 里最开始执行的 /bin/bash,是这个容器内部的第 1 号进程(PID = 1),而这个容器里一共只有两个进程在运行。可以知道,之前执行的 /bin/bash,和现在执行的 ps,已经被 Docker 隔离在了一个跟宿主机完全不同的世界当中。

这是怎么做到的?

原理

当我们在宿主机上运行了 /bin/bash 时,操作系统会给它分配一个进程编号,例如 PID=100,这是进程的唯一标识。

现在,我们要通过 Docker 把这个 /bin/bash 程序运行在一个容器当中,Docker 会给它施展一个障眼法,让它看不见前面的进程,这样,它便会错误地认为自己是第一个进程。

这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如 PID=1。可实际上,他们在宿主机的操作系统里,还是原来的第 100 号进程。

这种技术,就是 Linux 里面的 Namespace 机制。

如果我们创建了多个 PID Namespace,每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。

除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行障眼法操作。例如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。

总结一下,Docker 容器实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能看到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。所以说,容器只是一个特殊的进程罢了。

docker exec 是怎么进入容器里的呢?

实际上,Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。

首先,我们先查询 Docker 容器的进程号为25686。

然后,可以通过查看宿主机的 proc 文件,看到这个进程的所有 Namespace 对应的文件:

ls -l /proc/25686/ns
lrwxrwxrwx 1 root root 0 Aug 13 14:05 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 ipc -> ipc:[4026532278]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 mnt -> mnt:[4026532276]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 net -> net:[4026532281]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid -> pid:[4026532279]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 pid_for_children -> pid:[4026532279]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Aug 13 14:05 uts -> uts:[4026532277]

可以看到,一个进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。

也就是说,一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到进入这个进程所在容器的目的,这正是 docker exec 的实现原理。

Docker 与虚拟机的本质区别

如果我们想使用虚拟化技术作为应用沙盒,必须要创建一个真实存在的虚拟机,并且它里面必须运行一个完整的 Guest OS 才能执行用户的应用进程。这就不可避免地带来了额外的资源消耗和占用。

相比较,容器化后的用户应用,却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的;而另一方面,使用 Namespace 作为隔离手段的容器并不需要单独的 Guest OS,这就使得容器额外的资源占用几乎可以忽略不计。

相比于虚拟机,容器更为敏捷,更为高性能。

Namespace 的问题

基于 Linux Namespace 的隔离机制相比于虚拟化技术,存在一个严重缺陷:隔离得不彻底。

由于容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。如果你要在 Windows 宿主机上运行 Linux 容器,或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器,都是行不通的。

另外,在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。如果你在容器中修改了时间,那么整个宿主机的时间都会被随之修改,这显然不符合用户的预期。

还有,由于上述问题,尤其是共享宿主机内核的事实,容器给应用暴露出来的攻击面是相当大的。

参考:05 | 白话容器基础(一):从进程说开去

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值