ZooKeeper 實(shí)現(xiàn)負(fù)載均衡
1. 前言
在分布式的環(huán)境中,我們常常使用集群部署的方式來(lái)提高某個(gè)服務(wù)的可用性,為了讓高并發(fā)的請(qǐng)求能夠平均的分配到集群中的每一個(gè)服務(wù),避免有些服務(wù)壓力過(guò)大,而有些服務(wù)處于空閑狀態(tài)這樣的情況,我們需要制定一些規(guī)則來(lái)把請(qǐng)求進(jìn)行路由,這種分配請(qǐng)求的做法就叫做負(fù)載均衡,路由請(qǐng)求的規(guī)則就是負(fù)載均衡的策略。
那么負(fù)載均衡的策略有哪些呢?如何使用 Zookeeper 實(shí)現(xiàn)負(fù)載均衡呢?接下來(lái)我們就帶著這些問(wèn)題開始本節(jié)的內(nèi)容。
2. 負(fù)載均衡的策略
當(dāng)我們使用集群的方式部署的服務(wù)在不同的機(jī)器上時(shí),根據(jù)機(jī)器的性能以及網(wǎng)絡(luò)環(huán)境,我們可能需要使用負(fù)載均衡策略來(lái)分配請(qǐng)求到不同的機(jī)器,這里我們就開始講解負(fù)載均衡的策略。
-
Round Robin 輪詢策略
輪詢策略,按照集群的服務(wù)列表的順序,依次進(jìn)行請(qǐng)求的分配,直到列表中所有的服務(wù)都分配了一次請(qǐng)求,就完成了一輪的請(qǐng)求分配,然后再?gòu)牡谝粋€(gè)服務(wù)開始分配請(qǐng)求。
輪詢策略是很多負(fù)載均衡技術(shù)的默認(rèn)策略,這樣的方式保證了的每個(gè)服務(wù)所承受的請(qǐng)求壓力是平均的,我們可以把服務(wù)列表按照順序放到一個(gè)數(shù)組來(lái)循環(huán)分配請(qǐng)求。
/** * 輪詢策略 Demo */ public class RoundRobinStrategy { public static void main(String[] args) { // 模擬 Server 地址列表 String[] serverList = {"192.168.0.77","192.168.0.88","192.168.0.99"}; // 模擬 5 次請(qǐng)求 for (int i = 0; i < 5; i++) { // 根據(jù)數(shù)組長(zhǎng)度取模,順序獲取地址索引 int i1 = i % serverList.length; // 根據(jù)索引獲取服務(wù)器地址 System.out.println(serverList[i1]); } } }
執(zhí)行 main 方法,查看控制臺(tái)輸出:
192.168.0.77 192.168.0.88 192.168.0.99 192.168.0.77 192.168.0.88
我們可以觀察到控制臺(tái)輸出的服務(wù)地址是順序的。
-
Random 隨機(jī)策略
隨機(jī)策略,顧名思義就是根據(jù)隨機(jī)算法把請(qǐng)求隨機(jī)的分配給服務(wù)列表中的任意一個(gè)服務(wù)。
隨機(jī)策略的實(shí)現(xiàn)方式:我們可以把服務(wù)列表放到一個(gè)數(shù)組,然后根據(jù)數(shù)組的長(zhǎng)度來(lái)獲取隨機(jī)數(shù),取到的隨機(jī)數(shù)就是服務(wù)在數(shù)組中的索引,根據(jù)這個(gè)索引,我們就可以拿到服務(wù)地址來(lái)發(fā)送請(qǐng)求了。
/** * 隨機(jī)策略 Demo */ public class RandomStrategy { public static void main(String[] args) { // 服務(wù)地址數(shù)組 String[] serverList = {"192.168.0.77","192.168.0.88","192.168.0.99"}; // 模擬發(fā)送 5 次請(qǐng)求 for (int j = 0; j < 5; j++) { // 隨機(jī)獲取數(shù)組的索引 int i = new Random().nextInt(serverList.length); // 根據(jù)索引獲取服務(wù)器地址 System.out.println(serverList[i]); } } }
執(zhí)行 main 方法,查看控制臺(tái)輸出:
192.168.0.88 192.168.0.88 192.168.0.99 192.168.0.77 192.168.0.77
我們可以觀察到控制臺(tái)輸出的服務(wù)地址是隨機(jī)的,還有可能會(huì)出現(xiàn)多次請(qǐng)求連續(xù)隨機(jī)到同一個(gè)服務(wù)的情況。
-
Consistent Hashing 一致性哈希策略
一致性哈希策略的實(shí)現(xiàn)方式:我們先把服務(wù)列表中的地址進(jìn)行哈希計(jì)算,把計(jì)算后的值放到哈希環(huán)上,接收到請(qǐng)求后,根據(jù)請(qǐng)求的固定屬性值來(lái)進(jìn)行哈希計(jì)算,然后根據(jù)請(qǐng)求的哈希值在哈希環(huán)上順時(shí)針尋找服務(wù)地址的哈希值,尋找到哪個(gè)服務(wù)地址的哈希值,就把請(qǐng)求分配給哪個(gè)服務(wù)。
Tips: 哈希環(huán)的范圍,從 0 開始,到 2 的32 次方減 1 結(jié)束,也就是到 Integer 的最大取值范圍。
在示例的圖中,哈希環(huán)上有 3 個(gè) Server 的 Hash 值,每個(gè)請(qǐng)求的 Hash 值都順時(shí)針去尋找 Server 的 Hash 值,找到哪個(gè)就將請(qǐng)求分配給哪個(gè)服務(wù)。接下來(lái)我們用 Java 實(shí)現(xiàn)一致性哈希策略,使用 IP 地址進(jìn)行 Hash 計(jì)算:
/**
* 一致性哈希策略 Demo
*/
public class ConsistentHashingStrategy {
public static void main(String[] args) {
// 模擬 Server 地址列表
String[] serverList = {"192.168.0.15", "192.168.0.30", "192.168.0.45"};
// 新建 TreeMap 集合 ,以 Key,Value 的方式綁定 Hash 值與地址
SortedMap<Integer, String> serverHashMap = new TreeMap<>();
// 計(jì)算 Server 地址的 Hash 值
for (String address : serverList) {
int serverHash = Math.abs(address.hashCode());
// 綁定 Hash 值與地址
serverHashMap.put(serverHash, address);
}
// 模擬 Request 地址
String[] requestList = {"192.168.0.10", "192.168.0.20", "192.168.0.40", "192.168.0.50"};
// 計(jì)算 Request 地址的 Hash 值
for (String request : requestList) {
int requestHash = Math.abs(request.hashCode());
// 在 serverHashMap 中尋找所有大于 requestHash 的 key
SortedMap<Integer, String> tailMap = serverHashMap.tailMap(requestHash);
//如果有大于 requestHash 的 key, 第一個(gè) key 就是離 requestHash 最近的 serverHash
if (!tailMap.isEmpty()) {
Integer key = tailMap.firstKey();
// 根據(jù) key 獲取 Server address
String address = serverHashMap.get(key);
System.out.println("請(qǐng)求 " + request + " 被分配給服務(wù) " + address);
} else {
// 如果 serverHashMap 中沒(méi)有比 requestHash 大的 key
// 則直接在 serverHashMap 取第一個(gè)服務(wù)
Integer key = serverHashMap.firstKey();
// 根據(jù) key 獲取 Server address
String address = serverHashMap.get(key);
System.out.println("請(qǐng)求 " + request + " 被分配給服務(wù) " + address);
}
}
}
}
執(zhí)行 main 方法,查看控制臺(tái)輸出:
請(qǐng)求 192.168.0.10 被分配給服務(wù) 192.168.0.15
請(qǐng)求 192.168.0.20 被分配給服務(wù) 192.168.0.30
請(qǐng)求 192.168.0.40 被分配給服務(wù) 192.168.0.45
請(qǐng)求 192.168.0.50 被分配給服務(wù) 192.168.0.15
-
加權(quán)輪詢策略
加權(quán)輪詢策略就是在輪詢策略的基礎(chǔ)上,對(duì) Server 地址進(jìn)行加權(quán)處理,除了按照服務(wù)地址列表的順序來(lái)分配請(qǐng)求外,還要按照權(quán)重大小來(lái)決定請(qǐng)求的分配次數(shù)。加權(quán)的目的是為了讓性能和網(wǎng)絡(luò)較好的服務(wù)多承擔(dān)請(qǐng)求分配的壓力。
比如 Server_1 的權(quán)重是 3,Server_2 的權(quán)重是 2,Server_3 的權(quán)重是 1,那么在進(jìn)行請(qǐng)求分配時(shí),Server_1 會(huì)被分配 3 次請(qǐng)求,Server_2 會(huì)被分配 2 次請(qǐng)求,Server_3 會(huì)被分配 1 次請(qǐng)求,就這樣完成一輪請(qǐng)求的分配,然后再?gòu)?Server_1 開始進(jìn)行分配。
-
加權(quán)隨機(jī)策略
加權(quán)隨機(jī)策略就是在隨機(jī)策略的基礎(chǔ)上,對(duì) Server 地址進(jìn)行加權(quán)處理,Server 地址的加權(quán)有多少,那么 Server 地址的數(shù)組中的地址就會(huì)有幾個(gè),然后再?gòu)倪@個(gè)數(shù)組中進(jìn)行隨機(jī)選址。
-
Least Connection 最小連接數(shù)策略
最小連接數(shù)策略,就是根據(jù)客戶端與服務(wù)端會(huì)話數(shù)量來(lái)決定請(qǐng)求的分配情況,它會(huì)把請(qǐng)求分配到會(huì)話數(shù)量小的服務(wù),會(huì)話的數(shù)量越少,也能說(shuō)明服務(wù)的性能和網(wǎng)絡(luò)較好。
學(xué)習(xí)完負(fù)載均衡的策略,接下來(lái)我們使用 Zookeeper 實(shí)現(xiàn)負(fù)載均衡。
3. Zookeeper 實(shí)現(xiàn)負(fù)載均衡
Zookeeper 實(shí)現(xiàn)負(fù)載均衡,我們可以使用 Zookeeper 的臨時(shí)節(jié)點(diǎn)來(lái)維護(hù) Server 的地址列表,然后選擇負(fù)載均衡策略來(lái)對(duì)請(qǐng)求進(jìn)行分配。
我們回顧一下臨時(shí)節(jié)點(diǎn)的特性:當(dāng)創(chuàng)建該節(jié)點(diǎn)的 Zookeeper 客戶端與 Zookeeper 服務(wù)端斷開連接時(shí),該節(jié)點(diǎn)會(huì)被 Zookeeper 服務(wù)端移除。使用臨時(shí)節(jié)點(diǎn)來(lái)維護(hù) Server 的地址列表就保證了請(qǐng)求不會(huì)被分配到已經(jīng)停機(jī)的服務(wù)上。
在上面的講解中,輪詢策略,隨機(jī)策略和一致性哈希策略都使用 Java 簡(jiǎn)單的實(shí)現(xiàn)了 Demo,那么接下來(lái)我們就使用最小連接數(shù)策略來(lái)實(shí)現(xiàn)請(qǐng)求的分配。
3.1 臨時(shí)節(jié)點(diǎn)和最小連接數(shù)策略實(shí)現(xiàn)負(fù)載均衡
首先我們需要在集群的每一個(gè) Server 中都使用 Zookeeper 客戶端 Curator 來(lái)連接 Zookeeper 服務(wù)端,當(dāng) Server 啟動(dòng)時(shí),使用 Curator 連接 Zookeeper 服務(wù)端,并用自身的地址信息創(chuàng)建臨時(shí)節(jié)點(diǎn)到 Zookeeper 服務(wù)端。
我們還可以提供手動(dòng)下線 Server 的方法,需要 Server 下線時(shí)可以手動(dòng)調(diào)用刪除節(jié)點(diǎn)的方法,需要 Server 上線時(shí)再次使用自身的地址信息來(lái)創(chuàng)建臨時(shí)節(jié)點(diǎn)。
除了維護(hù) Server 的地址信息外,我們還需要維護(hù)請(qǐng)求的會(huì)話連接數(shù),我們可以使用節(jié)點(diǎn)的 data 來(lái)保存請(qǐng)求會(huì)話的連接數(shù)。
我們使用在 Zookeeper Curator 一節(jié)創(chuàng)建的 Spring Boot 測(cè)試項(xiàng)目來(lái)實(shí)現(xiàn):
/**
* 最小連接數(shù)策略 Demo
* Server 服務(wù)端注冊(cè)地址
*/
@Component
public class MinimumConnectionsStrategyServer implements ApplicationRunner {
@Autowired
private CuratorService curatorService;
// Curator 客戶端
public CuratorFramework client;
// 當(dāng)前服務(wù)地址的臨時(shí)節(jié)點(diǎn)
public static String SERVER_IP;
// 當(dāng)前服務(wù)地址臨時(shí)節(jié)點(diǎn)的父節(jié)點(diǎn),節(jié)點(diǎn)類型為持久節(jié)點(diǎn)
public static final String IMOOC_SERVER = "/imooc-server";
/**
* 服務(wù)啟動(dòng)后自動(dòng)執(zhí)行
*
* @param args args
* @throws Exception Exception
*/
@Override
public void run(ApplicationArguments args) throws Exception {
// Curator 客戶端開啟會(huì)話
client = curatorService.getCuratorClient();
client.start();
// 注冊(cè)地址信息到 Zookeeper
registerAddressToZookeeper();
}
/**
* 注冊(cè)地址信息到 Zookeeper
* 服務(wù)啟動(dòng)時(shí)和服務(wù)手動(dòng)上線時(shí)調(diào)用此方法
*
* @throws Exception Exception
*/
public void registerAddressToZookeeper() throws Exception {
// 判斷父節(jié)點(diǎn)是否存在,不存在則創(chuàng)建持久節(jié)點(diǎn)
Stat stat = client.checkExists().forPath(IMOOC_SERVER);
if (stat == null) {
client.create().creatingParentsIfNeeded().forPath(IMOOC_SERVER);
}
// 獲取本機(jī)地址
String address = InetAddress.getLocalHost().getHostAddress();
// 創(chuàng)建臨時(shí)節(jié)點(diǎn),節(jié)點(diǎn)路徑為 /IMOOC_SERVER/address,節(jié)點(diǎn) data 為 請(qǐng)求會(huì)話數(shù),初始化時(shí)為 0.
// /imooc-server/192.168.0.77
SERVER_IP = client.create()
.withMode(CreateMode.EPHEMERAL)
.forPath(IMOOC_SERVER + "/" + address, "0".getBytes());
}
/**
* 注銷在 Zookeeper 上的注冊(cè)的地址
* 服務(wù)手動(dòng)下線時(shí)調(diào)用此方法
*
* @throws Exception Exception
*/
public void deregistrationAddress() throws Exception {
// 檢查該節(jié)點(diǎn)是否存在
Stat stat = client.checkExists().forPath(SERVER_IP);
// 存在則刪除
if (stat != null) {
client.delete().forPath(SERVER_IP);
}
}
}
在客戶端的請(qǐng)求調(diào)用集群服務(wù)之前,先使用 Curator 獲取 IMOOC_SERVER 下所有的臨時(shí)節(jié)點(diǎn),并尋找出 data 最小的臨時(shí)節(jié)點(diǎn),也就是最小連接數(shù)的服務(wù)。
在客戶端發(fā)送請(qǐng)求時(shí),我們可以讓當(dāng)前 Server 的請(qǐng)求會(huì)話數(shù)加 1,并更新到臨時(shí)節(jié)點(diǎn)的 data,完成請(qǐng)求時(shí),我們可以讓當(dāng)前 Server 的請(qǐng)求會(huì)話數(shù)減 1,并更新到臨時(shí)節(jié)點(diǎn)的 data 。
/**
* 最小連接數(shù)策略 Demo
* Client 客戶端發(fā)送請(qǐng)求
*/
@Component
public class MinimumConnectionsStrategyClient implements ApplicationRunner {
@Autowired
private CuratorService curatorService;
// Curator 客戶端
public CuratorFramework client;
// 服務(wù)列表節(jié)點(diǎn)的 父節(jié)點(diǎn)
public static final String IMOOC_SERVER = "/imooc-server";
@Override
public void run(ApplicationArguments args) throws Exception {
// Curator 客戶端開啟會(huì)話
client = curatorService.getCuratorClient();
client.start();
}
/**
* 獲取最小連接數(shù)的服務(wù)
* 發(fā)送請(qǐng)求前調(diào)用此方法,獲取服務(wù)地址
*
* @return String
* @throws Exception Exception
*/
public String getTheMinimumNumberOfConnectionsService() throws Exception {
// 獲取所有子節(jié)點(diǎn)
List<String> list = client.getChildren().forPath(IMOOC_SERVER);
// 新建 Map
Map<String, Integer> map = new HashMap<>();
// 遍歷服務(wù)列表,保存服務(wù)地址與請(qǐng)求會(huì)話數(shù)的映射關(guān)系
for (String s : list) {
byte[] bytes = client.getData().forPath(IMOOC_SERVER + "/" + s);
int i = Integer.parseInt(new String(bytes));
map.put(s, i);
}
// 尋找 map 中會(huì)話數(shù)最小的值
Optional<Map.Entry<String, Integer>> min = map.entrySet().stream().min(Map.Entry.comparingByValue());
// 不為空的話
if (min.isPresent()) {
// 返回 服務(wù)地址 ip
Map.Entry<String, Integer> entry = min.get();
return entry.getKey();
} else {
// 沒(méi)有則返回服務(wù)列表第一個(gè)服務(wù)地址 ip
return list.get(0);
}
}
/**
* 增加該服務(wù)的請(qǐng)求會(huì)話數(shù)量
* 使用服務(wù)地址處理業(yè)務(wù)前調(diào)用此方法
*
* @param ip 服務(wù)地址
* @throws Exception Exception
*/
public void increaseTheNumberOfRequestedSessions(String ip) throws Exception {
byte[] bytes = client.getData().forPath(IMOOC_SERVER + "/" + ip);
int i = Integer.parseInt(new String(bytes));
i++;
client.setData().forPath(IMOOC_SERVER + "/" + ip, String.valueOf(i).getBytes());
}
/**
* 減少該服務(wù)的請(qǐng)求會(huì)話數(shù)量
* 請(qǐng)求結(jié)束時(shí)調(diào)用此方法減少會(huì)話數(shù)量
*
* @param ip 服務(wù)地址
* @throws Exception Exception
*/
public void reduceTheNumberOfRequestedSessions(String ip) throws Exception {
byte[] bytes = client.getData().forPath(IMOOC_SERVER + "/" + ip);
int i = Integer.parseInt(new String(bytes));
i--;
client.setData().forPath(IMOOC_SERVER + "/" + ip, String.valueOf(i).getBytes());
}
}
這樣我們就使用 Zookeeper 的臨時(shí)節(jié)點(diǎn)完成了一個(gè)簡(jiǎn)單的最小連接數(shù)策略的負(fù)載均衡。
4. 總結(jié)
在本節(jié)的內(nèi)容中,我們學(xué)習(xí)了為什么要使用負(fù)載均衡,負(fù)載均衡的策略,以及使用 Zookeeper 的臨時(shí)節(jié)點(diǎn)來(lái)實(shí)現(xiàn)負(fù)載均衡。以下是本節(jié)內(nèi)容的總結(jié):
- 分布式環(huán)境下為什么要使用負(fù)載均衡。
- 負(fù)載均衡的策略有哪些。
- 使用 Zookeeper 的臨時(shí)節(jié)點(diǎn)實(shí)現(xiàn)負(fù)載均衡。