引言
随着公司业务的飞速发展,以及业务的多样性,用户数会迅猛增长,系统的流量会越来越大。因此,大规模的并发用户访问会对系统的处理能力造成巨大的压力,系统必须要有足够强的处理能力才能应对。
这篇文章就来介绍一下高并发系统的通用设计原则之一:负载均衡。
什么是负载均衡
负载均衡,英文名称为 Load Balance,它的核心思想就是在用户和服务器中间加一层负载均衡服务,该层服务通过相应的负载均衡算法,将用户请求分发给应用服务器集群。
以前的单体应用时代:
随着用户规模不断扩大,单机对外提供服务越发显得力不从心:
集群时代:
常见的负载均衡服务器有:LVS、Nginx、Haproxy
负载均衡服务器会根据 应用服务器的健康状态来判断当前节点是否可以被转发,依次来保证整个应用系统的可用性。
常见的负载均衡算法包括:
- 随机算法
- 轮询算法
- 加权随机算法
- 加权轮询算法
- IP-Hash 算法
- 最小活跃连接算法
几种常见的负载均衡算法
定义两个个公用类 IpInfo、ServerRegister ,用来表示 IP 节点信息以及 IP 信息的存储。
package com.markus.service.load.balanced;
/**
* @author: markus
* @date: 2024/2/25 1:49 PM
* @Description:
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class IpInfo {
private String ipAddr;
private Integer weight;
private Integer activeLink;
public IpInfo(String ipAddr, Integer weight) {
this.ipAddr = ipAddr;
this.weight = weight;
this.activeLink = 0;
}
public String getIpAddr() {
return ipAddr;
}
public void setIpAddr(String ipAddr) {
this.ipAddr = ipAddr;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
public Integer getActiveLink() {
return activeLink;
}
public void setActiveLink(Integer activeLink) {
this.activeLink = activeLink;
}
@Override
public String toString() {
return "IpInfo{" +
"ipAddr='" + ipAddr + '\'' +
", weight=" + weight +
", activeLink=" + activeLink +
'}';
}
}
package com.markus.service.load.balanced;
import java.util.HashMap;
import java.util.Map;
/**
* @author: markus
* @date: 2024/2/25 2:05 PM
* @Description:
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class ServerRegister {
public static final Map<String, IpInfo> ipInfoMap = new HashMap<>();
static {
ipInfoMap.put("192.168.163.1", new IpInfo("192.168.163.1", 1));
ipInfoMap.put("192.168.163.2", new IpInfo("192.168.163.2", 2));
ipInfoMap.put("192.168.163.3", new IpInfo("192.168.163.3", 3));
ipInfoMap.put("192.168.163.4", new IpInfo("192.168.163.4", 4));
}
}
随机算法
随机算法就是在可用的应用服务器节点中随机选择一个节点来访问。例如当前有 4 个 IP 节点,那么通过随机算法每次随机生成一个 [0,size) 范围内的随机数,然后就可以得到要访问的节点,代码如下所示:
package com.markus.service.load.balanced;
import java.util.*;
/**
* @author: markus
* @date: 2024/2/25 1:35 PM
* @Description: 随机算法
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class RandomAlgorithm {
private static Map<String, IpInfo> ipInfoMap = new HashMap<>();
static {
ipInfoMap.put("192.168.163.1", new IpInfo(1));
ipInfoMap.put("192.168.163.2", new IpInfo(2));
ipInfoMap.put("192.168.163.3", new IpInfo(3));
ipInfoMap.put("192.168.163.4", new IpInfo(4));
}
private static List<String> getIpList() {
List<String> result = new ArrayList<>(ipInfoMap.size());
result.addAll(ipInfoMap.keySet());
return result;
}
public static String getIpByRandomAlgorithm() {
List<String> ipList = getIpList();
Random random = new Random();
// 在 [0,size) 中选择一个随机数
int index = random.nextInt(ipList.size());
return ipList.get(index);
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
String ip = getIpByRandomAlgorithm();
System.out.println("选择的 IP 为 : " + ip);
}
}
}
轮询算法
轮询算法就是,在可用的算法节点中,按照固定的顺序依次访问节点。例如当前有 4 个 IP 节点,那么通过预先指定的 round 起始索引位开始访问节点,拿到节点数据。round 每次 + 1 并对 ipList.size 取模(保证 round 始终在 [0,ipList.size) 范围内)。代码实现如下:
package com.markus.service.load.balanced;
import java.util.ArrayList;
import java.util.List;
import static com.markus.service.load.balanced.ServerRegister.ipInfoMap;
/**
* @author: markus
* @date: 2024/2/25 2:04 PM
* @Description: 轮询算法
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class RoundAlgorithm {
private static List<String> getIpList() {
List<String> result = new ArrayList<>(ipInfoMap.size());
result.addAll(ipInfoMap.keySet());
return result;
}
// 起始位置
private static Integer round = 0;
public static String getIpByRoundAlgorithm() {
List<String> ipList = getIpList();
String ip = ipList.get(round);
round = (round + 1) % ipList.size();
return ip;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
String ip = getIpByRoundAlgorithm();
System.out.println("选择的 IP 为 : " + ip);
}
}
}
加权随机算法
加权随机算法就是,在随机算法的基础上给每个节点添加一个权重,从而使每个节点被访问到的概率不相同,即权重大的节点被访问到的概率会更高,权重小的节点被访问到的概率会越小。示例代码如下:
package com.markus.service.load.balanced;
import java.util.*;
import static com.markus.service.load.balanced.ServerRegister.ipInfoMap;
/**
* @author: markus
* @date: 2024/2/25 1:35 PM
* @Description: 加权随机算法
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class WeightRandomAlgorithm {
private static List<String> getIpList() {
List<String> result = new ArrayList<>(ipInfoMap.size());
for (Map.Entry<String, IpInfo> entry : ipInfoMap.entrySet()) {
String ip = entry.getKey();
IpInfo ipInfo = entry.getValue();
// 根据权重 像可用列表里增加相应 IP 出现的次数
for (int i = 0; i < ipInfo.getWeight(); i++) {
result.add(ip);
}
}
return result;
}
public static String getIpByRandomAlgorithm() {
List<String> ipList = getIpList();
Random random = new Random();
// 在 [0,size) 中选择一个随机数
int index = random.nextInt(ipList.size());
return ipList.get(index);
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
String ip = getIpByRandomAlgorithm();
System.out.println("选择的 IP 为 : " + ip);
}
}
}
加权轮询算法
加权轮询算法就是,在轮询算法的基础上,给每个节点增加权重,从而使得每个节点被访问到的概率不相同。即权重大的节点被访问到的概率就越高,权重小的节点被访问到的概率就越低。示例代码如下:
package com.markus.service.load.balanced;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.markus.service.load.balanced.ServerRegister.ipInfoMap;
/**
* @author: markus
* @date: 2024/2/25 2:04 PM
* @Description: 加权轮询算法
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class WeightRoundAlgorithm {
private static List<String> getIpList() {
List<String> result = new ArrayList<>(ipInfoMap.size());
for (Map.Entry<String, IpInfo> entry : ipInfoMap.entrySet()) {
String ip = entry.getKey();
IpInfo ipInfo = entry.getValue();
// 根据权重 像可用列表里增加相应 IP 出现的次数
for (int i = 0; i < ipInfo.getWeight(); i++) {
result.add(ip);
}
}
return result;
}
// 起始位置
private static Integer round = 0;
public static String getIpByRoundAlgorithm() {
List<String> ipList = getIpList();
String ip = ipList.get(round);
round = (round + 1) % ipList.size();
return ip;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
String ip = getIpByRoundAlgorithm();
System.out.println("选择的 IP 为 : " + ip);
}
}
}
IP-HASH 算法
IP-HASH 算法也称一致性 Hash 算法,是通过某个 Hash 函数把同一来源的请求都映射到同一个节点上。其核心思想就是:同一个来源的请求只会分配到同一个服务节点上(具有记忆功能),只有当应用服务节点不可用时,才会将其分配到相邻的其他节点上去。示例代码如下所示:
package com.markus.service.load.balanced;
import java.util.ArrayList;
import java.util.List;
import static com.markus.service.load.balanced.ServerRegister.ipInfoMap;
/**
* @author: markus
* @date: 2024/2/25 2:41 PM
* @Description: IP-HASH 一致性 HASH 算法
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class IpHashAlgorithm {
private static List<String> getIpList() {
List<String> result = new ArrayList<>(ipInfoMap.size());
result.addAll(ipInfoMap.keySet());
return result;
}
// 这里 请求入参 仅是简单的设置 String remoteIP
public static String getIpByIpHashAlgorithm(String remoteIp) {
List<String> ipList = getIpList();
int hashCode = remoteIp.hashCode();
int index = hashCode % ipList.size();
return ipList.get(index);
}
public static void main(String[] args) {
String remoteIp = "127.0.0.1";
for (int i = 0; i < 20; i++) {
String ip = getIpByIpHashAlgorithm(remoteIp);
System.out.println("remoteIp " + remoteIp + " 选择的 IP 为 : " + ip);
}
remoteIp = "192.168.163.11";
for (int i = 0; i < 20; i++) {
String ip = getIpByIpHashAlgorithm(remoteIp);
System.out.println("remoteIp " + remoteIp + " 选择的 IP 为 : " + ip);
}
}
}
最小活跃连接算法
最少活跃连接算法就是,每次都选择连接数最少的服务节点来转发请求。由于后台服务器配置不尽相同,对请求的处理能力也各有不同,因此我们可以动态的选取集群节点中连接数最少的一个节点来处理当前请求。示例代码如下所示:
package com.markus.service.load.balanced;
import java.util.ArrayList;
import java.util.List;
import static com.markus.service.load.balanced.ServerRegister.ipInfoMap;
/**
* @author: markus
* @date: 2024/2/25 2:56 PM
* @Description: 最小活跃连接算法
* @Blog: https://markuszhang.com
* It's my honor to share what I've learned with you!
*/
public class MinimumActiveLinkAlgorithm {
private static List<String> getIpList() {
List<String> result = new ArrayList<>(ipInfoMap.size());
result.addAll(ipInfoMap.keySet());
return result;
}
public static String getIpByMinimumActiveLinkAlgorithm() {
List<String> ipList = getIpList();
IpInfo minimumActiveLinkIp = null;
int minimumActiveLinkCount = Integer.MAX_VALUE;
for (String ip : ipList) {
IpInfo ipInfo = ipInfoMap.get(ip);
int activeLinkCount = ipInfo.getActiveLink();
if (activeLinkCount < minimumActiveLinkCount) {
minimumActiveLinkCount = activeLinkCount;
minimumActiveLinkIp = ipInfo;
}
}
System.out.println("当前服务集群节点状态 :" + ipInfoMap);
if (minimumActiveLinkIp != null) {
// 本次 连接数加 +1
Integer activeLink = minimumActiveLinkIp.getActiveLink();
minimumActiveLinkIp.setActiveLink(activeLink + 1);
}
return minimumActiveLinkIp != null ? minimumActiveLinkIp.getIpAddr() : null;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
String ip = getIpByMinimumActiveLinkAlgorithm();
System.out.println("选择的 IP 为 :" + ip);
}
}
}