L4とL7、そしてヘルスチェックがやっていること
一言でいうと
ロードバランサーは、トラフィックを振り分ける装置ではなく、故障した対象を外す装置です。振り分けは副次的な効果で、値打ちはヘルスチェックから出てきます。
なぜ必要なのか
サーバーを2台に増やすと、すぐに2つの疑問が生まれます。ユーザーをどちらへ送るのか、そして、1台が死んだことを何が察知するのか。
DNSにアドレスを2つ書いておく方法が最も安く見えますが、2つ目の疑問で崩れます。DNSは対象が生きているかを知らないので、死んだサーバーのアドレスも配り続け、レコードを直しても、TTLが残っているあいだは反映されません。
そのため、トラフィックの前に、対象の状態を継続的に確認する装置を置きます。負荷を分けるのは、その装置がついでにやっていることで、値打ちを発揮する部分は、故障した対象をリストから外すほうです。
L4とL7
| L4(ネットワーク) | L7(アプリケーション) | |
|---|---|---|
| 見るもの | IP、ポート | HTTPヘッダー、パス、ホスト |
| できること | 転送 | パスベースのルーティング、ホスト分岐、リダイレクト |
| TLS | 通過させるか終端する | たいてい終端する |
| レイテンシ | 非常に低い | わずかに大きい |
| 用途 | TCP・gRPC・ゲーム・DB | Web API、マイクロサービス |
/api/*はAサービスへ、/img/*はBサービスへ送るのは、L7にしかできません。逆に、極端なスループットが必要な場合や、HTTPではない場合は、L4です。
ヘルスチェックが本当の仕事
ロードバランサーは、定期的に対象へリクエストを送り、失敗がしきい値を超えたら、その対象をリストから外します。この動作のおかげで、インスタンス1つが死んでも、ユーザーは気づきません。
設定で重要なこと。
- パス:
/healthzのような、軽い専用のエンドポイント。実際のビジネスロジックを通らないようにします。重いと、ヘルスチェックが負荷になります。 - 間隔としきい値: 短いと敏感で、一瞬の遅延でも対象を外し、長いと、障害に気づくのが遅れます。通常、間隔10–30秒、連続2–3回の失敗に設定します。
- 正常復帰のしきい値: 外したものを、また入れる基準。これを短くすると、対象が出たり入ったりします(フラッピング)。
前のコースで扱ったlivenessとreadinessの区別が、ここでもまったく同じように適用されます。ロードバランサーのヘルスチェックは、readinessに近いものです。「今、トラフィックを受けてもよいか」を尋ねるものであり、「再起動すべきか」を尋ねるものではありません。
接続ドレイニング
インスタンスを外すときに、処理中のリクエストを切ると、ユーザーにエラーが行きます。ドレイニング(deregistration delay)は、新しいリクエストは送らず、処理中のものは終わらせるために待つ時間です。
デプロイ時の順序は次のとおりです。
1. 대상을 목록에서 빼기 시작 (새 요청 중단)
2. 드레이닝 대기 — 진행 중 요청 완료
3. 애플리케이션에 SIGTERM
4. 정상 종료
3つ目が、コンテナのコースで扱ったあのSIGTERMです。ドレイニング時間が、アプリケーションの正常終了時間より短いと、リクエストが切られます。
DNSとTTL
ロードバランサーの前段は、たいていDNSです。ここでTTLが障害対応の速度を決めます。
- TTLが300秒なら、障害時にトラフィックを移しても、最大5分間は古いアドレスへ行きます。
- 短く(60秒に)すれば、切り替えは速くなりますが、クエリが増えます。
- クライアントがTTLを無視してキャッシュする場合もあります(一部のJVMが有名です)。
そのため、DNSの切り替えを障害対応の主な手段にしないのが原則です。ロードバランサーの中で対象を外すほうが、はるかに速く確実です。
スティッキーセッション(sticky session)
同じユーザーを同じサーバーへ送る機能です。便利に見えますが、代償があります。
- 負荷が均等に分かれません
- そのサーバーが死ぬと、そのユーザーたちのセッションがまるごと消えます
- オートスケーリングと合いにくいです
セッションを外部ストレージ(Redisなど)に置けば、固定が不要になります。可能なら、そちらのほうがよいです。
ロードバランサーの裏で崩れる瞬間
ロードバランサーは、普段は静かで、負荷がかかるときとデプロイするときに問題を表に出します。その2つの瞬間に何が起きるかを知っていれば、大半は避けられます。
ヘルスチェックがアプリケーションと同じリソースを使うと、一緒に崩れます。/healthがデータベースを照会するように作っておくと、DBが遅くなる瞬間に、すべてのインスタンスが同時にunhealthyに落ちます。1つではなくすべてが外れるので、サービスがまるごと止まります。生きているかを見る検査(liveness)と、トラフィックを受ける準備ができたかを見る検査(readiness)を分け、前者は依存先に触れないようにします。
外れる判定は緩く、戻る判定は厳しく。一時的な遅延でインスタンスが外れると、残ったインスタンスに負荷が集中して、それらも外れます。連鎖が始まる場所です。失敗のしきい値は余裕をもって(3–5回)、復旧のしきい値は短く設定するほうが安全です。
デプロイ時に502が出る理由は、たいてい順序です。ロードバランサーがそのインスタンスをリストから外す前に、アプリケーションが先に死ぬと、その間に入ってきたリクエストが行き場を失います。順序を逆にします。SIGTERMを受けたら、まずヘルスチェックを失敗に変え、ロードバランサーが気づく時間(チェック間隔 × しきい値)だけ待ったあと、そのとき処理中のリクエストを仕上げて終了します。Kubernetesでは、preStopフックとterminationGracePeriodSecondsが、この時間を作ります。
接続ドレイニングの時間は、最も長いリクエストより長くなければなりません。30秒にしたのに、60秒かかるレポート生成リクエストがあれば、そのリクエストはデプロイのたびに切れます。
タイムアウトは層ごとに違い、内側のほうが短くなければなりません。ロードバランサー60秒、アプリケーション55秒、データベース50秒のように、内側に行くほど短く設定します。逆になっていると、ロードバランサーが先に切り、アプリケーションは誰も待っていないレスポンスを作り続けながら、接続を握り続けます。
アイドルタイムアウトが、コネクションプールを壊します。ロードバランサーが静かな接続を60秒で切るのに、アプリケーションのプールがその接続を生きていると信じていると、次のリクエストでconnection resetが起きます。プールのアイドル時間を、ロードバランサーより短く設定することが、答えです。
現場での姿
- デプロイのたびに5xxが少しずつ → ドレイニング時間が短いか、SIGTERMの処理がありません。
- ヘルスチェックがDBまで確認 → DBが遅くなると、全対象が外れて、サービス全体が停止しました。
- 障害時にDNSを変えたのにトラフィックが移らない → TTLとクライアントキャッシュです。
次のコース
コスト。ここまでのすべての決定に値段が付いていて、その値段の読み方を扱います。