Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

初心者3人でISUCONに参加してめっちゃ楽しめた件

こんいす!バックエンドエンジニアのogatasoです。 今回は12月8日に開催された ISUCON14 に、私とshirakawaさん、yamakuraさんの3人でチームMirrormanとして参加しました。全員ISUCON未経験の状態から挑戦し、楽しく学びの多い体験になったので、この記事で共有したいと思います。

ISUCONとは?

ISUCON とは「Iikanjini Speed Up Contest」の略で、与えられた遅いWebサービスを制限時間内にどれだけ高速化できるかを競うコンテストです。 パフォーマンス改善を目的にインフラからアプリケーションまで多くのレイヤーに跨る技術知識を要求される総合格闘技的なところが魅力です。 ミラティブはISUCONのスポンサーであり、スポンサー枠が割り当てられていましたが、ISUCON経験者はすでに自分で枠を取っていたため余っており、私たちはmakinoさんの突然の声かけで10月半ばに参加が決まりました。

tech.mirrativ.stream

練習期間と準備

練習期間は2ヶ月程であり、チームは全員がISUCON未経験者だった中、以下のような準備を進めました。

  • 書籍購入制度で「達人が教えるWebパフォーマンスチューニング〜ISUCONから学ぶ高速化の実践」(通称:ISUCON本)を購入・読破
  • ISUCON本で紹介されている「private-isu」で実際に手を動かして練習

特にISUCON本は初心者の私たちにとって良い入門書となり、実際の競技のイメージをつかむのに役立つだけでなく、ログの設定、解析やMySQLのチューニング方法など、普段の業務にも活用できる知識を得ることができました。

本番当日

無事にチームメンバー全員が起床に成功し、YouTubeで問題概要を聞くことができました。 今年の問題はライドシェアチェアサービス「ISURIDE」がテーマで、初期状態では大規模障害が発生しているという設定からスタートしました。


www.youtube.com

やったこと

大会では、明確な役割分担はせず「各々ができそうなことをやる」というスタイルで進めました。以下が実際に取り組んだ内容です。

インフラセットアップと初期ベンチマークの準備

shirakawaさんに爆速で環境構築やGitHub設定をやっていただきました。このおかげでスムーズにスタートを切れました。

クエリのパフォーマンス改善

スロークエリログを見ながら、特に遅いクエリから順番に適切なインデックスを追加し、効率化を図りました。 ほとんどは改善策としてインデックスの追加をするものでしたが、chairの総走行距離を求める以下のクエリでは、クエリの度に計算し直す非効率的なものになっていました。そのため、chairの座標がPostされるタイミングで計算しておくことにより効率化を図りました。

if err := db.SelectContext(ctx, &chairs, `SELECT id,
       owner_id,
       name,
       access_token,
       model,
       is_active,
       created_at,
       updated_at,
       IFNULL(total_distance, 0) AS total_distance,
       total_distance_updated_at
FROM chairs
       LEFT JOIN (SELECT chair_id,
                          SUM(IFNULL(distance, 0)) AS total_distance,
                          MAX(created_at)          AS total_distance_updated_at
                   FROM (SELECT chair_id,
                                created_at,
                                ABS(latitude - LAG(latitude) OVER (PARTITION BY chair_id ORDER BY created_at)) +
                                ABS(longitude - LAG(longitude) OVER (PARTITION BY chair_id ORDER BY created_at)) AS distance
                         FROM chair_locations) tmp
                   GROUP BY chair_id) distance_table ON distance_table.chair_id = chairs.id
WHERE owner_id = ?
`, owner.ID); err != nil {
    writeError(w, http.StatusInternalServerError, err)
    return
}

座標の入力時に前回の座標との差分を計算し、総走行距離を更新するようにしました。

// 前回の座標を取得
var oldLat, oldLon *int
err = tx.QueryRowContext(ctx, "SELECT last_latitude, last_longitude FROM chairs WHERE id = ?", chair.ID).Scan(&oldLat, &oldLon)
if err != nil {
    writeError(w, http.StatusInternalServerError, err)
    return
}
if oldLat != nil && oldLon != nil {
    // 差分計算
    diffDistance := absInt(*oldLat-req.Latitude) + absInt(*oldLon-req.Longitude)
    // 累計距離更新
    _, err = tx.ExecContext(ctx, `
        UPDATE chairs
        SET total_distance = total_distance + ?,
            last_latitude = ?,
            last_longitude = ?,
            total_distance_updated_at = CURRENT_TIMESTAMP(6)
        WHERE id = ?`, diffDistance, req.Latitude, req.Longitude, chair.ID)

結果的にクエリはシンプルになり、高速に動作するようになりました。

if err := db.SelectContext(ctx, &chairs, `SELECT id,
       owner_id,
       name,
       access_token,
       model,
       is_active,
       created_at,
       updated_at,
       IFNULL(total_distance, 0) AS total_distance,
       total_distance_updated_at
FROM chairs
WHERE owner_id = ?
`, owner.ID); err != nil {
    writeError(w, http.StatusInternalServerError, err)
    return
}

キャッシュの活用

静的ファイルをNginx経由で配信するようにしました。同時に、クライアント側で1日間キャッシュを保持させるようにしました。具体的には以下の設定を/etc/nginx/sites-available/isuride.confに追加します。クエリ改善の手が止まってきた際に、ISUCON本で同様の手法でスコアを上げていたのを思い出し、やってみるとうまくいきました。本を読んでいて良かったと思った瞬間です。

location ~* ^/(css|js|png|svg|html|ico)/ {
    root /home/isucon/webapp/public/;
    expires 1d;
}

やろうとしたけどうまくいかなかったもの

認証周りの改善

リクエストの度に以下のようなクエリでDBからアクセストークンに合致するユーザを取得する実装になっていたため、メモリキャッシュに載せようとしましたが間に合いませんでした。

err := db.GetContext(ctx, user, "SELECT * FROM users WHERE access_token = ?", accessToken)

実装が間に合えばこの部分は以下のような、キャッシュにヒットしない時のみDBを参照する処理になる予定でした。

if x, found := ce.Get("user_token_" + accessToken); found {
    user = x.(*User)
} else {
    err = db.GetContext(ctx, user, "SELECT * FROM users WHERE access_token = ?", accessToken)
    // エラー処理
}

DBの別インスタンスへの分離

一つのインスタンス上でアプリとDBが動作しリソースを食い合っていたため、DBは別インスタンスのものを利用するように試みましたが、ベンチマーカーが落ちるようになってしまい、原因を特定できず撤退しました。エラー原因としてはDB側のインスタンスでもisuride_matcherが動作し続けていたことが考えられます。

最終結果と振り返り

スコアは5452まで上げることができ、私たちが想定していた以上にスコアを伸ばすことができました。ただ、残念なことに最終結果は負荷走行NGになっていました。終盤に追い込みをかけてスコアを上げることができた分、再起動試験を全くやっておらず、気づくことができませんでした。

良かったこと

インデックスの効果を実感できた

ミラティブは高トラフィックなサービスであるため、基本的にクエリを追加、実行する際はEXPLAINを利用して、フルスキャンやファイルソートを避けています。 ISUCONでは全くインデックスを貼らないとどうなるのかを身をもって知ることができ、普段の業務の決まりの重要さを改めて実感することとなりました。

素晴らしいチームメイトに恵まれた

知識不足をカバーし合いながら、助け合って進められたのが良かったです。特にログ設定やコマンド周りでは多くを教えていただき、自分一人だけでは解決できないクエリの改善も達成することができました。

とにかく楽しかった!

パフォーマンス改善の効果がスコアとして直に現れるのが面白く、少しでもスコアが上がるたびに大喜びしていました。ISUCONの中毒性を感じられたかなと思います。

反省点

再起動試験の重要性

負荷走行NGを防ぐため、終了前に確認すべきでした。次回は終了30分前には必ず確認しようとチーム内で話し合いました。

チームでの練習不足

チーム練習は全くやっておらず、ぶっつけ本番で挑みました。明確な役割分担や複数回の練習があれば、より効率的に進められたと思います。

マッチングアルゴリズムの改善まで手が回らなかった

得点を上げるためには椅子とユーザのマッチングアルゴリズムを根本的に見直すことが重要だったそうなのですが、DBやAPIの速度改善を優先しており、手が回りませんでした。 優先度付けのためにも、最初にマニュアルをもう少し落ち着いて読んでいても良かったかもしれません。

まとめ

今回の経験を通して、課題は多くありつつも初心者でもISUCONを存分に楽しめることが分かりました。少しでも興味がある方は、ぜひ次回チャレンジしてみてください!

We are hiring!

株式会社ミラティブは非常に多くのユーザを抱える配信プラットフォームMirrativを運営しており、大規模なトラフィックがあることからパフォーマンスは常に重要視されています。社内にはISUCON部も存在しています! ISUCONが好きな方もそうでない方もエンジニアは絶賛募集中なのでお気軽にご連絡ください!

speakerdeck.com mirrativ.notion.site www.mirrativ.co.jp