子供は生まれましたが今年もISUCONに参加しました。4度目くらいです。 前回は予選でそこそこ良い成績を残しそうになったので、今年は期待ですね。
準備
今年は予選がないということで、ISUCON11本線の問題を事前に見ておきました。結果的にたまたま今回もほぼ同じ技術スタック (nginx, MySQL 8) なので超ラッキーでした。
準備では思考停止で適用できるテクニックをいくつか習得しました。これらは基本的にやればスループットが上がるようです (速度と可用性・耐久性などのトレードオフがある中で、速度だけを優先している)。
- mitigations=off
- MySQLの設定 (
skip-name-resolve=ON, skip-log-bin, innodb_autoinc_lock_mode = 2, performance_schema=OFF, innodb_doublewrite=0
) - nginxのkeepalive
今回も上記をとりあえず初手で適用しています。時間がないので一つずつは計測していません (良くない) が、おそらくいずれもスコア向上に寄与していると思われます。ISUCON予備校があったとしたら真っ先に教えてそうな手法ですね。
メンツ
今年こそは友達と参加する予定だったのですが、急遽都合合わなくなり、去年に続きソロ参加となりました。やはり寂しいので、来年こそはチームで出たいです。
当日
近所にオアシスルームという施設があるため、子供はこちらに預けました。とても集中できたので、妻と子供と行政にはマジ感謝です。
10:00 - 11:30 定型作業
今年も言語はGoを利用し、初期スコアは3243でした。開始直後は何も考えずにできる(はずの)定型作業を行います。これは以下が含まれます:
本来は上記を1時間以内に終わらせる予定でしたが、今年はMySQLがpowerdnsというサービスにも利用されていたため、思いの外手こずりました。DNSなんてRoute 53しか知りませんよね。いろいろググって遂に /etc/powerdns
というディレクトリを発見し、無事MySQLの接続先を全て切り替えることができました。
この時点でスコアは4800程度でした。正直この時点ではルールもよく分かってない状態なので、何がなんやらです。
ちなみに自身のUtilizationを高めるため、上記の作業をしながらマニュアルをMS Edgeに読み上げさせて聞くということをやってみました。結果ほぼ耳に入ってこなかったものの、 DNS水攻め
(イケボ) というキーワードだけは頭に残ったので、少しは意味あるかもしれません。(その後結局通しで読んだ)
11:30-13:00 インデックスはる・dns改善
とりあえずスロークエリログを見ながら、一通りMySQLにインデックスを作成しました。余談ですが、今年は CREATE TABLE
クエリが /initialize
APIで発行されないため、インデックスを貼るのもやや面倒でした。一旦 DROP INDEX
して CREATE INDEX
するというような流れで書きましたが、後々不安定になったため、結局いつもの DROP TABLE IF EXISTS
を使う形に書き換えています。(この辺りの所作はもう毎年統一で良くないですかね?) これでスコア11937です。
次に、DNS floodingのせいでかなりのCPUを持っていかれるので、対策に移ります。DNSは今年の新規性で、私も全く詳しくないのでドキドキしました。とりあえずルールも良くわかってないまま、余っている3台目のインスタンスでDNS用のMySQLを動かすことにしました。これにより名前解決成功数は50938まで上がり3倍になりましたが、スコアは一切変動せずで??でした。一旦後回しにして、ルールを読むことにします。
13:30-14:00 iconのキャッシュ
マニュアルを読むと明らかにアイコンの画像取得がキャッシュできることが分かり、そのエンドポイントも多く叩かれていたので、こちらから着手することにしました。
アプリ側で304を返す方針にしたので、やるだけです (といいつつ手こずった)。これでスコアが13031になりました。
ちなみに気付け薬としての魔剤を用意していたのですが、これのせいで競技中めちゃくちゃ頻尿になりました。普段飲まないものを飲むべきではないです。
ISUCONメシはこれだね pic.twitter.com/YRJBah0UBS
— Masashi Tomooka (@tmokmss) November 24, 2023
14:00 - 15:00 dnsdistの導入
後回しにしたDNS問題に戻ります。
Amazon Bedrock Claude (AWS社員は無料なのです) と相談しつつ、ググりつつで、dnsdistなるソフトを導入すればレートリミットを実現できることがわかったので、導入しました。
初めてやる作業のため、案の定手こずって1時間かかりましたが、なんとか導入できました。初見の対応力も私の課題ですが、どうすれば鍛えられるんですかね… dnsdistにより一定QPS以上のリクエストをドロップするようにしましたが、ベンチが落ちがちになってしまいました。ドロップの代わりに一定の遅延を掛けるようにすると、ベンチは通りつつ攻撃がそれなりの負荷で止まるようになりました。優しい攻撃者です。
addAction(MaxQPSIPRule(100, 32, 64, 100, 60), DelayAction(750))
これで名前解決成功数が6050にまで落ち (8分の1程度)、スコア15982になりました。おそらく名前解決数自体がスコアに影響するというよりは、DNSの攻撃に割く余計なCPU負荷が減るため、スコアに直結する処理にCPUを割けるようになるという意味があるのだと思います。この時点でDNSのMySQLを動かしている3台目のCPU使用率は100→10%程度にまで落ちています。
15:00-17:20 いろいろと改善する
ここからは細かく記録を残さなくなったのですが、次のことをやりました:
- NGwordにMySQLの全文検索インデックスの導入
- 早くなるんじゃね?と思い、これを見ながらやってみたら、整合性チェックで落ちるようになりました。理由が分からなすぎましたが、単純なLIKE検索にしたら整合性チェックを通ったので、最終的にはインデックスを使ってません。String.containsな処理をMySQLに投げるという明らかに不合理な実装もあったりしました。
- (追記) 競技後あらためてドキュメントを読みましたが、
IN BOOLEAN MODE
では検索文字列に含まれる文字に特別な意味があるものがあります。+,-
や空白など。これらがNGワードに含まれていたとしたら、誤動作してもおかしくないなと納得。""
で括ってやればよかったかもしれません。
- 3台目でアプリの処理もする
- UserModelをインメモリキャッシュする
- あちこちで
SELECT * FROM users WHERE id =
が見られたので、インメモリキャッシュします。これでN+1も減りました。
- あちこちで
- インメモリキャッシュをさらに
- UserModelと同様にキャッシュできるもの (イミュータブルなやつ) が多々あったので、概ねすべてキャッシュしました (themeとlivestream)。
- タグをインメモリ化
- タグが初期化以降固定値だったので、全てメモリに持つようにしました。
インメモリキャッシュもまた (イミュータブルなものを見つけてしまえば) 作業ゲーでありかつそこそこスコア上がるので、コスパが良いですね。これが簡単にできるのがGoの便利なところでした。とはいえ後半頭を使わない作業に逃げてしまった感もあり、もっと持久力が欲しいです。
これでスコア34985です。
17:20 - 18:00
明らかにやばいエンドポイントであるstatistics系は横目で見ていたのですが、17時になってから20分くらい作業して結局諦めました。あと40分落ち着いて作業できれば、N+1の解消くらいはできたかもしれません。alpや他の人のスコアを見る限り、ここが大きなブレークスルーだったんだろうと思います。実装できずで残念です。GoはISUCONでしか使わない言語なので、瞬発力のなさが課題です。来年こそはGoが手に馴染んでると良いんですが、あまり期待できなそう 🌞
また、去年再起動試験で失格になったので、今年は終盤念入りに確認することにしました。結果として、終了10分前にインメモリキャッシュが3台目で初期化されない (POST /initializeでしか初期化してなかったので) ことに気づき、無事直せました!焦りながらの実装としてはよくやれたと思います。
再起動後のブラウザ確認も大丈夫そうだったので、安心して18時を迎えます。完。
最終的なサーバーの用途は以下のとおりです。
結果
最終スコアは37898でした。発表まで気づいてなかったのですが下4桁が3900に一番近いチームに対する副賞があり、受賞できました!!本と毛布がもらえるそうです。ありがとうございます。
同時にこれは失格にならなかったことも意味するので、安心しました!
統計系APIは頻出な気がするので、次回出るならもう少し慣れておこうと思います。それでは!
(追記) 本日順位が発表され、694組中43位でした。実装速度・精度が足りてない感があるため、競プロをまたやろうかと検討中… とりあえずはAdvent of Code 2023ですね。