記事ディレクトリ
1. 負荷分散アルゴリズムの概要
負荷分散とは、英語名はLoad Balanceであり、その意味は将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行
、例えば、FTPサーバー、Webサーバー、エンタープライズ基幹アプリケーションサーバー、その他の主要業務サーバーなどが連携して業務を完了することを指します。複数のマシンが関係するため、任务如何分发
負荷分散アルゴリズムの問題となります。
2.ラウンドロビンアルゴリズム
1。概要
ポーリングが次々とキューに登録されています。
2. Java はポーリング アルゴリズムを実装します
二重リンク リストを手動で作成して、サーバー リストのリクエスト ポーリング アルゴリズムを実装します。
public class RR {
//当前服务节点
Server current;
//初始化轮询类,多个服务器ip用逗号隔开
public RR(String serverName){
System.out.println("init server list : "+serverName);
String[] names = serverName.split(",");
for (int i = 0; i < names.length; i++) {
Server server = new Server(names[i]);
if (current == null){
//如果当前服务器为空,说明是第一台机器,current就指向新创建的server
this.current = server;
//同时,server的前后均指向自己。
current.prev = current;
current.next = current;
}else {
//否则说明已经有机器了,按新加处理。
addServer(names[i]);
}
}
}
//添加机器
void addServer(String serverName){
System.out.println("add server : "+serverName);
Server server = new Server(serverName);
Server next = this.current.next;
//在当前节点后插入新节点
this.current.next = server;
server.prev = this.current;
//修改下一节点的prev指针
server.next = next;
next.prev=server;
}
//将当前服务器移除,同时修改前后节点的指针,让其直接关联
//移除的current会被回收期回收掉
void remove(){
System.out.println("remove current = "+current.name);
this.current.prev.next = this.current.next;
this.current.next.prev = this.current.prev;
this.current = current.next;
}
//请求。由当前节点处理即可
//注意:处理完成后,current指针后移
void request(){
System.out.println(this.current.name);
this.current = current.next;
}
public static void main(String[] args) throws InterruptedException {
//初始化两台机器
RR rr = new RR("192.168.0.1,192.168.0.2");
//启动一个额外线程,模拟不停的请求
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
rr.request();
}
}
}).start();
//3s后,3号机器加入清单
Thread.currentThread().sleep(3000);
rr.addServer("192.168.0.3");
//3s后,当前服务节点被移除
Thread.currentThread().sleep(3000);
rr.remove();
}
class Server{
Server prev; // 前驱
Server next; // 后继
String name; // 名称
public Server(String name){
this.name = name;
}
}
}
初期化後、1、2、両方のポーリングのみ
3 参加後、1、2、3、3 つのポーリング
2 を削除すると、1 と 3 のポーリングのみが残る
3. メリットとデメリット
実装はシンプルで、マシンリストは自由に加算および減算でき、計算量は O(1) です。
ノードに偏ったカスタマイズを行うことは不可能であり、ノードの処理能力を異なるものとして扱うことはできません。
3.ランダムアルゴリズム
1。概要
サーバブルのリストから1 つを選択して随机
応答を提供します。
2. Java はランダム アルゴリズムを実装します
ランダム アクセスのシナリオでは、配列を使用してランダム添字読み取りをより効率的に実現するのが適しています。
配列を定義し、配列の長さ内の乱数を添え字として受け取ります。とてもシンプルです。
public class Rand {
// 所有服务ip
ArrayList<String> ips ;
//初始化随机类,多个服务器ip用逗号隔开
public Rand(String nodeNames){
System.out.println("init list : "+nodeNames);
String[] nodes = nodeNames.split(",");
//初始化服务器列表,长度取机器数
ips = new ArrayList<>(nodes.length);
for (String node : nodes) {
ips.add(node);
}
}
//请求
void request(){
//下标,随机数,注意因子
int i = new Random().nextInt(ips.size());
System.out.println(ips.get(i));
}
//添加节点,注意,添加节点会造成内部数组扩容
//可以根据实际情况初始化时预留一定空间
void addnode(String nodeName){
System.out.println("add node : "+nodeName);
ips.add(nodeName);
}
//移除
void remove(String nodeName){
System.out.println("remove node : "+nodeName);
ips.remove(nodeName);
}
public static void main(String[] args) throws InterruptedException {
Rand rd = new Rand("192.168.0.1,192.168.0.2");
//启动一个额外线程,模拟不停的请求
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
rd.request();
}
}
}).start();
//3s后,3号机器加入清单
Thread.currentThread().sleep(3000);
rd.addnode("192.168.0.3");
//3s后,当前服务节点被移除
Thread.currentThread().sleep(3000);
rd.remove("192.168.0.2");
}
}
1、2 として初期化されると、この 2 つは順番にポーリングされず、ランダムに表示されます。
3 サービス ノード リストに参加します。
2 を削除すると、1 と 3 だけが残りますが、この 2 つは依然としてランダムで順序が狂っています。
4. 送信元アドレスハッシュ(Hash)アルゴリズム
1。概要
現在アクセスしている IP アドレスのハッシュ値を作成すると、同じキーが同じマシンにルーティングされます。このシナリオは分散クラスター環境で一般的で、ユーザーのログイン時のルーティングとセッションの永続性を要求します。
2. Java はアドレス ハッシュ アルゴリズムを実装します
HashMapを利用すると、対応するノードに値を要求するサービスが実現でき、検索の計算量はo(1)となる。アルゴリズムを修正し、リクエストをキーにマッピングします。
たとえば、リクエストのソース IP の末尾で、マシンの数に応じた余りをキーとして取得します。
public class Hash {
// 所有服务ip
ArrayList<String> ips ;
//初始化hash类,多个服务器ip用逗号隔开
public Hash(String nodeNames){
System.out.println("init list : "+nodeNames);
String[] nodes = nodeNames.split(",");
//初始化服务器列表,长度取机器数
ips = new ArrayList<>(nodes.length);
for (String node : nodes) {
ips.add(node);
}
}
//添加节点,注意,添加节点会造成内部Hash重排,思考为什么呢???
//这是个问题!在一致性hash中会进入详细探讨
void addnode(String nodeName){
System.out.println("add node : "+nodeName);
ips.add(nodeName);
}
//移除
void remove(String nodeName){
System.out.println("remove node : "+nodeName);
ips.remove(nodeName);
}
//映射到key的算法,这里取余数做下标
private int hash(String ip){
int last = Integer.valueOf(ip.substring(ip.lastIndexOf(".")+1,ip.length()));
return last % ips.size();
}
//请求
//注意,这里和来访ip是有关系的,采用一个参数,表示当前的来访ip
void request(String ip){
//下标
int i = hash(ip);
System.out.println(ip+"-->"+ips.get(i));
}
public static void main(String[] args) {
Hash hash = new Hash("192.168.0.1,192.168.0.2");
for (int i = 1; i < 10; i++) {
//模拟请求的来源ip
String ip = "192.168.0."+ i;
hash.request(ip);
}
hash.addnode("192.168.0.3");
for (int i = 1; i < 10; i++) {
//模拟请求的来源ip
String ip = "192.168.0."+ i;
hash.request(ip);
}
hash.remove("192.168.0.2");
for (int i = 1; i < 10; i++) {
//模拟请求的来源ip
String ip = "192.168.0."+ i;
hash.request(ip);
}
}
}
初期化後は 1、2 のみで、添え字は最後の ip の残りであり、複数回実行しても応答するマシンは変化せず、セッションは維持されます。
3 結合後、再ハッシュすると、マシンの分布が変わります。
2 が削除された後、最初に 2 にハッシュされたリクエストは 3 のレスポンスに再配置されます。
3. 一貫したハッシュ化
送信元アドレス ハッシュ アルゴリズムにより、特定のリクエストが対応するサーバーに送信されることが許可されます。これにより、セッション情報の保持の問題が解決されます。
同時に、マシンノードの数が変化した場合、標準ハッシュも変更されます。その場合、リクエストは再ハッシュされ、元の設計意図が崩れてしまいます。これを解決するにはどうすればよいでしょうか? 答えは一貫したハッシュです。
(1) 原則
4 台のマシンを例にとると、一貫性のあるハッシュのアルゴリズムは次のとおりです:
まず各サーバーのハッシュ値を見つけて、 0 ~ 232の円に設定し、
次に同じ方法を使用して保存されたデータのハッシュ値を見つけます。 key ギリシャ値も円上にマッピングされます。
データがマッピングされている位置から時計回りに検索し、最初に見つかったサーバーにデータを保存します。
最大値がまだ見つからない場合は、最初の値を取得します。これが、イメージがリングと呼ばれる理由です。
ノードの追加:
ノードの削除の原理は同じです
(2) 特長
単調性: 単調性とは、一部のリクエストがハッシュによる処理のために対応するサーバーに割り当てられており、新しいサーバーがシステムに追加された場合、元のリクエストが元のサーバーにマッピングされるか、新しいサーバーに移動できることを確認する必要があることを意味します。他の元のサーバーにマッピングされるのではなく。
分散 (スプレッド): 分散環境では、クライアントはリクエスト時にサーバーの一部しか知らない可能性があり、その場合、2 つのクライアントは異なる部分を見て、見えているものが完全なハッシュ リングであると考え、問題が発生します。同じキーです。異なるサーバーにルーティングされる場合があります。上の図を例に挙げると、1,4 が表示されるように client1 を追加し、2,3 が表示されるように client2 を追加すると、2 ~ 4 の間のキーが 2 つのクライアントによって繰り返し 3,4 にマッピングされます。分散は問題の深刻さを反映します。
バランス (バランス): バランスとは、クライアント ハッシュ後のリクエストが異なるサーバーに分散できることを意味します。一貫したハッシュを可能な限り分散できますが、各サーバーで処理されるリクエストの数がまったく同じであることは保証できません。この偏差はハッシュ スキューと呼ばれます。ノード分散アルゴリズムの設計が合理的でないと、バランスに大きな影響を及ぼします。
(3) 最適化
仮想ノードを追加すると、ハッシュ アルゴリズムが最適化され、セグメンテーションと分散がより洗練されます。つまり、実際には m 台のマシンがありますが、それを n 倍に拡張し、m * n 台のマシンをリング上に配置すると、均等化の後、キー セグメントの分布がさらに洗練されます。
(4) Java は一貫したハッシュ アルゴリズムを実装します
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 不带虚拟节点的一致性Hash算法
*/
public class ConsistentHashingWithoutVirtualNode {
//服务器列表
private static String[] servers = {
"192.168.0.0", "192.168.0.1",
"192.168.0.2", "192.168.0.3", "192.168.0.4" };
//key表示服务器的hash值,value表示服务器
private static SortedMap<Integer, String> serverMap = new TreeMap<Integer, String>();
static {
for (int i=0; i<servers.length; i++) {
int hash = getHash(servers[i]);
//理论上,hash环的最大值为2^32
//这里为做实例,将ip末尾作为上限也就是254
//那么服务器是0-4,乘以60后可以均匀分布到 0-254 的环上去
//实际的请求ip到来时,在环上查找即可
hash *= 60;
System.out.println("add " + servers[i] + ", hash=" + hash);
serverMap.put(hash, servers[i]);
}
}
//查找节点
private static String getServer(String key) {
int hash = getHash(key);
//得到大于该Hash值的所有server
SortedMap<Integer, String> subMap = serverMap.tailMap(hash);
if(subMap.isEmpty()){
//如果没有比该key的hash值大的,则从第一个node开始
Integer i = serverMap.firstKey();
//返回对应的服务器
return serverMap.get(i);
}else{
//第一个Key就是顺时针过去离node最近的那个结点
Integer i = subMap.firstKey();
//返回对应的服务器
return subMap.get(i);
}
}
//运算hash值
//该函数可以自由定义,只要做到取值离散即可
//这里取ip地址的最后一节
private static int getHash(String str) {
String last = str.substring(str.lastIndexOf(".")+1,str.length());
return Integer.valueOf(last);
}
public static void main(String[] args) {
//模拟5个随机ip请求
for (int i = 0; i < 5; i++) {
String ip = "192.168.0."+ new Random().nextInt(254);
System.out.println(ip +" ---> "+getServer(ip));
}
//将5号服务器加到2-3之间,取中间位置,150
System.out.println("add 192.168.0.5,hash=150");
serverMap.put(150,"192.168.0.5");
//再次发起5个请求
for (int i = 0; i < 5; i++) {
String ip = "192.168.0."+ new Random().nextInt(254);
System.out.println(ip +" ---> "+getServer(ip));
}
}
}
4 台のマシンがハッシュ リングに参加して
リクエストをシミュレートし、ハッシュ値に従ってダウンストリーム ノードへのリクエストを正確にスケジュールします。
ノード 5 を追加し、キーを 150 に設定して
リクエストを再度開始します。
5. 加重ラウンドロビン (WRR) アルゴリズム
1。概要
WeightRoundRobin では、ポーリングは単なる機械的なローテーションであり、加重ポーリングはすべてのマシンが平等に扱われるという欠点を補います。ポーリングベースで、初期化時にマシンは 1 つの比重
.
2. Java は加重ラウンドロビン アルゴリズムを実装します
リンクされたリストを維持し、各マシンはその重量に応じて異なる番号を占有します。ポーリングを行うと重みが重くなり、数も大きくなり、フェッチ数も当然増えます。例: a、b、c 3 台のマシン、重みはそれぞれ 4、2、1 です。ランク付け後は、各リクエストがリストから順番に a、a、a、a、b、b、c になります。ノードを取得します。 、次にリクエストされたときに次のものを受け取ります。最後まで来たら、最初からやり直してください。
しかし問題があります。マシンが均等に分散されておらず、クラスターが発生します...
解決策: マシンの滑らかさの問題を解決するために、nginx のソース コードではスムーズな重み付きラウンドロビン アルゴリズムが使用されています。ルールは次のとおりです: 各ノードには、weight と currentWeight という 2 つの重みがあります。重みは常に同じです
。電流は変化し続けます。
変更ルールは次のとおりです: 選択前にすべての電流 += 重みを選択し、最大の電流を持つ応答を選択し、応答後にその電流 -= 合計とします。
統計: a=4、b=2、c=1、分布は滑らかでバランスが取れています。
public class WRR {
//所有节点的列表
ArrayList<Node> list ;
//总权重
int total;
//初始化节点列表
public WRR(String nodes){
String[] ns = nodes.split(",");
list = new ArrayList<>(ns.length);
for (String n : ns) {
String[] n1 = n.split("#");
int weight = Integer.valueOf(n1[1]);
list.add(new Node(n1[0],weight));
total += weight;
}
}
//获取当前节点
Node getCurrent(){
//执行前,current加权重
for (Node node : list) {
node.currentWeight += node.weight;
}
//遍历,取权重最高的返回
Node current = list.get(0);
int i = 0;
for (Node node : list) {
if (node.currentWeight > i){
i = node.currentWeight;
current = node;
}
}
return current;
}
//响应
void request(){
//获取当前节点
Node node = this.getCurrent();
//第一列,执行前的current
System.out.print(list.toString()+"---");
//第二列,选中的节点开始响应
System.out.print(node.name+"---");
//响应后,current减掉total
node.currentWeight -= total;
//第三列,执行后的current
System.out.println(list);
}
public static void main(String[] args) {
WRR wrr = new WRR("a#4,b#2,c#1");
//7次执行请求,看结果
for (int i = 0; i < 7; i++) {
wrr.request();
}
}
class Node{
int weight,currentWeight; // 权重和current
String name;
public Node(String name,int weight){
this.name = name;
this.weight = weight;
this.currentWeight = 0;
}
@Override
public String toString() {
return String.valueOf(currentWeight);
}
}
}
6. 加重ランダム (WR) アルゴリズム
1。概要
WeightRandom、マシンはランダムに選別されますが、重み付けされた値のセットが作成され、異なる重みに応じて、選択の確率が異なります。この概念では、ランダム性は重みが等しい特殊なケースと考えることができます。
2. Java は重み付きランダム アルゴリズムを実装します
設計思想は同じで、重みの大きさに応じて異なる数のノードが生成され、ノードがキューに入れられた後、ランダムに取得されます。ここでのデータ構造は主にランダムな読み取りを伴うため、配列であることが望ましいです。
配列もランダムに選別される点はランダムと同じですが、違いはランダムが各マシンに 1 つだけであり、重み付け後に複数になることです。
public class WR {
//所有节点的列表
ArrayList<String> list ;
//初始化节点列表
public WR(String nodes){
String[] ns = nodes.split(",");
list = new ArrayList<>();
for (String n : ns) {
String[] n1 = n.split("#");
int weight = Integer.valueOf(n1[1]);
for (int i = 0; i < weight; i++) {
list.add(n1[0]);
}
}
}
void request(){
//下标,随机数,注意因子
int i = new Random().nextInt(list.size());
System.out.println(list.get(i));
}
public static void main(String[] args) {
WR wr = new WR("a#2,b#1");
for (int i = 0; i < 9; i++) {
wr.request();
}
}
}
9 回実行すると、a と b が交互に表示され、a=6、b=3 となり、2:1 の比率が満たされます
。ランダムなのでランダム性があり、実行されるたびに厳密に比例しない可能性があります。サンプルが無限大になる傾向がある場合、比率はほぼ正確です
7、最小接続数 (LC) アルゴリズム
1。概要
LeastConnections、つまり、現在のマシンの接続数をカウントし、新しいリクエストに応答するための最小の接続を選択します。前のアルゴリズムはリクエストのディメンションに基づいていますが、接続の最小数はマシンのディメンションに基づいています。
2. Java は、最小接続数アルゴリズムを実装します。
マシンのノード ID とマシン接続数のカウンターを記録するリンク テーブルを定義します。内部的には、最小ヒープがソートに使用され、ヒープの最上位ノードが応答時の最小接続数になります。
public class LC {
//节点列表
Node[] nodes;
//初始化节点,创建堆
// 因为开始时各节点连接数都为0,所以直接填充数组即可
LC(String ns){
String[] ns1 = ns.split(",");
nodes = new Node[ns1.length+1];
for (int i = 0; i < ns1.length; i++) {
nodes[i+1] = new Node(ns1[i]);
}
}
//节点下沉,与左右子节点比对,选里面最小的交换
//目的是始终保持最小堆的顶点元素值最小
//i:要下沉的顶点序号
void down(int i) {
//顶点序号遍历,只要到1半即可,时间复杂度为O(log2n)
while ( i << 1 < nodes.length){
//左子,为何左移1位?回顾一下二叉树序号
int left = i<<1;
//右子,左+1即可
int right = left+1;
//标记,指向 本节点,左、右子节点里最小的,一开始取i自己
int flag = i;
//判断左子是否小于本节点
if (nodes[left].get() < nodes[i].get()){
flag = left;
}
//判断右子
if (right < nodes.length && nodes[flag].get() > nodes[right].get()){
flag = right;
}
//两者中最小的与本节点不相等,则交换
if (flag != i){
Node temp = nodes[i];
nodes[i] = nodes[flag];
nodes[flag] = temp;
i = flag;
}else {
//否则相等,堆排序完成,退出循环即可
break;
}
}
}
//请求。非常简单,直接取最小堆的堆顶元素就是连接数最少的机器
void request(){
System.out.println("---------------------");
//取堆顶元素响应请求
Node node = nodes[1];
System.out.println(node.name + " accept");
//连接数加1
node.inc();
//排序前的堆
System.out.println("before:"+Arrays.toString(nodes));
//堆顶下沉
down(1);
//排序后的堆
System.out.println("after:"+Arrays.toString(nodes));
}
public static void main(String[] args) {
//假设有7台机器
LC lc = new LC("a,b,c,d,e,f,g");
//模拟10个请求连接
for (int i = 0; i < 10; i++) {
lc.request();
}
}
class Node{
//节点标识
String name;
//计数器
AtomicInteger count = new AtomicInteger(0);
public Node(String name){
this.name = name;
}
//计数器增加
public void inc(){
count.getAndIncrement();
}
//获取连接数
public int get(){
return count.get();
}
@Override
public String toString() {
return name+"="+count;
}
}
}
初期化後、ヒープ ノードの値はすべて 0 になります。つまり、各マシンへの接続数は 0 になります。
ヒープの最上位接続の後、ヒープは沈み、ヒープは並べ替えられ、最小ヒープ ルールは true のままになります。 。
8. 適用事例
1、nginxアップストリーム
upstream frontend {
#源地址hash
ip_hash;
server 192.168.0.1:8081;
server 192.168.0.2:8082 weight=1 down;
server 192.168.0.3:8083 weight=2;
server 192.168.0.4:8084 weight=3 backup;
server 192.168.0.5:8085 weight=4 max_fails=3 fail_timeout=30s;
}
ip_hash: 送信元アドレス ハッシュ アルゴリズム
down: 現在のサーバーが一時的に負荷に参加していないことを意味します。 Weight:
重み付けアルゴリズム。デフォルトは 1 です。重みが大きいほど、負荷の重みも大きくなります。
バックアップ: バックアップ マシン。他のすべての非バックアップ マシンがダウンしているかビジー状態の場合にのみ、バックアップ マシンを要求します。
max_fails: 失敗の最大数。デフォルト値は 1、ここでは 3、つまり最大 3 回の試行です
。fail_timeout: タイムアウト期間は 30 秒、デフォルト値は 10 秒です。
知らせ!Weight および Backup を ip_hash キーワードと一緒に使用することはできません。
2、春雲リボンIRule
#设置负载均衡策略 eureka‐application‐service为调用的服务的名称
eureka‐application‐service.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
RoundRobinRule: ポーリング
RandomRule: ランダム
AvailabilityFilteringRule: 複数のアクセス障害によりサーキット ブレーカー トリップ状態にあるサービスと、同時接続がしきい値を超えているサービスをまず除外し、残りのサービスをポーリング WeightedResponseTimeRule: 平均応答時間に従って
計算すべてのサービスの重みは、応答時間が速いほど、サービスの重みが大きくなります。開始時に統計情報が不足している場合は RoundRobinRule 戦略を使用し、統計情報が十分に揃ったらこの戦略に切り替えます RetryRule: 最初は RoundRobinRule 戦略に従います サービスの取得に失敗した場合は、指定された時間内に再試行し
ますBestAvailableRule
: Yes 多重アクセス失敗によりサーキットブレーカーが落ちた状態のサービスを除外し、最も同時実行数の少ないサービスを選択 ZoneAvoidanceRule : デフォルトのルールは、該当する領域のパフォーマンスを総合的に判断します
。サーバーの場所とサーバーの可用性
3. ダボ負荷分散
サービスアノテーションを使用する
@Service(loadbalance = "roundrobin",weight = 100)
RandomLoadBalance: ランダム、このメソッドは dubbo のデフォルトの負荷分散戦略です
RoundRobinLoadBalance: ポーリング
LeastActiveLoadBalance: 最小アクティブ時間、dubbo フレームワークはサービス呼び出しの数を計算するためにフィルターをカスタマイズしました
ConsistentHashLoadBalance: 一貫したハッシュ