ngx_mrubyとdockerでblue-green deployment


目的

ngx_mrubyを使ってバックエンドのコンテナを切り替えることができるのかやってみました。
dockerを使いたいと思いつつも、ダウンタイムなしでdeployするにはこうしておけばよい、
という簡単な方法がないようでした。この方法は汎用的に使えるのではないかと思います。

サンプルを動かす

一式をGithub(https://github.com/muk-ai/rest-api) に置きました。
network機能を使うので docker >= 1.10, docker-compose >= 1.6 でお試しください。

git clone https://github.com/muk-ai/rest-api.git
cd rest-api/
git checkout 1f978500008e2126fdce2b31b1d4e189520e069b

docker-compose build
docker-compose up -d

ブラウザでport 80にアクセスすると、{"messages": "hello, world"} が表示されます。
ここから、deploy.shでコンテナをすり替えてみます。

sh deploy.sh mukai/rest-api:goodbye

port 80にアクセスすると、 {"messages": "goodbye, world"} が表示されるようになりました。
nginxの渡し先コンテナが切り替わったためです。
ロールバックもコンテナの切り替えで行えます。

sh deploy.sh restapi_falcon

ロールバックはコンテナイメージをpullする時間がないので早いです。

ダウンタイムはあるのか?

wrkでアクセスをかけつつ、バックエンドのコンテナのstop/startをかけた場合

$ ./wrk -c300 -d10s -H"User-Agent: strees-test" --latency --timeout 10 http://172.31.26.80/
Running 10s test @ http://172.31.26.80/
  2 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   738.10ms    1.37s    7.88s    87.79%
    Req/Sec   796.77    221.40     1.35k    81.82%
  Latency Distribution
     50%   64.22ms
     75%  832.17ms
     90%    2.57s
     99%    6.44s
  14137 requests in 10.01s, 2.70MB read
  Non-2xx or 3xx responses: 315
Requests/sec:   1412.04
Transfer/sec:    275.74KB

315回のエラー(Non-2xx or 3xx responses)が出てしまいました。
次に、wrkでアクセスをかけつつ、deploy.shでコンテナを切り替えた場合。

$ ./wrk -c300 -d10s -H"User-Agent: strees-test" --latency --timeout 10 http://172.31.26.80/
Running 10s test @ http://172.31.26.80/
  2 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   629.61ms    1.38s    9.43s    90.19%
    Req/Sec   792.55    196.76     1.34k    63.32%
  Latency Distribution
     50%   66.83ms
     75%  472.64ms
     90%    1.91s
     99%    6.90s
  15750 requests in 10.01s, 2.97MB read
Requests/sec:   1573.40
Transfer/sec:    303.44KB

エラーが出ませんでした( ̄ー ̄)ニヤリ

要所(nginx.conf)の説明

ngx-mruby/docker/conf/nginx.conf
        location / {
            resolver 127.0.0.11 valid=2s;
            mruby_set_code $backend '
              cache = Cache.new :namespace => "nginx"
              cache["proxy_dest"]
            ';
            proxy_pass http://$backend:3031;
        }

リゾルバの指定が127.0.0.11になっているのは、dockerネットワークにいる他コンテナの名前解決のためです。
(ビックリしますが、127.0.0.1以外のループバックアドレス宛にDNSを引くとコンテナの名前解決ができる)
cache["proxy_dest"]の中にバックエンドのサーバーを入れておきます。
この変数は↓のようにして初期化しています。

ngx-mruby/docker/conf/nginx.conf
    mruby_init_code '
      # mruby-cache
      cache = Cache.new :namespace => "nginx"
      cache["proxy_dest"] = "falcon"
    ';

Cacheクラスを使っているのはnginxプロセス全体で値を共有したいため。
falconというのはアプリケーションコンテナの名前です。
この名前を127.0.0.11に問い合わせると、IPが返ってきます。

ngx-mruby/docker/conf/nginx.conf
        location /container/switchover {
            allow 172.16.0.0/12;
            deny all;

            mruby_content_handler_code '
              v = Nginx::Var.new
              if v.arg_dest.nil?
                Nginx.errlogger Nginx::LOG_ERR, "Invalid parameter #{v.arg_dest}"
                Nginx.return Nginx::HTTP_BAD_REQUEST
              end
              cache = Cache.new :namespace => "nginx"
              cache["proxy_dest"] = v.arg_dest
              Nginx.echo "ack"
            ';
        }

/container/switchover というpathで、宛先コンテナの制御を行います。
v.arg_destでdestリクエストパラメータを取得し、その値でcache["proxy_dest"]を更新します。

curl http://localhost/container/switchover?dest=$container_id

のようにすることで、nginxの向き先を切り替えます。
deploy.shでは次のことをやります。
① コンテナイメージのpull
② コンテナの起動とコンテナIDの取得
/container/switchoverを叩いてコンテナ切り替え

既知の問題

  • nginxが太る (critical)
  • 旧コンテナの廃棄処理がない

nginxが太るのはmrubyの書き方に問題があるものと思います。負荷テストにかけると丸丸と太っていきます。
wrkをだいたい30秒くらいかけると1GBあったメモリが尽きます。識者の方、アドバイス頂けると幸いです・・・。

また、旧コンテナをそのままにしているのでdeployの度にコンテナが増えていきそのうち破綻すると思います。

追記

2016/04/11

@matsumotoryさんからアドバイスをもらい、Nginxのメモリリークについては解決しました。
https://github.com/muk-ai/rest-api/pull/10/files
ありがとうございました。m(__)m

ところで、wrkを長時間かけられるようになって、別の問題(高負荷時にしばらくパケットが落ちる)が発見されました。
docker networkの問題であるのか、uwsgiの問題であるのか未だ切り分けできていません。
→ この問題はnginxとuwsgiの間をhttpで繋ぐのをやめたところ見なくなったので、追うのをやめました。