ISUCON 8の予選を2日目4位で通過しました

y_matsuwitter
7 min readSep 18, 2018

--

いつものメンバーで今回もISUCON予選に参加してきました。昨年は家庭の事情で参加できなかったため久々のISUCONです。結果としてはスコア48698で2日目4位、全体でも5位となり本選出場となります。

http://isucon.net/archives/52459414.html

いつものメンバー、と言いつつも年々各位の立場が経営側になってしまい今回に至っては、

  • y_matsuwitter @ DMM CTO
  • TakatoshiMaeda @ トクバイ CTO
  • kani_b @ Cookpad インフラ部長、監査補助者

という組み合わせで全員しばらくコードを満足に書けていない状況からの参加になりました。コードより日本語のほうをたくさん書いてるメンバーです。

今回も何をやったのか、自分の視点中心で簡単に残しておこうかと思います。ロジックについてところどころ記載が雑かもしれないのですがご容赦ください。

今回はメンバーでの打ち合わせやツール等の事前準備なども無く、予選突破の自分たちの中での期待値も過去最低だったので、冒険せず淡々と優先度を決めて上から潰していく戦略を当日朝話し合って決めました。我々で言うところの冒険の定義はDBを自作し始めるとかすべてオンメモリで処理させるとかそういった類の施策です。

競技開始直後

予選スタート後、まずはインフラに各種ツールセットアップ、アクセス傾向の確認等をかにーさん、リポジトリのセットアップやスキーマチェックを前田が担当、自分はレギュレーションとコードの読み込みから入りました。

コードの確認では主に、CPU的ボトルネックになりそうな箇所、および複雑なクエリやN+1クエリなどのリストアップ、実際のアプリケーションのブラウザからの挙動確認を行い、嫌な臭いのする部分の優先度付けの材料を洗い出しました。

余談ですが、Goを利用していましたが依存管理がまさかのgbで、操作でちょこちょこつまづいてました。他のチームの様子も見ているとここを入れ替えておくのも手だったかもしれません。

その後、各メンバーと打ち合わせを開始し、

  • 座席予約と解約がポイントも高く重要、かつDBに過剰な負荷
  • 各所でロック地獄の様相

という二点に着目し、下記の施策を順に進めることにしました。

  • MariaDB自体と現状の基本的なスキーマはまずはそのまま利用しインデックスだけ貼る。
  • reservationsテーブルを単なる集計や読み出しログテーブル化し、予約や解約のハンドラにおいては最後にinsert/updateするのみとすることでRDB上の整合性だけは保つ。
  • RedisのString型で座席予約番号をシーケンシャルに払い出して予約済みの座席が何番までか管理。
  • 解約についてはRedisのList型にPop/Pushする形で管理
  • 別途、RedisのSet型として、各reservationsのIDをredisから引けるような構造を用意する。
  • N+1クエリの解消
  • SheetsをGoのコードに直接埋め込み

上記をすべてクリアすれば一旦reservations周りの高得点なハンドラはうまく捌けるだろうということ、またコーディング力が万全ではないのでちょっとでも意思決定をミスすると勝てないだろうという前提に立って、他の細かな改修は一旦無視しました。

ちなみに上記の改修項目に関して自分はKVS関連処理を担当しました。

14時ごろ

前田とかにーさんのクエリ周りチューニングがビルドできたあたりでスコアが30000点前後に到達しました。

自分の方ではKVS利用コードを淡々と実装していたのですが、完成しベンチを走らせてみたところ、「座席がランダムに払い出されていない」という趣旨のベンチマーカーからのエラーに出くわし、急遽実装を下記のように変更することになりました。

  • イベントの作成時に、予約可能な座席番号をシャッフルしRedisのListに詰め込んでおく。
// 下記のような利用可能座席番号をランダムに並べたListを予め用意
KEY = availableSeats:<eventID>:<rank>
VALUE = [123, 23, 4, 89, ...]
  • 予約時には上記リストから座席番号をpopしてくる。
  • 解約時には上記リストへ座席番号をpushする。
  • /initialize 時には、初期存在するイベントに関して辻褄があうようにRedisを初期化する。

16時ごろ

座席番号をベンチマーカーが想定する挙動に無事変更でき、このタイミングで38000点くらいまで到達しました。ちなみにこの前に一時的にはロック地獄の影響を引きずりランダムにfailしていましたが、前田・かにーさんの二人がうまいこと解決していたようで、別途記事等上がってくるかもしれません。

16時時点で特に課題だったのが予約時に400を返すべきケースで403を返してしまうというエラーのためにベンチマーカーが並列度を挙げられず本気を出せていなかったようで、今度はそこの修正に挑みました。

Redisを利用した実装で、一貫性に問題が一部出ていたために起きていた問題だろうと推測されます。

17:50ごろ

このタイミングまでウンウン唸りつついくつか対策をデプロイしてみました。例えばRedisを使ってGlobalなロックを予約・解約においてevent単位で行うなどですね。

ですが最終的には、イベント・ランク・座席の組から予約した人のuser IDを返せるようなキャッシュをRedis に構築しうまく利用することで高負荷時の整合性問題を先送りするようなものを仕込んで置くという逃げの手を採用しました。

結果としてこの実装を17:50ごろデプロイしたところ48000点を超えることができ、結果オーライという感じです。

感想

今回の予選課題はとにかく、「あ、この課題どっかで見た気がする!」「なのに分量も問題の精度も複雑度も高い!」と感じる良問で、個人的にもとても楽しめました。大量に登場するロックや、集計ロジック、N+1クエリなど分量も多く優先度をミスするとすぐに時間が削られてしまいそうです。あとベンチマーカーのサクサク感やエラー表示の親切さなど競技していて何度も感心してしまいました。

予選は、正直自分含むメンバー各位の準備不足などを補うべく進めた消極的な優先度設定による勝利といった感覚になってしまい、過去の自分が見たらつまらない勝ち方とか言いそうだなと思っています。ただ、準備不足といった制約や低い期待値が結果的に肩の力を抜いてメンバー間でうまく役割を切り分けることにつながった感覚があり、本選でもこの脱力具合は利用していきたいなと思います。

本選では負荷分析に役立ちそうなオレオレツールの整備、およびGoでのライブラリ整備など、準備を整えておきたいなと思う次第です。時間や立場など言い訳せず優勝したいですね。

やはりISUCON楽しいです。

運営の皆さん、本選もよろしくお願いいたします。

--

--

y_matsuwitter

正座エンジニア。DMM CTO, GunosyおよびLayerX Technical Advisor。早く電脳化させてください。ここには雑に考えた個人的な諸々を投げ込む。