ISUCON5 本戦に参加して5位でした

去年 に引き続き†空中庭園†《ガーデンプレイス》として @ryotarai@gfx と参加して、結果は5位でした http://isucon.net/archives/45869121.html 。コードは https://github.com/ryotarai/isucon5

ISUCON5 予選の反省として、最初はプロファイルと手元での動作に最低限必要な変更以外はせずにコードを読むと決めていて、nginx の log_format を変えたり stackproflineprof を仕込んだりしてベンチマークを実行した後は、各自アプリケーションの理解に集中していた。 僕は /data から叩かれている外部 API の挙動をとくに調べていて、

  • GET リクエストしかないし、エンドポイントの情報は DB に入ってるけど更新されることもない
  • ken と ken2 という2つの似たエンドポイントがあるけど、実は全く同じっぽくて、しかも zipcode に対して返ってくる答えは一定っぽいので、2つをまとめた上で zipcode をキーにしてキャッシュできそう
  • surname、givenname もそれぞれ q をキーにしてずっとキャッシュできそう
  • tenki は毎回違う値が返ってくる
    • 実際には3秒くらいで変わることに @ryotarai が後で気付いて、その期間だけキャッシュするようにしていた
  • perfectsec、perfectsec_attacked の2つは https でリクエストするようになっていて、http だと通らない
  • perfectsec、perfectsec_attacked の2つも毎回違う値が返っていてキャッシュできなさそう
    • 実際には perfectsec_attacked は20秒くらいはキャッシュできることが後からわかって、最終的にはキャッシュが入ってた
  • それぞれのエンドポイント間に依存関係はなくて、全部並列にとってこれそう
    • このときは適当に Thread.start すればいいかと思っていたけど、@gfx が並列化を実装するときにちゃんとスレッドプールくらいは使いたいという話から expeditor 使おうということになった

あたりを見ていた。

だいたい12:30頃から実際に app.rb を編集し始めて、僕は

  • ken2 の結果を Redis に無期限でキャッシュしてベンチマークが通るかどうか → 通った
  • endpoints を DB から Ruby の定数にもってくる
  • ken を ken2 にマージしてベンチマークが通るかどうか → 通った (これで実質 ken もキャッシュされている)
  • surname、givenname も無期限でキャッシュしてベンチマークが通るかどうか → 通った
  • users を PostgreSQL から Redis へ。同時にパスワードをハッシュするのをやめて直接保存するように

ということをしていた。このへんが終わったのが15時過ぎくらいで、1台のみ使って2万点くらい出していた気がする。 ここから @ryotarai が3台使うような構成に変更していって、5万点をこえ始めていた。 Web API コールがどうしても遅くて /data が詰まると unicorn のワーカを使い切ってしまうので、unicorn のワーカ数をかなり大きめにしつつ3台に配置し、a にはベンチマーカからリクエストを受ける nginx を置いて3台にプロキシし、b には Redis を置くようにした。 endpoints は Ruby の定数にしたし、users は /initialize で Redis に入れてるし、subscriptions も @gfx が /initialize で Redis に入れるようにしていたので、すでに PostgreSQL は /initialize 以外では不要になっていた。

ベンチマークがたまに fail するようになっていて、そのデバッグで16:30頃から1時間くらい使っていた。何で fail しているのか分かるまで時間がかかってしまったのがつらい。 最終的にわかったのはどうも /initialize がたまに30秒以内に終わってないっぽくて、/initialize は Redis がいる b に常にプロキシするようにしたら安定した。 スコアは5万点だったり3万点だったりして、あんまり安定しなくて結構不安だった。

最後は a にいる nginx の CPU 使用率が高いのが気になっていて、@ryotarai が振り分けの重み付けを調整しながら僕はアセットを gzip して gzip_static するようにしたりしていた。 終了15分前くらいに念のため3台とも reboot してみて、ちゃんと Redis が起動してベンチマークが fail しないことを確認して終了。 最終結果のスコアは86,210点で、競技中のベストよりも高い結果だった。

終了後に聞いた話の中では RACK_ENV=production が非常に痛かった http://diary.sorah.jp/2015/11/02/isucon5f 。3位との差はそんなに大きくなかったので、もしかしたら3位には入れていたかもしれないと思うと悔しいけどしょうがない。

去年から参加し始めたけど去年よりよくなってる感触はあるので次は入賞したい。

Unicorn 5.0.0 の sd_listen_fds(3) emulation とは

Unicorn 5.0.0 がリリースされていて、most boring major release と自称している中、新機能として sd_listen_fds(3) emulation がある http://bogomips.org/unicorn-public/20151101-unicorn-5.0.0-rele@sed/t/

sd_listen_fds(3) は systemd の socket activation という機能に対応するための単純な関数で、socket activation は簡単に言うと systemd が listen してその fd を service unit のプロセスに渡す機能。 この機能の詳細やモチベーションを説明するのは面倒なので作者のブログ記事などを読んでほしい http://0pointer.de/blog/projects/socket-activation.html

今回 Unicorn はそれに対応したため、以下のような unicorn.service と unicorn.socket を用意することで、socket activation が可能になる。

[Unit]
Description=Unicorn

[Service]
User=non-root
WorkingDirectory=/path/to/current
ExecStart=/usr/bin/bundle exec --keep-file-descriptors unicorn -c config/unicorn.rb -E production
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -QUIT $MAINPID
PIDFile=/path/to/current/tmp/pids/unicorn.pid

[Install]
WantedBy=multi-user.target
[Socket]
ListenStream=/run/unicorn.sock

[Install]
WantedBy=sockets.target

設定ファイルの unicorn.rb 側には変更は不要。socket activation を利用すると listen の設定が無視されるくらい。

bundle exec に --keep-file-descriptors を渡す必要がある点に気付くまでしばらくはまっていた。 ちゃんと LISTEN_PIDS と LISTEN_FDS は設定されてるのに、なぜか fd=3 が socket ではない別の何かになっていて謎だった…… Ruby 2.0.0 から 0, 1, 2 以外の fd は Kernel#exec するときにデフォルトでは閉じられてしまうそうで、それを防ぐためのオプションが bundle exec にある。 https://github.com/bundler/bundler/pull/2629 https://bugs.ruby-lang.org/issues/5041

また Unicorn のアナウンスでは「You may now stop using PID files and other process monitoring software when using systemd.」とあるけど、Unicorn の SIGUSR2 による graceful restart を利用している場合、 master の pid (systemd 的に言うと main pid) が変わるので、PIDFile は相変わらず必要になるはず。 勘違いだったら教えてください。

実際 Unicorn が socket activation に対応して嬉しいかというと、まぁそんなにメリットは無いと思う。 ほとんどアクセスが無いような個人のサービスで、実際にアクセスがくるまで Unicorn の起動を遅延できる、とかはあるかも…?

socket activation することで、Unicorn 自身が graceful restart しなくても、graceful shutdown ができればクライアントからのコネクションを途中で切ることなくアプリケーションを再起動できるメリットはある。 ただ、新しい worker を用意しつつ準備ができたら切り替える、というような細かい制御は当然 systemd 側からはできず、一旦完全に Unicorn を落としてから再度 socket activation によって起動する流れになるので、接続に失敗することはないけど Unicorn の起動にかかる時間だけブロックする形になる。 どうしても Unicorn を SIGUSR2 によるリロードではなく真っ新な状態から再起動したいときの手段が用意されているのは便利かもしれない。