画像生成のスケーリング: キュー、冪等キー、リトライ
画像生成は、多くの Web アーキテクチャが前提としている条件を壊します。リクエストはミリ秒ではなく数十秒かかり、処理能力は物理的である以上まさしく有限で、失敗したリクエストはすでに実際のコストを消費しています。これを正しく保つためのパターン自体はよく知られていますが、典型的な CRUD バックエンドが最初から備えているものではありません。
キューこそが設計そのもの
最初に思いつくのは、リクエストハンドラーから直接 API を呼ぶ方法です。開発環境では動きますが、実トラフィックが来た瞬間に破綻します。需要と処理能力の間に何もないため、スパイクはタイムアウトの壁になり、プロセスが再起動したときに何が実行中だったのかの記録も残りません。
間に永続キューを置いてください。「永続」が肝心な言葉です。メモリ上のリストはデプロイのたびに実行中のジョブをすべて失いますし、デプロイはトラフィックのある時間帯にも行われます。この規模ならデータベースのテーブルで十分に機能し、副産物として復旧可能な状態の記録も手に入ります。
バックプレッシャーは機能である
キューがあれば、同時に実行してよいジョブ数を決められます。その上限は技術的な詳細ではなくプロダクト上の判断です。高すぎれば全員にとって遅くなり、低すぎればキューが伸びる一方で処理能力が遊びます。重要なのは上限が存在し、1 か所で強制されていることです。そうすればトラフィックのスパイクは、すべてのリクエストを劣化させるのではなくキューを伸ばすだけで済みます。
冪等キーと曖昧な失敗
お金を失う失敗モードはエラーレスポンスではありません。エラーは簡単です。問題は曖昧なケースで、リクエストはプロセスを出たが接続が切れ、ジョブが作成されたかどうか判断できない状況です。リトライすれば二重に支払ったかもしれませんし、リトライしなければユーザーは何も得られずに支払ったかもしれません。
冪等キーはリトライを安全にすることでこれを解決します。同じキーなら 2 つ目のジョブを作るのではなく元のジョブが返ります。ルールは単純ですが間違えやすいものです。キーは HTTP 呼び出し時ではなくユーザーが操作した時点で生成し、ジョブレコードとともに保存し、その操作のすべてのリトライで再利用してください。
# 1 回のユーザー操作のリトライには同じキーを使う KEY=$(uuidgen) curl -X POST https://api.example/api/v1/jobs \ -H "X-API-Key: $API_KEY" -H "Idempotency-Key: $KEY" \ -F image=@photo.jpg -F feature=undress -F consent=confirmed
自動リトライしてはいけないもの
止まることが安全策になる失敗のカテゴリーがあります。投入を送信してからレスポンスを記録するまでの間にプロセスが落ちた場合、何が起きたのか本当に分かりません。重複排除の手がかりとなるキーがなければ、自動リトライは二重課金になりかねないコイントスです。そうしたジョブは闇雲にリトライせず、要確認としてマークしてください。目に見える形で止まっているジョブのほうが、気づかれない二重課金より良い結果です。
照合ループが救ってくれる
リアルタイムのイベントは便利ですが信頼できません。ソケットは切れ、プロセスは再起動し、肝心なメッセージは誰も聞いていないときに届きます。解決策は、データベース上でまだ実行中とされているすべてのジョブについて定期的に API に問い合わせ、その回答で状態を更新するバックグラウンドループです。
このループ 1 つが、バグの一群をまるごと吸収します。イベントの取りこぼしは問題ではなくなります。生成中の再起動も問題ではなくなります。その間に接続に何が起きていたとしても、すべてのジョブは 1 回の照合間隔のうちに正しい状態へ収束します。
データベースを叩かずに進捗を扱う
生成は進捗を頻繁に、ジョブあたり 1 秒に数回のペースで発行することもあります。その 1 回ごとをデータベースに書き込むと、情報量は増えないのに書き込み負荷だけが倍増します。永続化は 1 秒に 1 回程度にスロットリングし、最終状態は必ず書き込むようにしたうえで、リアルタイムの値はストレージに触れずに接続中のクライアントへ配信してください。
実務で通用するチェックリスト
- ユーザーと API の間に、再起動後も復旧できる永続キューを置く。
- 実行中ジョブ数の明示的な上限を、1 か所で強制する。
- 冪等キーをユーザー操作ごとに生成し、保存し、リトライ時に再利用する。
- 曖昧な送信失敗は自動リトライせず、フラグを立てる。
- イベントの取りこぼしに関係なく状態を収束させる照合ループを持つ。
- 進捗の書き込みはスロットリングし、リアルタイムの進捗はソケットで流す。
以上はどれも画像生成に固有のものではなく、ごく一般的な分散システムの衛生管理です。ただ、1 単位の処理が十分に遅く、十分に高価であるために、いつもなら見過ごされる手抜きがここでは表面化するというだけのことです。
よくある質問
メッセージブローカーは必要ですか。
この規模なら通常は不要です。claim クエリを備えたデータベースのテーブルがあれば、構成要素を増やさずに永続性と復旧可能な状態の記録が得られます。
同時実行ジョブ数はどれくらいにすべきですか。
低めから始めて、キューの待ち時間と完了時間を計測し、レイテンシーが改善しなくなるまで引き上げてください。数値そのものより、強制される上限が 1 つあることが重要です。
失敗したジョブは自動でリトライすべきですか。
明確なエラーであればリトライして構いません。処理が作成されたかどうか判断できない曖昧な送信失敗はリトライすべきではなく、フラグを立ててください。闇雲なリトライは課金済みの生成を重複させかねません。
uncloth.app で開発する
REST エンドポイント 1 本、11 種類のプリセット、結果はそのままお客様のバックエンドへ返却されます。開発中のプロダクトについてお知らせいただければ、アクセス情報をお送りします。
アクセスを申請する