ロードバランサ
Load Balancer ・ ろーどばらんさ
リクエストを複数のサーバへ振り分け、壊れた台を外して、規模と可用性を両立させる入口の装置。
✦ 30秒で試す — 航海仕事を4台で山分けしても、全員が速くなるとは限らないこのまま順番に配り続けると、Bの待ち行列は?予想してみる概要
ロードバランサ(負荷分散装置)は、外から届くリクエストを複数のサーバへ振り分ける仕組みです。クライアントから見える接続先はロードバランサ1つだけで、その後ろに並ぶサーバ群(バックエンド)の存在は隠されています。「窓口は1つ、中の担当者は大勢」という構図です。
役割は2つあります。1つはスケーリングで、1台では捌けない量のトラフィックを、台数を増やすことで受け止められるようにします。もう1つは可用性で、ヘルスチェックで故障したサーバを見つけて振り分けの対象から外し、残りのサーバでサービスを続けます。
かつては専用のハードウェア製品(F5 の BIG-IP など)が主流でしたが、いまはソフトウェア(nginx、HAProxy、Envoy)や、クラウドコンピューティングのマネージドサービス(AWS の ALB・NLB、Google Cloud の Cloud Load Balancing など)として使うのが一般的です。ある程度の規模の Web サービスで、入口にロードバランサがいない構成はまず見かけません。
なぜ生まれたか
Web の利用が広がった1990年代半ば、人気サイトのトラフィックは1台のサーバの処理能力をあっさり超えるようになりました。「もっと強い1台に買い替える」垂直スケーリングには、性能に対して価格が急激に高くなる、どんなに強くても上限がある、その1台が壊れれば全部止まる、という壁があります。
「普通のサーバを並べて全体で捌く」水平スケーリングのほうが筋が良いのは明らかでしたが、問題は入口です。クライアントに「空いているサーバを選んで接続してください」とは頼めません。初期には DNS で複数の IPアドレスを順繰りに返す DNS ラウンドロビンが使われましたが、答えはキャッシュされるので、壊れたサーバのアドレスもしばらく配られ続け、負荷の偏りも直せませんでした。そこで、トラフィックの入口に立って1件ずつ振り分け先を選び、死んだサーバをその場で外せる専用の装置として、ロードバランサが生まれました。水平スケーリングと高可用性は、ロードバランサが入口にいて初めて成り立ちます。
詳細
基本の動きとヘルスチェック
ロードバランサは仮想的な接続先(VIP や DNS 名)でリクエストを受け、登録されたバックエンドのうち健全な1台を選んで転送します。応答は同じ経路を戻り、クライアントは分散の存在に気づきません。
要になるのがヘルスチェックです。ロードバランサは各サーバへ定期的に確認のリクエスト(例: /healthz への HTTP GET)を送り、「一定回数続けて失敗したら外す」「一定回数続けて成功したら戻す」というしきい値で判定します。ここで見落としがちなのが、検知までの時間です。間隔10秒・失敗しきい値3回なら、止まったサーバが外れるまで最大30秒ほどかかり、その間に届いたリクエストはエラーになります。間隔やしきい値を小さくすれば早く外せますが、一瞬の遅延で健全なサーバまで外しやすくなります。多くのロードバランサは、実際のリクエストの失敗を見て外す受動的な検知(外れ値の検出)を組み合わせて、この隙間を狭めています。
ヘルスチェックのエンドポイントが何を確かめるかも設計のしどころです。「プロセスは生きているが DB につながらない」状態を検知できなければ、壊れたサーバに振り分け続けます。逆に DB を確かめるようにすると、DB が一時的に遅いだけで全台が一斉に外れる、という事故も起こりえます。
分散アルゴリズム
どのサーバに割り当てるかの戦略には定番がいくつかあります。最も単純なのがラウンドロビンで、順繰りに割り当てます。処理時間にばらつきがある場合は、その時点で抱えている接続が最も少ないサーバを選ぶ「最小接続数(Least Connections)」が有効です。遅くなったサーバは接続がなかなか減らないので、自然と配られる数が減ります。サーバの性能差を係数で反映する重み付け方式や、送信元 IP アドレスのハッシュで同じクライアントを同じサーバに寄せる「IP ハッシュ」もあります。均等に配ること自体より、「遅い1台に仕事を積みすぎない」ことが目的だと理解しておくと、選択を誤りません。
下の部品で、サーバを遅くしたり止めたりしながら、振り分け方式を切り替えてみてください。ラウンドロビンで遅い1台にだけ待ち行列が積み上がること、止めたサーバがヘルスチェックで外れるまでの間にエラーが漏れることを確かめられます。
4人の利用者から、処理時間のばらつく要求の粒が流れています。サーバを押すと「遅い」→「停止」→「正常」と切り替わります。まずサーバ B を遅くしてみてください。
一貫性ハッシュ: 台数が変わっても寄せ先を崩さない
キャッシュサーバの前段のように「同じキーは同じサーバへ」送りたい場面では、ハッシュで振り分けます。ところが素朴に「ハッシュ値 ÷ 台数の余り」で選ぶと、台数が4台から3台に変わった瞬間に、ほとんどのキーの行き先が変わってしまいます。キャッシュなら全台でいっせいにミスが起き、後ろのデータベースに負荷が押し寄せます。
一貫性ハッシュ(consistent hashing)は、サーバもキーも同じ円周上のハッシュ値に置き、「キーから時計回りに進んで最初に出会うサーバ」を担当にします。1台が抜けても、影響を受けるのはそのサーバが担当していた区間のキーだけで、ほかは動きません。1台あたり円周上に多数の仮想ノードを置いて偏りをならすのが普通で、分散キャッシュや NoSQL のデータベース(Cassandra や DynamoDB の設計の源流)、Envoy などのロードバランサの「リングハッシュ」方式で使われています。
L4 と L7: どの層で振り分けるか
ロードバランサは、動作する層で大きく2種類に分かれます(層の考え方は OSI参照モデルを参照)。L4(トランスポート層)ロードバランサは TCP/IP のレベルで動き、IP アドレスとポート番号だけを見て接続の単位で振り分けます。中身を解釈しないぶん速く、HTTP 以外のプロトコルにも使えます。一方 L7(アプリケーション層)ロードバランサは HTTP を解釈し、「/api は API サーバ群へ、/images は画像サーバ群へ」といったパスやヘッダでの振り分けができます。1本の接続の上で多数のリクエストが流れる HTTP/2 や gRPC では、接続単位の L4 では偏りが解消されないため、リクエスト単位で振り分けられる L7 が必要になります。L7 は TLS の終端(証明書の一元管理)を担うことも多く、機能的にはリバースプロキシとほぼ重なります。AWS でも NLB(L4)と ALB(L7)が使い分けられています。
外すときは静かに: コネクションドレイニング
デプロイや縮退でサーバを計画的に外すとき、いきなり切り離すと、そのサーバで処理中だったリクエストが途中で失われます。そこでロードバランサは、外すと決めたサーバに新しいリクエストだけを送らないようにし、処理中のものが終わるのを待ってから切り離します。これをコネクションドレイニング(登録解除の遅延)と呼び、待つ時間の上限を設定します。1台ずつ外して更新して戻すローリングアップデートが無停止で成り立つのは、ヘルスチェックとドレイニングの組み合わせのおかげです。
ステートの罠: セッションと設計のトレードオフ
水平分散の最大の落とし穴が「状態」です。あるサーバのメモリ上にセッション情報を保存していると、次のリクエストが別のサーバに振り分けられた瞬間にログイン状態が消えます。対処は2方向あります。1つはスティッキーセッションで、Cookie などで同じクライアントを毎回同じサーバに固定します。手軽ですが、そのサーバが落ちればセッションは失われ、負荷も偏ります。もう1つが本命で、セッションを外部のストア(Redis のような NoSQL の KVS など)に追い出すか、署名付きのトークン(JWT など)としてクライアントに持たせ、サーバ自体をステートレスにする方法です。どのサーバが受けても同じ結果になるなら振り分けは完全に自由になり、スケーリングもサーバの入れ替えも容易になります。「ロードバランサを活かすにはアプリをステートレスに作る」は、分散システム設計の基本原則です。
最後に、ロードバランサ自体が単一障害点にならないかという問いも残ります。オンプレミスでは2台を冗長化して仮想 IP を引き継ぐ構成が定番で、クラウドではマネージドサービスがこの冗長化ごと面倒を見てくれます。入口の可用性を自分で作り込まなくてよいことは、クラウド移行の大きな利点の1つです。
試問
利用者4人の要求がロードバランサを通って、サーバ4台へ振り分けられています。サーバを押すと「遅い」(処理に5倍かかる)→「停止」→「正常」と切り替わり、上のボタンで振り分け方式(ラウンドロビン=順番に配る / 最小接続数=抱えている接続が少ない台へ / IP ハッシュ=利用者ごとに固定)を選べます。1台が不調になった場面で、行列をあふれさせない配り方を探してください。
4人の利用者から、処理時間のばらつく要求の粒が流れています。サーバを押すと「遅い」→「停止」→「正常」と切り替わります。まずサーバ B を遅くしてみてください。
