Skip to content
Published on

デプロイこそが負荷テストだ — キャッシュを満たす費用を設計しないと起きること

シェア
Authors

はじめに — 問題は秒間リクエストではなくデプロイだった

セッション処理は普通こう最適化します。セッション情報を暗号化してクッキーに載せれば、ゲートウェイはリクエストのたびにデータベースを参照する必要がなくなります。リクエストあたりのネットワーク往復をひとつ消したわけで、秒間数十万件を処理するシステムでは大きな利得です。

ところがログアウトや権限変更が起きると、すでに発行されたクッキーを無効化しなければなりません。そのためゲートウェイごとに「無効化されたセッションの一覧」をインメモリで持つ必要があります。参照は相変わらずほぼ無料です。

Canvaが2026年7月22日に公開したSession revocations at scaleが扱う問題はその次です。そのインメモリの一覧を最初にどうやって満たすのかです。

原文の表現で、数百のゲートウェイポッドがそれぞれ100万件を超える無効化記録を起動時にMySQLから引いてきたことで、「デプロイがデータベースに対する組織的な殺到」になりました。

定常状態の費用と起動費用は別の問題だ

この話の核心はここにあります。私たちがキャッシュを評価するときに見る指標は、ほとんどが定常状態についてのものです。ヒット率、参照遅延、メモリ使用量。これらの指標がすべて優秀でも、システムは崩れることがあります。

インメモリキャッシュには、指標に乗りにくい二つ目の費用があります。

  • 定常状態の費用 — リクエストあたりの参照。ここでは事実上ゼロです。
  • 起動費用 — プロセスが立ち上がるときにデータセット全体をどこかから取ってくる費用。ポッドひとつで見ればたいしたことはありませんが、デプロイはポッドひとつではなく艦隊全体を同時に再起動します。

起動費用はポッド数に比例し、デプロイの頻度だけ繰り返され、よりによってロールアウトというもっとも神経質な瞬間に発生します。定常状態の負荷とはまったく別の軸です。

Canvaが一時しのぎに使った対応が、まさにこの性格を示しています。読み取りレプリカを大幅に増やして持ちこたえました。読み取りレプリカは定常状態の参照量ではなく、デプロイ時のスパイクを支えるために存在している状態だったわけです。デプロイしていない時間はほとんど遊んでいたということです。

12時間という数字はどこから出てきたのか

無効化の一覧をインメモリにどれくらい長く持っておくべきでしょうか。永遠にではありません。

Canvaのセッションクッキーは定期的に更新されます。更新の時点ではどのみちデータベースを参照するので、更新を経たトークンはインメモリキャッシュに依存しません。そこで12時間分の無効化だけをメモリに置き、更新が必要なトークンは遅いMySQLの参照で確認します。

この設計がよいのは、キャッシュサイズの上限がトークンの寿命から導かれる点です。利用者数ではなく時間の窓が大きさを決めます。利用者が二倍になれば無効化の発生率も二倍になりますが、窓の長さはそのままです。

キャッシュ設計では「何を捨ててよいか」に答えがあれば、大きさの問題はたいてい解けます。ここでは更新の経路が安全網なので、12時間より古いものを捨てても正確性は壊れません。

Redisを入れなかった理由

読み取りを拡張する標準的な解法は、データベースと読み手のあいだにキャッシュをひとつ入れることです。CanvaもRedisを検討しました。ゲートウェイが起動するときにRedisへデータセット全体を要求し、以後は新しい無効化を定期的にポーリングするという絵です。

却下した理由は二つで、どちらも技術選定一般に当てはまります。

第一に、Redisは普通、完全な耐久性の構成でデプロイされません。無効化の一覧が飛べば、ログアウトしたセッションが生き返ります。これは性能キャッシュではなくセキュリティ境界です。

第二に、原文の表現で「問題をあるデータストアから別のデータストアへ移しつつ、キャッシュの一貫性を保つためのかなりの複雑さを足すだけ」です。Redisクラスタ自体を運用しなければならない負担はそのまま残ります。

層をもうひとつ載せる解法の落とし穴がここにあります。新しい層は、新しい障害モード、新しい運用負担、新しい一貫性の問題を一緒に連れてきます。元の層の負担は減っても、システム全体の負担は増えることがあります。

スライディングウィンドウをオブジェクトストレージに収める方法

そこでS3を選びました。強い耐久性の保証と、大きなファイルの効率的な一括ダウンロードは、まさに必要だった性質です。

問題は、S3が静的な塊をうまく扱う一方で、このデータは流れ続ける窓だという点です。常に直近12時間で、あらゆる瞬間に前が入ってきて後ろが抜けていきます。

解法は窓を30分単位の断片に切り、断片ひとつをS3オブジェクトひとつに対応させることでした。この決定から三つのことが一度についてきます。

  • 古い無効化を個別に消す必要がありません。ゲートウェイは直近の断片だけを取ってくればよくなります。
  • 更新の単位が小さくなります。12時間全体ではなく30分分だけを書き直します。
  • 断片の名前に30分の窓の開始時刻を入れておけば、キーを整列順になめてカットオフ以降の断片だけを選び出せます。

最後の項目が特に実用的です。S3には「直近のものだけください」という問い合わせはありませんが、キーを時刻順に整列するよう名付ければ、接頭辞の列挙だけで同じ効果が得られます。

16バイト — 表現がそのまま性能だ

この記事でもっとも学ぶに値する部分です。

無効化ひとつが担うべき情報は二つです。誰に適用されるのか(principal)、そしてどのログイン時刻まで適用されるのか。ビットを節約して配置すると16バイトに収まります。断片はこの16バイトの要素の平らな配列です。

ここに整列を足します。principalを基準に整列しておけば、断片のなかで二分探索ができます。結果としてゲートウェイは、ダウンロードしたバイトを別の表現へ変換せずそのまま使います。

以前の実装は無効化ひとつを複数のJavaオブジェクトで追跡していました。密な二進表現に変えたところ、インメモリキャッシュの大きさが8分の1、すなわち87.5パーセント減りました。

実際には無効化の種類がいくつもあります。ログアウトさせずにクッキーへキャッシュされた情報だけを無効化する場合や、個人ではなくブランド全体を対象にする場合があります。そこでフラグ用のビットをいくつか残しておき、principalを基準にした整列さえ保てば、ひとつの配列に複数の種類を混ぜておけます。

まとめるとこうです。キャッシュを満たす費用を減らしたのは新しい層ではなく、逆シリアライズの要らない表現でした。ダウンロードが終われば、キャッシュも終わっています。

二乗の複雑度のワーカーで十分だった理由

断片を最新の状態に保つ側には別の問題があります。無効化が生じるたびに断片を書き直しては、費用が持ちません。

そこで非同期ワーカーがこの仕事を担います。データベースをなめ続けてまだS3に上がっていない無効化を大きなバッチで取ってきて、最新の断片を受け取り、整列した配列に挿し込んでから上げ直します。

原文自ら指摘するとおり、これは一見すると拡張しないように見えます。断片にN件を詰めるには、固定サイズのバッチごとに断片全体を処理しなければならないので、Nに対して二乗に近い時間がかかります。しかも更新損失の問題のせいで水平拡張も簡単ではありません。

ところが実測すると、バッチあたり数百件ずつ処理する最適化されていない実装でも秒間2000件以上の書き込みスループットが出て、これは予測可能な将来の要求を超えます。原文の結論が正確です。ワーカーは計算ではなくネットワーク遅延にボトルネックがありました。

理論的な複雑度は入力が大きくなるときの傾きを語るだけで、あなたの入力範囲での実際の時間を語りません。数十万個の密な配列を整列することは、現代のCPUにとってほとんど無料です。定数が支配する区間では、定数を測らなければなりません。

正確性は条件付きPUTにあり、リーダー選出は最適化だ

ワーカーは可用性とデプロイのために複数走ります。素朴に実装するとS3に対する競合状態が生まれ、読んで直して書くあいだに別のワーカーの更新が静かに消えます。

Canvaは二つを使います。

条件付きPUTで楽観的同時実行制御を実装します。すべての断片更新に「最初に読んだあとこの断片は変わっていない」という前提条件を付け、新しい断片を作るときにも別のプロセスが同じ断片をすでに作っていないかを確認します。この前提条件が、読んで直して書く操作を常に追加するだけの操作に変え、実行がどう入り混じってもデータが消えないことを保証します。

ZooKeeperのリーダー選出は、継続的な衝突がシステムに負荷を与えないようにするための最適化です。

この二つの文の順序が重要です。原文は、リーダー選出に正確性を依存させることはできないと明確に書きます。ノードは書き込む直前に任意の時間だけ止まることがあり、目覚めたときにはすでに別のノードがリーダーになって自分の変更を書いてしまっているかもしれません。PUTの条件がなければ、眠りから覚めたノードがそれを上書きします。

分散システムにおいてリーダー選出は、ほとんど常にこの位置にあるべきです。衝突を減らす装置であって、衝突を不可能にする装置ではありません。 正確性は、ストレージが提供する原子的な条件付き操作に掛けなければなりません。

参考資料

この記事の数値(無効化100万件以上、12時間の窓、30分の断片、16バイト、メモリ87.5パーセント減、秒間2000件以上、読み取りレプリカ2台)は、すべて上のCanvaの原文に書かれた値をそのまま移したものです。自分で再現した測定値ではありません。