🪑

ISUCON13参加ログ

2023/11/30に公開

はじめに

素敵な夜ですね。

今回ISUCON13に一人で参加し、約680チーム中約380位でした。初期値に対して2倍のチューニングができました。
とても楽しかったです。
任意のステークホルダーにありがとうございました。
下記はログです。

どんな準備をしたか

  • 一年半ほど前にISUCON本を読みました。
  • 一年ほど前にvagrantイメージでISUCON予選数回の解説見ながらの素振りをしました。
  • 三ヶ月ほど前に全ISUCONの全ブログ記事を読みました(ISUCON公式に載っている分)。
  • 一日前にKOBA789氏のISUCONポータルシミュレートサービスISUNARABEを使って、ISUCON12の素振りをしました。FAILEDでフィニッシュでした。

上記の準備でおおまかな知識はついたといえばついたのですが、実践不足で具体的手順の実行力には欠けました。

当日の流れ

定石通りindexをつけて露払いの後、本体MySQLとDNSどちらに対応するかの選択肢を考えました。DNSは一切触ったことがなく、難しい選択でしたが、DNSをある程度追いかけました。しかし、dnsdistなどによる抜本対策に手をつけきれる知見がなく、追いかけ方が半端になりました。結果、うまく対応できたのはindex2件とN+1解消1件といったところになりました。
なお、当日ログは末尾に全文載せました。

反省

まず、今回の参加それ自体についての反省はなかったです。上位50%以内に入れなかったのは残念ですが、一人で戦い抜けた事を思うと誤差だと思っています。
ただし、さらに上を目指すのであれば、下記を考えています。

  • DNSについては小手先はせず、一直線でdnsdistやiptablesの策を考えるのがよかったです。
  • 画像DBの解消の必要性すぐわかりましたが、やった事がなかったので捨てました。これは過去問でも頻出であり手を動かしておくのがよかったはずです。
  • 一度きりしか無い勝ちを狙いに行くなら、DNSを捨て他に全振りするべきでした。過去でも、メインの主題を放置して本戦出場したチームはいくつかあったはずです。
    • ただ、これについては繰り返しですが今回の参加についての反省ではありません。何か手に入るものがあるかもしれないし、何かできることがあるかもしれない以上、一度は組み合ってよかったです。
    • 複数人チームであれば上記を並行できましたが、一人チームなので一人でやる必要がありました。
  • 素振りのネタは大量に残っているので、来年にかけて実力のギャップを埋めます。

DNS水攻めについてもう少し

対応策は今のところ下記イメージ。

  • DNSパフォーマンスで殴る(主にオンメモリ化)
    • アプリに組み込む・独自DNSを書く
    • 俺はうまく使えませんでしたが、pdnsのBINDモードも使おうと思えば使えたようです
  • 水責めに対する無視化
    • DNSやそのプロキシで対応
      • dnsdist
      • アプリに組み込む・独自DNSを書く
    • iptablesで対応(更新はコマンドで行う)
  • 完全放置(メインテーマが解けない際にそれ以外に全振りするパターンは過去の成績優秀者の中にもあった)

次回目標

次回目標としては、

  • また一人参加なら上位30%、その次に上位5%(本戦出場レベル同等)
  • チーム参加なら上位5%(本戦出場レベル同等)
    を考えています。がんばります。

おわりに

技術の話に飢えており、ぜひmisskeyフォローお待ちしております。@kisihara_c@misskey.cloud
twitterは今あまり見ていませんが、フォローいただけると喜びます。@kisihara_c

当日ログ全文

1003
当日マニュアルを読む
DNSの説明文で嫌な予感、水責め攻撃の記述あり ボトルネックになるところまで踏み込めると嬉しいが…。
MySQLなのは嬉しい、ptを入れよう
MySQL再起動手順あり
スコアは投げ銭機能

1028
AWSに手間取ったが入った、まずベンチマーク
ベンチマークしながらブラウザ確認、git

1037
hostsファイルの上書きで手間取ったが設定完了、twitchだこれ
自由にサブドメインを設定できるサイト、これは確かにDNSがボトルネックになりそうだ
イメージは固まった、多分

1042
アプリケーション仕様確認、おそらくユーザーアイコン画像はキャッシュできるが、ボトルネックになるまで踏み込めるか
というかCDN対応はいい加減頻出だし、無意味選択肢なんじゃないかこれ
とはいえクソデカ画像とか載ってたらもちろん重いか
POST /api/livestream/7531/livecomment へのリクエストに対して、期待されたHTTPステータスコードが確認できませんでした
は無視でいいとの事 なんだろう

1055
コードをざっと見た限り、行数はいつもと変わらない、やれる事を多くしたんだったら、おそらくDNSやCDNの件も来るだろうな
topを回してみる
mysqldが最初80%くらいで途中から160%くらいに行ってる、とりあえずmysqlの改善でよさそう
なにか後半から重い処理が来るのか、それとも単純に初期だとそう見えるだけか?
名前解決数が出てる、初期は2512
例のDNS攻撃が来るのが後半ってことだろうか

1111
alpとpt入れる
alpではiconが重い、mysqlに入れてんじゃないか? あとはlive周りが重い、直す必要がある

/api/user/[0-z]+/icon
/api/livestream/[0-z]+
/api/livestream/[0-z]+/livecomment
/api/livestream/[0-z]+/reaction

ptでは下記が重い

SELECT * FROM livestream_tags WHERE livestream_id =  圧倒的に重い、10msごえも
SELECT content,ttl,prio,type,domain_id,disabled,name,auth FROM records WHERE disabled=0 and name='o3qur5f85kj3zg3dvmowxjydc9f0.u.isucon.dev' and domain_id=2\G 1msメイン、満遍なく重い
SELECT image FROM icons WHERE user_id = 1016\G 1msメイン、満遍なく重い

4位5位はtipのcountやらsumやらがなんとなく重いようだが、今は気にしなくて良さそう
第2位、これDNSか…意外と早く潰す必要が出るのかも…

1155
ボードを見たら0点ユーザーや初期状態より悪いユーザーがまあまあ多い
初期状態より悪いユーザーは別言語?
普段からこんなもんなのか、なにか落とし穴があるのか…

1156
やっぱりimageがlongblobに入ってる、過去問そのままで、こんなに頻出ならちゃんとやっておけばよかった
alp一位とslowquery一位どっちを優先するかは問題だが、一旦slowquery一位を優先するか
alp一位はCDNとの絡みも絡んでるのでアレ
現時点計画:slowquery上位→alp一位(CDN対応(→blob剥がし、は無理そうだが…))→(DNS含み何かできたら)→2台構成

1216
ボード上、上位40%だ…何かあったら終了時刻までにリセットしよう
現時点計画:1600時点で2台構成化目標、1700時点FAILEDならリセット(触ってないサーバーがあるならそこへ)

1224
slowquery一位対処
DB変更のうまい永続化がよくわからない、説明書にDBリセット手順はあるけど、これは多分ミスったときのものだろうし…
10_schema.sqlに書くしかないと思うが、どこで使われるのかよくわからないなこれ…ベンチマーカーが使うのかなあ
とりあえず10_schema.sql書く+手打ちで対応
やっぱinitializeが呼ばれるみたいなので2台構成はinit.shからいけそう

1248
slowquery一位対処完了、スコアは4963
topの支配率は変わらず。
alpの順位はほぼ変わってない、
/api/livestream/[0-z]+
/api/livestream/[0-z]+/livecomment
が変わったくらい
pt-query-digestを見てみる、まだ重い。
見ると、一部は軽くなったようで処理が多くできているが、まだ10msが多い。なんでだろう。

# Query 1: 64.29 QPS, 1.71x concurrency, ID 0xF7144185D9A142A426A36DC55C1D2623 at byte 18830586
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.01
# Time range: 2023-11-25T02:46:10 to 2023-11-25T02:47:42
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         11    5915
# Exec time     32    157s     2ms   107ms    27ms    51ms    17ms    28ms
# Lock time      8    35ms       0    10ms     5us     1us   167us     1us
# Rows sent     19  20.60k       0      11    3.57    4.96    1.85    4.96
# Rows examine  58  62.75M  10.71k  11.06k  10.86k  10.80k     181  10.80k
# Query size     1 323.24k      53      56   55.96   54.21    0.17   54.21
# String:
# Databases    isupipe
# Hosts        localhost
# Users        isucon
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms  ###################
#  10ms  ################################################################
# 100ms  #
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `isupipe` LIKE 'livestream_tags'\G
#    SHOW CREATE TABLE `isupipe`.`livestream_tags`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT * FROM livestream_tags WHERE livestream_id = 7522\G
# Query 1: 14.57 QPS, 0.35x concurrency, ID 0xF7144185D9A142A426A36DC55C1D2623 at byte 9283863
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.01
# Time range: 2023-11-25T03:39:17 to 2023-11-25T03:47:07
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count          5    6847
# Exec time     18    167s     1ms   137ms    24ms    51ms    17ms    26ms
# Lock time      3    24ms       0     5ms     3us     1us    68us     1us
# Rows sent     12  24.75k       0      11    3.70    4.96    1.83    4.96
# Rows examine  40  67.32M       0  11.06k  10.07k  10.80k   2.79k  10.80k
# Query size     0 374.22k      53      56   55.97   54.21    0.15   54.21
# String:
# Databases    isupipe
# Hosts        localhost
# Users        isucon
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms  ##########################
#  10ms  ################################################################
# 100ms  #
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `isupipe` LIKE 'livestream_tags'\G
#    SHOW CREATE TABLE `isupipe`.`livestream_tags`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT * FROM livestream_tags WHERE livestream_id = 7527\G
+----+-------------+-----------------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+
| id | select_type | table           | partitions | type | possible_keys        | key                  | key_len | ref   | rows | filtered | Extra |
+----+-------------+-----------------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | livestream_tags | NULL       | ref  | lts_livestreamid_idx | lts_livestreamid_idx | 8       | const |    5 |   100.00 | NULL  |
+----+-------------+-----------------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+

indexはちゃんと使われているはずだ。
呼び出し回数がおかしい気がするのでN+1があるにせよ、10msかかるのに説明がつかない。
変なロックでもかかってるのか…? アプリケーションコードを見るか…?
アプリケーションコード、すんごいN+1になってる、とりあえずこれ解くしかないかな?
fillLivestreamResponseはほかファイルからも呼ばれてるし絶対に弄りたくない、新しい関数を生やすか
ただ10msかかる理由が説明付かない、それともこんなもんだっけ…?

1357
N+1解除中にベンチマーク異常。身動きができなくなったので、blobの件やslowquery第二位など見て時間つぶし。
private-isuとuser_handler.goを見たが、imageはやっぱDB直保存だし、
それをうまく静的ファイルに置き直す知識等の記載も少ない。
手は出せないか、やってもキャッシュまでか。
現時点計画:slowquery上位→alp一位(CDN対応(blob剥がしはやらない))→(DNS含み何かできたら)→2台構成

slowquery第二位だが、どこで初期化しているかわからず、謎…
init_zone.sh以外にスクリプトが見当たらず。
ファイルをそのまま突っ込んでるってことなんだろうか?
アプリとかにも一切記載がない。おそらくpdns側の問題というか、pdns側が内部でmysqlを使ってる気がする。
おそらくindexをつけるみたいな話ではなさそう。

# Query 2: 13.49 QPS, 0.13x concurrency, ID 0x42EF7D7D98FBCC9723BF896EBFC51D24 at byte 12118529
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.00
# Time range: 2023-11-25T03:39:17 to 2023-11-25T04:03:25
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         11   19528
# Exec time     14    183s     1ms    72ms     9ms    21ms     7ms     8ms
# Lock time      5    65ms       0    13ms     3us     1us   101us     1us
# Rows sent      1   4.17k       0       1    0.22    0.99    0.41       0
# Rows examine  11  25.95M   1.25k   1.46k   1.36k   1.39k   53.98   1.33k
# Query size     3   2.63M     129     222  141.26  158.58   11.55  136.99
# String:
# Databases    isudns
# Hosts        localhost
# Users        isudns
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms  ################################################################
#  10ms  ####################################################
# 100ms
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `isudns` LIKE 'records'\G
#    SHOW CREATE TABLE `isudns`.`records`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT content,ttl,prio,type,domain_id,disabled,name,auth FROM records WHERE disabled=0 and name='x3gskf8t3s8q20.u.isucon.dev' and domain_id=2\G

対処としてはオンメモリ化になるのではないか。試してみよう。
dnsdistというのもあるらしいが流石に導入は厳しい。

現時点計画:N+1→DNSオンメモリ化試す→(alp一位(CDN対応(blob剥がしはやらない))→何ができたら→)2台構成
※オンメモリ化だが、残り時間によってはDNSを別サーバーに逃がすのみもありかも

1449
tagのみN+1を解除。
スコア5104。変わらずか、微増か。topも変わらず。
DNSでタイムアウトが発生してる。名前解決数は2200で変わらず。
alpのログは、tagのN+1で明らかに順位変化。

/api/user/[0-z]+/icon                            | 0.004  | 0.788  | 172.095
/api/livestream/[0-z]+/livecomment               | 0.004  | 1.264  | 112.863
/api/user/[0-z]+/statistics                      | 5.132  | 20.004 | 107.089
/api/livestream/[0-z]+/reaction                  | 0.012  | 1.276  | 96.605  |

以下livestream系が団子
pt-queryからは明らかに消えてる。
1位image、2位dns、3位謎join。これはもうN+1は対応不要だろう。

一旦livestream側の、つまり根本のN+1への対応は止め、DNSオンメモリ化やalp一位への対応を行う。
後一時間でどこまでできるか。

DNSで重いのはコレ。
SELECT content,ttl,prio,type,domain_id,disabled,name,auth FROM records WHERE disabled=0 and name='psz7rawy0oxmd7wul5ilr7bdq8cu2190.u.isucon.dev' and domain_id=2\G

alp一位でどうするかというとキャッシュレスポンス化か。

alpの件はアプリ側なのでなんとかなりそう、DNSの設定をいじる方が先か。

DNSの件の対応は、DNSキャッシュ化、MYSQL設定弄り、DNS水攻めに対する失敗返答など考えられるか。
DNS水攻めに対する失敗返答はひょっとしたらdnsdistっぽいかも。一旦考えるのを止める。

1510
DNS対応。
まずmysqldumpを取る。元となってるzoneファイルもバックアップする。
pdnsのドキュメントを確認。
キャッシュ化とMySQL設定弄りどっちが先か考える。
難しそうかつ手間の少ないキャッシュ化を先にやる。
pdns.confを見る。丁寧にデフォルトではいくらか数字があるttl系が0になっている。意図を感じる。
launch=bindという設定を試すがうまくいかない。mysqlの設定を呼び出そうとしていると言われる。
opt/init_zone_once.shというのを見つけたが、この中にはない。
/etc/powerdns/pdns.d/の方にあった。消した。
原因不明で落ちた。メモリ不足を疑い、対処不能と考える。切り戻しmysql策を取る。

1618
mysql策完了。エラーで落ちる他に考えられる事としては、むしろアクセスが詰まるようになる等か。
score3989。やはり、水攻め負荷があがっているログがある。
topは変わらず。水攻め前は少し負荷がマシになっている気もするが。
alpは変わらず。
pt-queryからはpdnsのクエリが消滅。よし。
これをロールバックする事はしない。やるべきではない逃げだと考える。
mysql支配のいま時点で2台構成にしても無駄と判断。このまま行く。最悪1台構成でも全力を出せたと考える。

pdnsキャッシュを有効化しよう。

16??
pdnsキャッシュの有効化。バックアップを取ってから、数字をいじられてるttl系の設定を全部戻す。
スコアは3726 名前解決65000
かなり耐えられている気がするし、水攻め負荷レベルは2倍に上がっているが、依然mysqlCPU使用率は100を超え、スコアは下がる。
こうなると、不必要で過剰な対処だったようだ。数時間かけてでも、dnsdistを入れるべきだった。

切り替えて考える。この時点で複数台構成にしても意味がないし、imageとかもたかが知れてるのではないか?
dnsdist導入か、imageのキャッシュ化か。
dnsdist導入は流石に無理だと考える。残り一時間はimageのキャッシュ化で使う。
キャッシュの有効化は戻す。水攻めレベルを下げる。

1651
pt-query一位の対処。

# Query 1: 152.95 QPS, 1.78x concurrency, ID 0x84B457C910C4A79FC9EBECB8B1065C66 at byte 36390347
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.01
# Time range: 2023-11-25T07:21:49 to 2023-11-25T07:22:49
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count         18    9177
# Exec time     30    107s     1ms    75ms    12ms    28ms     9ms     9ms
# Lock time      4    31ms       0     5ms     3us     1us    69us     1us
# Rows sent      9   7.90k       0       1    0.88    0.99    0.32    0.99
# Rows examine   3   1.35M       2     255  153.92  234.30   64.25  158.58
# Query size     1 393.43k      41      44   43.90   42.48    0.20   42.48
# String:
# Databases    isupipe
# Hosts        localhost
# Users        isucon
# Query_time distribution
#   1us
#  10us
# 100us
#   1ms  ################################################################
#  10ms  #############################################################
# 100ms
#    1s
#  10s+
# Tables
#    SHOW TABLE STATUS FROM `isupipe` LIKE 'icons'\G
#    SHOW CREATE TABLE `isupipe`.`icons`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT image FROM icons WHERE user_id = 1057\G

まずCREATE INDEX is_userid_idx ON icons(user_id);
score 5696 名前解決成功数 9465
topは変わらず
alp変わらず
pt-queryからは消滅。おそらく、クエリではなくmysqlからのblob読み込みに移った。
2台構成化と、キャッシュ化どちらにするか。
残り1時間なら、慣れているしrevertしやすいキャッシュ化にする。こちらが全力を出す選択肢だと考える。
キャッシュの方法はカラムに入れたりredisに入れたいところだがsqlにする。時間がない。

1737
キャッシュ機構作成完了。
ギリギリロールバック等も間に合う時刻か。
topは依然としてmysql支配的。おそらくこの状態で2台構成にしても無意味。
ただしalpは変わらず。バグを出してないとしたら、これはおそらくアクセス量が少なすぎてキャッシュを持ったレスポンスが飛んできてないか。
ここまでとする。

1753
キャッシュ機構はロールバック(git stash)。
再起動してベンチマークをパス。
最終スコアは6047。
手数不足と知識不足(dnsdist)があり非常に悔しいが(dns完全無視でも決勝進出レベルに行けた説があり、そうなると完全に手数不足)、2倍のチューニングには成功した。
694組中上位何%だろうか。

Discussion