「Docker と SO_REUSEPORT を組み合わせてみる。おそらくその1」のその2です。
結論から言うと、「単体ではリクエストの取りこぼしが若干あるけど、Reverse Proxyを工夫すればコンテナのHot Deployを実現できるかも」という感じです。
Rhebok の SO_REUSEPORT 対応
前回は簡単に検証するためにmemcachedを使いましたが、今回はアプリケーションサーバが対象ということで、 unicornの2倍ぐらい速いRackサーバであるRhebokに手をいれてSO_REUSEPORT対応しました。version 0.2.3〜です。
rhebok | RubyGems.org | your community gem host
起動時に ReusePort オプションを追加します。
$ bundle exec rackup -Ilib -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O ReusePort -E production
                                        Rhebok を起動するコンテナの作成
RhebokでHello Worldなアプリケーションを動かす為のDockerfileはこんな感じになりました。
FROM ubuntu:trusty
MAINTAINER Masahiro Nagano <kazeburo@gmail.com>
ENV DEBIAN_FRONTEND noninteractive
RUN locale-gen en_US.UTF-8 && dpkg-reconfigure locales
RUN apt-get update 
RUN apt-get -y build-dep ruby
RUN apt-get -y install libssl-dev curl git
RUN mkdir -p /opt/app
RUN git clone https://github.com/tagomoris/xbuild /opt/xbuild
RUN /opt/xbuild/ruby-install 2.1.5 /opt/ruby-2.1
ENV PATH /opt/ruby-2.1/bin:$PATH
WORKDIR /opt/app
COPY Gemfile /opt/app/Gemfile
RUN bundle install --path=vendor/bundle
COPY config.ru /opt/app/config.ru
EXPOSE 8080
CMD ["bundle","exec","rackup","-s","Rhebok","-O","Host=127.0.0.1","-O","Port=8080","-O","MaxWorkers=3","-O","MaxRequestPerChild=0","-O","BackLog=128","-O","ReusePort","-E","production","config.ru"]
                                        これをビルドして実行します
$ docker build -t hello_rhebok .
$ docker run --net=host hello_rhebok
                                        別の端末からもうひとつ同じポートにbindするコンテナを起動しても動くはずです
$ docker run --net=host hello_rhebok
                                        psで確認します
$ ps fax|grep rackup
28490 ?        Ssl    0:00  \_ ruby rackup -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O MaxWorkers=3 -O ReusePort -E production config.ru
28501 ?        Sl     0:00  |   \_ ruby rackup -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O MaxWorkers=3 -O ReusePort -E production config.ru
28504 ?        Sl     0:00  |   \_ ruby rackup -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O MaxWorkers=3 -O ReusePort -E production config.ru
28506 ?        Sl     0:00  |   \_ ruby rackup -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O MaxWorkers=3 -O ReusePort -E production config.ru
28544 ?        Ssl    0:00  \_ ruby rackup -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O MaxWorkers=3 -O ReusePort -E production config.ru
28559 ?        Sl     0:00      \_ ruby rackup -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O MaxWorkers=3 -O ReusePort -E production config.ru
28562 ?        Sl     0:00      \_ ruby rackup -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O MaxWorkers=3 -O ReusePort -E production config.ru
28565 ?        Sl     0:00      \_ ruby rackup -s Rhebok -O Host=127.0.0.1 -O Port=8080 -O MaxWorkers=3 -O ReusePort -E production config.ru
                                        ssコマンドでも確認
$ sudo ss -nltp|grep 8080
LISTEN     0      128               127.0.0.1:8080                     *:*      users:(("ruby",28565,7),("ruby",28562,7),("ruby",28559,7),("ruby",28544,7))
LISTEN     0      128               127.0.0.1:8080                     *:*      users:(("ruby",28506,7),("ruby",28504,7),("ruby",28501,7),("ruby",28490,7))
                                        Dockerコンテナをstart_serverを使って起動する
start_server (Server::Starter)はkazuhoさんがつくった、サーバをhot deployするためのツールです。最近ではlestrratさんのgolangバージョンもあります。
Server::Starterはlisten socketを保持しつつ、サーバをfork & execすることでsocketのfile descriptorを引き継ぎます。また、HUPシグナルを受け取った際に新しくサーバを起動してから、古いサーバにTERMを送ることでListen Socketを切らす事なくHot Deployを実現しています。
shibayuさんのblogが詳しいです。
start_server コマンドはTCPポートやUNIX DOMAIN SOCKETのパスを指定せずに起動することもでき、汎用のプロセスの無停止入れ替えツールとしても使う事が出来ます。もちろんDockerコンテナも起動できます。
今回はgolang版を使いました。
$ wget https://github.com/lestrrat/go-server-starter/releases/download/0.0.2/start_server_linux_amd64.tar.gz
$ tar zxf start_server_linux_amd64.tar.gz
$ cd start_server_linux_amd64
                                        docker run します
$ KILL_OLD_DELAY=5 ./start_server -- docker run --net=host hello_rhebok
                                        KILLOLDDELAY はプロセス入れ替え時に、新しいプロセスを起動してから古いプロセスをkillするまでに待つ秒数です。これがないと、新しいプロセスがlistenする前に古いプロセスが終了してしまい、サーバに接続出来ない時間ができてしまいます。
Hot Deployにチャレンジ
さて、この環境で ApacheBench を掛けながら start_server に HUPシグナルをおくって、エラーがでないか動画撮りつつやってみました。
左上がstartserverを実行しているウィンドウ、HUPを受けてプロセスを入れ替えている様子がわかります。左下はApacheBenchの実行、右側はstartserverにHUPを送ってます。
ApacheBenchの結果はこんな感じです
$ ab -rld -c 20 -n 100000 http://127.0.0.1:8080/
Server Software:        Rhebok
Server Hostname:        127.0.0.1
Server Port:            8080
Document Path:          /
Document Length:        Variable
Concurrency Level:      20
Time taken for tests:   16.093 seconds  
Complete requests:      100000
Failed requests:        40
   (Connect: 0, Receive: 20, Length: 0, Exceptions: 20)
Total transferred:      12797056 bytes  
HTML transferred:       1199724 bytes   
Requests per second:    6213.89 [#/sec] (mean)
Time per request:       3.219 [ms] (mean)
Time per request:       0.161 [ms] (mean, across all concurrent requests)
Transfer rate:          776.56 [Kbytes/sec] received
                                        何回かエラーがでています。Failed requests:        40
                                            とありますが、たぶんこれはエラーの数を単純に足しただけでエラーの数自体は20件だと思います。秒間6000リクエスト中の20件なので非常に少ないですが、リクエストの並列数があがるとエラーの件数も増えていく可能性があります。
                                        
これは古いサーバを終了した際に、そのプロセスのListen SocketのQueueに残っていたコネクションが解放される(RSTが送られる?)ために起きるエラーで、現状のLinuxの仕組み上、回避するのは難しそうです。
Reverse Proxy配下でのHot Deploy
通常、アプリケーションサーバを動かす際にはnginxなどのReverse Proxyがいますが、アプリケーションサーバとの接続でエラーが出た時にReverse Proxyが再接続を行ってくれれば、上の問題は回避できそうです。
nginxの設定はこんな感じ
worker_processes  1;
events {
  worker_connections  50000;
}
http {
  include     mime.types;
  access_log  off;
  upstream app {
    server 127.0.0.1:8080 max_fails=10;
    server localhost:8080 max_fails=10 backup;
  }
  server {
    location / {
      proxy_pass http://app;
      proxy_next_upstream error http_502;
    }
  }
}
                                        backupなupstreamはおまじないみたいなもんです。
今度はwrkをつかってアクセスしつつ、HUPシグナルを送ります。
左下のwrkの結果にはエラーが出ていません。無事nginxで再接続してくれたようです。これならHot Deployができそうですね。
まとめ
実際にdeployに使うならdocker runするところを別のscriptにして起動するのが良いのかな
$ KILL_OLD_DELAY=5 ./start_server -- run_docker.sh
                                        run_docker.shは
#!/bin/bash
docker pull image
exec docker run --net=host image
                                        とすれば起動前に必要な処理も行えます。
DockerとSO_REUSEPORTを使ってのコンテナのHot Deploy、単体ではリクエストの取りこぼしが若干あるけど、Reverse Proxyを工夫すれば実用できるかもという感じではないでしょうか。
