利用systemd按需激活Docker容器


【编者的话】本文讲述了一种在systemd下按需启动docker容器的实现框架,给出了按需启动nginx容器的一个实例。当在服务器配置不高的情况下,我们需要构建大量的容器时,这种方法非常有效。

systemd有这样一个特性,它可以通过socket激活进程延迟启动网络服务应用,直到此服务收到请求。这个方式并非首创,systemd借用的是OS X自2005年Tiger版本以来的launched的实现思路,再往前追溯,古老的Unix inetd在上世纪80年代就已经实现了这种启动方式的一个简单版本。不管是在脚本驱动还是在事件驱动的启动系统中,Socket激活的方式都具有很多优点,最为显著的是,它能够有效地对应用的启动顺序和服务描述进行解耦合,甚至可以解决一些应用的循环依赖问题。Docker容器(或者其他类型的应用服务例如systemd's nspawnLXC)大都是一些专用的网络进程,所以用socket激活的方式来启动这些进程会比较有用。

Socket激活

creation.jpg

Socket激活是这样工作的:systemd的守护进程代表其他应用监听相关的sockets,只有当连接传入的时候才会启动相应的应用服务,之后此连接交由新启动的应用服务负责响应。

但是这里的限制之一是,需要被激活的应用知晓它是可以被socket激活的,并且对现有socket的处理——尽管简单——却不同于从头创建一个监听socket。因此,很多被广泛应用的软件(如ngix)并不支持这种方式,软件容器化的趋势添加了需要激活的层,这又进一步加剧了这种限制条件的影响。但是,可以通过将其交给容器的方式解决此问题,这对任意的容器化应用都适用。

Socket激活和Docker

如果你在Google查找 "docker container systemd socket activation" 你会发现相关的讨论很少,大部分的结论是:除非Docker明确给出了这方面的支持,否则是不可能的。虽然Dcoker支持是最优的解决方式,但并不是全部。Sytemd的开发者知道,想要达到在任意情况下都可以激活的目标,可能还需要些时日。因此,在209版本中引入了systemd-socket-proxyd——一个小的TCP和UNIX域socket代理。这个东西才是真正的理解激活的含义,它可以在网络和我们的容器之间透明地转发包。因此,借助少许的units(systemd的配置系统),我们可以为Docker容器创建一个socket激活框架。

警示:当前的Ubuntu发行版本systemd是208版本,还没有systemd-socket-proxyd,要想尝试下述例子需要配备systemd 209或更高版本,Debian的Jessie 预发行版本、基于RedHat的最新发行版本(如Fedora)应该也可以。

如何工作的

通常,我们用一个演示来简化说明问题:
systemd-docker.gif

我们看一下到底发生了什么事情:
  1. 我们创建了一个socket监听相应的端口,该端口由代理/容器依事件驱动合并进行服务;
  2. 当第一个连接进来的时候,systemd激活代理服务来处理这个socket;
  3. 还有一个为容器提供的被动式服务——代理依赖于此服务,代理启动之前首先要启动这个容器;
  4. 代理负责在网络和容器之前传送所有的流量。


虽然需要一些技巧,但从概念上讲相当简单。那么我们以systemd和一个Nginx的容器来练习一下如何实现。

运行

创建容器

首先我们要创建目标容器。这里我们利用官方nginx镜像创建一个空的nginx容器。
docker create --name nginx8080 -p 8080:80 nginx


这与你执行任何容器均类似,不过注意我们用的是create而不是run,因为我们并不想这个容器马上启动,我们还对此窗口进行了命名,以便于之后的启动。

这里唯一一点技巧是你需要将此容器绑定到另外一个端口代替目标端口(这里我们用8080代替了80),这是因为这个端口(80)将由代理/容器持有,而且你不能将两个进程同时绑定到同一个端口进行交互。

现在我们有了一个容器,这样便可以创建激活管道了。首先我们需要初始的监听socket也就是socket unit,这个socket的行为是高度可配置的,在我们的例子中它的创建相当简单。
[Socket]
ListenStream=80
[Install]
WantedBy=socktes.target

[Socket]节描述一个简单的TCP socket用于监听80端口,[Install]节告诉systemd何时启动这个socket,在此例中它与系统配置的其他socket一起启动。我们将此unit写入名为/etc/systedm/system/nginx-proxy.socket的文件中,这是链条中唯一需要启动后激活的部分,因此我们需要告诉systemd启动它:
systemctl enable nginx-proxy.socket
systemctl start nginx-proxy.socket

代理服务

当systemd接收到此socket上的连接时,它将自动查找并启动同名服务。因此,我们需要创建服务文件/etc/systemd/systedm/nginx-proxy.service
[Unit]
Requires=nginx-docker.service
After=nginx-docker.service

[Service]
ExecStart=/lib/systemd/systemd-socket-proxyd 127.0.0.1:8080

[Unit]节描述了此服务的依赖,此例中我们要告诉systemd,在代理启动之前必须先将实际的容器启动(下面我们将要配置此服务)。[Section]节负责启动代理进程,当前此节还可以配置大量重要内容,比如对进程启动失败的处理,但是对于我们来说,简单的启动一下就OK了。注意我们将socket转发到了8080端口,这是我们的容器将要监听的端口;我们也并没有告诉systemd监听80端口,它只是用了systemd处理的socket。还要注意到上述配置文件中没有[Install]节,此服务并非是缺省启动,而是由socket激活它。

启动Docker容器

正如前面提到了,代理利用Requre/After机制触发了容器的启动,此容器的启动文件在/etc/systemd/system/nginx-docker.service,它是这样的:
[Unit]
Description=nginx container

[Service]
ExecStart=/usr/bin/docker start -a nginx8080
ExecStartPost=/bin/sleep 1

ExecStop=/usr/bin/docker stop nginx8080

基本概念与上述代理服务相同,ExecStart行告诉systemd如何启动这个容器,systemd偏爱不在后台运行的进程,所以我们添加了-a参数,它可在前台运行此容器,并且可将nginx的运行日志转发到journaldlogger中,ExecStop告诉systemd当运行systemctl stop ngix-docker时如何停止容器。

这里我们还利用ExecStartPost行耍了一个小聪明,这是systemd在主进程启动后马上运行的进程,此例中,我们在继续往下走之前让其sleep了1秒钟。这是必要的,因为尽管docker启动容器非常快,但是容器中的进程可能需要稍微长一点的时间完成初始化。systemd也非常快,所以有可能在nginx准备好接收之前,代理就开始转发了。因此我们添加了一点延迟,给Dcoker/nginx一个空闲启动,这有点耍赖但是很有效(但是看下面)。

一切就绪

好了,当systemd启动的时候会开启一个socket,当第一个连接到达时,级联的依赖关系会使nginx Docker通过代理对此连接进行响应。Easy。

容器服务改进

任何想要优化系统启动时间的人都恨死了在代码或配置文件中随意添加的sleep语句。他们要么是因系统很快而不需要此延迟,要么是因此引起了很多难以定位的随机错误。上述ExecStartPost中的sleep语句也同样让我神经过敏,所以我也想把它删了。我们真正想做的是检查端口是否已启动,只有它没启动的时候我们才需要sleep。我们还想在端口启动时间过长的时候返回失败。为了做到这些,我使用了netcat和一个wrapper脚本。
#!/bin/bash

host=$1
port=$2
tries=600

for i in `seq $tries`; do
if /bin/nc -z $host $port > /dev/null ; then
    # Ready
    exit 0
fi

/bin/sleep 0.1
done

#FAIL
exit -1

这个脚本需要一个host和port的参数,检查一下是否有响应,每秒钟检查10次,如果1分钟之内还没有响应就返回失败。我们将此脚本安装到/usr/local/bin/waitport(设置其为可运行)。现在我们的nginx-docker.service文件是这样的:
[Unit]
Description=nginx container

[Service]
ExecStart=/usr/bin/docker start -a nginx8080
ExecStartPost=/usr/local/bin/waitport 127.0.0.1 8080

ExecStop=/usr/bin/docker stop nginx8080

也就是说,waitport脚本可以根据你的系统应用灵活调整,如果你的容器启动速度很快的话通常都会立即返回。

原文链接:On-demand activation of Docker containers with systemd(翻译:deerlux 校对:夕口夕)

================
译者介绍:<deerlux@163.com>,现就职于一家军工科研机构,深度的技术控、Linux控、python控。

0 个评论

要回复文章请先登录注册