every Tech Blog

株式会社エブリーのTech Blogです。

ISUCON14 に ISUポンサーの枠で出場しました

この記事は every Tech Blog Advent Calendar 2024 の 10 日目の記事です。

エブリーで小売業界に向き合いの開発を行っている @kosukeohmura です。

エブリーは ISUCON14 にて ISUポンサーとして協賛いたしました。社に 1 枠の参加確定枠を頂き、僕は社内で きょー と mbook と組んでチーム EveryBitCounts として出場する機会をいただけました。残念ながら最終スコアは 0 と惨敗でしたが、前日までの準備と当日のこと、それから反省について書きたいと思います。

tech.every.tv

前日までの準備

チームの 3 人はいずれも業務で Go を使うバックエンドのエンジニアですが ISUCON の参加や学習経験はありませんでした。前準備として社内の ISUCON 参加経験者を招き、概要の説明を受け、それから準備のための数時間のミーティングを数回組みました。

過去回の競技内容・レギュレーションを確認し、下記の攻略記事を読み合わせたところ、

isucon.net

  1. 計測結果を見て、問題を見出す。
  2. 比較的簡単に(見える)修正を施す。
  3. 改善されたことを確認する。

以上を繰り返すとそれなりの高得点を目指せそうだと捉えました。また上記の中で「計測結果を見て、問題を見出す。」ステップには比較的自信がなかったことから、練習用の t3.micro の EC2 インスタンスを立ち上げ学習を行いました。

具体的に行ったことを記します:

  • GitHub でのソースコード管理
    • 当日用のリポジトリを作成、当日必要となりそうなコマンドを Makefile 化しておく
    • RSA 鍵ペアを生成し設置。EC2 サーバー上で git push などが行えるようにする
  • デプロイ
    • EC2 サーバー上で git pull した後サーバーアプリをビルドし、各種サービスの再起動までを 1 コマンドで行えるように
  • MySQL スロークエリの表示
    • スロークエリログの有効化、しきい値の設定方法メモ
    • mysqldumpslow の結果の読み方の把握
  • alp での NGINX のアクセスログの集計
    • コマンドの実行、結果の読み方の把握
    • -m オプションを使っていい感じに結果を束ねて表示する
  • pprof の使い方
    • プロファイリング方法、プロファイリング結果の読み方の把握
  • 問題の見出し方
    • 計測結果から変更対象の箇所を特定するまでどのような思考を経るかを話し合う

当日

チーム全員起床に成功しオフラインで集まり作業しました。時系列でざっと流れを書きます。

開始〜12:00

まず技術要素(MySQL, NGINX, go-chi/chi, jmoiron/sqlx)を軽く確認し、公開されたドキュメントを読み込みました。この間に 1 人はソースコードを Git 管理したりベンチマーク計測したりと開発準備を行いました。結果、開始 1 時間以内には変更をデプロイできるようになり、チームのうち 2 人はドキュメントを一通り把握した状態となりました。

その後 mysqldumpslowalp での集計を行いつつ、以下 3 つの問題に目をつけて 3 人でそれぞれ分担・着手しました。

  1. GET /api/owner/chairs で椅子をリストアップするクエリが重いこと
  2. GET /api/app/nearby-chairs での処理が遅く、中身を見ると椅子全件取得後に椅子とライドでの多重ループで O(N * M) なクエリ発行がされていること
  3. GET /api/internal/matching の処理内容がランダムかつ椅子 1 つのみに対しての処理であり、適切な椅子とライドのマッチングができずスコア算出に大きく影響を及ぼしそうなこと

前日までの準備の甲斐あって、デプロイや計測ツールの準備については問題なく行えました。また問題箇所の特定についても(通知周りの問題を後回しにしてはいますが)おおよそ的を得た内容だったと思います。

この時点のスコア: 900 前後 (初期状態)

12:00〜14:00

昼食を取りつつ、それぞれの問題の解消に向けて処理を読み改修方針を考える時間でした。それぞれが別々の問題に取り組んでいるので会話もまばらになります。

僕は定期実行される GET /api/internal/matching でのマッチング処理の改善を担当していました。表には出ない Endpoint であり、マッチングできさえすれば好きに変えて良い(Endpoint 自体廃止しても良い)とのことで、いろんなことを考え方針策定に時間をかけていました。具体的には下記のような事を考えていました。

  • マッチングの間隔:
    • 初期状態では 500ms ごとの処理だが、この間隔はどの程度椅子の稼働率に影響があるのか?
  • ユーザーからの評価の上げ方:
    • 高い評価を得るほどにユーザーが増え結果スコアも伸びるが、距離の長いライドに遅い椅子がマッチすると到着が遅くなり評価が得られないということになるか。
    • むしろ長距離ライドに対しては速い椅子が確保できるまでマッチングを控えたほうが平均の評価としては上がるのか?
    • とはいえマッチングまでの時間も評価対象だし、、むしろ長距離過ぎるライドは無視するのが得策か?でも緯度経度に制限がない以上どこからを長距離としてもいいかわからないな、、?
  • 全体効率の最適化:
    • 椅子の性能(スピード)にかかわらず片っ端から近い順にマッチングさせたほうが全体としては効率が良くなるか?
    • 速い椅子をフル稼働状態にすることを優先し、遅い椅子の稼働は控えめにしたほうが高効率か?

結果、考えても良くわからなくなってきたので、メンバーと相談し

  • マッチング処理の定期実行を廃止。ライドが新規発生した際と評価完了した際、それとオーナーにより新しい椅子が有効化された際にそれぞれマッチング処理を実行するようにする
    • マッチング間隔の短縮は、ライドのマッチング時間短縮と椅子の稼働率向上それぞれに寄与する。よって短いに越したことはないだろうということで
  • マッチングの際、速い椅子を遅い椅子より優先的に使い、また乗車位置に近い椅子を遠い椅子より優先的に使うように
    • 評価の上げ方は細かくは非公開であり、また全体効率も考えてもわからないと判断し一旦の決め。試行錯誤前提のロジック

と方針を立てて実装を開始しました。この時点ではスコアは伸びていないものの、まっとうに方針を決められたことで、これからしっかり実装すればスコアを爆上げできるとと考えていました。

この時点のスコア: 1,000 前後

14:00〜16:00

立てた方針に沿って実装を行いました。変更箇所が思ったより多く、見立てより時間がかかりました。

この時間帯にはぼちぼち他のメンバーの修正が完了し、デプロイが行われはじめました。しかしデプロイしても期待した上がり幅が得られなかったり、FAIL し revert したりしている様子でした。

このとき、開発フロー面での課題が明らかになってきました:

  • 変更後の検証作業が直列でしか行えない
    • 3 人で作業しているにも関わらず、共通の Makefile などを main ブランチにコミットした後、そのままの成り行きで全員 main ブランチを使っている
    • ブランチと同様に、3 台のマシンが与えられたにも関わらず 1 台のみを 3 人で共用している
  • デプロイ環境整備を行ったメンバー 1 人のみが成り行きでサーバー上での作業役となり、他のメンバーのデプロイ作業の肩代わりやデプロイ順番待ちの管理をする羽目になっている
  • ベンチマーカーが FAIL した際に直接の原因 (何の Assert に失敗したのか) はわかるものの、根本原因を知る手段 (エラーログを見るなど) がなく確実な修正ができない

この時点で開発フローを変えようという気にはなりませんでしたが、開発フローの問題は最後までつきまといました。

細かな経緯は忘れましたが、主に ride_statuses テーブルにインデックスが貼られたことにより、スコアは倍増しました。

この時点のスコア: 2,120

16:00〜終了

16:00 過ぎに僕の担当していたマッチング処理の修正が完了しました。僕としてはいち早く適用しスコア爆上げと行きたかったところですが FAIL し、その原因が修正できず revert しました。

サーバーを全員で 1 台しか使っておらず、他の修正のデプロイ&ベンチマーカー実行の試行錯誤が盛んに行われていたことから、僕は担当していたマッチング処理の修正適用を諦め、せめてサーバーと RDB のマシンを分けようとしましたが、間に合いませんでした。

終了前の数十分は焦りが増す中でベンチマーカーが FAIL を示すようになり、git reset で変更を切り戻しては混んできたベンチマーカーに Enqueue し、祈り、FAIL し絶望するという辛い時間を過ごし、終了時間を迎えました。

最終スコア: 0

悲しい結果となりましたが、以降記憶が新しいうちに振り返ってみます。

良かったところ

準備の成果を十二分に発揮できました。変更を Git 管理し反映できる状態をスムーズに作れ、また alp 等を使った問題特定も早期かつ妥当に行うことができました。

反省

圧倒的素振り不足

一度でも本番相当の演習を行っておけば気付けるような開発フローの問題が露呈し、クリティカルな敗因となりました。社内で一番良い成績を取った別チームのメンバーも「問題を何度も解いた」と書いてますし、全員揃った状態実践を想定した演習を一度でも行っておくべきでした。

tech.every.tv

パフォーマンス改善には改修を伴う

何を当たり前のことと思われるかもしれませんが、ISUCON の勘所は問題の特定だと捉え、それさえ正しくできればその後の修正は普段の開発業務の延長であろうと高をくくっていました(これはチーム全体というより僕だけかもしれません)。

本番では問題特定には概ね成功したものの、それを解消するための変更に失敗しました。普段の開発ではローカルでの開発環境があり、気軽に単体テストを書き CI/CD に反映を委ねます。それがない状況でどのように改修を行うのかを真面目に検討できていませんでした。

担当の割り振り方

これは結果論かもしれませんが、一見関係の薄そうに見える処理についても元をたどると共通の問題に当たったりします。今回(気軽に相談は挟むものの)ハッキリと問題ごとに担当を分け作業をしたので、多くの表面的な問題の背後に存在する根本的な原因について相談し共通の意思決定に持っていくことができませんでした。分担するにしてもその単位を細かくするなど担当の割振りは改善の余地があると考えています。

具体的には chairs テーブルに最新の位置情報やマッチング状態が存在しないことが、ridesride_statuses, chair_locations テーブルとの JOIN やループ処理を発生させパフォーマンス劣化につながるという事態が複数箇所で見られたはずで、そこはチーム共通で指針を立てられたら良かったと思います。

ドキュメントを全員で読み合わせなかった

最初にデプロイの仕組みを整備したメンバーは、その後クエリのチューニング作業を担当し、そのまま実作業に入りました。その結果、そのメンバーはスコアの算出方法すら後半に知るような事態が起こりました。

複数人で協力して作業するにあたり、まずドキュメントを読み合わせ、不明点を解消し、何を目指していくのかの認識をざっと揃えておくことがその後の作業のスムーズさに大きく寄与すると感じました。

さいごに

反省点はたくさんありますが、ISUCON14 に楽しく参加することができました。他の方のリポジトリやブログ記事を読みたいと思っていますが、全然追いついていません。

またこの場を借りて運営の方々への感謝を申し上げます。Discord や公式ブログでのアナウンスもとてもわかりやすかったですし、当日のライブ配信も楽しく観覧しました。出題内容やドキュメント、当日の Dashboard も良くできており、ワクワクしながら競技に参加することができました。ありがとうございました。

当日の問題やドキュメントを含むリポジトリが公開されており、docker compose でも動かせるようですので時間を見つけてまた触ってみようと思います。

github.com

エブリーでは、ともに働く仲間を募集しています。不甲斐ない結果でしたが、少しでもエブリーに興味を持っていただけた方は、一度カジュアル面談にお越しください!

corp.every.tv