「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を工夫すれば実用できるかもという感じではないでしょうか。