去年、ロードバランサーの後ろにサーバーを増やしてAPIをスケールしようとした。1,000同時ユーザーではトラフィックは問題なかった。5,000でぜんぶ壊れた。
ロードバランサーはランダムにリクエストをルーティングしていた。でもセッションはサーバーメモリに住んでいた。ユーザーAがサーバー1でログイン、次のリクエストがサーバー2に行く、突然ログアウト状態。チェックアウト中にショッピングカートが消えた。
修正はサーバーを増やすことではなかった。サーバーから完全に状態を取り除くことだった。
ステートレスサービスはリクエスト間で何も覚えていない。すべてのリクエストには処理に必要なものがすべて含まれている。リクエスト途中でサーバーを殺し、再起動し、同じリクエストを再送しても、同じ結果が得られる。
ステートフルサービスはリクエスト間でデータを保持する — メモリ内のセッション、ローカルディスクのファイル、ローカル変数のロック。そのデータがユーザーを特定のサーバーに縛り付ける。
コーヒーショップチェーンで考えろ。ステートレスな店:入って、注文して、払って、コーヒーを受け取る。どの店舗でも同じ。ステートフルな店:ポイントカードを店で保管している。今やその1店舗に戻り続けるしかない。
ほとんどのアプリはリクエスト間でユーザーを追跡する。一度ログインして、ブラウズして、カートに追加。HTTPはステートレスなので、セッションCookieで継続性を偽装する。
問題は:セッションデータはどこに住むか?
サーバーメモリ内: 速いが、ユーザーがサーバーに固定される。水平スケールできない、再起動すると全員ログアウト。
Cookie内: シンプルで本当にステートレス。小さなデータには最適 — ユーザーID、トークン、設定。でもCookieはすべてのリクエストについてくる。5KBのセッションデータは5KBのオーバーヘッドがすべてのHTTP呼び出しにかかる。モバイルユーザーはこれを感じる。
外部ストレージ内: Redisかデータベースがセッションを保持。CookieにはセッションIDだけ。どのサーバーでもセッションをルックアップできる。ほとんどの本番システムはこれをやる。CookieのセッションIDがRedisのデータを指す。どのサーバーでもどのリクエストでも処理できる。
セッションは明らか。ファイルはこっそり。
ユーザーがプロフィール写真をアップロード。サーバー2のディスクに着地。次のリクエストはサーバー1に行く。プロフィール写真が見つからない。
オブジェクトストレージ(S3、GCS、MinIO): ファイルを外部に保存。すべてのサーバーが同じバケットを読み書き。これが標準解。
読み込みにCDN: 静的コンテンツをエッジノードから配信。ユーザーに速い、サーバー負荷が減る。
生成ファイル — PDF、レポート、エクスポート — 同じ問題。生成して、オブジェクトストレージにプッシュして、署名付きURLを返す。ローカルに保存するな。
キャッシュ: ローカルのインメモリキャッシュは無害に見える。サーバー1でユーザーがプロフィールを更新、サーバー2のキャッシュはまだ古いデータ。解決策:集中キャッシュ(またRedis)かキャッシュ無効化戦略。
スケジュールジョブ: 特定サーバーでのcronジョブは暗黙の状態を作る。そのサーバーが特別になる。分散ジョブキューに移動 — SQS、RabbitMQ、データベースバックドキュー。
WebSocket接続: 長寿命の接続は本質的にステートフル。ユーザーがサーバー1に接続したら、メッセージをサーバー2経由でルーティングできない。解決策:スティッキーセッション(妥協)、またはpub/subレイヤーでどのサーバーからでも任意の接続にパブリッシュ。
ステートレスは無料ではない。外部セッションストレージはレイテンシを追加 — すべてのリクエストでRedisへのネットワークホップ。オブジェクトストレージはローカルディスクより遅い。分散キャッシュはMap<String, Object>より複雑。
でも代替案はスケールできないこと。ユーザーを追い出さずにデプロイできないこと。サーバー障害からグレースフルに回復できないこと。
ほとんどのWebアプリケーションでは、トレードオフは価値がある。サーバーを交換可能なクローンにしろ。状態はそのために設計された専用システムに置け。
サーバーが影響なしに死ねないなら、スケールできない。
— blanho
CRUDは履歴を上書きする。イベントソーシングはすべてを記憶する。それが重要な場面とオーバーキルな場面。
冗長パイプライン、インテリジェントなセグメント選択、カスタムストレージレイヤー — Netflix Live Originアーキテクチャの内側。
ロギング、認証、リトライ、レート制限 — 最初に設計しないが、後で全員が苦しむもの。