1、什么是負載均衡
- 負載均衡指多台服務器以對稱的方式組成一個服務器集合,每台服務器都具有等價的地位,都可以單獨對外提供服務而無須其他服務器的輔助。
- 通過某種負載分擔任務,將外部發送來的請求均勻分配到對稱結構中的某一台服務器上,而接受到的請求的服務器獨立地回應客戶的請求。
- 負載均衡能夠平均分配客戶請求到服務器陣列,借此提供快速獲取重要數據,解決大量並發訪問服務問題,這種集群技術可以用最少的投資獲得接近於大型主機的性能。
負載均衡方式:
- 軟件負載均衡:Nginx、LVS、HAProxy
- 硬件負載均衡:Array、F5
2 隨機算法實現
2.1 最簡單版本:
隨機平均選擇服務器
import java.util.Arrays;
import java.util.List;
public class ServerIps {
public static final List<String> LIST = (List<String>) Arrays.asList(
"192.168.0.1",
"192.168.0.2",
"192.168.0.3",
"192.168.0.4",
"192.168.0.5",
"192.168.0.6",
"192.168.0.7",
"192.168.0.8",
"192.168.0.9",
"192.168.0.10"
);
}
import java.util.Random;
public class RandomChoose{
public static String getServer() {
Random random = new Random();
return ServerIps.LIST.get(random.nextInt(ServerIps.LIST.size()));
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(getServer());
}
}
}
2.2 考慮權重的影響
但是每台機器的性能可能不同,性能強的機器能夠處理更多的業務量,這種情況平均輪詢不是很合理。
這樣的話,需要給不同的機器設置不同的權重。
public class ServerIps {
public static final Map<String, Integer> WEIGHT_MAP = new HashMap<>();
static {
WEIGHT_MAP.put("192.168.0.1", 2);
WEIGHT_MAP.put("192.168.0.2", 8);
WEIGHT_MAP.put("192.168.0.3", 1);
WEIGHT_MAP.put("192.168.0.4", 9);
WEIGHT_MAP.put("192.168.0.5", 4);
WEIGHT_MAP.put("192.168.0.6", 6);
}
}
2.2.1 暴力解法
創建一個新的List將所有的IP地址裝入List,注意根據權重來選擇裝入幾次IP地址,比如“198.168.0.1”這個IP地址的權重是2,那么就裝入2次該IP地址
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class WeightRandom{
public static String getServer() {
List<String> ips = new ArrayList<>();
for(String ip: ServerIps.WEIGHT_MAP.keySet()) {
Integer weight = ServerIps.WEIGHT_MAP.get(ip);
for(int i=0; i<weight; i++) {
ips.add(ip);
}
}
Random random = new Random();
return ips.get(random.nextInt(ips.size()));
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(getServer());
}
}
}
2.2.2 優化
當權重很大的時候,將會存入很多次IP地址,耗費很大的空間
可以采用坐標映射的想法,具體如下:
其中A、B、C分別代表3個IP地址,權重分別為5、3、2 A: 5 B: 3 C: 2
映射到坐標軸為
0-----5---8--10
隨意在這個坐標軸取整數就可以確定其在哪個IP地址上
如:
offset = 7
7在5---8這個區間里面,那么對應的就是B這台服務器
具體實現思路:
offset > 5; offset - 5; offset = 2;
offset < 3;
對應B
代碼實現:
import java.util.Random;
public class WeightRandom{
public static String getServer() {
int totalWeight = 0;
for (Integer weight: ServerIps.WEIGHT_MAP.values()) {
totalWeight += weight;
}
Random random = new Random();
int offset = random.nextInt(totalWeight);
for(String ip: ServerIps.WEIGHT_MAP.keySet()) {
Integer weight = ServerIps.WEIGHT_MAP.get(ip);
if (offset < weight) {
return ip;
}
offset -= weight;
}
return null;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(getServer());
}
}
}
3.輪詢
主要思想是服務器一個接一個的服務
3.1 簡單實現
public class RoundRobin {
private static Integer pos = 0;
public static String getServer() {
if(pos >= ServerIps.LIST.size()) {
pos = 0;
}
String ip = ServerIps.LIST.get(pos);
pos++;
return ip;
}
public static void main(String[] args) {
for(int i = 0; i < 20; i++) {
System.out.println(getServer());
}
}
}
3.2 優化:考慮權重
考慮權重的時候
- 暴力解決,將=創建一個LIST,將IP地址按照權重的大小放入LIST里面
- 還是按照隨機優化的思想
其中A、B、C分別代表3個IP地址,權重分別為5、3、2 A: 5 B: 3 C: 2
映射到坐標軸為
0-----5---8--10
offset的取值再不是一個隨機數,而是0,1,2,3,4,5,6,7,8,9
1 ——> A
2 ——> A
...
6 ——> B
...
代碼實現
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ServerIps {
public static final Map<String, Integer> WEIGHT_MAP = new LinkedHashMap<>();
static {
WEIGHT_MAP.put("192.168.0.1", 2);
WEIGHT_MAP.put("192.168.0.2", 8);
WEIGHT_MAP.put("192.168.0.3", 1);
WEIGHT_MAP.put("192.168.0.4", 9);
WEIGHT_MAP.put("192.168.0.5", 4);
WEIGHT_MAP.put("192.168.0.6", 6);
}
}
注意其中采用的是LinkedHashMap,這樣可以保證輸出的順序和輸入的順序一致
public class RequestId {
public static Integer num = 0;
public static Integer getAndIncrement() {
return num++;
}
}
上面代碼模仿的是客戶的ID號,根據客戶的ID號來控制服務器的輪詢。
public class RoundRobin2 {
public static String getServer() {
int totalWeight = 0;
for (Integer weight: ServerIps.WEIGHT_MAP.values()) {
totalWeight += weight;
}
int requestId = RequestId.getAndIncrement();
int offset = requestId % totalWeight;
for(String ip: ServerIps.WEIGHT_MAP.keySet()) {
Integer weight = ServerIps.WEIGHT_MAP.get(ip);
if (offset < weight) {
return ip;
}
offset -= weight;
}
return null;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(getServer());
}
}
}
3.3 優化:平滑加權輪詢算法
Nginx默認采用這種算法
A: 5
B: 1
C: 1
這樣的話,優化1中的訪問順序為AAAAABC,這樣的話對服務器A的壓力比較大
如果按照離散的話,就不會有這樣的問題,如下面這種順序
AABACAA
這樣不僅能使服務比較分散,也能保證權重,還能達到輪詢的目的
具體過程如下:
ip
weight: 靜態,5,1,1 currentWeight:動態
currentWeight: 0,0,0
currentWeight+=weight | max(currentWeight) | result | max(currentWeight)-=sum(weight)7 |
---|---|---|---|
5,1,1 | 5 | A | -2,1,1 |
3,2,2 | 3 | A | -4,2,2 |
1,3,3 | 3 | B | 1,-4,3 |
6,-3,4 | 6 | A | -1,-3,4 |
4,-2,5 | 5 | C | 4,-2,-2 |
9,-1,-1 | 9 | A | 2,-1,-1 |
7,0,0 | 7 | A | 0,0,0 |
5,1,1 | ... | ... | ... |
代碼實現
public class Weight {
private String ip;
private Integer weight;
private Integer currentWeight;
public Weight(String ip, Integer weight, Integer currentWeight) {
super();
this.ip = ip;
this.weight = weight;
this.currentWeight = currentWeight;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
public Integer getCurrentWeight() {
return currentWeight;
}
public void setCurrentWeight(Integer currentWeight) {
this.currentWeight = currentWeight;
}
}
=============================
import java.util.LinkedHashMap;
import java.util.Map;
public class WeightRoundRobin {
private static Map<String, Weight> weightMap = new LinkedHashMap<>();
public static String getServer() {
if(weightMap.isEmpty()) {
for(String ip: ServerIps.WEIGHT_MAP.keySet()) {
Integer weight = ServerIps.WEIGHT_MAP.get(ip);
weightMap.put(ip, new Weight(ip, weight, 0));
}
}
// currentWeight += weight
for(Weight weight: weightMap.values()) {
weight.setCurrentWeight(weight.getCurrentWeight() + weight.getWeight());
}
Weight maxCurrentWeight = null;
for (Weight weight: weightMap.values()) {
if (maxCurrentWeight == null || weight.getCurrentWeight() > maxCurrentWeight.getCurrentWeight()) {
maxCurrentWeight = weight;
}
}
// max(currentWeight) -= sum(weight)
int totalWeight = 0;
for (Integer weight: ServerIps.WEIGHT_MAP.values()) {
totalWeight += weight;
}
maxCurrentWeight.setCurrentWeight(maxCurrentWeight.getCurrentWeight() - totalWeight);
//result
return maxCurrentWeight.getIp();
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(getServer());
}
}
}
4. 一致性哈希實現負載均衡
4.1 為什么需要哈希算法
解決同一個用戶訪問服務器是,訪問的是不同的服務器的問題
場景:集群造成的session沒有同步
當一個用戶訪問服務器A的時候,該台服務器A會保存這台服務器的session,但是當下次再訪問的時候,被負載均衡算法可能算到了不同的服務器B,服務器B中沒有用戶的session,會要求用戶再次登錄。
解決:
- 加入redis,將session存到redis中
- Tomcat同步session
- 一致性哈希算法
4.2 什么是一致性哈希算法
服務器集群接收到一次請求調用時,可以根據請求的信息,比如客戶端的ip地址,或請求路徑與請求參數等信息進行哈希,可以得出一個哈希值,特點是對於相同的ip地址,或請求路徑和請求參數哈希出來的值是一樣的,只要能再增加一個算法,能夠把這個哈希值映射成一個服務端ip地址,就可以使用相同的請求(相同的ip地址,或請求路徑和請求參數)落到同一服務器上。
因為客戶端發起的請求是無窮無盡的(客戶端地址不同,請求參數不同等等),所以對於的哈希值也是無窮大的,所以我們不能把所有的哈希值都進行映射到服務端ip上,所有這里需要用到哈希環。
4.3 虛擬節點
- 解決一個服務器掛掉造成的服務不均勻問題
- 使得哈希環更加平滑
當時當一台服務器掛掉的話,會造成服務器服務不均勻的情況
會發現,ip3和ip1直接的范圍是比較大的,會有更多的請求落到ip1上,這是“不公平”,解決這個問題需要加入虛擬節點,比如:
其中,ip2-1,ip3-1就是虛擬節點,並不能處理節點,而是等同於對應的服務器。
實際上,這只是處理這種不均衡性的一種思路,實際上就算哈希環本身是均衡的,你也可以增加更多的虛擬節點來使得這個環更加的平滑。
上面的哈希環更加的散列,平滑
只需要找到大於hashcode的以一個虛擬節點即可
由於哈希環上的點是有序的,那么采用的數據結構是TreeMap
4.4 代碼實現
public class ServerIps {
public static final List<String> LIST = (List<String>) Arrays.asList(
"192.168.0.1",
"192.168.0.2",
"192.168.0.3",
"192.168.0.4",
"192.168.0.5",
"192.168.0.6",
"192.168.0.7",
"192.168.0.8",
"192.168.0.9",
"192.168.0.10"
);
}
======================================
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHash {
private static TreeMap<Integer, String> virtualNodes = new TreeMap<>();
private static final int VIRTUAL_NODES = 160;
static {
for(String ip: ServerIps.LIST) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
int hash = getHash(ip+i);
virtualNodes.put(hash, ip);
}
}
}
private static String getServer(String client) {
int hash = getHash(client);
//大於hash,virtualNodes的子樹的firstkey
//tailMap,能獲得大於等於hash值的一棵子紅黑樹
SortedMap subMap = virtualNodes.tailMap(hash);
Integer firstKey = null;
if(subMap == null) {
firstKey = virtualNodes.firstKey();
} else {
firstKey = (Integer) subMap.firstKey();
}
return virtualNodes.get(firstKey);
}
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for(int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
if(hash < 0)
hash = Math.abs(hash);
return hash;
}
public static void main(String[] args) {
for (int i = 0; i < 12; i++) {
System.out.println(getServer("client"+i));
}
}
}
5 最小活躍數
由於服務的時間並不是固定,如果平均分配服務器有的時候並不合理
比如:
有3個消息,分別請求了A、B、C三台服務器,處理的時間依次為3s、2s、1s,當1s后有一個新的請求過來,按道理說應該A服務器來服務,但是C台服務器已經空閑了,所有這樣的分配方式有時候並不合理。
解決:
采用最小活躍數,哪台機器服務最少,用哪台機器,當都大致相等的時候可以根據前面的隨機、輪詢等。
6 算法的魅力
- 將程序“復雜化”,提升程序效率到極致