Systemd 对垒 Docker

2016-03-07 08:22


DevConf.cz 是一个由红帽赞助的开发者大会,今年在捷克共和国布尔诺举行。会议上有很多不同的演讲,但最大的主题是容器。大部分演讲是实践性质的,要么是展示如何使用各种容器技术的教程,比如 Kubernetes 和 Atomic.app,要么是对新产品的介绍,比如 Cockpit

不过,红帽容器工程主管 Dan Walsh 所做的演讲无疑是容器主题中最有趣的。他展示的是 Linux 容器世界核心冲突之一:systemd 对垒 Docker 守护进程。这绝非是一个新问题;在 Ubuntu 采用 systemd,以及 CoreOS 引入围绕 systemd 构建的容器系统 Rocket 时,这个问题就出现了。 

Systemd vs. Docker

“这是 Lennart Poettering,”Walsh 一边展示一张照片一边说。“这是 Solomon Hykes”,他展示了另一张照片。“两个人都不愿意妥协。而我则介于他们之间。”

由于 Walsh 曾受命让 systemd 支持 Docker,他详述了两个系统间代码、个人及运营冲突的历史。从很多方面来说,这也是红帽与 Docker 公司之间补丁冲突的历史。Poettering 是 systemd 的主要作者,并在红帽工作,而 Hykes 则是 Docker 公司的创始人和 CTO。

依据 Walsh 的演讲,冲突的根源在于 Docker 守护进程被设计用来接管很多 systemd 已经从 Linux 中抢夺过来的功能。包括初始化、服务激活、安全及日志。“从很多方面讲,Docker 想成为一个新的 systemd,”他声称,“它梦想成为 systemd。”

他阐述的第一个冲突在于服务初始化与重启。在 systemd 模式中,这些都是由 systemd 控制的;在 Docker 的世界中,则全由 Docker 守护进程控制。比如说,可以在 systemd 单元文件中以 “docker run” 语句来定义服务,以便以容器方式运行它们,也可以在 Docker 守护进程中将服务定义成“自动重启”容器。两种方式都行得通,但混合起来就不行。Docker 文档推荐使用 Docker 自动重启,除非是在混合容器化服务与非容器化服务时;这种情况下它推荐使用 systemd 或 Upstart。

不过,当以容器方式运行的服务依赖于其他容器化服务时,问题就出现了。对于常规服务,systemd 有一个名为 sd_notify 的功能,可以在服务准备就绪时传递消息,然后就可以启动那些依赖于它们的服务。但是,Docker 采用的是 C/S 架构。客户端为每个用户会话调用 docker run 及其他命令,但容器的启动和管理是在 Docker 守护进程中进行的(相当于“服务端”)。客户端无法发送 sd_notify 状态消息,因为它不做容器服务的实际管理,也不清楚服务的就绪时间,守护进程也无法发送这类消息,因为它不是通过 systemd 的单元文件调用的。因此,Walsh 的团队尝试了一种迂回的变通方式来启用 sd_notify:

  1. systemd 请求来自 Docker 客户端的 sd_notify
  2. 该客户端发送一条 sd_notify 消息给 Docker 守护进程
  3. 守护进程设置一个容器来完成 sd_notify
  4. 守护进程从该容器获取一条 sd_notify 消息
  5. 守护进程发送一条 sd_notify 消息给该客户端
  6. 该客户端发送一条 sd_notify 消息通知 systemd Docker 容器已就绪

用于启用这个错综复杂系统的补丁未被 Docker 项目接纳,对此 Walsh 毫不意外。sd_notify 确实可以用于 Docker 守护进程自身,因此 systemd 可以依赖于守护进程的运行。但是还是无法为单个容器化服务执行 sd_notify,因此 Docker 项目依然没有可靠的办法来管理容器化服务依赖的启动顺序

Systemd 有一项功能叫“套接字激活socket activation”,可以在收到针对某个特定网络套接字的请求时自动启动服务。这使得服务器可以支持那些“偶尔需要”的服务,而无须始终运行。Docker 守护进程自身曾经支持过套接字激活,不过由于与 Docker 的自动重启相冲突而将其禁用了。

Walsh 的团队对单个容器的套接字激活更感兴趣。其好处是可以消除“始终运行”容器的额外开销。但是,开发人员发现他们不得不采用与 sd_notify 变通方式类似的方法,差别在于传递的是一个套接字,而不仅仅是一条消息。他们甚至没有尝试实现。

Linux 的 cgroup 可用来定义每个服务的系统资源额度,比如 CPU、内存及 I/O 限制。Systemd 允许在初始化文件中定义 cgroup 限制,这样你就可以定义服务启动时的资源策略。但是,使用 Docker 时,这又与其 C/S 模式相冲突。Systemd 的 cgroup 设置只对客户端产生作用;不对容器实际运行所在的守护进程产生作用。相反的,每个容器都继承了 Docker 守护进程的 cgroup 设置。不过,用户可以通过 docker run 命令参数来传递 cgroup 限制,这虽然可行,但未能与系统的整体管理策略整合。

Walsh 能联系得上的唯一成功之处是日志。Docker 的日志也无法与 systemd 的 journald 协作。容器的日志输出都是在每个容器本地,一旦删除容器,所有日志将被自动清除。从安全审计的角度来看,这是一大败笔。Docker 1.9 开始支持 --log-driver=journald 开关,将日志记录到 journald 中。不过,Docker 容器默认不使用 journald,因此每次都要传递这个开关。

容器内部的 systemd

Walsh 还想在 Fedora、红帽企业 Linux(RHEL)及 CentOS 容器基础镜像中启用 systemd,部分是因为很多软件包需要 systemctl 功能以便正确安装。他一开始尝试的是使用 “fakesystemd” 来代替 systemctl,该服务用于满足软件包的 systemctl 需要,没有其余的功能。结果这会造成问题,他很快就放弃了,不过还是迟了一步,未能阻止它在 RHEL 7.0 中发布。

在 RHEL 7.1 中,该团队添加了 “systemd-container”,这是 systemd 的一个大幅度删减版本。这依然会对那些软件中需要完整 systemd 的用户造成问题,Poettering 要求容器团队进行修改。在 RHEL 7.2 中,容器具有了真正的 systemd,减少了需要安装的依赖,因此尺寸更小。Walsh 的团队正着手进一步缩减这些依赖。

根据 Walsh 所说,容器中没有 systemd 最大的问题是它“退回到了使用初始化脚本之前。”每个镜像作者都在容器内创建自己的疯狂的启动脚本,而不是使用软件包作者精心制作的启动脚本。他演示了在具有 systemd 的容器内,服务初始化是何尝的简单,创建一个运行 Apache httpd 服务器的容器,其 Dockerfile 只有三行:

FROM fedora
RUN yum -y install httpd; yum clean all; systemctl enable httpd;
CMD [ "/sbin/init" ]

不过,要在 Docker 中使用 systemd,有一个主要障碍:运行具有 systemd 的容器要求运行时要带有--privileged标记,这让它变得不安全。这是因为 Docker 守护进程要求容器运行的“服务”应用程序其 PID 永远是1。在具有 systemd 的容器中,其 PID 是1,应用程序则具有其他的 PID,这会造成 Docker 认为容器失败并将其停止。

Poettering 说 PID 1 具有特殊要求。其中之一是杀死那些被它们的调用会话所遗弃的“僵尸”进程。对 Docker 来说,这是一个真正的问题,因为应用程序以 PID 1 运行,却不处理僵尸进程。比如,运行 Oracle 数据库的容器可能在退出时会遗留几千个僵尸进程。另一个要求是写入 syslog,除非配置容器写入日志到 journald,否则将进入 /dev/null。 

Walsh 尝试了几种办法以便 systemd 在非特权容器中工作,并提交了四个不同的拉取请求(7685109941352513526)给 Docker 项目。所有请求都被 Docker 维护人员拒绝了。当 Docker 贡献者之一 Jessie Frazelle 带着印有“我对systemd 相关拉取请求说不I say no to systemd specific PRs”的名牌来到 DockerCon.EU 2015 大会时,有关这些修改的争论达到了顶峰。

Systemd与容器的未来

红帽容器团队还大量参与了开放容器项目 runC 工具的开发。该项目是开放容器组织(OCI)的实践性产出,OCI 是由 Linux 基金会于2015成立的非营利组织,目的是为容器 API 设置行业标准。OCI 还维护着 libcontainer,这是 Docker 用于启动容器的类库。按照 Walsh 所述,Docker 最终需要采纳 runC 作为其技术栈的一部分,以便在其他平台上运行,尤其是 Windows。

使用来自 runC 的成果,红帽人员已经创建了一组名为 “oci-hooks” 的补丁,为 Docker 添加了大量 systemd 支持功能。它使用一个“钩子”,可以在容器启动之后应用程序运行之前触发指定目录下的所有可执行文件。使用这种方法执行的东西中间是 RegisterMachine 钩子,它会通知宿主机上 systemd 的 machinectl 容器正在运行。用户可以使用 machinectl 命令看到所有 Docker 容器,以及 runC 容器:

# machinectl
MACHINE                          CLASS     SERVICE
9a65036e4a6dc769d0e40fa80871f95a container docker 
fd493b71a79c2b7913be54a1c9c77f1c container runc
2 machines listed.

这些钩子还允许在非特权容器里运行 systemd。这个拉取请求(17021)同样被 Docker 项目拒绝了。尽管如此,它还是包含在了红帽发行的 Docker 包中。因此,Docker 与 systemd 未来的一部分可能会涉及 Docker 的分叉。

Walsh 同时指出,cgroup、sd_notify 以及套接字激活对 runC 都是开箱即用的。这是因为 runC 不使用 Docker 的 C/S 模型;仅仅是个可执行文件。他看不到 Docker 公司与红帽之间未来在 systemd 问题上修复的突破口。Walsh 预测红帽可能会更多地转向 runC,并远离 Docker 守护进程。按他的话来说,Docker 正忙于 “containerd”,这是一个新的 systemd 替代品,将取代初始化系统的功能。

但是,鉴于自 Docker 项目启动以来短时间内 Linux 容器生态系统的快速变化,几乎不可能预测 systemd、Docker 及 runC 今后一年的关系将如何。毫无疑问的是,将会有更多的变化和冲突报道。