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

このブログ記事について

このページは、Masahiro Naganoが2015年1月 8日 15:28に書いたブログ記事です。

ひとつ前のブログ記事は「Docker と SO_REUSEPORT を組み合わせてみる。おそらくその1」です。

次のブログ記事は「Taobaoが公開しているnginxベースのWebサーバ Tengine の health check 機能 を試す」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 4.27-ja