コンテナで複数プロセスを起動させる

1コンテナ上で、nginxとfluentdを動かした話。

どうして複数プロセスを起動してはいけないの

Dockerコンテナではプロセスを1つだけ起動させる、とは有名なContainerベストプラクティスですが、これはどうしてでしょう。いろいろ理由があると思いますが、明確な技術的理由として、DockerではCMDオプションやENTRYPOINTオプションで起動したプロセスがコンテナ上のPID1になるからだ、と思っています。

試しに、nginxのコンテナを起動してプロセスを確認してみると、PID1は、nginxのmaster processとなっています。

$ docker run --rm -d --name nginx nginx
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
1d4d0754f57a        nginx               "nginx -g 'daemon of…"   6 seconds ago        Up 4 seconds        80/tcp              nginx
$ docker exec -it 1d4d0754f57a bash
root@1d4d0754f57a:/# apt update && apt install -y procps
root@1d4d0754f57a:/# ps -aef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 06:28 ?        00:00:00 nginx: master process nginx -g daemon off;
nginx        6     1  0 06:28 ?        00:00:00 nginx: worker process
root       427     0  0 06:35 pts/0    00:00:00 bash
root       676   427  0 06:37 pts/0    00:00:00 ps -aef

つまりinitプログラムがおらず、Supervisorの機能もないので、複数プロセスを起動させる場合にはUNIXの世界でのルール違反になってしまう、と。以下のブログで詳しく説明してくれています。

Docker and the PID 1 zombie reaping problem

このあたりが分かりやすい記述でしょうか。

Let's look at a concrete example. Suppose that your container contains a web server that runs a CGI script that's written in bash. The CGI script calls grep. Then the web server decides that the CGI script is taking too long and kills the script, but grep is not affected and keeps running. When grep finishes, it becomes a zombie and is adopted by the PID 1 (the web server). The web server doesn't know about grep, so it doesn't reap it, and the grep zombie stays in the system.

つまり、PID1がinitじゃないから、親プロセスを殺された孫プロセスはゾンビプロセスになってしまいます。まあ、initいないのに、プロセスをforkやspawnしまくるのは危険ですよね。

runitについて

じゃあ複数プロセスを起動させたい場合にはどうすればいいのさ、という話ですが、コンテナ上でinit的なものをPID1で動かしてあげれば良い訳です。それにあたるものとして runit というものがあります。

各説明サイトたち。

runit - a UNIX init scheme with service supervision

arch linux - Runit

phusion/baseimage について

phusion/baseimage とは

で、この runit を同梱してくれたコンテナイメージがあり、それが phusion/baseimage です。

phusion/baseimage

phusion/baseimage は、 runit 以外にも以下のようなサービスをコンテナ上で起動してくれます。

  • Ubuntu 16.04 LTS as base system
  • A correct init process
  • Fixes APT incompatibilities with Docker
  • syslog-ng
  • The cron daemon
  • An optional SSH server (disabled by default)
  • Runit for service supervision and management

コンテナというより、仮想サーバって感あり。

Dockerfile構成

この phusion/baseimage を利用して、1コンテナ上でnginxとfluentdを動かしてみます。下記のようなコンテナを作成します。

  • nginxプロセスを起動
    • 一般的なwebサーバーとして動作
  • flutendプロセスを起動

folder/file structure

.
├── Dockerfile
├── service
│   ├── nginx
│   │   └── run
│   └── td-agent
│       └── run
└── td-agent
    └── td-agent.conf

Dockerfile

FROM phusion/baseimage:0.11

ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV DEBIAN_FRONTEND=noninteractive

# install nginx
# https://nginx.org/en/linux_packages.html#Ubuntu

RUN apt-get update && \
apt-get install -y --no-install-recommends curl=7.58.0-2ubuntu3.7 gnupg2=2.2.4-1ubuntu1.2 ca-certificates=20180409 lsb-release=9.20170808ubuntu1 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

RUN echo "deb http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" > /etc/apt/sources.list.d/nginx.list

RUN curl -o nginx_signing.key -fsSL https://nginx.org/keys/nginx_signing.key && \
apt-key add nginx_signing.key

RUN apt-get update && \
apt-get install -y --no-install-recommends nginx=1.14.0-0ubuntu1.2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

RUN rm /etc/init.d/nginx

EXPOSE 80

# install td-agent
# https://toolbelt.treasuredata.com/sh/install-ubuntu-bionic-td-agent3.sh

RUN curl -o GPG-KEY-td-agent -fsSL https://packages.treasuredata.com/GPG-KEY-td-agent && \ 
apt-key add GPG-KEY-td-agent

RUN echo "deb http://packages.treasuredata.com/3/ubuntu/bionic/ bionic contrib" > /etc/apt/sources.list.d/treasure-data.list

RUN apt-get update && \
apt-get install -y --no-install-recommends td-agent=3.4.1-0 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

COPY td-agent /etc/td-agent/

RUN rm /etc/init.d/td-agent

# upload runit service script

COPY service /etc/service

RUN chmod 755 /etc/service/*/run

以下はrunitのユーザーレベルサービスを定義しているファイル。initにおける、init.dみたいなもの。ここにおいた処理を、起動時に runsvdir というプログラムが起動してくれます。

service/nginx/run

#!/bin/sh
exec /usr/sbin/nginx -c /etc/nginx/nginx.conf  -g "daemon off;"

service/td-agent/run

#!/bin/sh
exec /usr/sbin/td-agent

以下はfluentdのコンフィグ。

td-agent/td-agent.conf

<source>
  @type tail
  path /var/log/nginx/access.log
  pos_file  /var/log/nginx/access.log.pos
  format nginx
  tag nginx
</source>
<match *>
  @type s3
  aws_key_id XXXXXXXXXXXXXXXXXXXXXX
  aws_sec_key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  s3_bucket fluentd-sample-kiritan
  path sample/
  s3_region ap-northeast-1
  time_slice_format %Y%m%d%H%M
</match>

コンテナ起動

ビルドして起動。

$ docker build -t baseimage:latest .
$ docker run --rm --name baseimage -p 8000:80 baseimage:latest /sbin/my_init

プロセスを確認すると、/sbin/my_init(runitのsvコマンドを読んでる処理)がPID1となっており、 runsvdir により各サービスを起動しています。

$ docker exec -it bb2d2028160b ps -ef awxf
UID        PID  PPID  C STIME TTY      STAT   TIME CMD
root        55     0  0 13:12 pts/0    Rs+    0:00 ps -ef awxf
root         1     0  0 12:30 ?        Ss     0:00 /usr/bin/python3 -u /sbin/my_init
root        13     1  0 12:30 ?        S      0:00 /usr/sbin/syslog-ng --pidfile /var/run/syslog-ng.pid -F --no-caps
root        21     1  0 12:30 ?        S      0:00 /usr/bin/runsvdir -P /etc/service
root        22    21  0 12:30 ?        Ss     0:00  \_ runsv sshd
root        23    21  0 12:30 ?        Ss     0:00  \_ runsv cron
root        26    23  0 12:30 ?        S      0:00  |   \_ /usr/sbin/cron -f
root        24    21  0 12:30 ?        Ss     0:00  \_ runsv nginx
root        28    24  0 12:30 ?        S      0:00  |   \_ nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf -g daemon
www-data    31    28  0 12:30 ?        S      0:00  |       \_ nginx: worker process
root        25    21  0 12:30 ?        Ss     0:00  \_ runsv td-agent
root        27    25  0 12:30 ?        S      0:00      \_ /bin/sh ./run
root        29    27  0 12:30 ?        Sl     0:01          \_ /opt/td-agent/embedded/bin/ruby /usr/sbin/td-agent
root        34    29  0 12:30 ?        Sl     0:02              \_ /opt/td-agent/embedded/bin/ruby -Eascii-8bit:ascii-8bit /usr/sbin

dockerホストのIPを確認してWEBサイトにアクセスすると、

$ docker-machine ip
192.168.99.100
$ curl http://192.168.99.100:8000

しばらくした後、s3上にnginxのアクセスログが転送されています。

$ aws s3 ls s3://fluentd-sample-kiritan/sample/
2019-05-25 10:16:01        172 201905250105_0.gz