2020/09/20

ISUCON 10 にfujiwara組で参戦しました

タイトルに「予選突破しました!」って書いてないのは突破してないからです!

今年もISUCONの季節がやってきましたね〜。昨年はいろいろあって1人チームで出ていたのですが、さすがに1人は厳しい! ということがわかりまして(知ってた)、今年はコロナもあり社内チームが無難だろうということで久々のfujiwara組からの参加にしました。ちなみに1人は厳しいのは元々分かってるんだけど、でも作問中の事前解答とかは1人でやることが多いわけでして、まぁまぁ行けるんじゃないかとかちょっと思ってたんだけどな〜。世の中そんなに甘くないですね。

さて、この記事はISUCON終わった後の水曜日からコツコツと書いていてようやく日曜日に書き終わったみたいな感じの記事なので、まだ運営の問題の解説を読まずに書いております。まぁ結構好き勝手書いていますが、解説読んじゃうとまた感想にいろんな成分が加わってしまってピュアな感想にならない気がする……と思ってまだ読まないようにしていたので、早く解説読みたい一心でモリモリ書きました。

はじまるまで

技術的な事前の準備は組長がだいたいやってくれた(サーバに迅速に入れるようにするところとか)ので私の出番はなくて、主に社内の会議室確保したり自分のディスプレイ運んできたりみたいな物理系の準備をやりました。

ということで、開始を待ってたのですが、開始2時間遅延のアナウンスがあり、最終的には2時間20分遅延でスタートということに。それまでわりとヒマだったので、YouTubeの配信を眺めながらBGMを流して待っていました。大量のVMをプロビジョニングして用意することは昔のISUCON運営でやったことがあるので、いろいろ大変なのわかります…… 運営お疲れさまです……。

そういえば、なんか11時半すぎから連続で地震ありましたよね〜。会社の会議室でやってたので、さすがにこれデカイヤツきたら避難どうしようみたいなことをちょっと考えながら準備していました。

はじまったぞ

12時20分に始まって、ISUUMOという題材で「なるほどリクルート〜〜〜」となり、さっそくベンチマークを回そうと思ったのですがサーバーリストが空で回せなかったので、ひとまずレギュレーションとお題アプリをいろいろ眺めたりしました。サーバリストにデータが入るまで小一時間ほど時間があったので、サーバからとってきたソースを眺めたりしていましたが、やはり1回もベンチが回せない状態だとどういうURLパスが重いのかとか、どういう検索条件が飛んでくるのかがわからないためこれは正直わかんねーなーという感じでした。

一通り読んでもまだサーバリスト問題が解決してなかったので、サーバリストが空でもベンチマークをかけたいサーバのIPアドレスは分かっているということで、このポータルのWeb APIをcurlかなんかで叩けばEnqueueして一足お先にベンチマークかけられるのでは?? と思ってポータルのwebpackされたJSなどをpackされたまま読んだりして時間を潰していました。ちなみにポータルはTypeScriptで書かれててprotobufでやりとりしていたのでcurlでprotobufのペイロードを投げつけるのは大変そうだ! というところまではわかり、うーんどうしようかなコレ深追いするか悩むな〜とちょっと考えた結果、まぁお題を高速化する方策を考えた方がよいだろう(そりゃそうだ)ということで深追いするのはやめました。

ベンチ回せるようになるまでいろいろ読む

小ネタなどを含むいろいろ気付いた話をメモっておきます。

  • サーバは3台、1コア2GB
    • このご時世にこのスペックで3台……! まぁISUCONってそういうもんですけど
  • 入稿処理(というベンチマークをかけないとなにがくるのかよくわからない処理)では画像自体はアップロードされない
  • みた感じ同じ画像だけどファイル名が異なるやつ、画像全体のMD5は異なるけど画像を head -c 1000 したやつのmd5sumは一致している。ISUCON7予選にもあったやつだけど、バイナリの最後のほうのメタデータに違いがあるやつっぽい
    • 結局そこのところはあとからベンチ回してボトルネックに関係ないことが分かったので、スルーした

お題は検索だった

はじまる前、チーム内の雑談で「今回の問題はなんですかねぇ」みたいな話をしてました。さすがに10回やってて3回目以降は予選本戦形式になったので15問以上の問題があるということで、もうネタがないんじゃないかみたいなことを思ってましたが、たまたま最近ちょっと全文検索方面に思いを馳せていたので「検索系とかありますかねぇ、MySQLで検索してるけどElasticSearchにまるっと載せ替えるのが正解みたいなやつ」みたいな話をしていたら今回はまさに検索の問題。

ベンチマークが使えるようになるまでちょっとお題アプリを実際に動かして触ってみて、うーーんこれはめっちゃたくさん検索条件の種類あるから、これはもしかしてElasticSearchか? と思いましたが、だれもちゃんと使ったことがなかったのでさすがに初手でElasticSearchに載せ替えはできないよねということで、普通にMySQLでがんばる方向で解き始めました。

ベンチマークが使えるようになって実際の負荷をみてみると、MySQLが9割ということで、ほぼほぼ予想通り(というかISUCONって大体そういう感じからスタートしますよね)。重そうなクエリの出所を眺めてみても、やはりchairとestateのsearchだったので、まずはここから手をつけていくことにしました。

Index Condition Pushdownでいくぞ作戦

検索条件側に、Featureという「日当たり良好」みたいな希望条件をANDで入れるやつがあったのですが、それはチームメイトのmacopyさんがいい感じにしてくれるとのことだったので、自分はラジオボタン系の検索をどうにか軽くすることにしました。

この大量の検索条件をINDEX一発で捌くのはなかなか骨が折れる話なのですが、今回はMySQLが5.7だったので、マルチカラムインデックスでIndex Condition Pushdownが効くよねということで、以下の大がかりな改修に取りかかります。

  • Width / Height / Depth / Price(estateではRent) の検索条件はIDで飛んでくるので、IDで引けるようにカラム追加
  • 追加したカラムでマルチカラムインデックスを構成してINDEXを効かせるようにする

結局小一時間ちょいでコードは書き終わって実際に投入してみたのですが、どうも最初のVerificationフェーズで落ちることがあり、発生頻度が半分くらいだったためそのブランチは結局マージできず。あれは結局なにが悪かったのかよくわかってない……。

ちなみに、Index Condition Pushdownするにはそのマルチカラムインデックスの1カラム目がちゃんと条件に入ってないとオプティマイザがそのインデックスを使ってくれないので、chairはstockを最初のカラムに、estateはそもそも対象カラムが3個しかないので、順番変えたマルチカラムインデックスを3個張る形で対応しました。後半の時間にmacopyさんに実データ調べてもらったときに「最初は全部stockが存在する状態から始まる(つまり stock > 0の条件はほぼ全てのカラムに当てはまるので意味がない)」ということがわかったので、まぁまぁ筋の悪いINDEXだったため、捨てて正解でしたね。

終わったあとのDiscordとかで「とはいえ飛んでくる条件は1個か2個くらい」みたいな話だったので、それならFeatureの指定があるならそっちで絞り、それが無い時は素直に各条件のIDを示す追加カラムで1個だけ張ったINDEXで絞り込んでしまうだけでもだいぶ負荷が下がったんじゃないかなぁと思いました。

CSV入稿がBulk Insertされてないやつ

Index Condition Pushdown狙いの7カラムマルチカラムインデックスは、UPDATEクエリによりINDEX更新負荷がすごくて、CSV入稿で500件飛んできたときに、Bulk Insertに変えてない状態だと2.9秒ほどかかるようになってしまいました。で、その結果どうやらクライアントが2秒くらいのタイムアウトで打ち切っているようで、チェックエラーとなりVerificationフェーズが完走しないというのがありました。レギュレーションにはその辺のタイムアウトの許容時間とかが書かれてなかったのでタイムアウトなのかどうかが良くわからん(今考えれば普通にnginxログみて499かどうか見れば良かったんだけど)、ということで質問フォームを送ったりもしましたが「答えられません」というような回答でした。

アクセスログの流れをみると2秒くらいでレスポンスまつの打ち切って入稿成功しているかどうかのリクエストがきて死んでるような感じであることは分かったので、まぁこれ高速化するしかないやろということでBulk Insertに変えました。すると400msくらいで終わるようになり、そのタイムアウト起因によるVerificationのチェックエラーは出なくなったのですが、結局別のエラーで半分落ちるのでどうしようもなかった。

ということで、ブランチまるごと捨てましたが、初期状態だとVerification時の1回しか来なかったCSV入稿が後のオンメモリ化が出来た後にはそれなりの頻度でやってきてたので、このBulk Insert化だけは入れて置いた方がよかったな〜というのが一つ反省として残っています。

SELECT * を撲滅する地味作業

ISUCONでよく出てくる、この手の複雑怪奇でRow ScanがデカくなりがちのSELECT文は SELECT * になっているのを SELECT id に変えて、一発目のクエリをセカンダリインデックスだけで完結するようにして改めて WHERE id IN(?) でクラスタインデックスを引くようにすることで、ちょっと軽くするみたいな小技があります。

まぁ今回もこれやっといたほうがいいだろうということで、全てではないですがRow Scanの大きそうなクエリをゴリゴリと SELECT id に変えていく作業をやりました。

これ1回目ミスってマージ出来ずだったのですが、その後1箇所nazotteのところで手を抜いたところの心当たりがあって、まさにそこでベンチマークが落ちていたのでそこをちゃんとしたらベンチマークが通り、なんとかマージ。ようやくバリュー出た説ある。

オンメモリ化に舵を切る

とはいえ、残り2時間ちょいの時点でアプリ側でバリューが出たのは私の SELECT id のやつとmacopy側のFeature縦持ちブランチくらいしかなかったため、そろそろ予選突破には飛び道具的な大技を決めないとダメですねとなり、オンメモリにデータを持って検索をMySQLからゴリッと引き剥がすことに。

estateとchairをそれぞれmacopyと私で分担し、macopyのestateのほうが(検索に絡むカラムが少ないこともあり)早くおわったのですが、それがなかなかベンチマークが通らずという感じで残り1時間を切り、私のブランチは残り30分くらいで完成し、1個だけバグあったけどそれだけ直したらバッチリ通ってスコアがモリモリッとあがりました。

その時点でもうあと20分ちょいしかねーぞということで、組長に3台のCPU負荷を教えてもらってnginxのlocation振り分けを調整し、再起動をしてもう1回まわして2000ちょいのスコアがでたところで残り10分。まぁ予選通過ラインに10%ほど足りなかった今からすると「もうちょっと粘って回しておけば……」という感じでしたがもうその時は疲れ果てていて「これ以上ベンチ回してせっかく2000乗ったスコアを壊したくない……」みたいな気持ちになってしまい、そのまま終了しました。

その他感想など

いやー難しかった! なんというかISUCONは回が進むにつれてどんどん予選問題難しくなってきてて、でも実在するサービスがベースになっている問題が多くて「ふつーのWebサービスだったら高速化できるけど、こういうオリジナリティがあっていかにもユーザが便利に使いそうなサービスの裏側ってマジで大変なんだな〜これ!」という感じになりますね。

ということで、本戦はもう2週間先の10月3日(土)ですね! 本戦に出るみなさまがんばってください! 今年はコロナで一箇所に集まれない分ライブ配信とかに力を入れてるっぽいので、その辺を楽しみにしております〜〜〜

うされもん @acidlemonについて

|'-')/ acidlemonです。鎌倉市在住で、鎌倉で働く普通のITエンジニアです。

30年弱住んだ北海道を離れ、鎌倉でまったりぽわぽわしています。

外部サイト情報

  • twitter
  • github
  • facebook
  • instagram
  • work on kayac