2015年1月アーカイブ

wrkに無理矢理なpatchをあてて、unix domain socket経由でHTTPサーバをベンチマークできるようにしてみました。

pullreqはしてない。

GazelleやRhebokといったアプリケーションサーバを作っていますが、TCP経由のベンチマークではEphemeral Portの枯渇やTIME WAITの上限にあたってしまい、ベンチマークがしづらいという問題があります。

そこでnginxをreverse proxyとして設置し、nginxとアプリケーションサーバ間をunix domain socketで繋いでベンチマークをとっていましたが、nginxがボトルネックになりやすく、直接アクセスしたいなと考えていたので、やってみました。

これを使ってRhebokとUnicornの “Hello World” ベンチマークを行ったところ、unix domain socketのベンチマーク結果はTCPに比べて、Rhebokで5倍、Unicornで3倍高速という数字がでました。

rhebok_tcp_unix.png

Rhebokは最新版でHTTP/1.1のサーバとなり、デフォルトでTransfer-Encoding: chunkedを使います。Unicornはchunked transferを行わないので、機能的にあわせるためrack middlewareを導入しているのが上記のグラフの「Unicorn + Chunked」です。

このベンチマーク結果をみると、やはりTCPの新規コネクションのコストは大きく、TCPの都度接続は避け、できるだけコネクションを使い回すか、unix domain socketの使用したいと思うでしょう。

ただ、Reverse Proxyとアプリケーションサーバ、もしくはクライアントとアプリケーションサーバ間のTCP接続をkeepaliveする場合、保持すべき接続数のチューニングが難しくなりがちなので、自分としてはお勧めしません。クライアントとのTCP接続の維持をReverse Proxyで行い、アプリケーションサーバ間との接続はunix domain socketを使うのがよいのでしょう。もちろん、両者が同じホスト上で動作していないと使えませんが。。

使い方

まずブランチ指定してcloneしてきて、make

$ git clone -b unixdomain https://github.com/kazeburo/wrk.git
$ cd wrk
$ make

-P オプションでパスを指定します。

$ ./wrk -t 1 -c 30 -d 30 -P /tmp/app.sock http://localhost/benchmark

簡単ですね

RhebokとUnicornのベンチマーク

ベンチマークはEC2のc3.4xlargeを使いました。コア数は16です。OSはAmazon Linuxです。

以下のカーネルチューニングをしました

$ sudo sysctl -w net.core.netdev_max_backlog=8192
$ sudo sysctl -w net.core.somaxconn=32768
$ sudo sysctl -w net.ipv4.tcp_tw_recycle=1

一番下はTCPのポート枯渇対策です。ただTCPのベンチマークはどれもtimeoutのエラーが出てしまっています。

Rubyはxbuildを使って2.1.0をインストール

$git clone https://github.com/tagomoris/xbuild.git
$ ./xbuild/ruby-install 2.1.5 ~/local/ruby-2.1
$ export PATH=/home/ec2-user/local/ruby-2.1/bin:$PATH
$ gem install rhebok unicorn

アプリケーションサーバのワーカー数は8とし、wrkはスレッド6個、同時接続数600で実行しました。

アプリケーションはconfig.ruに直接書いている

$ cat config.ru
class HelloApp
  def call(env)
    [
      200,
      {},
      ["hello world\n"]
    ]
  end
end
run HelloApp.new
# run Rack::Chunked.new(HelloApp.new)

unicornの設定はこちら

$ cat unicorn.rb 
worker_processes 8
preload_app true
#listen "/dev/shm/app.sock", :backlog=>16384
listen 8080, :backlog=>36384

Rhebok + unix domain socket

$ rackup -s Rhebok -O Path=/dev/shm/app.sock -O BackLog=16384 -E production -O MaxRequestPerChild=0 -O MaxWorkers=8 config.ru
$ ./wrk -t 6 -c 600 -d 30 --timeout 60 -P /dev/shm/app.sock http://localhost:8080/
Running 30s test @ http://localhost:8080/
  6 threads and 600 connections
  Thread Stats   Avg      Stdev  Max   +/- Stdev
    Latency  1.73ms  244.96us   6.62ms   81.28%
    Req/Sec 57.64k   4.04k   66.56k 69.06%
  9762337 requests in 30.00s, 1.28GB read
Requests/sec: 325426.21
Transfer/sec:    43.76MB

Rhebok + TCP

$ rackup -s Rhebok -O Port=8080 -O BackLog=16384 -E production -O MaxRequestPerChild=0 -O MaxWorkers=8 config.ru
$ ./wrk -t 6 -c 600 -d 30 --timeout 60 http://localhost:8080/
Running 30s test @ http://localhost:8080/
  6 threads and 600 connections
  Thread Stats   Avg      Stdev  Max   +/- Stdev
    Latency 36.02ms  194.43ms   1.83s   97.66%
    Req/Sec 11.87k   2.20k   21.89k 89.91%
  2021671 requests in 30.00s, 271.85MB read
  Socket errors: connect 0, read 0, write 0, timeout 56
Requests/sec:  67392.13
Transfer/sec:     9.06MB

Unicorn + unix domain socket

$ unicorn -c unicorn.rb -E production config.ru TCP
$ ./wrk -t 6 -c 600 -d 30 --timeout 60 -P /dev/shm/app.sock http://localhost:8080/
Running 30s test @ http://localhost:8080/
  6 threads and 600 connections
  Thread Stats   Avg      Stdev  Max   +/- Stdev
    Latency  3.21ms  170.17us  11.49ms   88.06%
    Req/Sec 32.71k   2.04k   43.67k 65.95%
  5534941 requests in 30.00s, 543.69MB read
Requests/sec: 184506.07
Transfer/sec:    18.12MB

Unicorn + TCP

$ unicorn -c unicorn.rb -E production config.ru TCP
$ ./wrk -t 6 -c 600 -d 30 --timeout 60 http://localhost:8080/
Running 30s test @ http://localhost:8080/
  6 threads and 600 connections
  Thread Stats   Avg      Stdev  Max   +/- Stdev
    Latency 33.58ms  179.94ms   1.75s   97.80%
    Req/Sec 11.01k   2.04k   22.00k 89.73%
  1873600 requests in 30.00s, 184.04MB read
  Socket errors: connect 0, read 0, write 0, timeout 49
Requests/sec:  62455.78
Transfer/sec:     6.13MB

Unicorn + chunked + unix domain socket

$ unicorn -c unicorn.rb -E production chunked.ru unix
$ ./wrk -t 6 -c 600 -d 30 --timeout 60 -P /dev/shm/app.sock http://localhost:8080/
Running 30s test @ http://localhost:8080/
  6 threads and 600 connections
  Thread Stats   Avg      Stdev  Max   +/- Stdev
    Latency  5.24ms  265.60us  13.67ms   77.33%
    Req/Sec 19.76k   1.79k   26.00k 68.71%
  3404283 requests in 30.00s, 457.77MB read
Requests/sec: 113481.40
Transfer/sec:    15.26MB

Unicorn + chunked + TCP

$ unicorn -c unicorn.rb -E production chunked.ru tcp
$ ./wrk -t 6 -c 600 -d 30 --timeout 60 http://localhost:8080/
Running 30s test @ http://localhost:8080/
  6 threads and 600 connections
  Thread Stats   Avg      Stdev  Max   +/- Stdev
    Latency   555.60ms  1.10s   3.43s   81.56%
    Req/Sec  7.85k   5.05k   26.89k 65.28%
  1339932 requests in 30.00s, 180.18MB read
  Socket errors: connect 0, read 0, write 0, timeout 29
Requests/sec:  44659.91
Transfer/sec:     6.01MB

HTTP/1.1に対応したRhebok、Gazelleと共にお試しくださいませ。

h2o はserver_starter経由のgraceful restartをサポートしているので試してみた。ついでにdockerで動かしてみた。

実際にwrkでベンチマークしながらコンテナにHUPシグナルを送り、graceful restartを行ってみたのが次の動画

左上がh2oをdocker経由で起動しているウィンドウ、HUPを受けてh2oのプロセスを入れ替えている様子がわかります。。左下はwrkの実行、右側はコンテナにHUPを送ってます。

HUPシグナルはdocker killを使って送ります。

$ docker kill --signal="HUP" $(docker ps |grep kazeburo/h2o|awk '{print $1}')

wrkの結果はこんな感じ。エラーは出ていません

vagrant@vagrant-ubuntu-trusty-64:~/wrk$ ./wrk -t 1 -d 10 https://localhost/
Running 10s test @ https://localhost/
  1 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     0.94ms    2.00ms  26.16ms   96.74%
    Req/Sec    12.71k     3.38k   24.78k    70.77%
  118981 requests in 10.00s, 45.39MB read
Requests/sec:  11896.85
Transfer/sec:      4.54MB

リクエストの取りこぼしなく、graceful restartが出来ることが確認できました。

h2o、X-Forwarded-Forやreverse proxy時にupstreamのサーバにHostヘッダを送る機能も付いたので、いよいよ実戦投入ができるようになって来たんじゃないかなと思うこのごろです。

特に、Dockerでアプリケーションサーバを動かし、デプロイの際に同じサーバ上のnginxやhaproxyの設定を書き換え、再起動しているところは、h2oにすることでパフォーマンスの向上が見込めるのではないでしょうか。

Dockerfileなど

Dockerファイルはこちらのrepositoryにあります。

Dockerfileはこのようになっています。start_serverはgolang版を使っています。インストール楽。

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 \
  && apt-get -y install git cmake libssl-dev \
    libyaml-dev libuv-dev build-essential \
    ca-certificates curl \
  && rm -rf /var/lib/apt/lists/*

# go-start-server
ENV GO_START_SERVER_VERSION 0.0.2
RUN curl -L https://github.com/lestrrat/go-server-starter/releases/download/$GO_START_SERVER_VERSION/start_server_linux_amd64.tar.gz | tar zxv -C /usr/local/bin --strip=1  --wildcards '*/start_server' --no-same-owner --no-same-permissions

# h2o
ENV H2O_VERSION 20150122
RUN git clone https://github.com/h2o/h2o \
  && cd h2o \
  && git submodule update --init --recursive \
  && cmake . \
  && make h2o
COPY h2o.conf /h2o/h2o.conf
COPY start.sh /h2o/start.sh
RUN chmod +x /h2o/start.sh
WORKDIR /h2o
ENV KILL_OLD_DELAY 1

ENTRYPOINT ["/h2o/start.sh"]
CMD ["/h2o/h2o.conf"]

最後の start.shはstart_serverのwrapper script。LISTENするポートを環境変数H2O_PORT経由で変更できるようにしてあります。

例えばポート、8080 でlistenした場合、h2o.confをこのように書き、

listen:
  port: 8080
  host: 0.0.0.0

hosts:
  "127.0.0.1.xip.io":
    paths:
      /:
       proxy.reverse.url: http://localhost:8081/
       proxy.preserve-host: ON

docker runする際に、環境変数にてポートを指定します。

$ docker run --net=host -e "H2O_PORT=80" -v $(pwd)/example_h2o.conf:/h2o/h2o.conf kazeburo/h2o

H2O_PORTはスペースで区切ることで複数のポートを書く事ができます。デフォルトは80と443をLISTENします。

ちなみに、現在のstart_serverはIPv4のみをサポートしているので、設定ファイルのhost: 0.0.0.0を書かないと、h2oがIPv6もbindしようとするので、エラーとなります。

ここも参考にしてくださいませ

Tengineはアジア最大級のECサイト「淘宝網」が公開しているWebサーバです。

Nginxをベースにいくつかの機能拡張を行い、また開発も続いていて最新のstableバージョンに追従しているようです。

主な機能拡張は上記のサイトにも上がっていますが、興味があるところを上げると、

  • nginx-1.6.2をベース。nginxと100%互換性がある
  • ダイナミックなモジュールの読み込みをサポート。モジュールの追加にTengineの再ビルドが必要ない
  • SO_REUSEPORT をサポート。接続がnginxの3倍高速化
  • SPDY v3をサポート
  • upstreamの負荷分散方式の追加。consistent hashやsticky session、upstreamのヘルスチェック、リクエスト処理中のホスト名の名前解決
  • access_logをremoteのsyslogに飛ばしたり、pipeを介しての出力に対応
  • cpu_affinityの自動設定

などなど、nginxを運用していていて(nginx plusにしかないとか、nginx plusにしかないとか、nginx plusにしかないとか)少し不満に思うところが強化されています。

このエントリではupstreamのヘルスチェックを試してみます。

Tengineのupstream health checkを試す

まず、Tengineをビルドします。環境はVagrant上のubuntu/trustyです。

sudo apt-get install -y language-pack-ja #警告避け
sudo apt-get install -y build-essential zlib1g-dev libjemalloc-dev
mkdir tengine
cd tengine
wget http://jaist.dl.sourceforge.net/project/pcre/pcre/8.36/pcre-8.36.tar.gz
tar zxf pcre-8.36.tar.gz
wget https://www.openssl.org/source/openssl-1.0.1k.tar.gz
tar zxf openssl-1.0.1k.tar.gz
wget http://tengine.taobao.org/download/tengine-2.1.0.tar.gz
tar zxf tengine-2.1.0.tar.gz
cd tengine-2.1.0
./configure --prefix=/home/vagrant/local/tengine \
        --without-dso \
        --with-http_stub_status_module \
        --with-pcre=../pcre-8.36 \
        --with-pcre-jit \
        --with-openssl=../openssl-1.0.1k \
        --with-jemalloc
make
make install

ここではpcreとopensslをstatic linkしていますが、システムのライブラリを使っても問題ありません。

upstream health checkの検証のために、tengineに8080、8081、8082の3つのポートをListenさせ、8080から他の2つへreverse proxyを行うように設定しました。

[tengine/8080] ---- [tengine/8081]
                 `- [tengine/8081]

設定ファイル

worker_processes  1;
daemon off;
error_log /dev/stderr info;
events {
  worker_connections  1024;
}

http {
  include     mime.types;
  default_type  application/octet-stream;
  access_log  off;
  upstream app {
    server localhost:8081;
    server localhost:8082;
    # 1000msec毎にチェックを行い、2回成功したらup、3回失敗したらdown、デフォルトはdown
    check interval=1000 rise=2 fall=3 timeout=1000 type=http default_down=true;
    check_http_send "GET /live.html HTTP/1.1\r\nConnection: close\r\nHost: localhost\r\n\r\n";
    check_http_expect_alive http_2xx;
  }
  server {
    listen 8080;
    location / {
      proxy_pass http://app;
    }
    location /status {
      check_status;
    }
  }
  server {
    listen 8081;
    location / {
      root html8081;
      index  index.html;
    }
  }
  server {
    listen 8082;
    location / {
      root html8082;
      index  index.html;
    }
  }
}

それぞれのドキュメントルートに、index.htmlとhealth check用のhtmlを置きます。

$ cd ~/local/tengine
$ mkdir html8081
$ echo "Hello World: 8081" >> html8081/index.html
$ echo "I'm live" >> html8081/live.html

$ mkdir html8082
$ echo "Hello World: 8082" >> html8082/index.html
$ echo "I'm live" >> html8082/live.html

そして、tengineを起動します。起動プログラム名はnginxのままです。

vagrant@vagrant-ubuntu-trusty-64:~/local/tengine$ ./sbin/nginx 
2015/01/14 06:08:26 [error] 1408#0: enable check peer: 127.0.0.1:8082 
2015/01/14 06:08:26 [error] 1408#0: enable check peer: 127.0.0.1:8081

さっそく何やらでてきました。チェックを行い、upstreamが2つ有効になったようです。ちなみに、上の設定では初期状態がダウンで、2回成功して初めてupstreamのサーバが有効になります。有効になる前にアクセスすると、502になってしまいます。

check_statusを設定したpathでupstreamの状態を取得できます。

vagrant@vagrant-ubuntu-trusty-64:~/local/tengine$ curl http://localhost:8080/status?format=json
{"servers": {
  "total": 2,
  "generation": 1,
  "server": [
    {"index": 0, "upstream": "app", "name": "127.0.0.1:8081", "status": "up", "rise": 59, "fall": 0, "type": "http", "port": 0},
    {"index": 1, "upstream": "app", "name": "127.0.0.1:8082", "status": "up", "rise": 60, "fall": 0, "type": "http", "port": 0}
  ]
}}

フォーマットは html, csv, jsonをサポートし、デフォルトはhtmlです。両方とも”up”となっています。残念ながらstatusを操作することはできないようです。

起動がちゃんとできているので、upstreamを一つ無効にしてみましょう。health check用のlive.htmlをrenameしてみます。

vagrant@vagrant-ubuntu-trusty-64:~/local/tengine$ mv html8082/live.html html8082/.live.html

するとエラーログに

2015/01/14 06:19:44 [error] 1426#0: *24 open() "/home/vagrant/local/tengine/html8082/live.html" failed (2: No such file or directory), client: 127.0.0.1, server: , request: "GET /live.html HTTP/1.1", host: "localhost"
2015/01/14 06:19:44 [error] 1426#0: check protocol http error with peer: 127.0.0.1:8082 
2015/01/14 06:19:45 [error] 1426#0: *28 open() "/home/vagrant/local/tengine/html8082/live.html" failed (2: No such file or directory), client: 127.0.0.1, server: , request: "GET /live.html HTTP/1.1", host: "localhost"
2015/01/14 06:19:45 [error] 1426#0: check protocol http error with peer: 127.0.0.1:8082 
2015/01/14 06:19:47 [error] 1426#0: *32 open() "/home/vagrant/local/tengine/html8082/live.html" failed (2: No such file or directory), client: 127.0.0.1, server: , request: "GET /live.html HTTP/1.1", host: "localhost"
2015/01/14 06:19:47 [error] 1426#0: check protocol http error with peer: 127.0.0.1:8082 
2015/01/14 06:19:47 [error] 1426#0: disable check peer: 127.0.0.1:8082

いくつかエラーが表示され、3回失敗しところで8082がdisableとなりました。check_statusでも確認します

vagrant@vagrant-ubuntu-trusty-64:~/local/tengine$ curl http://localhost:8080/status?format=json
{"servers": {
  "total": 2,
  "generation": 1,
  "server": [
    {"index": 0, "upstream": "app", "name": "127.0.0.1:8081", "status": "up", "rise": 103, "fall": 0, "type": "http", "port": 0},
    {"index": 1, "upstream": "app", "name": "127.0.0.1:8082", "status": "down", "rise": 0, "fall": 11, "type": "http", "port": 0}
  ]
}}

“down” と表示されました。

live.htmlを元に戻すと、

2015/01/14 06:22:51 [error] 1430#0: enable check peer: 127.0.0.1:8082

と表示され、再び8082が有効となりました。

health check機能は落ちたサーバを安全に切り離すのに使えるほか、デプロイ時にリクエストを取りこぼさないようにアプリケーションを再起動するのにも使えます。JVMなサーバを運用している場合はうれしいのではないでしょうか。あとはnginx plusと同じようにAPIで状態を操作できたら最高ですね

tengine、nginxのかゆいところに手が届く系のプロダクトとして覚えておくとよいかもしれません。

マスタリングNginxはisuconでも活躍しました

マスタリングNginx
マスタリングNginx
posted with amazlet at 15.01.14
Dimitri Aivaliotis
オライリージャパン
売り上げランキング: 260,111

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

SO_REUSEPORTはLinux Kernel 3.9からサポートされている機能で、複数のプロセス/Listenerから同じTCPポートをbind可能にして、Kernelが それぞれのプロセスに接続を分散してくれるという機能です。preforkなサーバはlistenしてからworkerをforkし、それぞれでacceptを行うという手順を踏みますが、SO_REUSEPORTを使えばその手順を踏まなくても複数プロセスから同じポートをListenして処理の並列性をあげたり、hot-depolyが実現できます。

Docker のHost networking機能とSO_REUSEPORTを使って、複数のコンテナから同じポートをbindできれば、コンテナのhot-deployができるんじゃないかと思ったので、試してみました。

SO_REUSEPORTについては以下のblogが参考になります。

DockerのHost networkingはdeeeetさんのblogがわかりやすい

Dockerなしで検証

まず、DockerなしでSO_REUSEPORTを試します。

サーバは Vagrant上のubuntu trusty

vagrant@vagrant-ubuntu-trusty-64:~$ uname -a
Linux vagrant-ubuntu-trusty-64 3.13.0-43-generic #72-Ubuntu SMP Mon Dec 8 19:35:06 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

検証はmemcahedを使います。簡単なので。。まず、memcachedにSO_REUSEPORT対応patchをあててbuildします。

patchはテキトウ

diff -ur memcached-1.4.22.orig/memcached.c memcached-1.4.22/memcached.c
--- memcached-1.4.22.orig/memcached.c   2015-01-01 16:50:52.000000000 +0900
+++ memcached-1.4.22/memcached.c        2015-01-06 22:48:07.000000000 +0900
@@ -4478,6 +4478,7 @@
 #endif

         setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, (void *)&flags, sizeof(flags));
+        setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, (void *)&flags, sizeof(flags));
         if (IS_UDP(transport)) {
             maximize_sndbuf(sfd);
         } else {

buildとmemcached起動

wget http://www.memcached.org/files/memcached-1.4.22.tar.gz
tar zxf memcached-1.4.22.tar.gz 
cd memcached-1.4.22
cat /vagrant/reuseport.patch |patch -p1
./configure
make
./memcached -U 0 -p 11211 -l 127.0.0.1

localhostの11211をbindしました。UDPは無効にします

ここでもう一つ vagrant sshして同じポートでmemcachedを起動

./memcached -U 0 -p 11211 -l 127.0.0.1

同じポートなので、通常であればエラーになりますが、SO_REUSEPORTを有効にしているので普通に起動します。

この状態で、11211に接続し、1回setしたのち、接続をやりなおしながら複数回getを繰り返すと、片方のmemcachedには値が存在しないので、getが成功したり失敗したりを繰り返すはずです。

こんなperlのスクリプトを書いて

#!/usr/bin/env perl

use strict;
use warnings;
use 5.10.0;
use Cache::Memcached::Fast;

sub connect_memd {
    Cache::Memcached::Fast->new({
        servers => [qw/127.0.0.1:11211/]
    });
}
connect_memd->flush_all for 1..10;
connect_memd->set("foo","bar");

for (1..10) {
    my $r = connect_memd->get("foo") // "-";
    say "$_, $r"
}

実行すると、

vagrant@vagrant-ubuntu-trusty-64:~$ perl /vagrant/test.pl
1, -
2, -
3, -
4, bar
5, -
6, bar
7, -
8, bar
9, bar
10, bar
11, bar
12, -
13, bar
14, -
15, -
16, -
17, bar
18, bar
19, -
20, bar

想定通りの動きとなりました。

Docker で SO_REUSEPORT

つぎはDockerでやってみます。

Dockerのバージョンは1.4.1

vagrant@vagrant-ubuntu-trusty-64:/vagrant$ docker -v
Docker version 1.4.1, build 5bc2ff8

Dockerfileはこんな感じ、memcachedにpatchをあててbuild

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 memcached 
RUN apt-get -y install curl
RUN mkdir -p /opt
RUN curl -s http://www.memcached.org/files/memcached-1.4.22.tar.gz > /opt/memcached-1.4.22.tar.gz
RUN cd /opt && tar zxf memcached-1.4.22.tar.gz
COPY reuseport.patch /opt/memcached-1.4.22/reuseport.patch
WORKDIR /opt/memcached-1.4.22
RUN patch -p1 < reuseport.patch
RUN ./configure
RUN make
EXPOSE 11211
CMD ["/opt/memcached-1.4.22/memcached","-u","nobody","-U","0","-l","127.0.0.1","-p","11211"]

docker buildして起動

$ docker build -t memcached_reuseport .
$ docker run --net=host memcached_reuseport

別のターミナルから、もう一つコンテナを起動

$ docker run --net=host memcached_reuseport

おなじ11211を使いますが、普通に起動できました。

psコマンドで2つ起動していることを確認

15548 ?        Ssl    1:10 /usr/bin/docker -d
  329 ?        Ss     0:00  \_ /bin/sh -c /opt/memcached-1.4.22/memcached -u nobody -U 0 -l 127.0.0.1 -p 11211
  340 ?        Sl     0:00  |   \_ /opt/memcached-1.4.22/memcached -u nobody -U 0 -l 127.0.0.1 -p 11211
  385 ?        Ss     0:00  \_ /bin/sh -c /opt/memcached-1.4.22/memcached -u nobody -U 0 -l 127.0.0.1 -p 11211
  397 ?        Sl     0:00      \_ /opt/memcached-1.4.22/memcached -u nobody -U 0 -l 127.0.0.1 -p 11211

ssコマンドで同じポートをlistenしていることを確認

vagrant@vagrant-ubuntu-trusty-64:~$ sudo ss -ltp|grep 11211
LISTEN     0      128             127.0.0.1:11211                    *:*        users:(("memcached",397,26))
LISTEN     0      128             127.0.0.1:11211                    *:*        users:(("memcached",340,26))

ホストサーバにて、先ほどのperlスクリプトを実行します

vagrant@vagrant-ubuntu-trusty-64:/vagrant$ perl test.pl
1, bar
2, bar
3, bar
4, -
5, -
6, bar
7, bar
8, -
9, -
10, bar
11, -
12, bar
13, bar
14, -
15, -
16, -
17, bar
18, bar
19, bar
20, bar

同じような結果が得られました。Dockerのhost networking機能とSO_REUSEPORTは組み合わせて使えそうです。

ということで、次はSO_REUSEPORTを有効にしたアプリケーションサーバを起動してhot-deployが可能どうか検証してみようと思います。わっふるわっふる

このアーカイブについて

このページには、2015年1月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2014年12月です。

次のアーカイブは2015年2月です。

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

ウェブページ

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