kamipoさんOracle ACEおめでとうございます。

MyNA(MySQLユーザ会)会 2015年8月 でメルカリのデータベース戦略とPHPについて喋って来たので、資料を公開します。

内容はWEB+DB PRESS Vol.88の記事に書いたこと+新ネタと、PHP(PDO)の話です。MySQL 5.7のところにみなさん驚かれていたようです。

他の方の発表では、dimSTATが面白かったですね。あのグラフをどうやって作っているのか全くしらなかったので、勉強になりました。あれはベンチマークしたくなります。また、MySQLで困っている人をみつけて助けてあげようとするkamipoさんの情熱も、どこから沸いてくるのか不思議ですが、さすがでした。

開場のyoku0825さんありがとうございました。みなさまお疲れさまでした。

実は、この会で喋る事をすっかり忘れていて、YAPC::Asiaの懇親会の時に、yokuさんに「木曜日のタイトルお願いします」僕「え??ああ、あああああ!」という会話をして思い出しました。ほんとすみませんでした。いやぁ、YAPC::Asiaで1時間のトークをしたあと中4日で40分のトークの準備は大変でした..

YAPC::Asia Tokyo 2015で「ISUCONの勝ち方」という発表をしてきました。

技術的な内容も後半にありますが、ISUCONがどうして始まったのか、ISUCONで良い成績をだすためにはどんな準備をして、チューニングのためにどんなことを考え、調査するのが良いのかについて喋ってきました。

発表が2日目の朝イチでしたが、多くの方にきて頂き、ありがとうございます。この発表でISUCONになるべく多くの方に興味を持って頂き、参加する方が増えればいいなと思ってます。

今年のISUCONの参加者募集はすでに始まってます。

ISUCON5 オンライン予選の参加登録を開始&参加チームとメンバーリスト : ISUCON公式Blog

ぜひ、ご応募ください。私はメルカリのインフラチームのエンジニアと共に出場します。よろしくお願いします。

Lightning Talk

@uzulla氏がPHPの話を所望されていたので、PHPのエラーログ周りを整理しつつNorikraの話をしました

1日目にLightning Talkができると、自分のセッションの宣伝ができるので良い

さいごのYAPC::Asia Tokyo

最高のYAPC::Asiaでした。牧さん、スタッフの皆様ありがとうございました!おつかれさまでした。 再来年(?)にまた会える事を楽しみにしています。

IMG_3757.jpg

奥さんと息子、娘が応援に来てくれたので、息子とホールの壇上でパシャリ。いつもありがとう。

久しぶりに喋ってきました。Mackerel meetup #4Shibuya.pm Tech Talk #17ではLTを、Norikra meetup #2では少し長めの話をさせて頂きました。

資料3つ貼っておきます。

メルカリでもサーバ・運用周りの仕事をしています。メルカリではzから始まるモニタリングツールをメインに使用していて、サーバ周りのさまざまなデータを突っ込んで監視に役立てていますが、カジュアルにグラフをつくって、アラートを仕掛けるという用途には向いていないなぁと思ってます。

そこで、Norikra と Mackerelを組み合わせて柔軟にログの可視化+閾値の設定を行うってのを思いついて設定したところ、結構うまく行って、それについてtwitterでつぶやいていたところ、今回のような機会を頂いたというわけです。

harukasanのログ解析ツールのまとめは非常にわかりやすく、fujiwaraさんの使い方はさすがでしたし、Gunosyの2段Norikraは目から鱗でした。みなさまありがとうございました。参考にさせて頂きます。

久々に開催されたShibuya.pmではyokohama.pmぐらいでしか紹介していなかったGazelleについて喋ってきました。前日にc4.8xlargeでベンチマークを行ったらすごい数字がでたのでびっくりした。livedoorBlogでまだ動いているようだし、安定していると思うので、ぜひ使って頂けたらと思います。

同僚のbokko氏のNginxのチューニングもよくまとまっていたし、kazuhoさんのHTTP/2の優先度制御も具体例があって参考になりました。弊社だとスマホアプリのバックエンドのAPIが中心になる訳だけど、API毎の優先度の制御とかできるのかなぁと考えてみたりしました。LTではひさびさにPerlの話がいっぱい聞けて、楽しかったです。みなさまありがとうございました。

最後に、YAPC::Asia Tokyo 2015に、「ISUCONの勝ち方」というトークを応募しています。今年も開催されることが発表され、多くの注目が集まっています。ご興味をもった方はぜひブックマークやtweet、いいねなどをして頂けるとうれしいです。

http://yapcasia.org/2015/talk/show/86ebd212-fab3-11e4-8f5a-8ab37d574c3a

1000万ダウンロードと会社の2周年という記念の日に入社しました。

iOS、Androidアプリのダウンロード数はもちろん、商品の出品数や流通額も大幅に伸びています。また、アメリカでの展開等も進んでいるので、サーバ・フロントのエンジニアを募集しています。ご興味のある方はぜひご連絡くださいませ。

3月には六本木ヒルズへのオフィス引越も予定されています。mixi入社前にlivedoorのセミナーでヒルズへ行ってから9年。ここに通うことになるとは思いもしませんでした。

今後の仕事

仕事は変わらずサーバ周りの運用・パフォーマンス改善、スケーラビリティの向上です。モニタリングツールの整備を足がかりにMySQL周りの最適化や可用性向上を行い、PHPのアプリケーションのチューニングなどに携わって行きたいなと考えております。

何よりも自分たちの選択した技術で、ユーザに信頼性の高いサービスを提供できるよう努力して参ります。引き続き、多くの皆様のお世話になると思いますので、よろしくお願いします。

ウィッシュリスト置いておきますね

去年から今年にかけて高速なPSGIサーバとRackサーバを作り、そしてPHPの会社に入ったということで、YAPC::Asia Tokyo、Ruby Kaigi、PHP カンファレンスの3つで喋ることが今年の目標です。

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が可能どうか検証してみようと思います。わっふるわっふる

サーバの監視やモニタリングなどのサービス・ソリューションを提供するMSP(マネージメントサービスプロバイダ)のハートビーツ馬場さんが「Webエンジニアが知っておきたいインフラの基本」という本を出版されました。



献本頂きありがとうございます!!

冬休みは実家に帰ったり、旅行に行ったりと何かとイベント事が多くなかなか本を読む時間が取れなかったり、年が明ければ弟妹・甥っ子姪っ子にお年玉を上げないとならず、自由に使えるお金も減ってしまうかもしれません。なので、読んでみて欲しい本が何冊もあっても困ってしまいますね。そこで自分がお勧めしたいのが、この一冊「Webエンジニアが知っておきたいインフラの基本」です。

本屋に行く時間がない方も安心。電子版があります

Webエンジニアが知っておきたいインフラの基本 インフラの設計から構成、監視、チューニングまで【委託】 - 達人出版会
http://tatsu-zine.com/books/web-engineer-ga-sitteokitai-infrastructure-no-kihon

この本の目次ですが、

第一章 Webサービスにおけるインフラの領域
第二章 インフラ技術の基礎知識
第三章 Webサービスのサーバ構成ベストプラクティス
第四章 インフラ手配の基礎知識
第五章 Webサービス運用(1) システム監視の基本
第六章 Webサービス運用(2) ステータスモニタリング
第七章 Webサービスのチューニング(1) ボトルネックの見つけ方
第八章 Webサービスのチューニング(2) チューニングレシピ

となっています。

前半の一章から四章はネットワークやシステム構成の基礎をふまえて、要件定義や設計のベストプラクティスが書かれています。広範囲な内容にも関わらずわかりやすくまとまっているので、この前半の章はエンジニアだけではなく、エンジニアと一緒に仕事をするディレクタにも読んで欲しいところです。某社でディレクタをやっている妻も「会社の同僚に読ませたい」と言っていました。

後半はまさに自分の得意としている領域ですが、運用監視を行う場合の指針、監視項目の作り方、障害対応の方法や心構えから始まります。六章以降はCactiのグラフを例にメトリクスグラフの読み方、負荷状況を調査するためのLinuxのコマンド、パフォーマンスを出すためのチューニングのポイントなどが紹介されています。

第六章以降は実際の業務で多いに役に立つ内容であることがもちろんなのですが、最も重要なことは第五章に書かれています。

その中でも

「運用とは育てること! システムそのものだけではなく、手順や監視定義などもはじめから多くは望まず、随時育てましょう」
「重要なのは方針定義・確認・復旧を繰り返しながら育てること!」

と2回も枠をとって書かれているのが印象的です。システムを運用している限り障害は必ず起きるものです、障害はシステムを成長させるチャンスです。そのチャンスを潰さずに運用を育てるヒントが五章で紹介されています。


DevOpsによってインフラを動かす主体がアプリケーションエンジニアに移り、さらにmicroserviceによって担当するサービスの可用性やスケーラビリティの責任が開発するチームに委ねられるようになってきています。障害に気付けない、対応できない、再発対策が実行できないままで居られることはできません。ぜひこの本を手に取って、サービスの運用監視を考える機会になればと思います。全てのWebエンジニア・ディレクタにお勧めの一冊です。

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

アイテム

  • IMG_3757.jpg
  • rhebok_tcp_unix.png
  • rhebok.png
  • gazelle_bench.png
  • IMG_2745.jpg
  • IMG_2748.jpg
  • benchui.png
  • gf_hook_gh.png
  • gf_gh_trigger.png
  • corelistweb2.png

ウェブページ

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