セッション固定がたまに外れる理由
一言でいうと
クラスターが「送れる先」を絞り込んだら、その中から1つを選ぶ作業が負荷分散です。選び方は、状態をどれだけ記憶するかで分かれます。何も記憶しなければランダム、順番を記憶すればラウンドロビン、リクエストのキーを記憶すればハッシュリングです。何を選んでも、代償がついてきます。
なぜ必要なのか
サーバーが3台なら、リクエストを3分の1ずつ分ければよさそうです。実際、ほとんどの場合はそれで済みます。問題になるのは、2つの要求が来るときです。
1つ目は、「このユーザーは常に同じサーバーへ送ってください」という要求です。サーバーがローカルキャッシュを持っていたり、セッションをメモリに置いていたりすると、この要求が性能と正確性の両方を左右します。もう1つは、「サーバーを1台外すときに、全体が混ざり直すと困ります」という要求です。キャッシュを持つサーバーが一斉に担当を入れ替えると、外した瞬間に全体のキャッシュヒット率が底まで落ち、バックエンドがその負荷を受けます。
この2つの要求を一緒に解くのがコンシステントハッシュで、EnvoyではRING_HASHとMAGLEVがそれです。
どう動くのか
| lb_policy | どう選ぶか | 代償 |
|---|---|---|
ROUND_ROBIN |
リストを順番に1つずつ | ワーカーごとに順番を別々に記憶します |
LEAST_REQUEST |
ランダムに2つを選び、処理中のリクエストが少ないほう | リクエストのコストにばらつきがあるときに有利です |
RANDOM |
毎回ランダム | 記憶するものがなく安価です。標本が小さいと偏ります |
RING_HASH |
キーをハッシュし、リング上で最も近いエンドポイント | リングを作るコストがかかります。リングが粗いと分布が偏ります |
MAGLEV |
固定サイズの参照テーブルで、同じ性質をより安価に実現 | テーブルサイズが固定なので、エンドポイントが非常に多いと制約になります |
エンドポイントごとにload_balancing_weightを指定できます。重み2は「2回に1回多く」ではなく、全体の取り分の2倍を占めるという意味です。3つの重みが2・1・1なら合計が4なので、12回で6対3対3になります。
ハッシュベースの方式にはキーが必要です。それを選ぶのがルートのhash_policyで、ヘッダー・クッキー・クエリパラメーター・送信元IPの中から選べます。ここでよく見落とされる点が1つあります。キーを取り出せなかったリクエストはハッシュがないため、単純にランダムに行きます。クッキーのない最初のリクエストや、ヘッダーを付け忘れた内部呼び出しがそうです。「ほとんどは固定されるのに、たまに別の所へ行きます」の正体は、たいていこれです。
リングの性質も知っておく価値があります。単純に「ハッシュ値をサーバー数で割った余り」を使うと、サーバー数が変わった瞬間に、ほぼすべてのキーが移動します。リング方式では、消えたサーバーに割り当てられていたキーだけが移動し、残りはそのままです。その代わり、リングが粗いと(minimum_ring_sizeが小さいと)、エンドポイントがリング上に均等に散らばらず、分布そのものが偏ります。
現場での姿
「9回送ったのに3対3対3になりません」という場合です。Envoyのワーカースレッドは、それぞれラウンドロビンの順番を記憶しています。デフォルトのconcurrencyはコア数なので、少ないリクエストで数えるとワーカーごとに散らばり、数字が毎回変わります。分配を数える実験は--concurrency 1で行います。本番運用では、数えずに統計で確認します。
「セッション固定を有効にしたのに、たまに外れます」という場合です。まず、キーのないリクエストが混ざっていないかを確認します。また、エンドポイントが出入りする瞬間には、一部が移動するのが正常です。コンシステントハッシュは「誰も移動しない」ではなく、「移動する数を最小にする」ものです。
特定のサーバーだけが熱くなる場合。ハッシュキーの分布が偏ったときに起きます。テナントIDをキーに使ったのに、テナント1つがトラフィックの半分を占めるなら、そのテナントが割り当てられたサーバーが半分を受けます。そのようなときは、キーをさらに細かく分けるか(ユーザー単位に)、そのテナントだけを別に切り出します。
方式を選ぶ基準。まとめると、2つの質問に絞れます。1つ目は、同じリクエストが同じ所へ行く必要があるかです。ローカルキャッシュやセッションが絡んでいればそうで、その場合はハッシュベースにします。2つ目は、リクエストのコストにばらつきがあるかです。あるリクエストは5ミリ秒、別のリクエストは2秒だとすると、順番に分ける方式では、遅いリクエストを受けたサーバーに仕事を積み続けてしまいます。そのときは、処理中のリクエスト数を見るLEAST_REQUESTのほうが優れています。どちらでもなければ、ラウンドロビンのままにするのが最も予測しやすく、規模が非常に大きくなると、状態を記憶しないランダムのほうがむしろ安価になります。
公式ドキュメント: Load balancing overview・Cluster configuration・HTTP route components
次のラボですること
同じアップストリーム3つを置き、方式だけを変えながら数字を数えます。ラウンドロビンが正確に分けること、重みが取り分を変えること、ランダムが小さな標本で偏ることを順に確認し、ハッシュリングでは、同じユーザーが1か所に固定されることと、サーバーを1台外したときに何人が移動するかを自分で数えます。最後にキーをヘッダーからクエリパラメーターに変えて、キーのないリクエストが固定されないことまで見ます。