ISUCON 13に参加して12位 + 打ち上げ成功賞をいただきました

今年もいつものメンバー(pgmot, syusui)で「チーム7年目」という名前で参加して、12位 + 株式会社アークエッジ・スペース「打ち上げ成功賞」賞をいただきました!

衛星開発現場見学ツアーはメンバー全員が気になっていたのでとても嬉しいです。 せっかくなのでスコアが一気に上がった理由についても後ろの方に書いておこうと思います。

言語とツール

ISUCON 9ぐらいからずっとGo言語で参加しています。 自分は普段Go言語をほぼ書かないので、毎年ISUCONのときだけGo言語を書くみたいになってます。

分析ツールはkataribeとpt-query-digestを使っています。 alpが人気らしいですが、昔kataribeを使ってからそのまま使い続けています。 今年はパスにusernameが含まれていたので、以下のような置換用の設定を入れたりしてました。

[[replace]]
regexp = '/user/\w+(/|\s)'
replace = '/user/<account_name>$1'

kataribeとpt-query-digestを実行してDiscordに結果をアップロードするスクリプトを作ってあるので、ベンチの後にそれを実行するという運用をしています。

最近はいろいろとISUCON向けに便利なツールがあるみたいなので、いくつか試してみてもいいかもなと思ったりしてます。

自分がやったこと

主にアイコン周りとFillXxxResponse周りの改善を行いました。

kataribeを見るとアイコンのアクセスに時間がかかっていることに気がついたので、まずはここから取り掛かりました。 とりあえずDBに画像のhashを記録して、アイコンのエンドポイント以外では画像のバイナリを取り出さないようにしました。

If-None-Matchを見て304を返す部分は少し苦戦しました。 同じ値がヘッダーに来ているはずなのになぜか一致しないと思って悩んでましたが、ダブルクォーテーションで囲まれる仕様だったんですね…。 あと、ブラウザで挙動を確かめようとしても再現できなかったので少し戸惑ってました。

アイコンの画像については結局DBに入れたままにして、singleflightと1秒のインメモリキャッシュでごまかす形になりました。 実際のところnginxのログを見るとほとんど304を返していたので、ラッキーと思いながら放置してました。 304の割合が少なかったら追加で対応を考えていたかもしれません…。

アイコン周りの実際の変更はこんな感じです。

FillXxxResponse周りについては、メソッドを横断したN+1を愚直に直していくのは大変そうだったこととデータの更新がほぼなかったことから、IDに対応するデータの取得を行いつつsingleflightと60秒のインメモリキャッシュで対応しました。 どういうことかというと、func fetchLivestreamResponseWithCache(livestreamID int64) (*Livestream, error)func fetchLivestreamResponseWithCache(livestreamID int64) (*Livestream, error)というメソッドを作り、この内部でIDに対応するデータのフェッチとfillの処理を行っています。 ただ、アイコンについては反映までの猶予が定められていたため、アイコンのhashの取得だけは1秒のインメモリキャッシュから別途取得して上書きする処理もしました。

kataribeで確認して遅かったエンドポイントや、他のFillXxxResponseから呼ばれている箇所などは置き換えていきましたが、一部はそのままの処理になってます。 N+1になっている部分も一部残したままになったので、全体的に置き換えておけばよかったかもと今更ながら思ってます。

singleflightとインメモリキャッシュの組み合わせは結構簡単にできて効果が大きいので、データの更新が少ない箇所では積極的に取り入れていきました。 結構コピペ実装な部分が多いので、ライブラリ化するか同等の機能を持ったライブラリを探したいところです…。

他のメンバーがやったこと

pgmot

initializeでDBのスキーマを適用するようにしてくれたのがとても助かりました。 他にはDBのインデックス周りを一通り対応してくれたり、統計情報の重いクエリを倒してくれました。

syusui

NGワード周りの改善とPowerDNSの分離をやってくれました。 元々pdnsutilでドメインを追加しているところをPowerDNSのREST APIを使った方法に変えていてすごかったです。

最終的な構成

  • isu1: PowerDNS + PowerDNS用のMySQL + go(PowerDNSのinitialize用)
  • isu2: nginx + go
  • isu3: MySQL(アプリ用)

DNS水責め攻撃でMySQLのCPU使用率がやばいのでPowerDNSを1台に隔離しよう、という話になってこの構成になりました。 isu1のDNSではisu2のIPアドレスを指定する形にして、HTTPリクエストはisu1に直接飛ばないようになってます。

スコアが一気に上がった理由

PowerDNSの分離自体には成功しましたが、何故かHTTPリクエストが全然来なくなってスコアが上がらないという現象に悩まされていました。 終了1時間前ぐらいからこの現象の解決方法を全員で模索していて、終了20分前ぐらいにSetMaxOpenConnsが少ないので増やしてみたところ、一気に60,788点まで上がりました。

ただ、この時点ではisu2にMySQLが同居した状態でisu3が使われていなかったため、残り10分ぐらいしかないところをsyusuiくんが冷静にisu3にMySQLを移行してくれました。 isu2からisu3にネットワーク越しにMySQLを接続できるように設定しているのがスムーズですごかったです。 自分はやることがないな…と思いながらslow query logを切ってないことを思い出したのでその対応をしてました。

そして終了2分前に最後のベンチマークをエンキューして、なんとか125,603点を出すことができました。

感想

PowerDNSの分離後にうまくスコアが伸びなかったり、こんなにも時間ギリギリまでやっていたりで今までで一番しんどかった気がします。 しかし、ギリギリまで粘ったことで12位に入ることができ、企業賞も頂けたのでとても嬉しかったです。

ランキングが隠れてから一番スコアが上がったチームに贈られる「伸びしろすごいで賞」がもらえるのでは…?と終了時に思っていましたが、もっと上のチームがいて(しかも1位のチームが更に点を伸ばしていて)かないませんでした…。

来年もISUCONがあれば参加したいです。そして上位に入賞できるように頑張りたいです。