一、負載均衡的概念
在大型的分佈式架構中,對於負載較高的服務來說,往往對應着多臺機器組成的集羣,當請求到來的時候,爲了將請求均衡的分配給後端服務器需要有相應的負載均衡算法來支持,通過相應的負載均衡算法選取一臺機器來處理客戶端請求,這個過程稱爲服務的負載均衡。
二、常用負載均衡算法實現
常用的負載均衡算法主要有隨機法,輪詢法,加權隨機,加權輪詢,最小連接數,一致性hash等。隨機法,輪詢法都比較簡單,隨機算法不考慮服務器的負載實際中很少使用,輪詢算法在服務的上線下線時無法及時感應到也較少使用,下面重點介紹比較常用的加權輪詢和一致性hash。
2.1 負載均衡接口
import java.util.List;
import cn.fzjh.vo.ServiceInstance;
public interface LoadBalance {
//根據請求選擇一個服務實例
ServiceInstance chooseServerInstance();
/**
* 設置服務的信息
*
* @param serviceName 服務名
* @param version 服務版本
*/
void setService(String serviceName, String version);
/**
* 初始化
*/
void init();
/**
* 獲取全部服務列表
*
* @return
*/
List<serviceinstance> getServiceInstanceList();
/**
* 更新服務實例列表
*/
void updateServerInstanceList();
/**
* 隔離一個服務實例
*
* @param server
*/
void isolateServerInstance(String server);
/**
* 恢復一個服務實例
*
* @param server
*/
void resumeServerInstance(String server);
}
2.2 抽象負載均衡基類
import java.util.List;
import java.util.Random;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.fzjh.jk.LoadBalance;
import cn.fzjh.rule.DynamicUploadRule;
import cn.fzjh.vo.ServiceInstance;
//抽象的負載均衡基類,提供基本的服務信息和相關服務實例列表的管理
public abstract class AbstractLoadBalance implements LoadBalance {
protected final Logger logger = LoggerFactory
.getLogger(AbstractLoadBalance.class);
@Resource(name="dynamicUploadRule")
private transient DynamicUploadRule dynamicUploadRule;
protected String serviceName;
protected String version;
// 特定版本的服務的標識
private transient String serviceKey;
protected List<serviceinstance> serviceInstanceList;
@Override
public void setService(String serviceName, String version) {
this.serviceName = serviceName;
this.version = version;
this.serviceKey = genKey(serviceName, version);
}
// 隨機算法獲取可利用的服務列表
@Override
public ServiceInstance chooseServerInstance() {
List<serviceinstance> allServiceList = getAllServiceInstanceList();
if (null == allServiceList) {
return null;
}
ServiceInstance serviceInstance = null;
int indexOfLoop = 0;
Random random = new Random();
if (null != allServiceList && allServiceList.size() > 0) {
int serviceCount = allServiceList.size();
while (null == serviceInstance && indexOfLoop < serviceCount * 5) {// 由於是隨機選取,不能在serverCount內選出
int index = random.nextInt(serviceCount);
serviceInstance = allServiceList.get(index);
logger.info("隨機選擇算法獲取可用的服務:" + serviceInstance.getServerName());
if (serviceInstance.isIsolated()) {
logger.info("選擇的服務暫時不可用:" + serviceInstance.getServerName()
+ ",重新選擇");
indexOfLoop++;
serviceInstance = null;
}
}
}
return serviceInstance;
}
@Override
public void init() {
// 拿到所有的服務器列表
List<serviceinstance> serviceInstances = getAllServiceInstanceList();
setServiceInstanceList(serviceInstances);
}
@Override
public List<serviceinstance> getServiceInstanceList() {
return serviceInstanceList;
}
@Override
public void updateServerInstanceList() {
// 這裏實際上應該重新獲取註冊的服務信息更新,此處默認
List<serviceinstance> serviceInstanceList = getAllServiceInstanceList();
setServiceInstanceList(serviceInstanceList);
}
@Override
public void isolateServerInstance(String serverName) {
for (final ServiceInstance serverInstance : serviceInstanceList) {
if (serverName.equals(serverInstance.getServerName())) {
serverInstance.setIsolated(true);
break;
}
}
}
@Override
public void resumeServerInstance(String serverName) {
for (final ServiceInstance serverInstance : serviceInstanceList) {
if (serverName.equals(serverInstance.getServerName())) {
serverInstance.setIsolated(false);
break;
}
}
}
//通過服務名獲取服務實例
protected ServiceInstance getServiceInstanceByServiceName(String serviceName) {
ServiceInstance serviceInstance = null;
List<serviceinstance> serviceInstances = getAllServiceInstanceList();
if (null == serviceInstances) {
return null;
}
for (final ServiceInstance instance : serviceInstances) {
if (instance.getServerName().equals(serviceName)) {
serviceInstance = instance;
break;
}
}
return serviceInstance;
}
private String genKey(String serviceName, String version) {
return new StringBuffer().append(serviceName).append('#')
.append(version).toString();
}
private List<serviceinstance> getAllServiceInstanceList() {
// 模擬服務器註冊後的服務實例
List<serviceinstance> serviceInstanceList = dynamicUploadRule.getServiceInstanceRule();
return serviceInstanceList;
}
protected String getServiceNameByServiceKey(String serviceKey){
int index = serviceKey.indexOf('#');
return serviceKey.substring(0, index);
}
public String getServiceName() {
return serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getServiceKey() {
return serviceKey;
}
public void setServiceKey(String serviceKey) {
this.serviceKey = serviceKey;
}
public void setServiceInstanceList(List<serviceinstance> serviceInstanceList) {
this.serviceInstanceList = serviceInstanceList;
}
2.3 一致性hash算法
目前在企業裏面常用的基於內存的緩存服務memcached默認採用的負載均衡算法就是一致性hash算法,一致性hash算法可用保證對同一參數的請求將會是同一個服務器來處理,其原理大概如下:我們可以將可利用的服務器列表映射到一個hash環上,當客戶端請求過來時,求出key的hash值映射到hash環上,然後順時針查找,找到的第一臺機器就是出來該請求的服務,如果循環完了還是沒找到,那麼就將請求轉給hash環上的第一臺機器處理,可以想象的是,如果咱們的集羣足夠大,可能循環很久才能找到對應的服務器,這就加大了網絡IO
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import cn.fzjh.abstrac.AbstractLoadBalance;
import cn.fzjh.consistenthash.ConsistentHash;
import cn.fzjh.consistenthash.MurmurHash3;
import cn.fzjh.vo.ServiceInstance;
@Service("consistentHashLoadBalance")
//基於一致性hash算法實現負載均衡
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
private static final Logger logger = LoggerFactory
.getLogger(ConsistentHashLoadBalance.class);
// 虛擬節點總數,用來計算各個服務器的虛擬節點總數
public static final int VITRUAL_NODE_NUMBER = 1000;
// 默認的每個服務器的虛擬節點個數
public static final int DEFALUT_NODE_NUMBER = 30;
//原子更新引用
private AtomicReference<consistenthash<serviceinstance>> hashRing = new AtomicReference<consistenthash<serviceinstance>>();
//虛擬節點個數,初始化時根據服務節點數計算,不隨服務節點變化而變化
private int numberOfReplicas;
public ServiceInstance chooseServerInstance(String serviceKey) {
this.serviceName = getServiceNameByServiceKey(serviceKey);
//從哈希環中找到對應的節點以及後續的節點
List<serviceinstance> instances = hashRing.get().getNUniqueBinsFor(serviceName, getServiceInstanceList().size());
ServiceInstance serviceInstance = null;
//循環每個階段,直至找出沒有被隔離的服務器
for(ServiceInstance instance: instances){
if(instance.isIsolated()){
logger.info("服務被隔離了,暫時不可用:" + serviceName);
} else {
//順時針找到第一個後就返回
serviceInstance = instance;
break;
}
}
return serviceInstance;
}
@Override
public void init() {
super.init();
numberOfReplicas = getServiceInstanceList().isEmpty()?DEFALUT_NODE_NUMBER:VITRUAL_NODE_NUMBER/getServiceInstanceList().size();
buildHashLoop();
}
private void buildHashLoop() {
logger.info("開始構建hash環");
hashRing.set(new ConsistentHash<serviceinstance>(MurmurHash3.getInstance(),numberOfReplicas,serviceInstanceList));
}
}
一致性hash測試類:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import cn.fzjh.loadBalance.rule.ConsistentHashLoadBalance;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext.xml" })
public class TestConsistentHashLoadBalance {
@Autowired
private transient ConsistentHashLoadBalance consistentHashLoadBalance;
@Test
public void testConsistentLoadBalance() throws InterruptedException{
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(15, 20, 60,
TimeUnit.SECONDS, new ArrayBlockingQueue<runnable>(3));
for(int i=1;i<=15;i++){
final int index = i;
poolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("當前線程: " + Thread.currentThread().getName() + ",serviceKey:" + "127.0.0." + index + ",選擇服務:" + consistentHashLoadBalance.chooseServerInstance("127.0.0." + index + "#1.0.0" ).getServerName());
}
});
}
}
}
////////////////////////////////////多跑幾次效果如下////////////////////////////////////////////
當前線程: pool-1-thread-2,serviceKey:127.0.0.2,選擇服務:127.0.0.2
當前線程: pool-1-thread-5,serviceKey:127.0.0.5,選擇服務:127.0.0.3
當前線程: pool-1-thread-10,serviceKey:127.0.0.10,選擇服務:127.0.0.1
當前線程: pool-1-thread-9,serviceKey:127.0.0.9,選擇服務:127.0.0.3
當前線程: pool-1-thread-3,serviceKey:127.0.0.3,選擇服務:127.0.0.1
當前線程: pool-1-thread-4,serviceKey:127.0.0.4,選擇服務:127.0.0.2
當前線程: pool-1-thread-1,serviceKey:127.0.0.1,選擇服務:127.0.0.1
當前線程: pool-1-thread-14,serviceKey:127.0.0.14,選擇服務:127.0.0.1
當前線程: pool-1-thread-6,serviceKey:127.0.0.6,選擇服務:127.0.0.1
當前線程: pool-1-thread-13,serviceKey:127.0.0.13,選擇服務:127.0.0.2
當前線程: pool-1-thread-7,serviceKey:127.0.0.7,選擇服務:127.0.0.2
當前線程: pool-1-thread-8,serviceKey:127.0.0.8,選擇服務:127.0.0.2
當前線程: pool-1-thread-11,serviceKey:127.0.0.11,選擇服務:127.0.0.2
當前線程: pool-1-thread-15,serviceKey:127.0.0.15,選擇服務:127.0.0.1
當前線程: pool-1-thread-12,serviceKey:127.0.0.12,選擇服務:127.0.0.1
//////////////////////////////////////////////////////////////////
當前線程: pool-1-thread-1,serviceKey:127.0.0.1,選擇服務:127.0.0.1
當前線程: pool-1-thread-2,serviceKey:127.0.0.2,選擇服務:127.0.0.2
當前線程: pool-1-thread-10,serviceKey:127.0.0.10,選擇服務:127.0.0.1
當前線程: pool-1-thread-5,serviceKey:127.0.0.5,選擇服務:127.0.0.3
當前線程: pool-1-thread-14,serviceKey:127.0.0.14,選擇服務:127.0.0.1
當前線程: pool-1-thread-6,serviceKey:127.0.0.6,選擇服務:127.0.0.1
當前線程: pool-1-thread-3,serviceKey:127.0.0.3,選擇服務:127.0.0.1
當前線程: pool-1-thread-7,serviceKey:127.0.0.7,選擇服務:127.0.0.2
當前線程: pool-1-thread-11,serviceKey:127.0.0.11,選擇服務:127.0.0.2
當前線程: pool-1-thread-15,serviceKey:127.0.0.15,選擇服務:127.0.0.1
當前線程: pool-1-thread-9,serviceKey:127.0.0.9,選擇服務:127.0.0.3
當前線程: pool-1-thread-13,serviceKey:127.0.0.13,選擇服務:127.0.0.2
當前線程: pool-1-thread-4,serviceKey:127.0.0.4,選擇服務:127.0.0.2
當前線程: pool-1-thread-8,serviceKey:127.0.0.8,選擇服務:127.0.0.2
當前線程: pool-1-thread-12,serviceKey:127.0.0.12,選擇服務:127.0.0.1
可以看到,同一個請求經過hash後找到的處理請求的服務器是同一臺
2.4 加權輪詢
對一些性能高,負載低的服務器,我們可以給它更高的權值,以便處理更多的請求
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import cn.fzjh.abstrac.AbstractLoadBalance;
import cn.fzjh.rule.DynamicUploadRule;
import cn.fzjh.vo.ServiceInstance;
//按照服務器權重的賦值均衡算法
@Service("weightedRoundRoBinLoadBalance")
public final class WeightedRoundRoBinLoadBalance extends AbstractLoadBalance {
protected final Logger logger = LoggerFactory
.getLogger(WeightedRoundRoBinLoadBalance.class);
@Resource(name="dynamicUploadRule")
private transient DynamicUploadRule dynamicUploadRule;
// 所有服務器負載因子的最大公約數
private int gcd;
// 負載因子的最大值
private int max;
// 輪詢週期
private int cycle;
// 當前使用的輪詢的索引值
private int currentIndex = -1;
/**
* 一個輪詢週期的服務集合,長度爲負載因子除以最大公約數相加確定,可重複,AtomicReference保證原子操作
*/
private AtomicReference<list<string>> WRRList = new AtomicReference<list<string>>();
@Override
public void init() {
super.init();
buildWRRList();
}
@Override
public ServiceInstance chooseServerInstance() {
if (!isNotEmpty(WRRList.get())) {
logger.info("還未建立起權值輪詢服務集合,採用隨機算法返回可利用的服務");
return super.chooseServerInstance();
}
ServiceInstance serviceInstance = null;
synchronized (this) {
int index = 0;
while (index < cycle && null == serviceInstance) {
currentIndex = (currentIndex + 1) % WRRList.get().size();
String serviceName = WRRList.get().get(currentIndex);
serviceInstance = getServiceInstanceByServiceName(serviceName);
if (null == serviceInstance || serviceInstance.isIsolated()) {
index++;
}
}
}
return serviceInstance;
}
/**
* 初始化可利用的輪詢週期內的服務集合
*/
private void buildWRRList() {
boolean isGetSucc = false;
if (!getServiceInstanceList().isEmpty()) {
logger.info("獲取的服務列表不爲空,開始初始化各個服務器對應的負載因子");
isGetSucc = calcLoadFactors();
}
if (isGetSucc) {
// 生成輪詢的server集合
int total = getServiceInstanceList().size();
// 上一次服務庫索引
int i = -1;
// 上一次權值
int cw = 0;
List<string> newWrrList = new ArrayList<string>(total);
// 下面的算法,第一次分配時,把當前權重設置爲最大權重,服務器中權重大於等於當前權重的,都會分配負載;
// 第一輪分配完後,當前權重減最大公約數,進行第二輪分配;
// 如此循環到當前權重爲負,則把當前權重重置爲最大權重,重新進行循環。一直到輪迴週期
for (int j = 0; j < cycle; j++) {
while (true) {
i = (i + 1) % total;// 服務器下標
if (i == 0) {
cw = cw - gcd;// 獲得處理的權重
if (cw <= 0) {
cw = max;
// 如果沒有需要分配的服務
if (cw == 0) {
newWrrList.add(null);
break;
}
}
}
ServiceInstance serviceInstance = getServiceInstanceList()
.get(i);
String serverName = serviceInstance.getServerName();
// 如果被輪詢到的server權值滿足要求記錄servername.
if(serviceInstance.getQzValue() >= cw){
newWrrList.add(serverName);
break;
}
}
}
WRRList.set(newWrrList);
}
}
/**
* 初始化最大公約數,最大權值,輪詢週期
* @return
*/
private boolean calcLoadFactors() {
// 獲取所有服務器列表,根據列表返回一開始設置的各服務器負載因子(這裏可以是動態文件,可以是查詢DB等方式)
/**
* 1:獲取所有的服務名 2:根據服務名獲取不同的服務設置的負載因子
*/
// 獲取所有服務的負載因子
List<integer> factors = getDefault();
if (null == factors || factors.size() == 0) {
return false;
}
// 計算最大公約數 eg:10,20的最大公約數是10
gcd = calcMaxGCD(factors);
max = calcMaxValue(factors);
cycle = calcCycle(factors, gcd);
return true;
}
/**
* 計算輪迴週期,每個因子/最大公約數之後相加
* eg:100/100 + 200/100 + 300/100 = 6
* @param factors 存儲所有的負載因子
* @param gcd 最大公約數
* @return
*/
private int calcCycle(List<integer> factors, int gcd) {
int cycle = 0;
for (int i = 0; i < factors.size(); i++) {
cycle += factors.get(i) / gcd;
}
return cycle;
}
/**
* 計算負載因子最大值
*
* @param factors
* @return
*/
private int calcMaxValue(List<integer> factors) {
int max = 0;
for (int i = 0; i < factors.size(); i++) {
if (factors.get(i) > max) {
max = factors.get(i);
}
}
return max;
}
/**
* 計算最大公約數
* @param factors
* @return
*/
private int calcMaxGCD(List<integer> factors) {
int max = 0;
for (int i = 0; i < factors.size(); i++) {
if (factors.get(i) > 0) {
max = divisor(factors.get(i), max);
}
}
return max;
}
/**
* 使用輾轉相減法計算
* @param m 第一次參數
* @param n 第二個參數
* @return int最大公約數
*/
private int divisor(int m, int n) {
if (m < n) {
int temp;
temp = m;
m = n;
n = temp;
}
if (0 == n) {
return m;
}
return divisor(m - n, n);
}
/**
* 獲取默認的負載因子
* @param serviceNames 服務名
* @return
*/
private List<integer> getDefault() {
List<integer> list = new ArrayList<integer>();
List<serviceinstance> instances = dynamicUploadRule.getServiceInstanceRule();
for (final ServiceInstance serviceInstance : instances) {
list.add(serviceInstance.getQzValue());
}
return list;
}
/**
* 判斷非空
* @param list
* @return
*/
private boolean isNotEmpty(List<string> list) {
return null != list && list.size() > 0;
}
}
加權輪詢測試類
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import cn.fzjh.loadBalance.rule.WeightedRoundRoBinLoadBalance;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext.xml" })
public class TestWeightRoundRoBinLoadBalance {
@Autowired
private transient WeightedRoundRoBinLoadBalance weightedRoundRoBinLoadBalance;
@Test
public void testWeightedRoundRoBinLoadBalance(){
ExecutorService executorService = Executors.newFixedThreadPool(18);
for(int i=1;i<=18;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("當前線程: " + Thread.currentThread().getName() + "選擇服務:" + weightedRoundRoBinLoadBalance.chooseServerInstance().getServerName());
}
});
}
}
}
///////////////////////////////////////效果/////////////////////////////////////////
當前線程: pool-1-thread-1選擇服務:127.0.0.2
當前線程: pool-1-thread-2選擇服務:127.0.0.2
當前線程: pool-1-thread-3選擇服務:127.0.0.3
當前線程: pool-1-thread-4選擇服務:127.0.0.1
當前線程: pool-1-thread-5選擇服務:127.0.0.2
當前線程: pool-1-thread-6選擇服務:127.0.0.3
當前線程: pool-1-thread-7選擇服務:127.0.0.2
當前線程: pool-1-thread-8選擇服務:127.0.0.2
當前線程: pool-1-thread-9選擇服務:127.0.0.3
當前線程: pool-1-thread-10選擇服務:127.0.0.1
當前線程: pool-1-thread-12選擇服務:127.0.0.2
當前線程: pool-1-thread-14選擇服務:127.0.0.3
當前線程: pool-1-thread-16選擇服務:127.0.0.2
當前線程: pool-1-thread-13選擇服務:127.0.0.2
當前線程: pool-1-thread-18選擇服務:127.0.0.3
當前線程: pool-1-thread-17選擇服務:127.0.0.1
當前線程: pool-1-thread-11選擇服務:127.0.0.2
當前線程: pool-1-thread-15選擇服務:127.0.0.3
考慮不完善的地方歡迎大家多交流,共同學習進步!