公開日

ISUCON7に参加してきました

(image)ISUCON7に参加してきました

isuconに参加してきました。結果は400組中154位で予選敗退。予選敗退となりましたが初めてのisucon参加にしては中の上に食い込めてまぁまぁだったのではないでしょうか。

※使用言語はRubyでした。

事前準備

  • isuconの練習時間は取れなかったので、参加ブログや過去問を読み漁りました
    • 使えそうな設定・ブログエントリなどはgistにまとめて秘伝のタレとして準備しておきました
    • また協議中に使えそうなツール・コマンドもさらっと触っておきました
  • 当日の開発用レポジトリは事前にGitHub上にプライベートレポジトリを作成しておきチームメンバーを招待しておきました
  • ボトルネック把握用にカジュアルに導入できるisucon用NewRelicのアカウントも作成しておきました
  • コミュニケーションツールとしてはSlackに専用のチャンネルを用意しておきました(今考えると新規にSlackチームを作ってもよかったかもしれません)

方針

上述の事前準備のなかで当日の競技の流れをイメトレしてなんとなくの方針を決めました。

  • 作業がバッティングしないようにどの領域(App, WebServer, DB, CacheStore)を誰が見るかを決めてから作業する
  • 競技開始1時間くらいは全員でマニュアル読み、アプリケーション触ってアプリケーション把握、なんとなくのボトルネックのあたりを付ける
  • 競技終了1時間前にはコードをフリーズして再起動試験に備える

当日の立ち回り

今回は同僚の redfit と s-jcs [敬称略]の三人と出たのですが僕含め三人ともに業務ではアプリケーションを主に担当しているので今回はチーム的にインフラ・ミドルウェアチューニングが弱いチーム構成になってました。

なので当日の立ち回りとしては、わりとインフラ業務経験がそこまで深くない s-jcs がアプリケーションを中心に担当してもらい、僕はインフラ・ミドルウェア周りを中心に攻めることにして、redfit には全般的に見てもらおうということになりました。

やったこと

MySQL

まず僕はMySQLの設定をしました。遅いクエリを特定して、インデックスを貼ればすぐに高速化が実現すること、また吐かれているクエリからAppの特性が透けてみえてくるのでは?という意図からMySQLから着手することにしました。

まずはスロークエリとしてクエリログを全部吐くように設定。

slow_query_log                = 1
slow_query_log_file           = /var/lib/mysql/mysqld-slow.log
long_query_time               = 0
log-queries-not-using-indexes = 1

吐かれたクエリをざっとみてインデックス貼れそうな部分を貼ります。

CREATE INDEX idx_channel_id ON message(channel_id)
CREATE INDEX idx_image_name ON image(name)

my.cnfの設定は後にtoo many connectionを観測していたので、max_connectionsを少しいじったくらいでそれ以外はいじってません。

吐かれるクエリをザッと見て同じタイプのクエリが複数吐かれているのを観測できたので、アプリで複数のN+1の問題があることは想像がつきました。一方、例えば10秒とかかかるような激遅クエリが走っているわけではないので、何かしらの激遅クエリがボトルネックにはなってなさそうであることがわかりました。

App

上記の作業の裏でアプリケーションのチューニングをメンバーがやってたのですが劇的に得点が伸びません。上位陣の点数を見てここから抜けるには何かしらのブレークスルーが必要であろうと思い、ここでicons問題を解決しなければどーにもならなそうということに気づきます。

ここで僕はミドルウェア関係を手を止めicons問題の解決のためにアプリに手を出すことにしました(この時点で16時位だったと思います)。まずはDBに入った icons を書き出して NGINXでサーブするところまでをやりました。

File.open(row["name"], "w") { |f| f.puts(row["data"]) }

というコードでDBの画像を書き出していたのですが、ベンチが互換性チェックで落ちることに気づきます。

最初は画像がぶっ壊れてるのかと思ったのですが、見かけ上は全く同じ画像になっています。cmpを使ってバイナリのdiffをとってもこれといった差分は出ません。しかし画像のmd5を見てみると確かに値が違う。

うーん…となり違うとしたら画像のメタデータかファイル末尾のなにかだろうと思い、viのxxdを使ってhexdump形式でファイルを確認してみました。ここでRubyの file.puts で画像ファイル末尾に改行が余計に入ってしまう点に気づけました。

この問題で30分以上は時間を潰してしまいました。最初からシンプルに File.write あたりを使っておけばよかったなぁと後悔。

NGINX

Iconsは書き出せた、では次はそのファイルの効率的な配信だ、ということでassets関連のファイルがキャッシャブルな状態で配信されているかを確認しました。

ここでCacheControlヘッダーあたりが怪しいということには気づけたのですがうまく設定を煮詰めることができずタイムアップ。

location /fonts/ {
    add_header Cache-Control "public";
}
location /js/ {
    add_header Cache-Control "public";
}
location /css/ {
    add_header Cache-Control "public";
}
location /icons/ {
    add_header Cache-Control "public";
}

普段仕事でこのへんのNginxのconfは触る機会がないので設定がスムーズに進まず時間を浪費してしまいました。(実際作業時間の半分以上はああでもないこうでもないと言いながら適当な情報をクグる時間になっていたと思います)

問題について

今までのisucon の傾向を考慮しつつ、事前にある程度どんな構成で出題されそうか予測を立てていました。予想とその結果は以下の通り。

  • App
    • 予想候補: Rails/Sinatra
    • 予想: Sinatra
    • 結果: Sinatra → 的中
  • App Server
    • 予想候補: Unicorn/Puma
    • 予想: Unicorn
    • 結果: Puma → 外れ
  • DB
    • 予想候補: MySQL/Postgres
    • 予想: MySQL
    • 結果: MySQL → 的中
  • WebServer
    • 予想候補: NGINX /Apache
    • 予想: NGINX
    • 結果: NGINX → 的中
  • メモリストア
    • 予想候補: Memcache/ Redis
    • 予想: Memcache
    • 結果: 無し → ハズレ
  • サーバー構成
    • 予想候補: Web+DB一体型の小さいサーバー一台 / WebとDBを分離した小さいサーバー二台
    • 予想: Web+DB 一体型のサーバー一台
    • 結果: Web2台 DB1台の三台構成 → 大外れ

App, DB, Webは概ね予想の通りでしたがまさか複数台構成、それも三台出くるとは思いもしませんでした(運営様お疲れ様です)。

複数台を想定していなかったので競技中はサーバー間のプロキシ戦略とか分散戦略とか全然ぱっと思い浮かばなかったです。これは大きな敗因だったと思います。

またAWS脳の僕としては画像の扱いは「はぁ? AWS におくやろ普通(少なくともファイル配信サーバーおくやろ)」と思考停止しており、画像をDBに保存するなんて発想はとうの昔に忘れてしまっておりました。なのでicons問題に対してぱっとスマートなソリューションを頭に浮かべられなかったのは甘かったところでした。

今回は2台のWebだったけどこれが10台だったらという思考実験をやってみるのも面白そうです。

反省点まとめ

  • systemd 慣れてなさすぎてダメだった
  • サーバー作業中、vimという存在しないコマンドを叩いて何度エラーになったかわからないので大人しくサーバーにはvimを入れよう
  • nginx力が低く効率的なサーバー構成を組めなかった
    • 今回の敗因は何と言ってもicons問題を解決できずボトルネックをiconsからアプリケーションに移せなかったこと
  • マニュアルちゃんと読んでおこう
    • sleep の値変えてスコア変わって一喜一憂していたけどマニュアルにはこう書いてあった ->「GET /fetch へのアクセスには点数が加算されません。」
    • 304加点に関する記述もされており、そこで静的ファイルに関してピンと来てればもっと早い段階でスコア伸ばせていたと思う
    • 次は一番最初に声に出して読み合わせとかすると良いかもしれない
  • 業務でサーバー作業全然しないので作業時間の半分くらいはGoogleでああでもないこうでもないと調べる時間だった感覚
    • サーバー作業もっとスラスラできるようになりたい
  • Nginxのログをalpで解析するのができなかった
    • Nginxのログをベンチマーカーの気持ちになりながら目グレップする必要あった
  • アプリケーションのデータベースconnection 閉じ忘れ気づけなかった。これで too many connectionが発生してたっぽい
  • デプロイスクリプト用意する予定だったけどちゃんと用意できなかった
    • これにより再起動漏れとかが発生した
  • confファイルのrepo管理が徹底できず雑管理してしまった
  • 複数台でそれぞれのホスト間の名前解決どうしてるかを見るのに /etc/hosts はすぐに確認しておいてよかった
$ cat /etc/hosts
127.0.0.1       localhost
192.168.101.1   app1011
192.168.101.2   app1012
192.168.101.3   app1013 db

良かったこと

  • Discord良い
  • MacのGUIは有効に使えたと思う。MacのGUI便利。
    • DBはSequel Proでカジュアルに覗くのが良い
    • FileはforkLift使ってカジュアルに転送できたのが良い(今回DBのdumpファイルとか持ってきた)
  • 同僚と出場したので、会社利用させてもらったのでWiFiも快適だったしホワイトボード・ディスプレイなどの設備も使えて良かった

運営の方へ

ベンチマーカーの結果が安定しなかったり、開催日両日共に開始時間が遅れたりもしましたが、参加を終えてその不満も忘れるくらい準備が大変だったろうなぁとお察しします。あれだけのサーバー台数を参加者全員に用意するのは本当にすごい!

出題についでですが、Cache-Controle: Public, max-ageに関して何か色々物申す方がいらっしゃるようですが、これに関しては気づける人はちゃんと気づけたし特段奇問という印象は僕は受けなかったです。

コンテスト規模は年々拡大する一方ですが、今年も開催してくださりありがとうございました!

最後に

今回のisuconはエンジョイ勢として臨んだのですがずっと出たかったisuconだったので実際参加してみてとてもエンジョイできました。

とはいえ圧倒的なスコア差を上位陣に見せられて悔しさはかなり残りました。今年なんとなくの雰囲気を掴めたので来年も出場し、上位陣に食い込めるように精進していきたいと思います。

参考