一、Docker 容器并不神秘
二、使用 Docker 构建容器是多余的
三、部署隔离并非新鲜事
四、默认情况下 Docker 对安全性无用
五、应用程序容器太荒谬了
六、Docker 的替代品
Docker 最近非常流行,可惜它不太好。
提前说明:这绝对不是说我认为 Docker 太“有主见”,也不是说其他工具更灵活。我认为,学习和使用 Docker 比学习和使用我下面介绍的工具要复杂得多。Docker 确实比其他工具更复杂、更难用,这些工具也碰巧比 Docker 更灵活,但这不是我推荐它们的原因:我之所以推荐,是因为它们更容易学习和使用。如果它们除了更简单易用之外,还真的更灵活,那也只是因为它们整体设计更出色而已。
一、Docker容器并不神秘
首先,简要介绍下容器的工作原理。Linux 容器[1]建立在两个内核特性之上,即命名空间和 cgroups。它们的架构相当容易理解。
我鼓励大家阅读主要的命名空间手册页:man 7 namespaces。它写得很好,很容易让人理解这个概念。如果你为所有[2]这两个命名空间创建一个新实例,你就得到了一个类似容器的东西。
cgroups 文档(位于Linux 源代码本地副本中的Documentation/cgroups-v1/和Documentation/cgroup-v2.txt )不太直观,但仍然比我能写的解释更好。其基本思想是 cgroups 是一种进程分组机制。此机制用于实现其他系统,如 man 7 cpuset,后者用于跟踪和调度容器进程。
如果你进行相应的系统调用,进入 cpuset cgroup 并创建新的命名空间,你就拥有了一个容器。这并不难。
可以编写一个相对简短的 C 语言程序并创建一个Unix 风格的实用程序,使用这些系统调用来启动一个新的容器。你可以通过使用man 1 nsenter和man 1 unshare亲自了解这一点,它们是命名空间系统调用的最小包装器。
解释这一点的目的是为了表明 Linux 容器功能相当简单。Docker(或任何其他容器软件)在启动容器这一特定领域并没有做任何特别神秘的事情。有了这些知识,让我们看看 Docker 实际上还做了什么。
二、使用 Docker 构建容器是多余的
我们首先看看 Docker 如何为你构建容器镜像。你从 Docker 中心下载某种镜像,Docker 会兴奋地运行一会儿,同时你会看到滚动的内容和进度条填充。最终,你会得到一个来自某个 Linux 发行版的文件系统树,其顶部还添加了一些内容。
有些人可能会感到惊讶,我们这样做已经有几十年。
事实上,每次在机器上安装 GNU/Linux 时,我们都会这样做。该文件系统树中的大多数文件来自某个发行版的软件包。软件包管理器当然能够将软件包安装到任意目录中;这就是它们安装新系统的方式。
事实上,大多数软件包管理器甚至有整洁的小封装脚本来帮你安装!而这些只需要安装apt-get install debootstrap(或类似软件)即可!为最流行的几个发行版[3]构建文件系统树:
debootstrap focal /srv/trees/ubuntu
debootstrap stable /srv/trees/debian
dnf -y --releasever=33 --installroot=/var/lib/machines/f33 --disablerepo='*' --enablerepo=fedora --enablerepo=updates install systemd passwd dnf fedora-release vim-minimal glibc-minimal-langpack
pacstrap /srv/trees/arch
当然,你也可以使用这些命令选择要安装的其他软件包,或进行其他更改。几十年来,人们一直使用这种方法来构建 chroot,稍后我会详细介绍。还有更多新颖的软件包管理器,如 Nix 和 Guix,它们具有有趣的功能,可以让事情变得更加简单。
但是等等,node.js 的发行版太旧了!我该如何获取最新版本?
如果你需要更新版本,首先要做的就是启用发行版的更新软件包仓库:Ubuntu backports、Debian backports、Fedora EPEL。软件包管理器不仅仅是摆设;它们确实可以让你的系统保持最新状态,还有许多其他优点[4]。
如果无法通过发行版渠道获得合适的更新版本,我认为的下一个最佳选择是更新发行版软件包,这可能相当容易,具体取决于你的发行版。或者,如果没有可用的软件包,你可以自己创建一个。如果你处于早期开发阶段,这可能会有点麻烦(尽管有一些工具可以让这变得更容易[5]),但同样也会有很多好处[4]。
不过,大多数人还是使用传统的黑客手段。你可以通过 chroot 进入,然后像使用 Dockerfile 中的一些 "RUN "指令一样,执行通常的 pip install foo 或 gem install bar 或 npm install baz 或 ./configure && make && make install。
所以至少在这里,Docker 没有真正的优势。
最重要的是,你可以使用与普通 Linux 机器相同的安装脚本,不需要将所有内容重写到 Dockerfile 中。你可以手动安装,可以使用 shell 脚本,可以使用 Ansible,可以用 Java 编写精致的 ConfigurationManagementFactory,想做什么都行。这只是安装软件而已,这并不复杂,除非你把它弄复杂。据说,Dockerfile 比debootstrap在脚本开头运行更简单,但我不确定我是否理解为什么。在我看来,Docker 并不比标准方式更简单或更容易。
现在,Docker 确实使用分层技术,提高构建新容器的磁盘空间和时间效率。它默认使用OverlayFS来执行此操作[6]。你可以使用一个小的 shell 脚本和一些 mount 调用轻松地自己重新实现它,但没有必要。
相反,我只使用man 8 btrfs-subvolume。btrfs 是一种写入复制(copy on write)文件系统,它可以在“子卷(subvolume)”中即时创建节省空间的文件系统树副本,用户看到的只是普通目录。
你可以使用 btrfs subvolume create /srv/trees/ubuntu && debootstrap focal /srv/trees/ubuntu/, 将现有的 Ubuntu 文件系统树复制到子卷中 。然后,当你想要使用特定软件构建新容器时,只需复制该子卷,并在副本上执行修改;也就是说,btrfs subvolume snapshot /srv/trees/ubuntu /srv/containers/webapp,继续进行操作/srv/containers/webapp。如果要复制这些修改,再拍一次快照即可。
这实际上比 OverlayFS 更好,因为无需维护大量有关挂载层的状态,也无需在重启时重新设置它们。你的容器文件系统只是位于卷中,等待你去启动。
当然,如果你出于某种原因不喜欢 btrfs,你完全可以使用 zfs、OverlayFS、AUFS 或其他任何文件系统;没必要为了进行简单的写入复制或分层操作而实施“存储驱动程序”。
如果你想在构建系统时进行某种更改跟踪,应将其保存在适当的层,或者使用专用工具。/usr应该是不可变的,并由软件包构建,你的应用程序数据应该位于/srv或/var中,并被挂载在其中,因此作为系统构建过程中的所有配置数据都应该保存在/etc中。要跟踪这些数据,只需使用 etckeeper 并将/etc保存在 git 存储库中。这是正确和恰当的,因为/usr应该是不可变的。如有必要,OSTree 可让你对整个文件系统进行版本控制。
如果出于某种原因,你仍需要提取 Docker 镜像,可以将其视为构建文件系统树的另一种方式。有一些工具可以帮你做到这一点,例如 machinectl pull-dkr。
三、部署隔离并非新鲜事
但是等等!Docker 不仅仅是构建文件系统树这一简单任务上,添加了一个毫无意义的抽象层!它能让你可以在容器中实际使用这些文件系统树!
不过,这并不是什么新鲜事。正如我之前所说,这些工具被用来构建 chroot 已经有几十年的历史了。
什么是 chroot?man 1 chroot是一个已有几十年历史的工具,它可以让你更改根目录/指向的内容;例如,你可以 point/at/srv/container/webapp,所有程序都在根目录的子目录中查找库和二进制文件,例如/usr/lib和/usr/bin。因此,通过使用 chroot,你可以拥有一组完全不同的库和二进制文件;当你在 chroot 中运行程序时,它们将只看到你在该文件系统树中安装的库和软件。
为了更好解释 chroot 的用途,这里有我“写”的一篇关于 chroot 的小短文。
系统管理员使用 chroot 为开发、QA 和生产团队提供标准化环境,从而减少“在我的机器上工作”的相互指责。通过对应用平台及其依赖项进行“chroot”,系统管理员可以消除操作系统发行版和底层基础架构之间的差异。
这听起来确实很有用。但等等,Docker 又出现了。让我们看看他们怎么说。
系统管理员使用 Docker 为他们的开发、QA 和生产团队提供标准化环境,从而减少“在我的机器上工作”的相互指责。通过“Docker 化”应用平台及其依赖项,系统管理员可以抽象出操作系统发行版和底层基础架构之间的差异。
Docker 提供这些功能并不是什么新鲜事。不过,他们如此大张旗鼓地营销,倒是很有新意。
四、默认情况下 Docker 对安全性无用
但是等等!Docker 是“容器”,新颖、奇特、令人兴奋。chroot 既陈旧又无趣。容器肯定比 chroot 好!
好吧,chroot 虽老旧且无趣,但确实有优势,比如“它不会随机地坏掉”。但可以肯定的是,容器确实有其自身的显著优势。
举个例子:chroots 的安全并不可靠的,如果在 chroot 中以 root 身份运行,很容易就会被破解。容器特别安全,独一无二,对吗?
错了!对于大多数用途来说,Docker 容器提供的主要有趣功能是隔离网络。也就是说,Docker 容器会阻止容器内的应用程序绑定外部网络接口上的端口。还有什么能阻止应用程序使用端口?是服务器上安装的防火墙。同样,这又是一个毫无意义的抽象,用来解决已经解决的问题。
事实上,如果你遵循疯狂的默认做法,以 root 身份在容器中运行应用程序,那么你的系统的安全性可能会大大低于正确实施的 chroot。
从非特权 chroot 中突破取决于一个众所周知且研究充分的漏洞领域:Linux 特权升级。Linux 命名空间容器带来了全新的安全问题;它们很可能存在固有漏洞,内核无法在不破坏未包含功能的情况下纠正这些漏洞。事实上,Docker 自己的开发人员也热情地承认,Docker 目前还不能以 root 身份安全地运行代码。几十年来,人们一直在 chroot 中以非特权用户身份运行应用程序,以减少这种威胁。默认情况下,Docker 并不会这样做。
五、应用程序容器太荒谬了
不过,容器还是很酷的,对吧?只有随着命名空间和 cgroups 的发展,Docker 才能最终正确使用“应用容器”。与 chroot 相比,Docker 带来的隔离功能大大增强了功能;我们终于可以在生产中部署 "应用容器 "了。我们终于可以通过运送整个文件系统来实现应用程序的主机独立性!对吧?
对于那些不了解术语的人来说,Docker 将其容器方法称为“应用容器”。基本思想是,你拥有所有这些命名空间和 cgroup,然后创建一个容器,然后在容器内运行单个软件。我想,这很酷。另一种方法是在容器内运行 init 系统,这将启动一个完整的“传统”操作系统。容器提供足够的隔离性,因此你可以把它们视为非常轻量级的虚拟机。Docker 反对这种做法,因为……
好吧,我其实不确定 Docker 开发人员是怎么想的。难道是为了让容器更 "轻量级",不把它们当做虚拟机,而是运行一个 init 系统?难道他们只是想到可以在容器内运行单个服务,而不是运行完整的系统,却从未想过这是否是个好主意?
“应用容器”的实际问题众所周知。僵尸孤儿进程[7]会填满你的容器并消耗资源,却没有启动程序来获取资源;传统的 cron 和 syslog 守护进程不会自动可用;等等。这些都是问题,但如果我们编写了足够多的新软件,使应用程序容器运行良好,就一定能克服这些问题。
更根本的问题是,“应用容器”并不意味着什么。我们已经解决了文件系统隔离方面的问题;我们知道,没有Docker,没有容器,我们也能做到这点,那么什么是“应用容器”呢?
它只是另一个系统服务!只是另一个守护进程!所以如果你想要隔离一个服务,那就这么做吧!没有必要把它称为“容器”,以免混淆术语。
就像其他人一样,只需使用 Linux 命名空间功能即可隔离你的应用程序。几十年来,我们一直使用 chroot 和 su 来保护和隔离应用程序;命名空间和 cgroups 只是这个工具箱中的另一个工具。我将以 systemd (它是将这些技术用于系统服务的先驱)为例,但 sysvinit 和其他 init 系统也可以同样轻松地使用命名空间和 cgroups 进行隔离。
由此可见,应用容器的概念显然并没有什么特别新颖之处。当然也没有什么值得采用 Docker 的全新方法,因为它抛弃了大量现有的 GNU/Linux 堆栈!
六、Docker 的替代品
我想我已经深入介绍了 Docker 各个部分的替代方案。还有一点要说,我在第一部分提到,一个简单的、Unix 风格的实用程序可以提供容器化功能,其模式与 chroot 类似。我的感觉是man 1 systemd-nspawn 就是这个实用程序。它的手册页甚至明确将其与 chroot 进行了比较:
systemd-nspawn 可用于在轻量级命名空间容器中运行命令或操作系统。它在很多方面与 chroot(1) 类似,但功能更强大,因为它完全虚拟化了文件系统层次结构、进程树、各种 IPC 子系统以及主机和域名。
而且每个 systemd 系统上都有该功能,因此很容易上手。请查看手册页中的示例debootstrap。将它与 GNU/Linux 生态系统的其他部分(如 debootstrap 和 btrfs)相结合,你可以获得具有 Docker 的所有功能甚至更多[7]的功能,而无需复杂性开销。最终,Docker 相对于其提供的简单功能来说太复杂了;根本就不需要它。
脚注
[1] 当然,容器(或更广泛地说是“操作系统级虚拟化”) 的使用并不是特别新奇。多年以来,Solaris 一直拥有区域,FreeBSD 拥有监狱,其他操作系统也拥有其他此类技术。这些都是针对其各自操作系统的完善且有效的解决方案。(至少,我认为是这样,从他们的拥护者如何吹嘘它们来判断)事实上,即使在 Linux 中也有Linux-VServer和OpenVZ。Linux 容器(或“基于命名空间的容器”)的主要区别在于它实际上包含在上游 Linux 内核中。Linux-VServer 和 OpenVZ 是“树外”补丁集,与主内核项目分开维护,并作为补丁应用于 vanilla 内核以添加各自的功能。这极大地增加了维护负担并降低了代码的清洁度,事实上这两个项目现在都已经过时,无法使用。另一方面,命名空间和 cgroup 存在于主 Linux 源代码树中,内核开发策略意味着它们将与 Linux 代码库的任何未来变化保持同步。因此,所有将容器化引入 Linux 的进一步尝试似乎都将使用这些技术作为基础。
[2] 用户命名空间对于保护容器很有用,但可以说仍在开发中;Docker 未实现它们,许多其他容器工具也没有实现它们。我相信 LXC 是唯一实现它们的主流容器工具。我听说用户命名空间有点奇怪,与其他命名空间不同;例如,它们可以在没有特权的情况下使用,并且它们允许你“假装”拥有能力。谁知道这会引入什么新的安全漏洞?
[3] 这些例子取自这里。
https://freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#Examples
[4] 当你需要升级、广泛部署软件或安装多个自定义库时,包管理器可以帮你省去很多工作。这里,请看这个页面。
https://fedoraproject.org/wiki/Package_management_system#Advantages_of_package_management_systems
[5] Checkinstall和fpm是快速构建软件包的工具,适合不关心软件包管理的新手。当然,在某个时候,你真的应该学习如何直接构建自己喜欢的格式(rpm 或 deb)的软件包。
[6] Docker 还支持 btrfs 和 Linux 设备映射器来实现分层。
[7] Unix 类操作系统上的进程被组织成一个层次结构;一个正常进程将有一个父进程和零个或多个子进程。当任何进程终止时,它依赖于其父进程等待它;在此之前,终止的进程被称为“僵尸”。关于孤儿进程,来自Wikipedia:
“孤立进程是指其父进程已完成或终止但其自身仍在运行的计算机进程。在类 Unix 操作系统中,任何孤儿进程都会立即被特殊的 init 系统进程收养。此操作称为重新指定父进程,会自动执行。尽管从技术上讲,该进程以“init”进程为父进程,但它仍被称为孤儿进程,因为最初创建它的进程已不存在。”
因此,如果 pid 1 没有wait(2)终止(“僵尸”)孤儿进程,它将永远存在。创建将由 init 清理的孤儿进程是一种相当常见的 Unix 编程习惯,因此这是一个相当严重的问题。请参阅Phusion baseimage以了解该问题的另一种解释,以及一些为解决 Docker 的这个问题而编写的软件。
[8] 用户空间检查点和恢复 ( CRIU ) 允许“冻结”和恢复 Linux 进程,并支持各种有趣的应用程序。Docker尚不支持 CRIU。其他软件(如LXC)通过使用 CRIU 完全支持实时迁移。
*本文为dbaplus社群编译整理,如需转载请取得授权并标明出处!欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721