ISUCON8本選で3位でした

とりあえず自分目線でやったことを書いていきます。

他のメンバーのブログもぜひ参考にしてください。

リポジトリはこちらです。

事前準備

チームメンバーでランチに行って、役割分担などを確認しました。基本的には予選を踏襲しました。

その中でストリームで流すチャンネルとスニペットを投稿するチャンネルを別にしたいと言われたので、自作のcatatsuy/notify_slackで別に指定できるようにしました。

また事前にGitHubのリポジトリを用意してissueもいくつか作っておきました。

当日の流れ

時間はざっくりなので目安です。

10:00 開始

最初の30分間にやることは決まっていました。私の担当はざっくり以下の3点です。

  • ローカルで開発環境を作る
  • MySQL・画像などのバックアップを作成
  • デプロイスクリプトを作る

でした。

マニュアルが丁寧だったのでまずマニュアルを確認し始めました。そこでDockerが使われていることを確認したので自分の担当範囲だった開発環境構築やデプロイ方法が普段と違うことが想定されました。

仕事で使っているMacならDockerを入れていたのですが、今回使っていたのは普段あまり使わない個人用のMacだったのでDockerインストールから始まりました。Dockerのダウンロードはアカウントがないといけないので少し手間取りました(アカウントはあったが、パスワードを保存していたLastPassへのログインがされていなかった)。

MySQLのバックアップはドキュメントに取り方が書かれていたのでそれ通りやればできました。

11:00 開発環境とデプロイスクリプトに手間取り

マニュアルに開発環境構築方法も書かれていたのでそれ通りにやれば行けるかと思いきや、MySQLのデータやユーザーがないと作れませんでした。その辺を試行錯誤しつつドキュメントを書きました。

デプロイスクリプトもrsyncして再起動するだけのやつをとりあえず作りました。いつもは手元でLinux用のバイナリをビルドして、バイナリだけをrsyncするのですが、今回は設定を色々変更しないとその構成はとれなかったので一旦諦めました。

最初に作ったデプロイスクリプトのrsync先やignoreの設定が効いていないなど問題があり、色々修正していたので少し時間がかかってしまいました。

その間にcubicdaiyaさんがフロントサーバーだけDockerから剥がしてOpenRestyに変更しました。パフォーマンス面ではなく、いじりやすさからの変更でした。

12:00 ログのsend_bulkに手を付ける

その後にとりあえずコードやドキュメントを読んでいき、logのapiを叩きすぎなのでまとめたいけど send_bulk 的なAPIないと無理でしょーってぼやいたら、チームメンバーのsyu_creamさんから「ドキュメントに書いてあったよ」って言われたので、とりあえず近いうちに問題になることが確実な send_bulk 化を自分はやることにしました。

send_bulkのAPI仕様がよく分からなかったので、実際に叩き方を調べてから叩いてみたりして進めていきました。

13:00 send_bulk化が動く

13時台後半になってやっとsend_bulk化が動き始めました。

やったこととしてはsync.Mutexでロックを取りながらログを追加するAppendと、追加されたログを吐いて内部を空にするRotateという関数を作って、それを1sに1回叩くというGoだから簡単にできる実装にしました。今回の初期実装はpackage分けがされていたのでどこで実装すればいいのかとか戸惑いましたが、何とか実装できました。

当初はベンチが通らず予想外に苦戦しました。答えとしてはベンチの度にマイクロサービスのURLが変わっているのでinitialize時に更新する必要がありました。
なのでこのときは/initializeを叩かれたらDBから引き直して叩き先を変更する実装にしました。
これがしばらく後になりますが、複数台構成にしたタイミングでベンチが通らなくなる原因となりました。それについては後述します。

自分がこの実装をやっている間にクエリにLIMIT 1が付いていない問題などの解決をsyu_creamさんがやってくれていました。

またcubicdaiyaさんがMySQLの負荷を少しでも下げるためにSQLのソート部分をGo側に持たせる実装や、将来的に問題になりそうなhttp.DefaultClientではなく、違う設定のhttp.Clientを使用するのをマージしたりしていました。

14:00 スロークエリを眺める

その後スロークエリを見始めましたが、一筋縄では行かなそうでした。

今回PARTITIONをいい感じに制限することで最適化できないのかと思って、FORCE INDEXみたいなことができないのか調べたりしていました(PARTITION指定はできるみたいです)。

ただ終了後に実際は消せばいいと聞いて、なぜ気付かなかったのかと悔やみました。PARTITIONは業務で何回も使っていて、仕様も知っていたので完全に自分が気付くべきでした。非常に悔しいです。

Candlestick周りについてはsyu_creamさんがとりあえずキャッシュするコードを入れてくれたので少しマシになりました。

15:00 SNSシェアを有効化

この辺りで負荷が上がりきってないことにcubicdaiyaさんが気付きました。どの辺りでSNSシェアを有効にするべきか把握していなかったのですが、ここで有効にします。

有効にすることで負荷が上がり、failするようになってしまいました。そこで何とかfailを回避する手段を考え始めます。

根本的にはMySQLの負荷が高すぎるところなので、クエリの改善が行われなければ永遠に成功しません。そこでとりあえずOpenResty側でluaを使って一部のルーティングにsleepを入れる実装をcubicdaiyaさんが入れてベンチが何とか通るようになりました。

16:00 複数台構成

ここで複数台構成に踏み込みます。APを複数台にした瞬間にログ周りでベンチが通らなくなりました。幸いすぐに自分のsend_bulkの変更が原因だと気付いたので、最終的に2sに1回、毎回DBから引き直すような実装にしました。

今回ベンチの度に外部サービスのURLが変わるのは結構なハマりポイントでした。分かっていれば大した問題ではないのですが、ISUCONだと最初は分からないので割と焦ります。

17:00 最後にスコアを少しでも上げる

cubicdaiyaさんがsleepの時間やサーバー構成をいじりながら最もスコアを出せる構成を探っていました。自分はもう時間がなく、根本的な変更を入れるのが難しいので小手先の変更を入れたりしました。

この間にプロファイリングを取り、圧倒的に暗号化の処理に時間を使っている(golang.org/x/crypto/blowfish encryptBlockがアプリケーションのCPU時間を83.84%使っていた)ことが分かりましたが、現状のサーバー構成だと1台しかないMySQLがCPUを使い果たしている状況だったのでアプリケーション側を変更してもなーと思い、手を付けませんでした。

結果発表

最終的に3位でした。最終的に3位に食い込めたのはcubicdaiyaさんのsleep時間の調整が理由だと思います。

優勝するにはMySQLの負荷をもっと下げなければ勝ち目がないことが分かっていたのに、対策を自分が提案できなかったことに敗因があると思っています。非常に悔しいです。来年こそは優勝したいと思います。