Nginx的負載均衡默認算法是加權輪詢算法,本文簡單介紹算法的邏輯,並給出算法的Java實現版本。
本文參考了Nginx的負載均衡 - 加權輪詢 (Weighted Round Robin) 。
算法簡介
有三個節點{a, b, c},他們的權重分別是{a=5, b=1, c=1}。發送7次請求,a會被分配5次,b會被分配1次,c會被分配1次。
一般的算法可能是:
1、輪訓所有節點,找到一個最大權重節點;
2、選中的節點權重-1;
3、直到減到0,恢復該節點原始權重,繼續輪詢;
這樣的算法看起來簡單,最終效果是:{a, a, a, a, a, b, c},即前5次可能選中的都是a,這可能造成權重大的服務器造成過大壓力的同時,小權重服務器還很閑。
Nginx的加權輪詢算法將保持選擇的平滑性,希望達到的效果可能是{a, b, a, a, c, a, a},即盡可能均勻的分攤節點,節點分配不再是連續的。
Nginx加權輪詢算法
1、概念解釋,每個節點有三個權重變量,分別是:
(1) weight: 約定權重,即在配置文件或初始化時約定好的每個節點的權重。
(2) effectiveWeight: 有效權重,初始化為weight。
在通訊過程中發現節點異常,則-1;
之后再次選取本節點,調用成功一次則+1,直達恢復到weight;
此變量的作用主要是節點異常,降低其權重。
(3) currentWeight: 節點當前權重,初始化為0。
2、算法邏輯
(1) 輪詢所有節點,計算當前狀態下所有節點的effectiveWeight之和totalWeight;
(2) currentWeight = currentWeight + effectiveWeight; 選出所有節點中currentWeight中最大的一個節點作為選中節點;
(3) 選中節點的currentWeight = currentWeight - totalWeight;
基於以上算法,我們看一個例子:
這時有三個節點{a, b, c},權重分別是{a=4, b=2, c=1},共7次請求,初始currentWeight值為{0, 0, 0},每次分配后的結果如下:
| 請求序號 | 請求前currentWeight值 | 選中節點 | 請求后currentWeight值 |
| 1 | {c=1,b=2,a=4} | a | {c=1,b=2,a=-3} |
| 2 | {c=2,b=4,a=1} | b | {c=2,b=-3,a=1} |
| 3 | {c=3,b=-1,a=5} | a | {c=3,b=-1,a=-2} |
| 4 | {c=4,b=1,a=2} | c | {c=-3,b=1,a=2} |
| 5 | {c=-2,b=3,a=6} | a | {c=-2,b=3,a=-1} |
| 6 | {c=-1,b=5,a=3} | b | {c=-1,b=-2,a=3} |
| 7 | {c=0,b=0,a=7} | a | {c=0,b=0,a=0} |
觀察到七次調用選中的節點順序為{a, b, a, c, a, b, a},a節點選中4次,b節點選中2次,c節點選中1次,算法保持了currentWeight值從初始值{c=0,b=0,a=0}到7次調用后又回到{c=0,b=0,a=0}。
算法實現
下面附上筆者自己的Java版算法實現:
1 package com.example.demo.arithmetic; 2 3 import java.util.ArrayList; 4 import java.util.HashMap; 5 import java.util.List; 6 import java.util.Map; 7 8 /** 9 * Created by caojun on 2018/2/20. 10 * 11 * 基本概念: 12 * weight: 配置文件中指定的該后端的權重,這個值是固定不變的。 13 * effective_weight: 后端的有效權重,初始值為weight。 14 * 在釋放后端時,如果發現和后端的通信過程中發生了錯誤,就減小effective_weight。 15 * 此后有新的請求過來時,在選取后端的過程中,再逐步增加effective_weight,最終又恢復到weight。 16 * 之所以增加這個字段,是為了當后端發生錯誤時,降低其權重。 17 * current_weight: 18 * 后端目前的權重,一開始為0,之后會動態調整。那么是怎么個動態調整呢? 19 * 每次選取后端時,會遍歷集群中所有后端,對於每個后端,讓它的current_weight增加它的effective_weight, 20 * 同時累加所有后端的effective_weight,保存為total。 21 * 如果該后端的current_weight是最大的,就選定這個后端,然后把它的current_weight減去total。 22 * 如果該后端沒有被選定,那么current_weight不用減小。 23 * 24 * 算法邏輯: 25 * 1. 對於每個請求,遍歷集群中的所有可用后端,對於每個后端peer執行: 26 * peer->current_weight += peer->effecitve_weight。 27 * 同時累加所有peer的effective_weight,保存為total。 28 * 2. 從集群中選出current_weight最大的peer,作為本次選定的后端。 29 * 3. 對於本次選定的后端,執行:peer->current_weight -= total。 30 * 31 */ 32 public class RoundRobinByWeightLoadBalance { 33 34 //約定的invoker和權重的鍵值對 35 final private List<Node> nodes; 36 37 public RoundRobinByWeightLoadBalance(Map<Invoker, Integer> invokersWeight){ 38 if (invokersWeight != null && !invokersWeight.isEmpty()) { 39 nodes = new ArrayList<>(invokersWeight.size()); 40 invokersWeight.forEach((invoker, weight)->nodes.add(new Node(invoker, weight))); 41 }else 42 nodes = null; 43 } 44 45 /** 46 * 算法邏輯: 47 * 1. 對於每個請求,遍歷集群中的所有可用后端,對於每個后端peer執行: 48 * peer->current_weight += peer->effecitve_weight。 49 * 同時累加所有peer的effective_weight,保存為total。 50 * 2. 從集群中選出current_weight最大的peer,作為本次選定的后端。 51 * 3. 對於本次選定的后端,執行:peer->current_weight -= total。 52 * 53 * @Return ivoker 54 */ 55 public Invoker select(){ 56 if (! checkNodes()) 57 return null; 58 else if (nodes.size() == 1) { 59 if (nodes.get(0).invoker.isAvalable()) 60 return nodes.get(0).invoker; 61 else 62 return null; 63 } 64 Integer total = 0; 65 Node nodeOfMaxWeight = null; 66 for (Node node : nodes) { 67 total += node.effectiveWeight; 68 node.currentWeight += node.effectiveWeight; 69 70 if (nodeOfMaxWeight == null) { 71 nodeOfMaxWeight = node; 72 }else{ 73 nodeOfMaxWeight = nodeOfMaxWeight.compareTo(node) > 0 ? nodeOfMaxWeight : node; 74 } 75 } 76 77 nodeOfMaxWeight.currentWeight -= total; 78 return nodeOfMaxWeight.invoker; 79 } 80 81 public void onInvokeSuccess(Invoker invoker){ 82 if (checkNodes()){ 83 nodes.stream() 84 .filter((Node node)->invoker.id().equals(node.invoker.id())) 85 .findFirst() 86 .get() 87 .onInvokeSuccess(); 88 } 89 } 90 91 public void onInvokeFail(Invoker invoker){ 92 if (checkNodes()){ 93 nodes.stream() 94 .filter((Node node)->invoker.id().equals(node.invoker.id())) 95 .findFirst() 96 .get() 97 .onInvokeFail(); 98 } 99 } 100 101 private boolean checkNodes(){ 102 return (nodes != null && nodes.size() > 0); 103 } 104 105 public void printCurrenctWeightBeforeSelect(){ 106 if (checkNodes()) { 107 final StringBuffer out = new StringBuffer("{"); 108 nodes.forEach(node->out.append(node.invoker.id()) 109 .append("=") 110 .append(node.currentWeight+node.effectiveWeight) 111 .append(",")); 112 out.append("}"); 113 System.out.print(out); 114 } 115 } 116 117 public void printCurrenctWeight(){ 118 if (checkNodes()) { 119 final StringBuffer out = new StringBuffer("{"); 120 nodes.forEach(node->out.append(node.invoker.id()) 121 .append("=") 122 .append(node.currentWeight) 123 .append(",")); 124 out.append("}"); 125 System.out.print(out); 126 } 127 } 128 129 public interface Invoker{ 130 Boolean isAvalable(); 131 String id(); 132 } 133 134 private static class Node implements Comparable<Node>{ 135 final Invoker invoker; 136 final Integer weight; 137 Integer effectiveWeight; 138 Integer currentWeight; 139 140 Node(Invoker invoker, Integer weight){ 141 this.invoker = invoker; 142 this.weight = weight; 143 this.effectiveWeight = weight; 144 this.currentWeight = 0; 145 } 146 147 @Override 148 public int compareTo(Node o) { 149 return currentWeight > o.currentWeight ? 1 : (currentWeight.equals(o.currentWeight) ? 0 : -1); 150 } 151 152 public void onInvokeSuccess(){ 153 if (effectiveWeight < this.weight) 154 effectiveWeight++; 155 } 156 157 public void onInvokeFail(){ 158 effectiveWeight--; 159 } 160 } 161 162 public static void main(String[] args){ 163 Map<Invoker, Integer> invokersWeight = new HashMap<>(3); 164 Integer aWeight = 4; 165 Integer bWeight = 2; 166 Integer cWeight = 1; 167 168 invokersWeight.put(new Invoker() { 169 @Override 170 public Boolean isAvalable() { 171 return true; 172 } 173 @Override 174 public String id() { 175 return "a"; 176 } 177 }, aWeight); 178 179 invokersWeight.put(new Invoker() { 180 @Override 181 public Boolean isAvalable() { 182 return true; 183 } 184 @Override 185 public String id() { 186 return "b"; 187 } 188 }, bWeight); 189 190 invokersWeight.put(new Invoker() { 191 @Override 192 public Boolean isAvalable() { 193 return true; 194 } 195 @Override 196 public String id() { 197 return "c"; 198 } 199 }, cWeight); 200 201 Integer times = 7; 202 RoundRobinByWeightLoadBalance roundRobin = new RoundRobinByWeightLoadBalance(invokersWeight); 203 for(int i=1; i<=times; i++){ 204 System.out.print(new StringBuffer(i+"").append(" ")); 205 roundRobin.printCurrenctWeightBeforeSelect(); 206 Invoker invoker = roundRobin.select(); 207 System.out.print(new StringBuffer(" ").append(invoker.id()).append(" ")); 208 roundRobin.printCurrenctWeight(); 209 System.out.println(); 210 } 211 } 212 }
