2015年02月

yohoushi (rails) + h2o 1.0.1 でHTTP2有効にしたときのバッドノウハウ

yohoushi というグラフツールがある。 小ネタ: kurado + h2o 0.9.1 でHTTP2有効にしたときのバッドノウハウ にインスパイアされて、yohoushi でも h2o 使いたい!と思って試してみた。

HTTP2 の特徴

HTTP2 には以下のような特徴がある

  1. バイナリでの通信
  2. リクエストとレスポンスの多重化によるレイテンシの削減
  3. ヘッダの圧縮によるプロトコルオーバーヘッドの最小化
  4. リクエスト優先度設定
  5. サーバープッシュ

今回は多重化によるレイテンシの削減効果を狙っている。 大量の画像ファイルを配信するので、yohoushi サイドでブロッキング処理をしている間に、グラフ画像は仮想ストリームで並列的に転送しておいてもらえると助かるわけだ。

やってみる

h2o のビルド

h2o.conf は以下のようにした。

max-connections: 1024
num-threads: 4

listen:
  port: 443
  ssl:
    certificate-file: examples/h2o/server.crt
    key-file: examples/h2o/server.key
hosts:
  "*":
    access-log: /dev/stdout
    paths:
      /:
        proxy.reverse.url: http://127.0.0.1:4804/

HTTP2 的には https 必須ではなくなったらしいが、chrome は(まだ) https 必須のようなので、SSL を有効にしている。

で、chrome を --enable-spdy4 付きで起動するか、chrome://flagsからEnable SPDY/4を有効にしてアクセス。してみたけど、動かない!!!!  

HTTP2 クライアント

デベロッパーツールの console に net::ERR_SPDY_COMPRESSION_ERROR と出てるが意味がわからない。

Chrome だと埒が明かなかったので、HTTP2 の通信をみれるクライアントツールがないか聞いてみた(つぶやいてみた)ところ、以下のツールがあるようだった。

nghttp2 をインストールして nghttp を使った。

$ nghttp --verbose https://localhost
  0.067] recv (stream_id=1, noind=0) :status: 200
[  0.067] recv (stream_id=1, noind=0) server: h2o/1.0.1-alpha1
[  0.067] recv (stream_id=1, noind=0) date: Tue, 24 Feb 2015 04:47:57 GMT
[  0.067] recv (stream_id=1, noind=0) status: 200 OK
[  0.067] recv (stream_id=1, noind=0) x-frame-options: SAMEORIGIN
[  0.067] recv (stream_id=1, noind=0) x-xss-protection: 1; mode=block
[  0.067] recv (stream_id=1, noind=0) x-content-type-options: nosniff
[  0.067] recv (stream_id=1, noind=0) content-type: text/html; charset=utf-8
[  0.067] recv (stream_id=1, noind=0) etag: W/"4f1320dd91df8855ee7ca822875bd087"
[  0.067] recv (stream_id=1, noind=0) cache-control: max-age=0, private, must-revalidate
[  0.068] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=COMPRESSION_ERROR(0x09), opaque_data(0)=[])
Some requests were not processed. total=1, processed=0

body が返らずにすぐ GOAWAY になってしまっているようだった。

バッドノウハウ

色々と試してみた結果、レスポンスから Set-Cookie ヘッダを削除するとなんと動く。rails new したばかりの素の rails でも試してみたが、同様だった。rails の場合、ActionDispatch::Cookies Rackミドルウェアを削除すれば付与自体を防げる。

--- a/config/application.rb
+++ b/config/application.rb
@@ -34,5 +34,7 @@ module Yohoushi
+
+    config.middleware.delete ActionDispatch::Cookies

って、これじゃ困るので Issue にしたためました! もし「こうするべきだよ!」という方法をご存じの方がいたら是非教えてください!

(追記) 2015/02/25 早速 kazuho 先生に直して頂きました!=> pull request 
ということで、1.0.2 から h2o on rails が問題なく動くはずです!

(追記) http2 RFC 発行記念 祝賀飲み会 LT 大会 でどれぐらい高速化したのか具体的な数字付きで話して来たので資料をおいておきます => HTTP/2 でリバプロするだけでグラフツールを 高速化できた話 

Server::Starter で Unicorn を起動する場合の Slow Restart

「Server::Starterに対応するとはどういうことか」の補足 の続き。

Unicorn の新しいプロセス(master + worker) が全て新規に立ち上がるのを見届けてから古いプロセスを殺す方法だと、一時的にプロセス数が全体の2倍になり、最悪の場合メモリ使用量が平常時の2倍になりえる (CoW が効くのでよっぽどの状況でなければ2倍にはならない)

これを防ぐためには、新しい worker プロセスを1つ起動できたタイミングで古い worker プロセスを停止し、最大でも N 個のプロセスしか立ち上がらないように制御を行う。また、新しいプロセスも一気に立ち上げるのではなく徐々に立ち上げるとより安心だ。

通常のやり方

Unicorn 単体で SIGUSR2 でホットスタートを行っている場合は、unicorn.conf.rb のサンプルに書いてあるやり方でスローリスタートできる。

cf. https://github.com/defunkt/unicorn/blob/master/examples/unicorn.conf.rb

pid "/path/to/pid/file/to/generate.pid"

before_fork do |server, worker|
  # This allows a new master process to incrementally
  # phase out the old master process with SIGTTOU to avoid a
  # thundering herd (especially in the "preload_app false" case)
  # when doing a transparent upgrade.  The last worker spawned
  # will then kill off the old master process with a SIGQUIT.
  old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end

  # Throttle the master from forking too quickly by sleeping.  Due
  # to the implementation of standard Unix signal handlers, this
  # helps (but does not completely) prevent identical, repeated signals
  # from being lost when the receiving process is busy.
  sleep 1
end

Unicorn は SIGUSR2 を受け取ると新しい unicorn master プロセスを起動しつつ、古い master プロセスの pid を .oldbin ファイルに書き込む。

worker が1つ起動される度に、旧 unicorn master に TTOU シグナルを送る。Unicorn は TTOU シグナルを受け取ると worker の数を decrement し、graceful shutdown する

worker が全て立ち上がったら 旧 unicorn master に QUIT シグナルを送り、master プロセスも終了させる。

Server Starter と組み合わせた場合

--status-file と ENV['SERVER_STARTER_GENERATION'] を利用する。また、--signal-on-hup=CONT のように無害なシグナルを設定する(理由は後述)。

start_server --status-file=/path/to/app/log/unicorn.stat \
  --port=10080 --signal-on-hup=CONT --dir=/path/to/app -- \
  bundle exec --keep-file-descriptors unicorn -c config/unicorn.conf config.ru

status file に世代id と server starter が fork したプロセス (unicorn master) の pid が : 区切りで書き込まれている。

1:10658

server starter に HUP シグナルを送ると、unicorn の新世代を起動してくれる。status file の内容は一時的に次のように新旧世代が存在するようになる。

1:26237
2:12104

このファイルを unicorn.conf から読み込むように書いておき、新しい unicorn プロセス起動中の処理で、unicorn worker が fork されるたびに 旧世代のプロセスに TTOU を送り、徐々に古いプロセス数を減らしていくようにする。旧世代のプロセスが残り1個になったら最後に QUIT シグナルを送り、旧 unicorn master ごと終了させる。

status_file = '/path/to/app/log/unicorn.stat'

before_fork do |server, worker|
  # Throttle the master from forking too quickly by sleeping.  Due
  # to the implementation of standard Unix signal handlers, this
  # helps (but does not completely) prevent identical, repeated signals
  # from being lost when the receiving process is busy.
  sleep 1
end

after_fork do |server, worker|
  # This allows a new master process to incrementally
  # phase out the old master process with SIGTTOU to avoid a
  # thundering herd (especially in the "preload_app false" case)
  # when doing a transparent upgrade.  The last worker spawned
  # will then kill off the old master process with a SIGQUIT.
  begin
    pids = File.readlines(status_file).map {|_| _.chomp.split(':') }.to_h
    old_gen = ENV['SERVER_STARTER_GENERATION'].to_i - 1
    if old_pid = pids[old_gen.to_s]
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, old_pid.to_i)
    end
  rescue Errno::ENOENT, Errno::ESRCH => e
    $stderr.puts "#{e.class} #{e.message}"
  end
end

ちなみに、status file の内容は旧世代が殺されると(亡くなったことが確認されると)、新世代のみになる

2:12104

unicorn.conf 内に旧 unicorn master を QUIT する処理を書いたので、server starter では--signal-on-hup=QUIT だったものを --signal-on-hup=CONT として無害な CONT シグナルを送るようにしたわけだ。--kill-old-delay=10 で時間を調節する必要もなくなった。

なお、before_fork だと1時的にプロセス数が 1 減る(起動前に旧 worker を減らしてしまう)ので、after_fork に移動している。 最大で N+1 個のプロセスが同時に立ち上がることにはなる。

まとめ

まとめるとこう > gracefully restart unicorn using Server::Starter - gist

最近 Qiita に書いてる Chef ネタ一覧

大体黒い

A Ruby and Fluentd committer working at DeNA. 記事本文および記事中のコード片は引用および特記あるものを除いてすべて修正BSDライセンスとします。 #ruby #fluentd #growthforecast #haikanko #yohoushi #specinfra #serverspec #focuslight
はてぶ人気エントリー