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エンジニア・ディレクタにお勧めの一冊です。

“Hello World”なベンチマークでUnicornに比べ2倍高速に動作するRackサーバをリリースしました。

rubygems: http://rubygems.org/gems/rhebok
github: https://github.com/kazeburo/rhebok

PerlのGazelleをベースに作っています。Rackアプリケーションの運用経験がほぼないので、機能不足があると思います。issue等で教えて頂ければ幸いです。

なぜ高速に動作するアプリケーションサーバが必要なのか

Unicornは高速に動作します。多くのアプリケーションにとっては十分でしょう。それでもRhebokでさらに上のパフォーマンスを出そうとしたのは、技術的なチャレンジの他に以下のようなアプリケーションで高速なアプリケーションサーバが必要とされると考えているからです。

  • ソーシャルゲーム、広告サーバ、SNSなど、高度に最適化が行われているサービス。1台のサーバで数百req/secを処理する
  • サーバコストに敏感な1PVあたりの利益が小さいサービス。効率のよいアプリケーションサーバによりサーバコストを下げられる可能性がある

同じ技術を用いて作られたGazelleは既にいくつかのサービスに投入され、CPU使用率を数%さげるなどの効果がありました。

ベンチマーク結果

nginxでReverse Proxyして、”Hello World”を返すベンチマークで約2倍のパフォーマンスを発揮します。

rhebok.png

ベンチマークはEC2上で行いました。32コアのc3.xlargeを使い、nginxのworker数を16、unicornとrhebokのworker数は8で起動し、wrkで500並列にて負荷を掛けました。現実とはかけ離れた環境となっているのは、unicornが高速に動作するので通常の環境ではなかなか差がでないためです。

ベンチマークの詳細はREADMEに書きました。

Rhebokの特徴と機能

Rhebokは以下のような特徴・機能をもっています

  • kazuhoさんのpicohttpparserを使った高速なHTTP処理
  • io処理をC拡張により実装
  • Linuxではaccept4(2)を使用
  • writevを使ったレスポンス出力
  • reverse proxyの背後に起動することを前提に、HTTP/1.0の機能のみをサポート
  • MaxRequestPerChild/MinRequestPerChildによる定期的なプロセス入れ替え
  • prefork_engineをつかったPreforkアーキテクチャ
  • start_serverを使用して、unix domain socketのlistenとhot deployが可能

PerlのStarlet、Gazelleといったサーバの移植なので同じオプションをサポートしています。詳しくはまた別の機会に紹介したいと思います。

ISUCON4 予選でUnicornをRhebokにしたときのスコア

sonotsさんがRaptorで試しているので、同じ事をRhebokでもチャレンジします。現実的なWebサービス環境でRhebokによるパフォーマンス向上がどの程度あるのか調べるのに、ISUCONの予選問題は適当な題材です。

ISUCON4 予選でアプリケーションを変更せずに予選通過ラインを突破するの術を参考に

  • mysqlのindex作成
  • my.cnfの変更
  • sysctl.confでポート数の拡大
  • nginxの設定

を行います。

まず、unicorn。

$ cat Procfile 
unicorn: bundle exec unicorn -E production -c unicorn_config.rb -l /dev/shm/app.sock
$ cat unicorn_config.rb 
worker_processes 4
preload_app true

プロセス数を10から4に下げています。

ベンチマーク結果

$ ~/benchmarker bench --workload 8
16:58:58 type:info      message:launch benchmarker
16:58:58 type:warning   message:Result not sent to server because API key is not set
16:58:58 type:info      message:init environment
16:59:07 type:info      message:run benchmark workload: 8
17:00:07 type:info      message:finish benchmark workload: 8
17:00:12 type:info      message:check banned ips and locked users report
17:00:13 type:report    count:banned ips        value:638
17:00:13 type:report    count:locked users      value:4468
17:00:14 type:info      message:Result not sent to server because API key is not set
17:00:14 type:score     success:193450  fail:0  score:41789

つぎに Rhebok

$ cat Procfile 
unicorn: bundle exec start_server --path /dev/shm/app.sock -- rackup -E production -s Rhebok -O MaxRequestPerChild=500000 -O MinRequestPerChild=400000 -O MaxWorkers=4 config.ru

unix domain socketを使用するためにstart_serverも使います

結果

$ ~/benchmarker bench --workload 8
16:57:05 type:info      message:launch benchmarker
16:57:05 type:warning   message:Result not sent to server because API key is not set
16:57:05 type:info      message:init environment
16:57:13 type:info      message:run benchmark workload: 8
16:58:13 type:info      message:finish benchmark workload: 8
16:58:18 type:info      message:check banned ips and locked users report
16:58:20 type:report    count:banned ips        value:748
16:58:20 type:report    count:locked users      value:4581
16:58:20 type:info      message:Result not sent to server because API key is not set
16:58:20 type:score     success:203370  fail:0  score:43933

Unicornと比べ、2000程度のスコアが上昇し、Rhebokに変更することで一定の効果があるということがわかりました。

ぜひ、使ってみてくださいませ!

Gazelle という新しいPlack::Handler(Server)をリリースしました

https://metacpan.org/release/Gazelle

前のISUCONの結果報告で「Chobi」として紹介していたものを名前を変更しました。

GazelleはnginxやApacheでreverse proxyを行うことを前提に書かれたPlack::Handlerです。nginxの後ろにunix domain socketを利用して配置した場合、”Hello World”のベンチマークで、Starmanの3倍、Starletの1.7倍程度高速に動作します。

gazelle_bench.png

一番左はnginxで静的ファイルを配信した場合のQPSです

ベンチマークの詳細はこちら↓です

https://github.com/kazeburo/Gazelle/wiki/Benchmark

特徴など

Gazelleは以下のような特徴をもっています

  • HTTP/1.0のみをサポート。ただしKeepAliveはサポートしない。これにより実装がシンプルに
  • io処理をすべてXSで記述。Perlのファイルハンドルは使わない
  • Linuxではaccept4(2)を使う。accept4(2)がサポートされていないプラットフォームではaccept(2)を使います
  • kazuhoさんのpicohttpparserを使って高速にリクエストのパースを行う
  • acceptから最初のpacketを読み込み、ヘッダーをパースするまで1つのXSで書かれた関数内で処理する。Perlの処理が入らず、またZero Copyで動作する
  • レスポンスの書き出しはwritev(2)を使う
  • Starletを使っているところはそのままの設定で入れ替え可能

その他、Starletと同じく

  • Parallel::Prefork によるPreforkアーキテクチャ
  • Server::Starterを使ってのhot deploy

一言でまとめると、kazuhoさんのソフトウェアで出来ているサーバです。既に某サービスの1台のサーバで使い始めたのでぜひ使ってみてください。

去年に引き続き、ISUCONにLINEの選抜チーム「チーム生ハム原木」で出場して優勝することが出来ました!!!!

@tagomoris、@sugyan お疲れ様でした!!

#isucon 2014で優勝しました - すぎゃーんメモ

最後の最後、残り15分でnginxの設定を行う場所を間違えていたということに気付き、ローカルのベンチマークでしか検証ができず、どの程度のスコアになるのか、またfailするのか分からない状況でしたが、結果的に良いスコアになってほっとしました。

自分でも何度も言いながら「nginxのrewriteはinternal redirect」の大原則を忘れていました。はい。1日100回唱えるようにします。

予選アプリケーションの復習

劇的なスコアは出ていませんが、地道に復習をしていて、

$ ~/benchmarker bench --workload 8
07:26:29 type:info      message:launch benchmarker
07:26:29 type:warning   message:Result not sent to server because API key is not set
07:26:29 type:info      message:init environment
07:26:44 type:info      message:run benchmark workload: 8
07:27:44 type:info      message:finish benchmark workload: 8
07:27:49 type:info      message:check banned ips and locked users report
07:27:52 type:report    count:banned ips        value:1048
07:27:52 type:report    count:locked users      value:5478
07:27:53 type:info      message:Result not sent to server because API key is not set
07:27:53 type:score     success:291580  fail:0  score:62985

これくらいの数字が出るようになってました。nginx、perl、redisの構成で、CSSの出力も変えず、GOGCもデフォルトのままです。CSSと画像を消すと、23万ぐらいだったかな。

具体的に、どんなことをやったのかは別のエントリに書こうと思いますが、新しく2つのモジュールを作りました。

  • Redis::Jet - 高速に動作し、Redisサーバからの返事を待たない「noreplyモード」を備えるRedisクライアント
  • Plack::Handler::Chobi - nginxのupstreamのサーバとして動作するという前提で書いたアプリケーションサーバ。HTTP/1.0のみサポート、KeepAliveもサポートしませんが、クライアントのコネクションとヘッダーのパースがすべてC言語で書かれている。環境にもよるがStarletの1.5倍から2倍ぐらい性能がでる

両方とも銀の弾丸ではないですが、高RPS勝負になった時に少しでも上に行く為の武器になるんじゃないかと考えてました。

復習したrepositoryはこちら

https://github.com/kazeburo/isucon4-elimination-myhack

準備

ISUCONは準備も大切です。

去年の本選や今年の予選と同じようにgithubのprivate repositoryを用意し、そこに最初に行うべきsshの鍵の配置やgitの設定、使いそうなソフトウェアのインストール方法をまとめました。

ソフトウェアはOpenRestyやmemcached、MySQL、cpanfileに追加するモジュールのリストなど、ほとんど去年と同じ内容ですが、今年はRedisが新しく加わっていました。

本選のアーキテクチャ

動画ファイルを配信するサービスということで、とりあえず動画ファイルを全てのサーバに配るという去年と同じようなアーキテクチャをとりました。これがその時に書いたメモ。

IMG_2748.jpg

この図では1番でアップロードを受けて、2,3に配っていましたが、実際には1番のサーバにも動画ファイルを配りました。

nginxの設定をtagomoris、動画の配布とログをRedisに書きだすアプリケーションの改修をsugyan、サーバの足回りの設定を自分が担当して作業を行い、上の構成となったのが14時ぐらい。そこで1Gbpsの壁にあたってしまい、何をしてもスコアがあがらない時間がはじまりました。

スコアが上がらない中で、tagomorisが広告取得時のredirectをなくしたり、nginxから直接動画を返したり、sugyanが動画の中で一番サイズの小さいものを返すように改修を行っていましたが、どれも1Gbpsに壁にあたっている中では効果がでずに悩んでいました。

動画を返さない方法を探る

1Gbpsの壁を突き破る為には動画ファイルを返さない方法があるはずだ、ということであれこれ開始したのが17時過ぎ。gitのcommitをみると、どうやら「チームフリー素材」さんが33万点を出す前からやっていたようですが、33万点がでたことで、必ずその方法あるだろうということでいろいろ試しました。

例えばこんなのとかやってました。

location ~ /asset$ {
  # /slots/{slot:[^/]+}/ads/{id:[0-9]+}/asset
  #rewrite ^/slots/(.*)/ads/([0-9]+)/asset$ /data/$1-$2.mp4;
  return 206;
}

他にもetagをonにしてみたりしましたが、どれも効果がなく、straceしてもIf-None-MatchやIf-Modified-Sinceを送って来ている形跡がみつからないので、もう半分諦めムードでしたが、最後に一つnginxの設定を自分が間違えていた事に気付きました。

 location ~ /asset$ {
   # /slots/{slot:[^/]+}/ads/{id:[0-9]+}/asset
   rewrite ^/slots/(.*)/ads/([0-9]+)/asset$ /data/$1-$2.mp4;
+  expires 24h;
 }

 location /me {

これ、17:44のcommitです。はい、すみません、死にます。大変申し訳ございません。

「nginxのrewriteはinternal redirect」「nginxのrewriteはinternal redirect」「nginxのrewriteはinternal redirect」「nginxのrewriteはinternal redirect」もう皆さん分かりますよね。正解はこう書かなければなりません。

location /data {
  root /data;
  client_body_temp_path /data/client_temp;
  dav_methods PUT DELETE MKCOL COPY MOVE;
  create_full_put_path  on;
  dav_access            all:rw;
  expires 24h;
}

location ~ /asset$ {
  # /slots/{slot:[^/]+}/ads/{id:[0-9]+}/asset
  rewrite ^/slots/(.*)/ads/([0-9]+)/asset$ /data/$1-$2.mp4;
}

このcommitが18:43。いやぁ、厳しかった。。。

あれこれ

最終的に何が起きたか、確認はしてないのですが、たぶんこういう事でしょう

  • expiers 24hがついたことで、レスポンスにCache-Contol: max-age=86400、Expiresヘッダが付くようになった
  • ベンチマークツールがそれをみて、初めてIf-Modified-Sinceを送るようになった

長年画像配信野郎をやってますが、正直「おや。。」という感想をもちます。通常であれば

  • Last-ModifiedヘッダがあればブラウザやCDN、Proxyサーバはコンテンツをとりあえずキャッシュする
  • ソフトウェアによって異なるが、ブラウザやCDNは任意のタイミングでサーバにIf-Modified-Since付きでリクエストを送る。サーバはコンテンツが更新されてなければ304を返す事ができる
  • サーバ側がCache-ControlやExpiresヘッダを返した場合は、そのヘッダで定められた期限までブラウザ、CDN、Proxyはキャッシュを行い、サーバには一切アクセスしない。 — ただし、CDNやProxyの設定で最大の有効期限が決まっていることもあるので、その場合はCache-Controlヘッダで決めた期限より速くアクセスがくることがある — 期限がすぎてアクセスした場合にIf-Modified-Sinceヘッダが付く(と思う)

もし、ベンチマークツールがLast-Modifiedを検出するような仕組みになっていれば、うちのチームでは14:26の時点で304が返せるようになって、もっとアプリケーションのチューニングに集中できていたのかなと思う。その上でCache-Controlを指定することでアクセスすら来なくなってさらに高得点がでるとかあったかもしれない。

んで、どうしたらこのあたりの動作が分かりやすくなるか、一つの案としてはベンチマークがLast-Modifiedをみて、If-Modified-Sinceを送り、さらに「Cache-Control: max-age=0」を送っていれば分かりやすいかなと思いました。これはブラウザが「リロード」を繰り返している状態、またはCDNでcacheの有効性を毎回確認する機能を使っている状態と同じです。

これだと、動画をファイルに落としてnginxから配信したチームから点数をかなり上げて、高RPS勝負になりやすかったのかなと思います。 ただ、簡単にそうはさせない罠を用意する必要があったかもしれませんね。そのネタはあります。

最後に

問題出題のクックパッドの@mirakuiさん、@rosylillyさん、@sora_hくん、大変おつかれさまでした。ぎりぎりでした!!wishlist公開お願いします!

テコラスの皆様、941さん、やぶたさんをはじめ運営スタッフのみなさまありがとうございました!あと、途中で応援に来てくれた奥さんと、息子と娘にも感謝!

最後に 2

今回、選抜チームをのぞき、弊社から本選に残ったチームがいなかった。これはどうにかしたい

最後に 3

頂いたドローン、家の中で動かしたら音が大きかったのか娘が号泣しました。次は外でやりたい

IMG_2745.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
  • corelistweb.png

ウェブページ

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