にゃみかんてっくろぐ

猫か百合を見守る壁になりたい

Traefik でお手軽に Docker コンテナの無停止デプロイを実現した

3 行でまとめると

  • Traefik を使って Docker Compose だけで無停止デプロイを実現
  • docker-compose.yml に定義するだけ、他の設定ファイルは不要のお手軽さ
  • コンテナは graceful shutdown するようにしておく

docker-compose.yml

ざっくり作り上げた docker-compose.yml がこちらです。

gist.github.com

これだけで動きます。リクエストを投げながら nginx-blue, nginx-green のどちらか一方を落としてもエラーになることはありません。

f:id:no_clock:20200603225248p:plain
nginx-blue を停止してもエラーなくレスポンスし続けている様子

Traefik とは

Traefik はある種のリバースプロキシです。ただし、 HAProxy や nginx と違ってサービス検出機能が備わっているほか、環境変数メタデータ (Docker の場合は labels) で設定ができるのも特徴的です。

サービス検出機能と 仲良く なる必要はあるものの、設定の記述量が少なくて済みます。 Docker の場合は、 Traefik 自身が Docker デーモンにアクセス出来るようにします。

    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

ちょっと気をつけたいところ

  1. 公式ドキュメントのみを頼ったほうが良いです。Google 検索で調べても情報は出てきますが、 frontendbackend といった v1.x 系列の設定を取り上げた記事がヒットして非常に混乱します。
  2. EntryPoints, Routers, Services と複数段定義する必要があり、とっつきにくい印象があるかもしれません。めちゃくちゃ端折って Nginx と対比すると下記の通りです、難しくないと思い込みましょう。
Traefik Nginx
EntryPoints listen ディレクティブ
Routers server ディレクティブ(ホスト名) と location ディレクティブ
Services upstream ディレクティブ

無停止デプロイする

さて、無停止デプロイをしましょう。ポイントは 2 点です。

  • コンテナが graceful shutdown *1 するか、リクエストが冪等であること。
    • traefik はクラウドロードバランサーサービスにあるような接続ドレインの機能を持ちません。コンテナ自身が受け付けたリクエストを正しく処理しきることが望ましく、それが出来ない場合はリクエストの処理を冪等にしておく必要があります。
  • Retry middleware を使う

冒頭の docker-compose.yml では、 nginx に SIGQUIT を投げることで graceful shutdown を実現しています。

    image: "nginx:alpine"
    stop_signal: SIGQUIT
    labels:
(略)
      - "traefik.http.middlewares.nginx-retry.retry.attempts=4"

healthcheck はなくても大丈夫

traefik には healthcheck 機能があり、これを使ったコンテナの切り離しも可能です。

    labels:
(略)
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.path=/"
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.interval=1s"
      - "traefik.http.services.nginx-bg.loadbalancer.healthcheck.timeout=1s"

ただし、 Retry middleware を使うことで下記の動きとなるため、 healthcheck を設定しなくても問題はありません。

  1. Listen をやめた停止中のコンテナにルーティング
  2. コネクションが確立出来ないため、即座にリトライ
  3. 実行中のコンテナにルーティング

メモリ使用量

かなり雑ですが、 1MB のファイルを 100 同時接続で計 10,000 リクエスト、 10GB 分の転送をさせました。

$ ab -n 10000 -c 100 -H "Host: localhost" http://localhost/
# 起動直後
$ docker stats
CONTAINER ID        NAME                    CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
903a420aa79c        traefik_nginx-blue_1    0.00%               1.898MiB / 592.5MiB   0.32%               800B / 42B          6.21MB / 0B         2
bb10478ecb4b        traefik_traefik_1       0.03%               10.18MiB / 592.5MiB   1.72%               800B / 0B           46.3MB / 0B         6
7f123e90926c        traefik_nginx-green_1   0.00%               1.777MiB / 592.5MiB   0.30%               578B / 0B           348kB / 0B          2

# リクエスト後
$ docker stats
CONTAINER ID        NAME                    CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
903a420aa79c        traefik_nginx-blue_1    0.00%               2.191MiB / 592.5MiB   0.37%               4.28MB / 5.21GB     7.27MB / 0B         2
bb10478ecb4b        traefik_traefik_1       0.00%               25.42MiB / 592.5MiB   4.29%               10.5GB / 10.5GB     46.8MB / 0B         19
7f123e90926c        traefik_nginx-green_1   0.00%               2.293MiB / 592.5MiB   0.39%               4.22MB / 5.22GB     1.4MB / 0B          2

nginx のメモリ使用量の少なさが際立ちますが、 traefik も 25MB に収まっています。実行中も 30MB を超えることはありませんでした。

参考

*1:明確な定義はないですが、ここでは、 Listen をやめ新たなリクエストを受け付けないようにした後、既存のリクエストを処理しきってから終了する振る舞いを指します。参考: Apache HTTP Server の停止と再起動 - Apache HTTP サーバ バージョン 2.4