1. 一般的な負荷分散アルゴリズムについて話す
いわゆるロード バランシングは、圧力 (コンピューティング プレッシャー、ネットワーク プレッシャー、ストレージ プレッシャーなど) を複数のサービス ノードに分散することです。一般的な負荷分散アルゴリズムには、ラウンド ロビン、ランダム、加重ラウンド ロビン、加重ランダム、スムーズ加重ランダム、従来のハッシュ、一貫性のあるハッシュなどが含まれます。
まず、次のタイプの負荷分散アルゴリズムを見てください。
1. ランダム、ポーリング: 最も単純で、差別はありません
ランダム: ノードをランダムに選択します。
ラウンドロビン: ノードを順番に選択します。
上記 2 つのアルゴリズムの利点は、実装と構成が簡単であることです。
ただし、クラスター内のノードのパフォーマンスに差がある場合、2 つのアルゴリズムは要件を満たすことができません。たとえば、クラスター内のサービス ノードには 2 コア、4 コア、16 コアなどさまざまな仕様がありますが、16 コアのマシンにはより多くのトラフィックが割り当てられることが期待されます。
2. 重み付け:能力が高いほど責任も重くなる差別待遇
加重ランダムおよび加重ラウンドロビン: ランダムおよびラウンドロビンのアルゴリズムに基づいて、ノードのパフォーマンスが考慮され、より強力なパフォーマンスを持つノードがより多くの負荷に耐えられるようにサポートされます。
加重ランダム、加重ラウンドロビンを使用する場合、ノードのリストだけでなく、ノードの重みも構成する必要があります。
例えば:
ノード | マシンコアの数 | 重み |
---|---|---|
ノード0 | 4 | 2 |
ノード1 | 10 | 5 |
ノード2 | 14 | 7 |
3. スムーズな重み付け
重み付きポーリングはノード間の性能差を考慮していますが、実際に使用すると、常に重みの高いノードが選択され続けることがわかります。たとえば、1、2、3、4、5 の番号が付けられた 5 つのノードの重みは 4 です。 、1、1、1、1、加重ラウンドロビンの選択順序は次のようになります:
1,1,1,1,2,3,4,5,1,1,1,1,2,3, 4、5、1、1、1、1、2、3、4、5...
最も重みの高いノード 1 が連続して選択されることがよくあります。プログラムによっては、同じノードを連続的に選択することによる同時実行性の高さが動作効率に影響します。次のように、選択されたノードが「分散」されることが望まれます。 :
1、2、1、3、4、1、5、1、1、2、1、3、4、1、5、1、1、2、1、3、4、1、5、1…
スムーズな重み付きラウンドロビンの「平滑化」は、上記の問題を解決することです。各ノードの「設定された重み」に加えて、スムーズなラウンドロビン アルゴリズムの実装では、各ノードの追加の変数「現在の重み」も維持する必要があります。ノード。
次のように計算されます。
-
各ノードの現在の重み©の初期値を各ノードの設定重み(w)に設定し、重みの合計(wSum)を計算する
-
現在の重み (cMax) が最大のノードを選択し、ノードの現在の重み © = 現在の重み © - 重みの合計 (wSum) を更新します。
-
各ノードの現在の重み © = 各ノードの現在の重み © + 各ノードの設定された重み (w)
この時点で、選択できる負荷分散戦略がすでにたくさんあります。しかし、まだ満足できないアプリケーション シナリオがいくつかあります。
例えば:
- セッションの永続性が必要なシナリオ (再接続後に同じノードに割り当てられる)。
- 永続データが必要なシナリオ (データが永続化されている場合、シャーディングにロード バランシング アルゴリズムを使用し、クエリ時にデータがどのノードに保存されているかを知ることができる必要があります)。
4. 従来のハッシュ アルゴリズム: ノードは変更されず、マッピングも変更されません。
従来のハッシュ アルゴリズム: ノードが変更されない場合、同じ ID が常に同じノードにマッピングされます。
たとえば、リクエストが 5 つあり、それぞれに ID が付いています(ハッシュ化のパラメータのフィールドを使用
) 。リクエスト3:{id:3}、リクエスト4:{id:4}]
この時点で、3 つのサービス ノードが存在します。ノード = [node1、node2、node3]
従来のハッシュでは、次のルールを使用してノードを選択します。
リクエスト内の特定のフィールドは ID としてハッシュされ、その ID を使用してノードの総数の残りを取得し、ノードの添字を取得します。
request0 はノード 1 にマップされます (0 % 3 = 0)
request1 はノード 2 にマップされます (1 % 3 = 1)
request4 はノード 2 にマップされます (4 % 3 = 1)
このようなマッピング ルールを使用すると、マッピング関係を独自に維持することなく、リクエストがどのノードで処理され、データ フラグメントがどのノードに格納されるかを計算できます。
しかし、この美しい前提は「ノードの数は変わらない」ということです。
永続ストレージのシナリオでは、サービス ノードの拡張や縮小、シャットダウン、保守、更新、または障害が発生する必要がある場合、ノード数の変更は避けられません。ノードの数が変わると、マッピングに新しいルールが発生します。古いデータを新しいルールで確認することは現実的ではありません。同じデータが古いルールと新しいルールによって異なるノードにマッピングされる可能性があります。現時点での一般的な解決策は、古いデータを「移動」(リバランス)することです。
2. 従来のハッシュリバランスのコスト
リバランス(再調整)、つまりデータ移行により、データを「新しいルール」に準拠した位置に置きます。
古いデータを「移動」するコストを見てみましょう 0から9999までの1w個のデータIDが5つのノードに分散されている場合、ノード数が4、6になった場合、どのくらいのデータを移行する必要がありますか?コードのシミュレーションは次のとおりです。
public static void main(String[] args) {
reBalance(10000, 5, 4);
System.out.println();
reBalance(10000, 5, 6);
}
public static void reBalance(int dataCount, int nodeCountBefore, int nodeCountAfter) {
System.out.println("数据量:" + dataCount + ", 迁移前节点数:" + nodeCountBefore + ", 迁移后节点数:" + nodeCountAfter);
int[] transfer = new int[nodeCountBefore];
int transferTotal = 0;
for (int i = 0; i < dataCount; i++) {
int nodeIdBefore = i % nodeCountBefore;
int nodeIdAfter = i % nodeCountAfter;
if(nodeIdBefore != nodeIdAfter) {
transfer[nodeIdBefore] += 1;
}
}
for (int i = 0; i < transfer.length; i++) {
transferTotal += transfer[i];
System.out.println("需要从节点" + i + "迁出" + transfer[i] + "条数据");
}
System.out.println("共迁出" + transferTotal + "条数据,占比:" + transferTotal*1.0d/dataCount);
}
このコードを実行した結果は次のようになります。
数据量:10000, 迁移前节点数:5, 迁移后节点数:4
需要从节点0迁出1500条数据
需要从节点1迁出1500条数据
需要从节点2迁出1500条数据
需要从节点3迁出1500条数据
需要从节点4迁出2000条数据
共迁出8000条数据,占比:0.8
数据量:10000, 迁移前节点数:5, 迁移后节点数:6
需要从节点0迁出1666条数据
需要从节点1迁出1666条数据
需要从节点2迁出1666条数据
需要从节点3迁出1666条数据
需要从节点4迁出1666条数据
共迁出8330条数据,占比:0.833
ノード数が 5 から 4 に、5 から 6 に変化すると、移行する必要があるデータ量がそれぞれ全体の80%と83.33%を占め、非常に恐ろしい数字であることがわかります。
特に分散ストレージのシナリオでは、このような大量の移行は帯域幅とコンピューティング リソースの浪費につながります。さらに深刻なのは、短期間に大量のデータを移行するとマシン リソースが過剰に消費され、サービスが一定期間利用できなくなること。
3. 一貫したハッシュ
1. 一貫性のあるハッシュ アルゴリズムの概要
リバランスのコストを削減するには、可能な限り「移動不能」を維持する必要があります。Consistent Hash はこれを行います。
- 一連の連続する数字 (0 から 9999 までの数字など) を端から端まで接続して使用して、ハッシュ リングを形成します。
- ハッシュ リング上にノードを分散し (通常、ハッシュにはノードの特定のタグを使用します)、ノードの対応する位置の番号を覚えます。
例如:node0:2000, node1:4000, node2:6000, node3:8000
- 4つのノードに分散するデータは10,000件あり、データは0~9999の範囲にハッシュ化されます。
- データの ID がハッシュ化されており、その位置が 666 である場合、ハッシュ リング上のこの点の位置を見つけ、時計回りの方向に最も近いノードを見つけます。この例では、時計回りの方向に最も近いノードが見つかります。データはノード0:2000です。
2.一貫性のあるハッシュアルゴリズムのリバランス(reBalance)
次に、1 つのノード (node0) が削減された場合に、一貫性のあるハッシュ アルゴリズムがどのようなデータ移行を実行する必要があるかを見てみましょう。
ノード 0 のデータを最も近いノード (ノード 1) に時計回りに移行するだけで済みます。
これがconsistent Hashの最も素晴らしい点で、node0のデータを除いて、他のノードのデータは変更されないまま維持できます。
ノードを追加するとどうなるでしょうか?
たとえば、ノード 5 がノード 4 とノード 0 の間に追加された場合、ノード 0 で元々必要だった量の一部がノード 5 によって「インターセプト」されるため、データのこの部分のみをノード 0 からノード 5 に移行する必要があります (図のオレンジ色の部分)。形)。
このようにして、データ移行量はより狭い範囲内で制御されます。
でもちょっと待って、何か問題があるように思えませんか?
移行されるデータの量は減少しますが、1 つのノード (node0) が減少すると、時計回りに隣接するノード (node1) が元々ノード 0 に属していたすべての圧力を負担することになりますが、これは容認できず、各ノードに予約を許可することはできません。資力。
新しいノード(node5)を追加すると、時計回りに隣接するノード(node0)の圧力が緩和されるだけで、他のノードの圧力はまったく緩和されません。
この問題を解決するために、コンシステント ハッシュでは仮想ノードが導入されています。
3. 一貫性のあるハッシュ仮想ノード
仮想ノードは、実ノードに対して十分な仮想「クローン」を作成し、それらをハッシュ リングに配布します。不均一な圧力分布を避けるために、十分なクローンと十分な分散が存在します。
実ノードをクラスターに追加するときは、特定のルールに従って複数の「クローン」をハッシュ リングに追加します (実装の 1 つは、node0_0、node0_1、node0_2、node1_0、node1_1、node1_2 などの数値サフィックスをノード ID に追加することです)。 .. )、仮想ノードを使用したハッシュ リングは次のようになります。
実ノードを削減するときは、他のノードの仮想ノードがその圧力に耐えられるように、ノードのすべての「クローン」もそれに応じて削除する必要があります。
図では、node0 はオフラインであり、時計回りにその仮想ノードは、node3、node2、node3、node2、node4、node1 を表す最も近い仮想ノードであり、負荷もこれらのノードによって共有されます
。
ここまで、一貫性のあるハッシュ アルゴリズムの基本的な内容を紹介しました。
Consistent Hash の「実装の詳細」、「ハッシュ関数の選択」、「Consistent Hash を使用する場合のノードのパフォーマンスの違いを考慮する方法」、「仮想ノードの数を設定する方法」などのトピックについては、別途説明します。
Consistent Hash は分散ストレージ テクノロジの基礎の 1 つであり、そのシンプルさと強力さはさまざまなオブジェクト ストレージ、分散データベース、分散キャッシュ製品に反映されています。多くの場合、一部の非データ ストレージ領域で負荷分散戦略としてこれを使用するのが良い選択となります。
ただし、技術ソリューションに「万能」な選択肢はありません。どの負荷分散アルゴリズムを選択するかは、特定のビジネス シナリオによって異なります。結局のところ、最適なものが適切です。開発者として、適切な意思決定を行うためには、さらに多くのことを蓄積し、吸収する必要があります。