1.普通輪詢演算法
輪詢(Round Robin,RR)是依次將使用者的訪問請求,按迴圈順序分配到web服務節點上,從1開始到最後一臺伺服器節點結束,然後再開始新一輪的迴圈。這種演算法簡單,但是沒有考慮到每臺節點伺服器的具體效能,請求分發往往不均衡。
程式碼實現:
/**
* 普通輪詢演算法
*/public class RoundRobin {
private static Integer index = 0;
private static List<String> nodes = new ArrayList<>();
// 準備模擬資料
static {
nodes.add("192.168.1.101");
nodes.add("192.168.1.103");
nodes.add("192.168.1.102");
System.out.println("普通輪詢演算法的所有節點:"+nodes);//列印所有節點
}
// 關鍵程式碼
public String selectNode(){
String ip = null;
synchronized (index){
// 下標復位
if(index>=nodes.size()) index = 0;
ip = nodes.get(index);
index++;
}
return ip;
}
// 併發測試:兩個執行緒迴圈獲取節點
public static void main(String[] args) {
new Thread(() -> {
RoundRobin roundRobin1 = new RoundRobin();
for (int i=1;i<=5;i++){
String serverIp = roundRobin1.selectNode();
System.out.println(Thread.currentThread().getName()+"==第"+i+"次獲取節點:"+serverIp);
}
}).start();
RoundRobin roundRobin2 = new RoundRobin();
for (int i=1;i<=nodes.size();i++){
String serverIp = roundRobin2.selectNode();
System.out.println(Thread.currentThread().getName()+"==第"+i+"次獲取節點:"+serverIp);
}
}
}
執行結果:不同執行緒訪問,結果依舊是按順序迴圈分配節點
普通輪詢演算法的所有節點:[192.168.1.101, 192.168.1.103, 192.168.1.102]
main==第1次獲取節點:192.168.1.101
Thread-0==第1次獲取節點:192.168.1.103
Thread-0==第2次獲取節點:192.168.1.102
Thread-0==第3次獲取節點:192.168.1.101
Thread-0==第4次獲取節點:192.168.1.103
Thread-0==第5次獲取節點:192.168.1.102
main==第2次獲取節點:192.168.1.101
main==第3次獲取節點:192.168.1.103
2.加權輪詢演算法
加權輪詢(Weighted Round Robin,WRR)是根據設定的權重值來分配訪問請求,權重值越大的,被分到的請求數也就越多。一般根據每臺節點伺服器的具體效能來分配權重。
2.1.實現方式一
將需要輪詢的所有節點按權重數迴圈生成一個List 集合,然後就跟普通輪詢演算法一樣,來一個、分配一個、進1位。
例如:
所有節點資訊:{{“192.168.1.100“,5},{“192.168.1.101“,1},{“192.168.1.102“,3}}
那麼生成的List 集合為:
{“192.168.1.100“,
“192.168.1.100“,
“192.168.1.100“,
“192.168.1.100“,
“192.168.1.100“,
“192.168.1.101“,
“192.168.1.102“,
“192.168.1.102“,
“192.168.1.102“}
後面就是普通輪詢演算法的邏輯
程式碼實現:
類似於二維陣列 降維成 一維陣列,然後使用普通輪詢
/**
* 簡單版的加權輪詢
*/public class WeightedRoundRobinSimple {
private static Integer index = 0;
private static Map<String,Integer> mapNodes = new HashMap<>();
// 準備模擬資料
static {
mapNodes.put("192.168.1.101",1);
mapNodes.put("192.168.1.102",3);
mapNodes.put("192.168.1.103",2);
/* -- 以下程式碼只為了方便檢視所有節點,刪除不影響 -- S */
List<String> nodes = new ArrayList<>();
Iterator<Map.Entry<String, Integer>> iterator = mapNodes.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
for (int i=0;i<entry.getValue();i++){
nodes.add(key);
}
}
System.out.println("簡單版的加權輪詢:"+nodes);//列印所有節點
/* -- 以上程式碼只為了方便檢視所有節點,刪除不影響-- E */
}
// 關鍵程式碼:類似於二維陣列 降維成 一維陣列,然後使用普通輪詢
public String selectNode(){
List<String> nodes = new ArrayList<>();
Iterator<Map.Entry<String, Integer>> iterator = mapNodes.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
for (int i=0;i<entry.getValue();i++){
nodes.add(key);
}
}
String ip = null;
synchronized (index){
// 下標復位
if(index>=nodes.size()) index = 0;
ip = nodes.get(index);
index++;
}
return ip;
}
// 併發測試:兩個執行緒迴圈獲取節點
public static void main(String[] args) {
new Thread(() -> {
WeightedRoundRobinSimple roundRobin1 = new WeightedRoundRobinSimple();
for (int i=1;i<=6;i++){
String serverIp = roundRobin1.selectNode();
System.out.println(Thread.currentThread().getName()+"==第"+i+"次獲取節點:"+serverIp);
}
}).start();
WeightedRoundRobinSimple roundRobin2 = new WeightedRoundRobinSimple();
for (int i=1;i<=6;i++){
String serverIp = roundRobin2.selectNode();
System.out.println(Thread.currentThread().getName()+"==第"+i+"次獲取節點:"+serverIp);
}
}
}
執行結果:兩個執行緒迴圈測試,輸出結果會出現交替分配到不同的IP,但最終的效果都是一個個按順序分配,類似於普通輪詢演算法。
簡單版的加權輪詢:[192.168.1.103, 192.168.1.103, 192.168.1.101, 192.168.1.102, 192.168.1.102, 192.168.1.102]
main==第1次獲取節點:192.168.1.103
main==第2次獲取節點:192.168.1.103
main==第3次獲取節點:192.168.1.101
main==第4次獲取節點:192.168.1.102
main==第5次獲取節點:192.168.1.102
Thread-0==第1次獲取節點:192.168.1.102
Thread-0==第2次獲取節點:192.168.1.103
main==第6次獲取節點:192.168.1.103
Thread-0==第3次獲取節點:192.168.1.101
Thread-0==第4次獲取節點:192.168.1.102
Thread-0==第5次獲取節點:192.168.1.102
Thread-0==第6次獲取節點:192.168.1.102
2.2.實現方式二(重點難點)
本文的重點難點。
在實現方式一的演算法中可以很明顯的看到,同權重的IP會被連續分配,也就是說同一個IP在短時間內收到不同的請求,過了這個連續點,就要等到下一輪才會被分配到,並沒有做到均勻分配節點。
實現方式二將儘可能地均勻分配每個節點,節點分配不再是連續的,但最終的權重比和上一個方式一樣,這種加權輪詢又被稱為平滑加權輪詢。
理解關鍵的幾個引數和演算法邏輯,方便理解程式碼的實現。
2.2.1.概述
關鍵引數
ip:負載IP weight:權重,儲存配置的權重 effectiveWeight:有效權重,輪詢的過程權重可能變化 currentWeight:當前權重,比對該值大小獲取節點
注意幾個點:
weight 權重,在整個過程不會對它做修改,只用來儲存配置時的權重引數值。如果直接拿weight 運算而不儲存配置的最原始權重引數,那麼將會丟失最關鍵的使用者配置的權重引數。
effectiveWeight 有效權重,在整個過程可能會變化,初始值等於weight,主要用於當節點出現分配失敗時降低權重值,成功時提高權重值(但不能大於weight值),本案例為了簡化演算法,並未加入這功能,因此本案例中effectiveWeight始終等於weight。
currentWeight 當前權重,通過迴圈所有節點比對該值大小來分配權重最大的節點,初始值等於weight。
三個權重引數的變化情況
僅僅針對本案例,因為本案例為了簡化演算法,並未加入[節點出現分配失敗時降低權重值,成功時提高權重值(但不能大於weight值)的功能],所以有效權重effectiveWeight 不會發生變化。
第一次加權輪詢時:currentWeight = weight = effectiveWeight; 後面每次加權輪詢時:currentWeight 的值都會不斷變化,weight 和effectiveWeight 的值不變; 被分配的節點的currentWeight = currentWeight - 權重之和 所有節點的currentWeight = currentWeight + effectiveWeight
2.2.2.舉個例子理解演算法
你面前有三個瓶子A、B、C,分別裝有1L、3L、2L水。
第一輪分配情況:B多,所以把B瓶子的3L水,分1L給A,分2L給C(按權重分),分完之後:A、B、C分別為:2L、0L、4L
第二輪分配情況:C多,所以把C瓶子的4L水,分1L給A,分3L給B(按權重分),分完之後:A、B、C分別為:3L、3L、0L
第三輪分配情況:A和B一樣多,那麼拿誰去分呢?拿誰其實都一樣(演算法中寫了A大於B才選A,現在等於,所以不選A),所以把B瓶子的3L水,分1L給A,分2L給C(按權重分),分完之後:A、B、C分別為:4L、0L、2L
然後不斷的進行下去……
簡化成數學邏輯(程式碼實現)的關鍵兩步
被分配的節點的currentWeight = currentWeight - 權重之和 所有節點的currentWeight = currentWeight + effectiveWeight
下面通過閱讀程式碼來理解
2.2.3.程式碼實現
節點物件
/**
* String ip:負載IP
* final Integer weight:權重,儲存配置的權重
* Integer effectiveWeight:有效權重,輪詢的過程權重可能變化
* Integer currentWeight:當前權重,比對該值大小獲取節點
* 第一次加權輪詢時:currentWeight = weight = effectiveWeight
* 後面每次加權輪詢時:currentWeight 的值都會不斷變化,其他權重不變
*/public class Node implements Comparable<Node>{
private String ip;
private final Integer weight;
private Integer effectiveWeight;
private Integer currentWeight;
public Node(String ip,Integer weight){
this.ip = ip;
this.weight = weight;
this.effectiveWeight = weight;
this.currentWeight = weight;
}
public Node(String ip, Integer weight, Integer effectiveWeight, Integer currentWeight) {
this.ip = ip;
this.weight = weight;
this.effectiveWeight = effectiveWeight;
this.currentWeight = currentWeight;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Integer getWeight() {
return weight;
}
public Integer getEffectiveWeight() {
return effectiveWeight;
}
public void setEffectiveWeight(Integer effectiveWeight) {
this.effectiveWeight = effectiveWeight;
}
public Integer getCurrentWeight() {
return currentWeight;
}
public void setCurrentWeight(Integer currentWeight) {
this.currentWeight = currentWeight;
}
@Override
public int compareTo(Node node) {
return currentWeight > node.currentWeight ? 1 : (currentWeight.equals(node.currentWeight) ? 0 : -1);
}
@Override
public String toString() {
return "{ip='" + ip + "', weight=" + weight + ", effectiveWeight=" + effectiveWeight + ", currentWeight=" + currentWeight + "}";
}
}
加權輪詢演算法
/**
* 加權輪詢演算法
*/public class WeightedRoundRobin {
private static List<Node> nodes = new ArrayList<>();
// 權重之和
private static Integer totalWeight = 0;
// 準備模擬資料
static {
nodes.add(new Node("192.168.1.101",1));
nodes.add(new Node("192.168.1.102",3));
nodes.add(new Node("192.168.1.103",2));
nodes.forEach(node -> totalWeight += node.getEffectiveWeight());
}
/**
* 按照當前權重(currentWeight)最大值獲取IP
* @return Node
*/
public Node selectNode(){
if (nodes ==null || nodes.size()<=0) return null;
if (nodes.size() == 1) return nodes.get(0);
Node nodeOfMaxWeight = null; // 儲存輪詢選中的節點資訊
synchronized (nodes){
// 列印資訊物件:避免併發時列印出來的資訊太亂,不利於觀看結果
StringBuffer sb = new StringBuffer();
sb.append(Thread.currentThread().getName()+"==加權輪詢--[當前權重]值的變化:"+printCurrentWeight(nodes));
// 選出當前權重最大的節點
Node tempNodeOfMaxWeight = null;
for (Node node : nodes) {
if (tempNodeOfMaxWeight == null)
tempNodeOfMaxWeight = node;
else
tempNodeOfMaxWeight = tempNodeOfMaxWeight.compareTo(node) > 0 ? tempNodeOfMaxWeight : node;
}
// 必須new個新的節點例項來儲存資訊,否則引用指向同一個堆例項,後面的set操作將會修改節點資訊
nodeOfMaxWeight = new Node(tempNodeOfMaxWeight.getIp(),tempNodeOfMaxWeight.getWeight(),tempNodeOfMaxWeight.getEffectiveWeight(),tempNodeOfMaxWeight.getCurrentWeight());
// 調整當前權重比:按權重(effectiveWeight)的比例進行調整,確保請求分發合理。
tempNodeOfMaxWeight.setCurrentWeight(tempNodeOfMaxWeight.getCurrentWeight() - totalWeight);
sb.append(" -> "+printCurrentWeight(nodes));
nodes.forEach(node -> node.setCurrentWeight(node.getCurrentWeight()+node.getEffectiveWeight()));
sb.append(" -> "+printCurrentWeight(nodes));
System.out.println(sb); //列印權重變化過程
}
return nodeOfMaxWeight;
}
// 格式化列印資訊
private String printCurrentWeight(List<Node> nodes){
StringBuffer stringBuffer = new StringBuffer("[");
nodes.forEach(node -> stringBuffer.append(node.getCurrentWeight()+",") );
return stringBuffer.substring(0, stringBuffer.length() - 1) + "]";
}
// 併發測試:兩個執行緒迴圈獲取節點
public static void main(String[] args){
Thread thread = new Thread(() -> {
WeightedRoundRobin weightedRoundRobin1 = new WeightedRoundRobin();
for(int i=1;i<=totalWeight;i++){
Node node = weightedRoundRobin1.selectNode();
System.out.println(Thread.currentThread().getName()+"==第"+i+"次輪詢選中[當前權重最大]的節點:" + node + "\n");
}
});
thread.start();
//
WeightedRoundRobin weightedRoundRobin2 = new WeightedRoundRobin();
for(int i=1;i<=totalWeight;i++){
Node node = weightedRoundRobin2.selectNode();
System.out.println(Thread.currentThread().getName()+"==第"+i+"次輪詢選中[當前權重最大]的節點:" + node + "\n");
}
}
}
執行結果:
main==加權輪詢--[當前權重]值的變化:[1,3,2] -> [1,-3,2] -> [2,0,4] main==第1次輪詢選中[當前權重最大]的節點:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=3}
Thread-0==加權輪詢--[當前權重]值的變化:[2,0,4] -> [2,0,-2] -> [3,3,0] Thread-0==第1次輪詢選中[當前權重最大]的節點:{ip='192.168.1.103', weight=2, effectiveWeight=2, currentWeight=4}
main==加權輪詢--[當前權重]值的變化:[3,3,0] -> [3,-3,0] -> [4,0,2] main==第2次輪詢選中[當前權重最大]的節點:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=3}
main==加權輪詢--[當前權重]值的變化:[4,0,2] -> [-2,0,2] -> [-1,3,4] main==第3次輪詢選中[當前權重最大]的節點:{ip='192.168.1.101', weight=1, effectiveWeight=1, currentWeight=4}
Thread-0==加權輪詢--[當前權重]值的變化:[-1,3,4] -> [-1,3,-2] -> [0,6,0] Thread-0==第2次輪詢選中[當前權重最大]的節點:{ip='192.168.1.103', weight=2, effectiveWeight=2, currentWeight=4}
main==加權輪詢--[當前權重]值的變化:[0,6,0] -> [0,0,0] -> [1,3,2] main==第4次輪詢選中[當前權重最大]的節點:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=6}
Thread-0==加權輪詢--[當前權重]值的變化:[1,3,2] -> [1,-3,2] -> [2,0,4] Thread-0==第3次輪詢選中[當前權重最大]的節點:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=3}
main==加權輪詢--[當前權重]值的變化:[2,0,4] -> [2,0,-2] -> [3,3,0] main==第5次輪詢選中[當前權重最大]的節點:{ip='192.168.1.103', weight=2, effectiveWeight=2, currentWeight=4}
Thread-0==加權輪詢--[當前權重]值的變化:[3,3,0] -> [3,-3,0] -> [4,0,2] Thread-0==第4次輪詢選中[當前權重最大]的節點:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=3}
main==加權輪詢--[當前權重]值的變化:[4,0,2] -> [-2,0,2] -> [-1,3,4] main==第6次輪詢選中[當前權重最大]的節點:{ip='192.168.1.101', weight=1, effectiveWeight=1, currentWeight=4}
Thread-0==加權輪詢--[當前權重]值的變化:[-1,3,4] -> [-1,3,-2] -> [0,6,0] Thread-0==第5次輪詢選中[當前權重最大]的節點:{ip='192.168.1.103', weight=2, effectiveWeight=2, currentWeight=4}
Thread-0==加權輪詢--[當前權重]值的變化:[0,6,0] -> [0,0,0] -> [1,3,2] Thread-0==第6次輪詢選中[當前權重最大]的節點:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=6}
為了方便分析,簡化兩執行緒執行後的結果
[當前權重]值的變化:[1,3,2] -> [1,-3,2] -> [2,0,4]
[當前權重]值的變化:[2,0,4] -> [2,0,-2] -> [3,3,0]
[當前權重]值的變化:[3,3,0] -> [3,-3,0] -> [4,0,2]
[當前權重]值的變化:[4,0,2] -> [-2,0,2] -> [-1,3,4]
[當前權重]值的變化:[-1,3,4] -> [-1,3,-2] -> [0,6,0]
[當前權重]值的變化:[0,6,0] -> [0,0,0] -> [1,3,2]
[當前權重]值的變化:[1,3,2] -> [1,-3,2] -> [2,0,4]
[當前權重]值的變化:[2,0,4] -> [2,0,-2] -> [3,3,0]
[當前權重]值的變化:[3,3,0] -> [3,-3,0] -> [4,0,2]
[當前權重]值的變化:[4,0,2] -> [-2,0,2] -> [-1,3,4]
[當前權重]值的變化:[-1,3,4] -> [-1,3,-2] -> [0,6,0]
[當前權重]值的變化:[0,6,0] -> [0,0,0] -> [1,3,2]
因為整個過程只有當前權重發生變化,所以分析清楚它就明白了整個過程。
結論:
分配完成後當前權重發生變化,但許可權之和還是等於最初值;
每6輪(1+3+2權重)就出現權重全部為0,所以會出現重新迴圈,6正好等於權重之和,權重比等於1/6 : 3/6 : 2/6;
a=權重1,b=權重3,c=權重2,那麼權重變化的6(a+b+c)次中,分配情況為:b c b a c b,很明顯,每個節點均勻按權重分配,節點分配不再是連續的。這也是最重要的結論,正是實現方式二在文初提到的要實現的關鍵點。
該演算法在權重比相差很大時,比如:A=1,B=5,那這個演算法的結果就跟方式一沒啥區別了,分配結果就變成了:{A,B,B,B,B,B},既然沒區別,那根據演算法複雜情況,那肯定方式一更好了,所以方式一和方式二可以互補,可以根據權重比選擇不同的演算法。
留下懸念
第一點:節點出現分配失敗時降低有效權重值,成功時提高有效權重值(但不能大於weight值)的功能。理解了方式二,後面再加這塊功能進去就很好理解了;
第二點:該演算法實現的背後數學證明,用的是什麼數學理論?