aeroastroの日記

Over the Sky, Into the Future.

初参戦のISUCONで予選敗退した話 (ヽ´ω`) < 22位

お久しぶりです。この度、はじめてISUCONに参加することが出来たので、ブログを書いてみました。

ISUCON8 !!

エントリーするまでの話

「アプリエンジニアもすなるISUCONなるものを、インフラエンジニアもしてみむとてするなり。」

巷でISUCONというものが流行ってて、優勝したらお金ももらえるし、参加してる人楽しそうだし参加したいなぁという気持ちをこじらせてたのですが、ISUCON8 で初参加を果たす事ができました。

今回は、会社つながりで アプリエンジニアの 私 @aeroastro と @abish 、そしてインフラエンジニアの @naoka と一緒に「ほうじ茶ラテ部」を結成し、参加してきました! 締切直前に声をかけたにも関わらず、集まっていただいて本当にありがとうございました。

言語はGo と PerlRuby で迷ったのですが、せっかくだし業務で使うことの多いRubyで書いてみようということで、Rubyで挑戦してみました。(Perlとかにしたら良かったですね。。。)

結果

じゃじゃーん、初参戦で決勝進出ー!とやりたいところですが、予選敗退してしまいました。

22位

f:id:aeroastro:20180918001821p:plain

isucon.net

最後に数時間分の作業をロールバックしたのがきつかった。。。

結果は出せませんでしたし、100万円もゲット出来なかったのですが、めっちゃ楽しかったのは幸いです!

やったこと

ISUCONの参加者が得られている情報を前提として書いているものがあるので、ご容赦ください。(公式に講評がでたら、そこへのリンクも張ります)

戦略と全体感

インフラ周りの構成変更やセットアップ、各種プロファイラの設定等については @naoka におまかせ。ローカルの環境構築や動作確認手法の確立、RubyのWebAppの下回りを自分が担当。 @abish がコードリーディングとチューニングを進める。 下回りのセットアップが完了次第、自分もコードリーディング&チューニングに移行するという戦略で進めました。

この戦略は割と良い感じでした。お互いの得意な部分をメインでやりつつ、最終的には合流していくスタイルです。

失敗したなぁと感じたのは、後半の戦いにおいて、凡ミス等が重なり failが継続しているにも関わらず、ロールバックせず、追加開発を継続する判断をしてしまったことです。最終的には、予選終了直前に git checkout で、数時間前の状態に大幅ロールバックさせて、とりあえずベンチを走らせるということになってしまいました。 ロールバック判断をより細かく、より早くして、常に成功する状態を保っておけば、もっと良い成績を残せたのではないか?という反省があります。

インフラ構成変更

インフラの構成は、以下のように変更されました。

  • h2o から nginx への換装
  • MariaDB 5.5 から MySQL 5.7 への換装
  • 1台でDB, Webの相乗り構成から3台構成への変更 (Web専用2台、DBとinitialize用Web相乗り1台)

ミドルウェア等のバージョンアップは、多くの先人達が何年もかけてチューニングしてきた巨人の肩に乗れるので、下方互換を保つ範囲において、副作用の少ないチューニングです。

nginx への換装は慣れていないものを利用してハマるより、慣れているもので最高のパフォーマンスを出したほうが良いという判断からすぐに換装しました。 MySQL は 5.7 にすると、全般的な性能が改善する為バージョンアップを行いました。8.0まで上げるのもありだったのですが、普段使い慣れていないので、ややリスクがあると判断して避けました。 また、カーネルのバージョンアップもしてみようか?(笑)という話になりましたが、再起動出来なかったら死が待っているので、リスク高すぎと判断して避けました。 予め検証しておけば問題ないはずなので、次回挑戦するときは、カーネルのバージョンアップも是非やっていきたい気持ちが強くなりました。

1台構成から3台構成への変更は、3台あるマシンのCPUとメモリをキチンと使い切る為です。(とはいえ、実際のベンチ実行中はDBのCPUは残っちゃいますが) リクエストは、ベンチ対象のサーバのnginx からWebの2台に振り分けられます。また、/initialize の初期化処理のみは、DB上に立てた アプリにルーティングします。 これは、初期化スクリプトがデフォではローカル向いてるというのもありますが、それ以上にデータベースの初期化が行われる際に、細い内部ネットワークを使わずに済むので効率的という話があります。

チューニングを進めると、インスタンス間のネットワークもサチって、path毎に向き先を調整する必要や、さらなる効率化も出てくるかもしれませんが、今回は残念ながらアプリのチューニングがその域に達しませんでした。。。 (ヽ´ω`)

また、nginx と alp の組み合わせによるアクセス解析や、 tcpump と pt-query-digest によるクエリ解析、vmstat等によるパフォーマンスの継続的な監視や、「〇〇して欲しい」という無茶振りへの対応もしていただきました。

github.com

www.percona.com

各種下回りについて

CPU2コアかつ、メモリ1GBしか無い環境だったので、プロセス数2、それぞれのスレッド数8という雑な設定ではじめました。スレッドの再生成時のオーバーヘッドをなくすため、常に8スレッドにしています。 また、プロファイラとしては Stackprof を導入しました。 NYTProf 等の高機能プロファイラと比較すると、ちょっと扱いづらい部分はありますが、オーバーヘッド少なく色々と調べてくれる良いやつです。

github.com

そして、migration用の rake タスクや再起動スクリプトを追加していました。が、このあたりはコードリーディングが不十分なうちに、下回りを整備しようとした自分のミスが散見されます。今回は /initialize でキックされるスクリプト環境変数の分離がキチンと出来ていたので、下回りの整備は最小限で良かったと思われます。

また、curljqを用いて、コピペするだけで簡単な動作確認が出来るようなスクリプト群も作りました。以下のようなものです。 これにより、超絶雑にであれば、ローカルでサクッと動作確認が出来るようになります。 Cookie-b-c で使えるので、覚えておくと便利です。

# 初期化処理
curl -XGET localhost:9292/initialize

# ユーザー系のリクエスト
id=$(curl -XPOST localhost:9292/api/users --data '{ "nickname": "hoge", "login_name": "hoge", "password": "hoge"}' | jq '.id') # 新規作成
curl -XPOST -c cookie.txt localhost:9292/api/actions/login --data '{"login_name": "hoge", "password": "hoge"}' # ログイン
curl -XGET -b cookie.txt localhost:9292/api/users/$id
curl -XGET -b cookie.txt localhost:9292/api/events
curl -XGET -b cookie.txt localhost:9292/api/events/11
sheet_num=$(curl -XPOST -b cookie.txt localhost:9292/api/events/11/actions/reserve --data '{"sheet_rank": "S"}' | jq '.sheet_num')
curl -XDELETE -b cookie.txt "localhost:9292/api/events/11/sheets/S/$sheet_num/reservation"

active_reservations の追加

これはかなり効果が大きかった案件です。今回は座席の予約を行うアプリですが、キャンセルされた座席は座席予約時に意味の無い情報になります。

そのため、有効な座席管理を active_reservations で、予約履歴を これまで通り reservations で管理するようにしました。また、これをやるタイミングで get_events における n + 1 問題を解消し、 reservationsセカンダリインデックスに対して、 event_id のみを指定する rangeアクセス一発で終わるような構造にしました。

変更行は10行前後でしたが、これを入れることによりパフォーマンスの大幅改善ができ、その当時は3位に一気に浮上しました。思い出の瞬間として、スクショ取っておけばよかったですw

n + 1 問題で忘れ去られやすいことですが、単純に 1 にすれば良いものではなく、 MySQLInnoDB の B+木に対しての走査パターンまでキチンと考えられると良いです。 1 にしたとしても、そこに (sheet_id) を入れてたりすると、セカンダリインデックスを n 回走査することになるので。

sheets のキャッシュ

これは、ものは出来ていましたが、最後の先祖返りで無かったことにしました。

今回、 sheets というテーブルは 座席を表すものでしたが、このテーブルは更新されません。 参照しか受けないのと、データ量としてもそこまで多くはなかったので、アプリのプロセスにプロセスキャッシュする戦略に出ました。 readonlyなので、スレッドで分ける必要も無いので、プロセスキャッシュしています。

sheets は生成頻度が高かったので、これが上手く実装されていれば、太りやすさも解消されつつ、クエリ削減によるパフォーマンス向上もあったのではないかと思います。

get_event の引数に event を追加

これは、ものは出来ていましたが、最後の先祖返りで無かったことにしました。

今回、 get_event が呼ばれることが多く、1リクエストで数回呼ばれることもありましたが、この内部で event を fetch するのではなく、外部で events を fetch して、それを与えるようにすることで改善しました。キチンと実装が動いていれば、かなり大きい削減効果がありそうです。

セールスレポートの最適化

これは、ものは出来ていましたが、最後の先祖返りで無かったことにしました。

セールスレポート生成は不安定かつ時間もかかっていたので、多少のコード改善を行いました。

  • 全イベントの取得時には、MySQLにソートをさせている無駄処理があったので、Ruby側でのソートに移行しました。
  • 特定イベントの取得時には、MySQLでの無駄なJOINがあったので、RubyレイヤでのアプリケーションJOINへの変更を行いつつ、インデックスを聞かせました。

この辺は、 Using temporary; Using filesort が出ていましたが、この組み合わせが非常に邪悪で、 Using temporary については、生成されるテーブルサイズが 大きいので、Disk IO が発生していました。 これは、 My ISAM の中間テーブルが生成されているためですね。

aeroastro.hatenablog.com

銀だこ

お昼ご飯は銀だこを食べました。 @naoka が買い出しに行ってくれて、3人でホクホクのたこ焼きを食べました。 とても美味しかったです。

その他やったこと

  • reservation テーブルに対してのindex貼り直し
    • これは、そもそもベンチをキチンと通すためにやっていました。
  • 全件取得してソートをかけるレポートにおいて、Rubyレイヤーでソートをかけ、DB負荷を下げる

困ったこととやっておけばよかったこと

  • GCにかなりのCPUが使われたり、スワップが発生して、Disk IOがボトルネックになっていた部分もあったので、定期的にpumaのプロセスを殺すとかをすればよかった。
  • 今回はRubyのプロセスが太りすぎたが、メモリネックになった場合にオブジェクトの生成箇所や生存箇所を高速で調べて解決する手段を、予め準備しておくべきだった。
  • 参照実装が、全体的に破壊的だったので、ちょっとした改善を行おうとすると容易にバグるのが辛かった。(これは参考実装が、問題として非常に上手く作られていることの証です。)
  • ロック周りとかは全般的にアーキテクチャ変更すべきだった。が、そこがネックになるところまで行けなかった。

次回に向けて

今回は、色々なミス等が重なり、当日アタフタしちゃった感がありましたが、例えば、以下のようなことを改善できれば、もっとキチンと上を目指せてたのではないかと思いました。

  • 基本的なやることリストは明文化し、可能であればスクリプトレベルまで落とし込んでおく
  • 最初期のコードリーディングにもっと時間をかけて、無駄な落とし穴にハマったり、不必要なことをしなくて済むようにする
  • 動かなければすぐに切り戻し、常に動いている状態を保ちつつ、改善を図っていく

特に、常に動く状態をキープするのは、精神衛生上も絶対にやるべきだった感があるので、次回はキチンとやっていきたいです。

また、これらは、本番サービスにおけるパフォチューをやる上では、ごくごく当たり前にやるべきことの幾つかではあるので、ISUCONにおいても気を抜かないということが大切なのだということを思い知らされました。

謝辞

今回のISUCON予選を通して、運営の皆様には大変お世話になりました。 怪しげな飛び道具を使わずとも、キチンと地道に改善すれば予選突破できる問題。安定しているポータルサイトベンチマーク。ローカルでも簡単に動かせるようなアプリ設計等、今回のISUCONの出題は非常にレベルが高かったし、スピードアップの本質ではない所で引っかかるリスクが少なくなっていたのではないかと思います。

特に、メモリが少なく、レコード数が多いというのも、非常に良い問題だと思っています。これにより、本番サービスではつかえないような、オンメモリに全部載せる作戦等が封じられたのではないかと思います。今後もこの傾向を続けて欲しい。

オンラインでの戦いとなったので、直接顔を合わせることは出来なかったのですが、Discord や Twitter でのふれあい等、戦って頂いた他チームの方もありがとうございました。いい試合でした。

そして、一緒に戦ってくれたチームメンバーの方々には、非常に感謝しています。決勝まで一緒に行くことは出来ず申し訳ない気持ちでいっぱいですが、初めてのISUCONで、ドタバタしつつ悩んだ8時間は、とても楽しかったです。

関わった皆様、本当にありがとうございました!