スケールの方向 — 増やし方を決める
見積もった負荷が 1 台に収まらないと分かったとき、増やし方には 2 つの方向があります。1 台を大きくするか、台数を増やすか。
このうち台数を増やす側には条件が付きます。**サーバーが状態を持っていると増やせません。**この条件を満たす作業が、スケールの話のほとんどを占めます。
この章で学ぶこと
- 垂直と水平の違いと、それぞれの限界
- 水平にするために状態を外へ出す手順
- ロードバランサーが何を単位に配るかとヘルスチェック
- 台数を自動で増減させるときの遅れ
- 読み取りをキャッシュへ、書き込みをキューへ逃がす
- ボトルネックが移動する順序
規模の見積もり で負荷の数字を出していることを前提にします。HTTP キャッシュ の「誰がキャッシュを持つのか」も使います。
この章で扱わないこと
キューを挟んだ先で起きることは本章の射程を超えます。メッセージの再送と重複、順序の保証、失敗したメッセージの退避といった運用は、サービス間連携を扱うガイドの領域です。ここではキューを「負荷を平準化する仕組み」として扱うにとどめます。
リトライとタイムアウトのクライアント側の設計は リトライと冪等性、キャッシュヘッダーの書き方は HTTP キャッシュ が扱います。
2 つの方向
| 方向 | やること | 効く場面 | 限界 |
|---|---|---|---|
| 垂直 (スケールアップ) | 1 台の CPU・メモリ・ディスクを増やす | 分割できない処理、書き込みが集中するデータベース | **1 台の上限で止まる。**再起動を伴うことが多い |
| 水平 (スケールアウト) | 同じ役割のサーバーを増やす | リクエストを独立に処理できる部分 | 状態を持っていると増やせない |
垂直の利点は、アプリケーションを変えずに済むことです。設定を変えて再起動すれば終わります。だから最初の一手としては合理的です。
限界は 2 つあります。上限があることと、大きくしても 1 台であることです。1 台のままでは、その 1 台が落ちるとサービスが止まります。この問題は 可用性と冗長化 が扱います。
水平にするには状態を外へ出す
台数を増やしても正しく動くための条件は 1 つです。どのサーバーがリクエストを受けても同じ結果になること。
これを壊すのは、サーバーの中に置かれた状態です。
代表的な状態は 3 つあります。どれも「サーバーの外の共有された場所」へ移します。
| 状態 | サーバーに置くと何が起きるか | 移し先 |
|---|---|---|
| セッション | 別のサーバーへ振られた瞬間にログインが切れる | 共有のデータストア。またはクッキーに署名付きで持たせる |
| アップロードされたファイル | 保存したサーバーでしか読めない | オブジェクトストレージや共有ファイルシステム |
| メモリ上のキャッシュ | サーバーごとに内容が食い違う | 共有のキャッシュサーバー。または食い違ってよい用途に限定する |
3 行目には例外があります。**食い違ってもよいデータなら、各サーバーが自分のメモリに持って構いません。**マスタデータのように更新が稀なものは、むしろそのほうが速くなります。判断の分かれ目は「食い違ったときに何が起きるか」です。
セッションをサーバーの外に出さずに済ませる方法もあります。同じ利用者を毎回同じサーバーへ振るやり方です。ただしこれは、そのサーバーが落ちるとその利用者のセッションが消えることを意味します。台数を増やしても負荷が均等に散らない原因にもなります。
ロードバランサーが配る単位
ロードバランサーは、届いたリクエストをどのサーバーへ渡すかを決めます。配り方には方式があり、既定は順番に配る (ラウンドロビン) です。
| 方式 | 配り方 | 向く場面 |
|---|---|---|
| ラウンドロビン | 順番に 1 台ずつ | 各リクエストの処理時間が揃っているとき (既定) |
| 接続数が最も少ないサーバーへ | 処理中の接続が少ない台を選ぶ | 処理時間にばらつきがあるとき |
| クライアントの IP アドレスのハッシュ | 同じ利用者は同じ台へ | サーバー側にセッションを残したいとき |
| 応答時間が最も短いサーバーへ | 平均応答時間と接続数で選ぶ | 台ごとに性能差があるとき |
nginx では、ラウンドロビンが既定で、least_conn (接続数) と ip_hash を指定すると切り替わります (nginx — Using nginx as HTTP load balancer)。応答時間で選ぶ least_time は、1.31.0 より前は商用版でしか使えませんでした (ngx_http_upstream_module)。方式の名前を見たら、使っている版で利用できるかを確認します。
どの層で振り分けるかも選択肢です。TCP のポート番号だけを見て振り分けるか、HTTP の中身 (パスやホスト名) を見て振り分けるかで、できることが変わります。この区別は TCP/IP で扱った L4 と L7 の話です。
壊れた先へ配らない
台数を増やしても、**落ちたサーバーへ配り続けたら意味がありません。**ロードバランサーは、応答しないサーバーを配り先から外します。
nginx のリバースプロキシは、実際のリクエストの成否を見て判定します。fail_timeout に設定した時間の中で max_fails 回失敗したサーバーを、同じ長さの時間だけ配り先から外します (ngx_http_upstream_module)。時間が経つと、生きているかどうかを実際のリクエストで確かめ、成功すれば戻します。
ここには設計上の注意が 2 つあります。
- **判定に使うエンドポイントは、依存先まで見るかどうかを決めます。**プロセスが生きているかだけを返すなら、データベースが落ちていても「正常」と答えます。逆に依存先まで見ると、データベース 1 台の不調で全サーバーが配り先から外れます
- **外す判定が速すぎると、一時的な遅さで台数が減ります。**減ると残りの負荷が上がり、さらに外れます
2 つ目は、性能の問題が可用性の問題に変わる典型です。可用性と冗長化 で改めて扱います。
台数を自動で増減させるときの遅れ
負荷に応じて台数を自動で増減させる仕組みはどのクラウドにもあります。ただし即座には増えません。
- 負荷の上昇を検知する (数十秒〜数分の計測間隔)
- サーバーを起動する (数十秒〜数分)
- アプリケーションが応答できる状態になる (依存関係の読み込み、暖機)
合計すると数分かかります。数分で終わる負荷の山には間に合いません。開始時刻が決まっているイベントのように、来ることが分かっている山には先に増やしておきます。
自動増減が効くのは、時間帯による緩やかな変動です。夜に増えて朝に減る種類の波であれば、遅れは問題になりません。
読み取りをキャッシュへ逃がす
読み取りが多いなら、同じ結果を毎回作り直さないのが最も効きます。
キャッシュの入れ方には名前が付いています。
| 型 | 手順 | 特徴 |
|---|---|---|
| キャッシュアサイド (遅延読み込み) | 読むときにキャッシュを見て、無ければデータベースから取って書き戻す | 要求されたデータだけが載る。取りこぼしたときに 3 往復する。更新時に古い値が残りうる |
| 書き込み時に更新 (ライトスルー) | データベースを更新した直後にキャッシュも更新する | キャッシュが新しい状態に保たれる。読まれないデータも載るので容量を食う |
この 2 つは排他ではなく、組み合わせて使うのが一般的です (Amazon ElastiCache — Caching strategies / AWS — Caching patterns)。どちらの型でも、有効期限を付けて古い値が残り続けないようにします。
HTTP キャッシュとは層が違う
HTTP キャッシュ で扱ったブラウザや CDN のキャッシュと、ここで言うキャッシュは別の層にあります。
| 層 | 誰が持つか | 減らすもの |
|---|---|---|
| HTTP キャッシュ | ブラウザ、CDN、プロキシ | サーバーへ届くリクエストの数 |
| アプリケーションのキャッシュ | サーバーが読む共有キャッシュ | データベースへのクエリの数 |
**手前の層ほど効果が大きくなります。**CDN で返せるなら、サーバーもデータベースも動きません。逆に、利用者ごとに違う内容は手前でキャッシュできないので、後ろの層で効かせます。
書き込みをキューへ逃がす
書き込みは複製して散らせません。代わりに使えるのが時間をずらす方法です。
リクエストの中で全部やらず、受け取ったことだけを記録して、処理は後ろに回します。
これで効くのは 2 点です。
- **応答が速くなります。**重い処理を待たずに返せます
- **山を平らにできます。**瞬間的に 1,000 件来ても、ワーカーが一定の速度で消化します
代わりに払うものがあります。**結果がすぐには反映されません。**利用者に「受け付けました」と伝える設計が要ります。処理の途中で失敗したときにどう気づかせるかも決める必要があります。
メッセージが 2 回届く可能性も考えておきます。多くのキューは「少なくとも 1 回」の配送を保証する代わりに、重複を許します。同じ処理を 2 回行っても結果が変わらない作りにしておくのが対策で、これは リトライと冪等性 が扱った冪等性そのものです。
ボトルネックは移動する
**1 か所を速くすると、次に遅い場所が現れます。**多くの場合、この順に移動します。
アプリケーションサーバーは増やしやすいので最初に解決します。すると全台からのクエリがデータベース 1 台に集まり、そこが次の限界になります。データベースを分けると、今度は転送量や書き込みの物理的な速度が見えてきます。
**大事なのは、次にどこが詰まるかを予測しておくことです。**アプリケーションを 10 台に増やす前に、データベースが 10 倍のクエリを受けられるかを見ます。
増やしても解けない問題
台数で解決しない種類の問題があります。
- 1 件が遅い処理 — 1 リクエストに 30 秒かかるなら、台数を増やしても 30 秒のままです。速くするか、非同期にします
- 全体で 1 つしかない資源への競合 — 同じ行を全員が更新するなら、台数と無関係に順番待ちになります (ロックと分離レベル)
- 順序が意味を持つ処理 — 並列に処理すると順序が崩れる種類の仕事は、そもそも分散させられません
「遅いから台数を増やす」の前に、どの種類の遅さかを見分けます。
判断の手順
- どこが詰まっているかを測る — 推測で増やさない。アプリケーションかデータベースかを先に分ける
- 垂直で足りるかを見る — 上限に余裕があるなら、まず 1 台を大きくする。アプリケーションを変えずに済む
- 水平にするなら状態を洗い出す — セッション・ファイル・メモリ上のキャッシュ。3 つとも外へ出せるか確認する
- 読み取りが多いならキャッシュを検討する — 手前の層から順に。CDN で返せるものはサーバーまで来させない
- 書き込みの山が問題ならキューを検討する — 反映が遅れることを利用者にどう伝えるかまで決める
- 次に詰まる場所を予測しておく — 増やした先で何が限界になるかを見てから実行する
よくある誤解
「水平にスケールすれば無限に伸びる」 — 伸びるのは状態を持たない部分だけです。データベース、共有キャッシュ、外部 API といった共有された部分が、その先の上限になります。
「垂直は時代遅れ」 — 書き込みが集中するデータベースでは、垂直が最も素直な解決になることがあります。分散させると整合性の設計が必要になるので、1 台で足りるならそのほうが単純です。
「キャッシュを入れれば速くなる」 — 効くのは、同じ結果が何度も要求されるときだけです。利用者ごとに違う内容や、毎回変わるデータでは当たりません。キャッシュに当たらなかったとき、往復が増えるぶん遅くなります。
「自動増減があるから急な負荷でも大丈夫」 — 起動と暖機に数分かかります。数分で終わる山には間に合いません。来ると分かっている山には先に増やしておきます。
確認問題
問 1. アプリケーションサーバーを 1 台から 4 台に増やしたところ、ログインが頻繁に切れるようになりました。原因として何を疑いますか。
セッションがサーバーの中に置かれていることを疑います。
1 台のときは、同じ利用者のリクエストが必ず同じサーバーへ届いていました。4 台になると、2 回目のリクエストが別のサーバーへ振られます。そのサーバーはセッションを知らないので、ログインしていない扱いになります。
対処は 2 つあります。
- セッションを共有のデータストアへ移す — どのサーバーが受けても同じ状態を見られる
- 同じ利用者を同じサーバーへ振る — 変更は小さいが、そのサーバーが落ちるとセッションが消える。負荷も均等に散らなくなる
**前者が本筋です。**後者は移行までの一時しのぎとしては使えます。
問 2. ヘルスチェックのエンドポイントで、データベースへの接続確認まで行うべきでしょうか。
用途によって答えが変わります。両方を用意するのが実務的な解です。
| 見るもの | 使い道 | 依存先まで見ると |
|---|---|---|
| プロセスが生きているか | 再起動の判断 | 依存先の不調で不要な再起動が起きる |
| 依存先まで含めて処理できるか | 配り先から外す判断 | 共有のデータベースが不調だと全台が同時に外れる |
危ないのは、依存先まで見る判定を配り先の選択に使う場合です。データベース 1 台の不調で全サーバーが「異常」と判定され、配り先がゼロになります。1 台の障害がサービス全体の停止に変わります。
依存先を見るなら、外れる台数に上限を設けるか、依存先の不調を別の扱いにする必要があります。
問 3. 読み取りが 95%、書き込みが 5% のサービスで、データベースの負荷が高くなっています。キャッシュとキューのどちらを先に検討しますか。
まとめ
- 増やし方は垂直と水平の 2 つ。垂直は 1 台の上限で止まり、水平は台数で伸ばせる代わりに条件が付きます。水平にも上限はあり、共有された部分がそれになります
- 水平にする条件は「どのサーバーが受けても同じ結果になる」こと。セッション・ファイル・メモリ上のキャッシュを外へ出します
- ロードバランサーの既定はラウンドロビン。処理時間にばらつきがあるなら接続数の少ない台へ配る方式を選びます
- ヘルスチェックが依存先まで見ると、1 台の障害で全台が外れることがあります
- 台数の自動増減は数分の遅れを伴います。来ると分かっている山には先に増やします
- 読み取りはキャッシュへ、書き込みはキューへ逃がします。手前の層で返せるものほど効果が大きいです
- **ボトルネックは移動します。**増やす前に、次にどこが詰まるかを見ます
- HTTP キャッシュ — 手前の層で返す仕組みと有効期限の指定
- nginx — Using nginx as HTTP load balancer — 分散方式の既定と切り替えの入門
- nginx — ngx_http_upstream_module — 各ディレクティブの正式な定義。版やエディションの条件はこちらにしか載りません
- Amazon ElastiCache — Caching strategies — 遅延読み込みと書き込み時更新の利点と欠点
- AWS 実践ガイド — ALB — クラウドのロードバランサーの設定と挙動
次に読む
- 可用性と冗長化 — 増やした台数を、壊れたときの備えとして読み直す
- HTTP キャッシュ — キャッシュを手前の層で効かせる