
アプリが自分自身にDDoSを仕掛けた日
起動時に一気に放たれた保留中アップロードの滞留が、あらゆるGeekByeクライアントを、自社サーバーへの小さなサービス拒否攻撃へと変えました。その修正 — そしてそれが私たちに作らせた接続生存性のはしご — は、v2が教えてくれた最も役立つことの一つです。
分散システムのエンジニアは、いつか必ず殺到(thundering herd)に出会います: 大量のクライアントが同じ瞬間に同じことをして、その背後にある共有リソースが崩れ落ちる、というやつです。たいていは誰か他人のクライアントです。GeekBye v2.0.1では、それは私たちのものであり、攻撃者は私たち自身のアプリでした。
議事録アプリはどうやって自分自身を攻撃するのか
GeekByeは録音をあなたのGoogle Driveへアップロードできます。もし録音がすぐにアップロードできなければ — オフラインだった、アプリが閉じた、Driveが不調だった — それは後で再試行するために滞留に入ります。理にかなっています。
失敗は「後で」がいつ起きるか、にありました。次の起動時に、アプリは滞留の全体を一度に吐き出そうとするのです。1週間分の保留録音を抱えた一人のユーザーが、アプリを開いた瞬間に同時アップロードリクエストのバーストへと変わります。それを、朝に起動する全ユーザーで掛け算してください。すると私たちのバックエンドは、認証し、レートを確認し、トラフィックの壁を処理することを、同じ数秒のうちに求められることになります — しかも、技術的には全員が行儀よく振る舞っているクライアントから。
私たちのバックエンドは正しいことをして、その洪水をレート制限しました。そして、そこから二次被害が始まりました。レート制限のレスポンスはHTTPの429であり、429は、注意していないと他の失敗によく似て見えます:
- 起動時のプロフィール取得が
429を受け取り、アプリはそれを認証失敗として扱いました — まったく有効なセッションから、ユーザーを一瞬ログアウトさせてしまったのです。 - 文字起こし接続が汎用的な
429を受け取り、音声上限に達したアップグレード表示を出しました — 有料ユーザーに、達してもいないクォータに達したと告げてしまったのです。
こうして一つの根本原因 — ペース配分されていない滞留 — が、三つの目に見える症状を生みました: サーバーの逼迫、偽のログアウト、そして偽のアップセルです。自己DoSの典型的な形です: 負荷は悪いものですが、ユーザーが実際に感じるのは、その負荷の症状の誤読のほうなのです。
修正、二つの層で
殺到を止める。 アップロードの滞留は今やペース配分されます — リクエストはバックオフで分散され、いかなる429の後にもクールダウンが入り、クライアントは逼迫したサーバーにさらに寄りかかるのではなく、そこから引きます。殺到(thundering herd)が、秩序ある行列になります。
誤読を止める。 起動時の一時的な429や5xxは、もうあなたをログアウトさせません — クライアントは「サーバーが一瞬だけ忙しい」を「あなたのセッションが無効」と区別します。そして文字起こし経路での汎用的なレート制限429は、もう音声クォータエラーの振りをしません。本物のクォータレスポンスだけがアップグレード表示を出します。身についた教訓はこうです: エラーコードは、エラーの意味ではない。 429は「速度を落とせ」を意味するのであって、「あなたは未認証だ」でも「あなたはクォータ切れだ」でもありません — そしてクライアントはその違いを知っていなければなりません。
それに続いた生存性のはしご
殺到をペース配分したことで、より静かな問題が露わになりました: 負荷の下で接続が本当におかしくなったとき、私たちはどれだけ速く気づいたのか? 望んでいたより遅かったのです。そこでv2.0.4は、リアルタイム文字起こしのための接続生存性のはしごを組み上げました:
- ハートビート — 文字起こしソケットは固定間隔でpingされ、静かに死んだ接続が、次の音声チャンクが失敗するのを待つのではなく、数秒で検知されます。
- ネットワーク復帰時の再接続キック — OSがネットワークが戻ったと報告したとき、アプリはその発見につまずくのを待つのではなく、能動的に接続を張り直します。
- 制御用ping/pongの生存性 — アプリケーションレベルの往復で、「ソケットが開いている」だけでなく「相手が実際に応答している」ことを確認します。
- サービスが所有する再接続ゲート — 再接続するかどうかを一箇所が決めるので、アプリの複数の部分が競り合ってお互いを踏みつけることがありません。
どれも華やかではありません。そのすべてが、v2のセッションがただ…つながり続けているように感じられる理由です。
今週また起きた — 一段下で
ここが、これを単なる武勇伝以上のものにする部分です。この同じ失敗クラスが数日前、私たちの一つ下の階層で再浮上しました。一つのクライアントが400ミリ秒未満で21回の文字起こしセッション開始を発射し — 私たちの音声プロバイダーのアカウント全体で15並列という上限に引っかかったのです。共有インフラへの殺到、まさにあのアップロード滞留と同じで、ただ自社バックエンドではなく依存先に向けられただけです。
形は同一で、修正も同一です: 殺到するクライアントには、一つの意図で20回の開始を発射できないようシングルフライトのガードが必要で、共有リソースには、一つのクライアントがプールを食い尽くせないようユーザーごとの上限が必要です。私たちはこのクライアント側の版を以前に作ったことがあります — アップロードのペーサーこそこのパターンです。今度はそれをセッション開始に適用します。殺到(thundering herd)は一度直せば終わりのバグではありません。それは、クライアントが共有の上限に出会うあらゆる場所で見分けられるようになる、一つの形なのです。
持ち帰るべき三つのこと
- あなた自身のクライアントは、あなたが予定していなかった負荷テストだ。 起動時にまとめて処理するもの、起動時に再試行するもの、復帰時に再接続するもの — それらはすべて、十分なユーザーを待つ殺到(thundering herd)です。ユーザーを抱える前にペース配分しましょう。
- コードを意味と区別せよ。
429は、存在するステータスの中で最も誤読されるものです。「速度を落とせ」は「ログアウトしろ」でも「金を払え」でもありません。それぞれの失敗を、それが実際に意味することへ振り分けましょう。 - 生存性はフラグではなく、はしごだ。 「接続は生きているか?」には、異なる層で複数の誠実な答えがあります — ソケットが開いている、バイトが流れている、相手が応答している、ネットワークがある。堅牢なアプリは、一つ以上を確認します。
これがGeekBye v2の穏やかさの下にある、華やかさのない機構です。それが可能にした信頼性機能については、AIノートテイカーはなぜ悪いWi-Fiで止まるのかとファイアウォールがWebSocketを塞ぐときのリアルタイム文字起こし(v2.0.8)をご覧ください。このシリーズの近隣リリースについては、AIノートテイカーはなぜ会議の途中で録音を止めるのか(v2.0.9)をどうぞ。