去年中在 Hacker News 上有篇热帖 “Goodbye Docker: Purging is Such Sweet Sorrow” 这篇文章内容其实很常规,无非是自己使用 Docker 的时候遇到了一些问题,最后切换到了 Podman buildahSkopeo 组合的工具集,以作为 Docker 的一种替代方案。

这样的文章在近一年左右应该算是比较常见了,但为何会成为 Hacker News 上的热帖呢?主要有两方面的原因: 1. 还是因为此文的作者是 Ian Miell 他是 Docker in Practice (中译本叫做 《Docker 实践》)的作者,一个 Docker 相关技术书籍的作者将自己的 Docker 给替换掉,会让人比较好奇;2. 替换 Docker 貌似是一种方向,多数人也想要了解这种技术变迁会带来什么优势或者有什么样的坑。

背景介绍完了,我们来开始正文。

容器时代

Docker 在 2013 年 PyCon 上首次亮相,随后开源。由于其简单易用,以及切实解决了因环境不一致导致的问题,迅速获得到一大批粉丝。

接下来的几年中,Docker 改变了软件的交付方式,更多的人为之着迷。随之而来的是 Docker 生态的蓬勃发展。

Docker 在大多数人眼中几乎是容器(container)的代名词,即使是现在我也会常听到有人说“我有几个 docker 跑 xx 服务” 类似这样的话,无疑 Docker 引领了容器的时代

容器是什么

一直在提容器,我们不如深入点先来探究下容器到底是什么?

Docker 官网上对容器的描述是: “A standardized unit of software” – 软件的标准单元,并没有什么更详细的内容了。多数人对容器的看法也都停留在很浅显的认识:认为容器是轻量级的虚拟机,所以后来也就有一段时间有人推“富容器”技术。

这里我想更深入一些,可能会涉及一些容器的历史,但我认为这些内容有助于读者理解我的角度和观点。

说白了 容器其实是在某台机器上的“一组”进程,当然这组进程可能只有一个;它们有相同的特性,当然所受的限制也是相同的;既然叫做容器,很自然的我们认为它们与外界可以进行隔离/应该有一个分界线。

谈完了它的基本特性,那我们来看看如何来创建一个容器。

chroot

如果你对 Linux 相对熟悉的话,你可能知道 chroot ,我们有时会使用 chroot 改变某进程的根目录,并且它不能访问该目录之外的其他目录。

这个和我们在一个容器内的感觉很像了。事实上在几年前确实有人用一百多行的 bash shell 利用 chroot 写了一个模拟 Docker 创建容器的实现,称之为 bocker , 有兴趣的朋友可以去看看该项目的代码。

接下来我们使用 chroot 创建一个隔离环境:

首先它需要有一个 rootfs 的根文件系统,我们可以很简单的使用 Docker 获得我们需要的内容。

(MoeLove) ➜  ~ mkdir chroot-dir
(MoeLove) ➜  ~ cd chroot-dir
(MoeLove) ➜  chroot-dir docker save -o debian.tar debian:buster                                          
(MoeLove) ➜  chroot-dir ls
debian.tar
(MoeLove) ➜  chroot-dir tar -xf debian.tar 
(MoeLove) ➜  chroot-dir ls
098963abf3c3b87b8114ff67d164097dfac2d5659e39f9beb5604db91585f375.json  debian.tar     repositories
0f28619fe69181d3af529d56692f1362b7a7c8a6bf8dc9ab0d6d4f9ef9b0004d       manifest.json
(MoeLove) ➜  chroot-dir mkdir -p debian
(MoeLove) ➜  chroot-dir tar -C debian -xf 0f28619fe69181d3af529d56692f1362b7a7c8a6bf8dc9ab0d6d4f9ef9b0004d/layer.tar  
(MoeLove) ➜  chroot-dir ls debian
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

可以看到我们通过对镜像文件的解压,得到了我们所需的 rootfs 的全部内容,接下来我们看看 chroot 的能力:

(MoeLove) ➜  chroot sudo chroot debian /bin/bash -i
[sudo] tao 的密码:
root@localhost:/# whoami 
root
root@localhost:/# cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

我们目前就已经在一个 “容器” 内了,我们来看下使用这个容器我们能做些什么。

首先我们看看当前“容器”内的路由表:

root@localhost:/# mkdir -p /sys
root@localhost:/# mount -t sysfs sys /sys
root@localhost:/# ip r
default via 192.168.0.1 dev wlp2s0 proto dhcp metric 600 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
192.168.0.0/24 dev wlp2s0 proto kernel scope link src 192.168.0.108 metric 600 
192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1 linkdown

很自然的,我们发现它可以访问所有的网络设备

root@localhost:/# ls /sys/class/net/
docker0  enp1s0  lo  vboxnet0  virbr0  virbr0-nic  wlp2s0

另外,我们还可以将 /proc 也挂载进去

root@localhost:/# mkdir -p /proc
root@localhost:/# mount -t proc proc /proc
root@localhost:/# ls -al  /proc/31730/ns/pid
lrwxrwxrwx. 1 1000 1000 0 Jul 29 16:47 /proc/31730/ns/pid -> 'pid:[4026531836]'

可以看到我们在这个“容器”内,可以访问到主机上的进程及网络等信息,这表示没有任何的进程或者网络隔离,这带来的危险是我们甚至可以在容器内杀掉容器外的进程,或者通过容器来攻击主机。

为了能更好的解决这个问题,接下来出现了另一个技术:namespace 。

namespace

Namespace 是在 2002 年由 Linux 2.4.19 开始加入内核的特性,它主要的作用就做了一层抽象和隔离,使得在 namespace 中的进程/进程组可以看起来拥有自己的独立资源,具体的“资源”表现形式取决于给它赋予了哪些 namespace 。

随着 2013 年 Linux 3.8 中 user namespace 的引入,对于我们现在所熟知的容器所需的全部 namespace 就都实现了:mnt pid net ipc uts usercgroup 对于这些 namespace 和 Docker 的关系,我们稍后会逐步来看,这里先对 namespace 做下介绍,然后继续前面”容器”的内容。

我们可以通过三个系统调用直接操作 namespace ,这三个系统调用分别是:

  • clone: 可以通过传递不同 namespace 的标志来为新的(子)进程指定其所属的 namespace;
  • unshare: 允许一个进程(或线程)取消当前与其他进程(或线程)共享的执行上下文;
  • setns: 进入文件描述符指定的 namespace;

知道了这些基础知识后,我们回到前面”容器”的内容中。

我们在前面 chroot 的例子中看到没能做到进程或网络隔离,现在我们来试试看用 namespace 完成该需求。

(MoeLove) ➜  ~ sudo unshare -fp --mount-proc -n
[sudo] tao 的密码:
[root@localhost]/home/tao# ip l
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
[root@localhost]/home/tao# ps -a
  PID TTY          TIME CMD
    1 pts/15   00:00:00 zsh
   33 pts/15   00:00:00 ps
[root@localhost]/home/tao# 

这里很明显,我们当前所在进程的 PID 为 1 并且看不到宿主机上的其他进程,我们达到了基础的隔离效果。网络也同样,现在只包含一个 lo 接口。关于网络部分的内容其实能聊的东西还有很多,我在专栏《Docker 核心知识必知必会》中,用 8 节内容来深入的聊了聊 Docker 容器网络相关的内容,感兴趣的读者可以看看。

小结

上面聊了这么多,无非是想说明容器的发展历程,以及它是什么。它其实就是用了各种 Linux 内核提供的特性/功能协同实现的对进程组的资源隔离。用 Docker 或者 namespace 或者 chroot 之类的都可以造出来一个 “容器” ,那我们为什么要用 Docker 呢?

Docker 崛起之路

想必大家对 cgroups 不会太陌生,它在 2008 年进入 Linux 2.6.24 后,基于它并且瞄准容器世界的一个项目诞生了。

Linux Container (LXC)结合了 namespace 和 cgroups 等技术,目标就是要创造出运行在 Linux 系统中,并且隔离性良好的容器环境。

LXC 的事情发生在 2008 年,但值得注意的是 cgroups 最初是由 Google 的工程师开发的,最早的记录是在 2006 年,事实上当时 Google 确实也在做类似的容器化项目。

时间一晃而过,就到了 2013 年的 PyCon 上,在这次大会上 Docker 正式面世。而它当时其实也只是构建在 LXC 之上的一个工具,屏蔽掉了 LXC 的使用细节,让用户可以一句 docker run 便创建出自己的容器环境。 同时,它允许用户将容器环境打包成为一个 Docker 镜像进行分发,这也大大降低了用户使用的门槛。 Docker 镜像分发可以说是 Docker 成功的一个关键要素了。

2014 年 Docker 发布 1.0 正式进入生产就绪的状态。在此之前它也将 LXC 逐步从它的底层移除,换成了自己实现的 libcontainer ,幸运的是我也在 0.9 版本时开始了我的 Docker 之路。

此后 Docker 便成为了风靡技术界的新热潮。

了解了 Docker 的发展背景后,我们来看看前面我们提到的问题:为什么要用 Docker 呢?

2008 年到 2013 年这之间大约 5 年左右的时间,以 LXC 为首的容器技术并没有得到类似 Docker 出现后那么广泛的普及,我在之前的线下演讲中也提到过这个点,最主要的原因在于 LXC 太偏向技术了,使用 LXC 有一定的门槛,导致了好多人的退却。

Docker 则提供了简单易用的 CLI , 优雅灵活的容器生命周期管理,以及镜像的构建,分发等配套设施,这为后期的推进提供了很多的便利。

再加上 Docker 的策略很好,以及在 Docker 公司内部也有大量的实践经验,所以这些事情做起来也都很顺畅了。(关于 Docker 公司的实践经验可以说是信息量巨大了,以后再看机会分享吧)

使用 Docker 面临的问题

前面分别聊了容器的发展历程,以及 Docker 的发展历程。我们要正视 Docker 是一个已经 7 岁的项目,自它 1.0 发布以来已经 6 年之久了,它在生产环境中已经得到了大量的实践和验证。当然不可避免的它也会存在一些历史遗留问题或者是软件 bug 。

尤其是随着 K8S 的发展,国内外大量公司都在落地 K8S。其中有超过半数的公司都是使用 Docker 作为容器运行的。

当发生故障或者异常时,有些人可能会束手无策,或是只从上层进行问题的排查,殊不知问题很可能发生在底层的容器运行时。比较典型的例子:比如说容器 Hang 住,或者是无法创建容器等。

或者有人由于对 Docker 网络相关的方面不了解,所以在学习和使用 K8S 时,也会走不少的弯路。比较典型的例子:不理解 K8S 中数据包的流向。

最后一个很重要的方面就是安全相关的了。在企业中使用 Docker 尤其需要注意。这里包含着很多信息:比如容器运行时安全,镜像安全,容器策略安全等,我在专栏的安全篇也都进行了介绍。

总结

Docker 的上手使用非常简单,这是 Docker 的一大优势,旨在降低开发者的使用门槛。尤其是 Docker Desktop 提供了交互式 UI,用户可以通过鼠标点击就完成容器的相关操作和管理。

但是 Docker 作为一个正在被大量使用,且往后会被应用更多的技术,如果要用在生产环境中,我建议读者去更深入的学习和掌握它。

以便在遇到问题时,可以更快速的定位和解决;构建镜像时,可以更加高效;使用 Docker 时,能为企业提供更适宜的安全策略。

当然,还有一个最重要的,当你对 Docker 的了解越深入时,你也会越开心,你能接触到更多有趣的知识和技能。