【译】使用 Docker 地 5 大心得

@zen
2016年9月29日

前言#

在近一年多的时间里,我在多个平台上都频繁地使用了 Docker (包括本地 Linux 和云服务)。也因此学会了如何管理我的镜像,如何构建跨平台的弹性镜像,甚至于如何编写“非 Docker 特定”的程序。最终,我整理了五条便于理解的心得,以便于后继的新 Docker (甚至不是 Docker)的项目。

1. 明晰细节(以及一点点偏执)才能动手构建镜像#

不用使用 root 帐号来跑应用程序。绝大多数的服务软件包都会在安装时创建一个相应的系统帐号。比如 Apache 在安装时,会创建名为 http 或 apache 或 www-data 的系统帐号。

我之前有这样一个项目,通过源代码编译 ejabberd ,使用构建脚本来创建名为 xmpp 的系统帐号,使用 Docker 的 Automated Builds 服务来确保最终镜像里的 ubuntu 是最新的。

但期间有一个低级错误,我直接用 FROM ubuntu 而非 FROM ubuntu:12.04 。因此有一天,最终镜像里的 Ubuntu 升级到了 14.04 的版本(新增了一个系统帐号),因而 xmpp 系统帐号 UID 也变了。使用这个版本的镜像启动容器时,外部卷因为 UID 不一致就无法读写了。

这教育了我两个道理:

  1. 创建任何 Docker 镜像时都应该指定基础镜像的版本;
  2. 任何情况下都应该有一份启动脚本来自我矫正。

启动脚本以 root 帐号启动并完成如下工作:

  • 确保必要的配置文件存在——没准外部卷挂载失误就导致配置丢失了;
  • chown 相应的目录和文件确保可以被正确地读写。

最终我因此而节约了大量的时间。我对基础镜像十分了解,我也通过启动脚本确保了环境是可靠的(而非直接将主程序硬上 ENTRYPOINT)。

2. 我们无从了解他人系统的工作机制#

直到现在,我仍然只使用最新版本的 Ubuntu 作为基础镜像。因为在使用了一些所谓的云服务之后,我学到了以下几点:

  • 不是每个人都会运行最新版本的 Docker;
  • 不同版本的 Docker 提供的功能是不一样的;
  • 大多数人都没有 Docker 服务环境的 root 权限。

这极大地改变了我构建 Docker 镜像时出习惯——我不再在说明里写“启动容器时请添加选项 --volumes-from”,或者“需要一个名为 DB 的关联容器”——因为我不知道其他人会在什么环境下使用我的镜像。我开始尽可能地让镜像变得更弹性化。如果镜像需要 MySQL 数据库,那么优先使用关联容器,其次是用于指明链接信息的环境变量,等等。

这会增加很多前置工作,但我觉得是有价值的。比如使用一个 MySQL 代理容器来链接到真实的 MySQL 数据库,然后使用 link 选项将主容器关联上去。这很有用!

3. 写 Dockerfile 很折腾,但之后能省很多事#

要创建 Docker 镜像有两种方法。一是创建一个基础容器,然后在其中通过命令交互来完善。当容器能够如我预期的方式运转时,通过打标签(tag)地方式来生成镜像。

这种方式相对更简单。我可以通过安装包提供的向导工具完成配置,然后使用顺手的编辑器来修改配置。

另一种方式是 Dockerfile,刚开始是会让人非常不习惯。如果安装包有交互提示时会发生什么?如何通过命令来编辑文件?这个过程很微妙,所有的操作都是自动完成的,无法人工干预。

我的个人体会,是优大于劣。我已经无数次地发现:在我手工配置好镜像后,我忘记了我究竟干过什么。而 Dockerfile 可以将其中的过程精确地展示给所有人。而且基于此,我还可以通过版本控制系统来进一步的管理 Dockerfile 。是的,我们需要做更多的工作才能用 Dockerfile 来构建镜像,但我现在只用这种方法了。

4. 不管是不是用 Docker,派生进程都需要慎重#

应用程序在执行时会派生子进程,这种事情并不稀奇。我自己的程序也尝尝会做这样的事情。在大多数的系统里,我可以派生进程,读取输出内容,检查结束状态,或者什么别的。初始化系统会在这些进程结束时自动清理。多年以来,我几乎都是下意识地就会这么做。

但很多情况下,Docker 容器并不会跑任何的初始化系统,因此派生进程会最终变成“僵尸进程”,继续占据系统资源无法释放。我知道该如何在程序中有效地监视并销毁一个进程,但那些在他们的 Docker 镜像里使用我的程序的人并不知道。我们永远也不知道我们的程序会在什么情况下被使用。

5. 一个容器一个任务并不等同于一个容器一个进程#

这是一个有争议的观念。如果在 Freenode 的 #docker 频道里,咨询关于容器内部进程管理地问题,我们会得到各种迥异的回答。基本上每个人都赞同一个容器应该只执行一个任务,但这是否意味着就应该是一个进程,众说纷纭。

但当我明确什么是任务,和任务需要哪些程序之后,我认为使用进程管理器(如 supervisord、runit 或 s6)完全是可以接受的。对于 web 应用程序来说,这尤为必要,因为我们经常会需要为 Apache 或 Nginx 编写特定的 rewrite 规则。

比如,某套 web 应用程序需要 PHP-FPM、Nginx、Cron 和 MySQL。那么我会在一个容器里运行 PHP-FPM 、Nginx 和 Cron,在另一个容器里运行 MySQL 。当某个关键进程结束或崩溃时,进程管理器能够干净地清理然后重启相关进程。这就避免了在主进程结束时 Docker 容器也会停止地尴尬。

结语#

我真诚地希望这些经验可以帮助你们更好的使用 Docker。基于容器的工作方式改变了我构建软件的习惯,而不同 Docker 平台又让我改变了构建容器的习惯。

如果你也有使用 Docker 的经验,我很乐意向你学习!你可以使用下面的评论功能来分享你的故事和心得,或者其它的反馈方式。感谢阅读!


点击这里阅读原文。