一、负载均衡的概念
在大型的分布式架构中,对于负载较高的服务来说,往往对应着多台机器组成的集群,当请求到来的时候,为了将请求均衡的分配给后端服务器需要有相应的负载均衡算法来支持,通过相应的负载均衡算法选取一台机器来处理客户端请求,这个过程称为服务的负载均衡。
二、常用负载均衡算法实现
常用的负载均衡算法主要有随机法,轮询法,加权随机,加权轮询,最小连接数,一致性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
考虑不完善的地方欢迎大家多交流,共同学习进步!
3万+



被折叠的 条评论
为什么被折叠?



