目录
Ribbon
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。
Load Blancer (LB)
Ribbon客户端组件提供一系列的配置项如连接超时、重试等。简单的说,就是在配置文件中列出Load Blancer(简称LB) 后面的所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询/随机连接等)去连接这些机器。
我们很容易使用Ribbon实现自定义的负载均衡算法。Ribbon目前也进入维护,基本上不准备更新了 。
@LoadBalanced 我们之前学习Eureka 开启这个注解会默认自定义负载均衡
LB负载均衡(Load Balance)是什么 ?
简单的说就是将用户的请求平摊的分配到多个服务商,从而达到系统的HA(高可用)。
常见的负载均衡有软件Nginx、LVS,硬件F5等。
进程内LB
本地负载均衡
将LB逻辑集成到消费方,消费方从服务注册中心获取有哪些地址(注册中心服务器)可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址(本地jvm进程进行负载均衡)
集中式LB
服务端负载均衡
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方
区别
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡,客户端所有请求都会交给Nginx,然后由Nginx实现转发请求,即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
Ribbon就是 负载均衡 + RestTemplate
总结:Ribbon其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。 |
Ribbon工作步骤
在工作时分成两步
第一步先选择EurekaServer,它优先选择在同一个区域内负载较少的server.
第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。
Ribbon 示例
1.pom配置文件
默认我们使用eureka的新版本时,它默认集成了ribbon:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
这个starter中集成了ribbon了,我们也可以手动引入ribbon。放到order模块中,因为只有order访问pay时需要负载均衡
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
2. controller
//返回对象为响应体中数据转化成的对象,基本上可以理解为Json
@GetMapping("/consumer/payment/getId/{id}")
public CommonResult<Payment> getId(@PathVariable("id") String id) {
return restTemplate.getForObject(PAYMENT_URL + "/payment/getPaymentByid/" + id, CommonResult.class);
}
@GetMapping("/consumer/payment/getEntity/{id}")
public CommonResult<Payment> getId1(@PathVariable("id") String id) {
//返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/getPaymentByid/" + id, CommonResult.class);
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();//这个ResponseEntity中有判断,这里是判断,状态码是不是2xx,
} else {
return new CommonResult<>(444, "操作失败");
}
}
RestTemplate的: xxxForObject()方法,返回的是响应体中的数据 xxxForEntity()方法.返回的是entity对象,这个对象不仅仅包含响应体数据,还包含响应体信息(状态码等)
执行: http://localhost/consumer/payment/getEntity/31
负载均衡分发到支付模块8001与8002机器上面
Ribbon常用负载均衡算法
IRule接口,Riboon使用该接口,根据特定算法从所有服务中,选择一个服务,IRule接口有7个实现类,每个实现类代表一个负载均衡算法
-
RandomRule:随机
该策略实现了从服务实例清单中随机选择一个服务实例的功能。
从下面的源码可以看到,该实现类的choose方法传入了一个负载均衡器,并且使用负载均衡器获取对应的可用服务列表和全部服务列表,并通过chooseRandomInt方法获取一个随机数,该随机数作为可用服务列表的索引来获取具体的实例。
这里有个问题,选择服务实例时使用的是while获取,正常情况下,每次选择都应该能选择一个实例进行返回,但是如果出现异常导致每一次都获取不到可用的实例,那么如果出现死循环而获取不到服务实例时,则很有可能存在并发的BUG
/**
* 从负载均衡器中选择一个服务器
*
* @param lb 负载均衡器对象
* @param key 选择服务器的键
* @return 选定的服务器,如果没有可用服务器则返回null
*/
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers(); //获取可达的服务器列表
List<Server> allList = lb.getAllServers(); //获取所有的服务器列表
int serverCount = allList.size();
if (serverCount == 0) {
/*
* 如果没有可用的服务器,直接返回null。
*/
return null;
}
int index = chooseRandomInt(serverCount); //随机选择一个服务器
server = upList.get(index);
if (server == null) {
/*
*如果选定的服务器为空,说明服务器列表可能被修剪,这是一个短暂的情况,通过让出线程后重试
*/
Thread.yield();
continue;
}
if (server.isAlive()) {
return server;
}
// 实际上不应该发生,但必须是短暂的或者是一个bug。
server = null;
Thread.yield();
}
return server;
}
-
RoundRobinRule:轮询
该策略实现了按照轮询的方式依次选择每个服务实例的功能。该实现和上述的RandomRule类似,只是获取逻辑不同,该负载均衡策略实现逻辑是直接获取下一个可用实例,如果超过10次没有获取到可用的实例,则返回空且打印异常信息
/**
* 从负载均衡器中选择一个服务器
*
* @param lb 负载均衡器对象
* @param key 选择服务器的键
* @return 选定的服务器,如果没有可用服务器则返回null
*/
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) { //遍历所有服务器
List<Server> reachableServers = lb.getReachableServers(); //可达
List<Server> allServers = lb.getAllServers(); //所有
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount); //CAS自增获取下一个服务器索引
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) { //从负载均衡器中尝试10次后没有可用的活动服务器warn
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
-
AvailabilityFilteringRule
该策略实现了轮询获取Server并校验Server状态的功能。会先过滤掉由于多次访问故障而处于断路器状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问
public class AvailabilityFilteringRule extends PredicateBasedRule {
private AbstractServerPredicate predicate;
public AvailabilityFilteringRule() {
super();
predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, null))
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
.build();
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
predicate = CompositePredicate.withPredicate(new AvailabilityPredicate(this, clientConfig))
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
.build();
}
@Override
public Server choose(Object key) {
int count = 0;
Server server = roundRobinRule.choose(key);
while (count++ <= 10) { //轮询10次满足就返回
if (predicate.apply(new PredicateKey(server))) {
return server;
}
server = roundRobinRule.choose(key);
}
// 10次不行走父类的server
return super.choose(key);
}
@Override
public AbstractServerPredicate getPredicate() {
return predicate;
}
}
AvailabilityFilteringRule继承自PredicateBasedRule,从其choose方法可见,其并没有完全使用父类的实现方式。
- 先轮询获取一个Server
- 判断该Server是否满足需要
- 如果满足,直接返回
- 如果不满足,就继续获取下一个Server
- 如果一直轮询10次还没有符合要求的Server
- 那么再使用父类的实现方式
(先获取所有满足需求的Server列表,然后从该Server列表中轮询获取一个Server对象)
如果10次没有返回就走super.choose(key)
PredicateBasedRule继承自ClientConfigEnabledRoundRobinRule,是一个抽象类,它首先使用getPredicate方法获取一个AbstractServerPredicate的实现。而choose方法则是调用AbstractServerPredicate类的chooseRoundRobinAfterFiltering方法获取对应的Server实例并返回
。。。。。。其他等等不用管到最后父类的实现到apply方法,从下述源码可见,apply方法主要是通过shouldSkipServer方法进行判断的,在该方法中,有两个判断维度:是否故障(断路器是否断开)、实例的并发请求数是否大于阈值(int的最大值)
private static final DynamicBooleanProperty CIRCUIT_BREAKER_FILTERING =
DynamicPropertyFactory.getInstance().getBooleanProperty("niws.loadbalancer.availabilityFilteringRule.filterCircuitTripped", true);
private static final DynamicIntProperty ACTIVE_CONNECTIONS_LIMIT =
DynamicPropertyFactory.getInstance().getIntProperty("niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit", Integer.MAX_VALUE);
private ChainedDynamicProperty.IntProperty activeConnectionsLimit = new ChainedDynamicProperty.IntProperty(ACTIVE_CONNECTIONS_LIMIT);
//判断是否应用谓词过滤器到特定的服务器
@Override
public boolean apply(@Nullable PredicateKey input) {
LoadBalancerStats stats = getLBStats();
if (stats == null) { //获取负载均衡器的统计信息stats,如果统计信息为null,则返回true
return true;
}
return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
}
//判断是否应该跳过该服务器
private boolean shouldSkipServer(ServerStats stats) {
//如果CIRCUIT_BREAKER_FILTERING为true且服务器的断路器已触发,或者服务器的活动请求数超过了activeConnectionsLimit的限制,则返回true,表示应该跳过该服务器
if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped())
|| stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
return true;
}
return false;
}
-
WeightedResponseTimeRule
该策略继承自RoundRobinRule,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,以达到更优的分配效果,其核心内容分为三块:定时任务、权重计算、实例选择。
根据平均响应时间计算所有服务的权重,响应时间越快的服务权重越大被选中的概率越大。刚启动时如果统计信息不足,则使用RoundRobinRule(轮询)策略,等统计信息足够,会切换到WeightedResponseTimeRule
定时任务
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
if (lb instanceof BaseLoadBalancer) {
name = ((BaseLoadBalancer) lb).getName();
}
initialize(lb);
}
// 默认的定时器间隔为30秒
public static final int DEFAULT_TIMER_INTERVAL = 30 * 1000;
// 存储服务器权重任务的定时器间隔
private int serverWeightTaskTimerInterval = DEFAULT_TIMER_INTERVAL;
void initialize(ILoadBalancer lb) {
if (serverWeightTimer != null) {
// 如果serverWeightTimer定时器存在则取消之前的定时器
serverWeightTimer.cancel();
}
// 创建一个新的定时器serverWeightTimer
serverWeightTimer = new Timer("NFLoadBalancer-serverWeightTimer-"
+ name, true);
// 使用DynamicServerWeightTask任务以serverWeightTaskTimerInterval间隔执行更新服务器权重的操作
serverWeightTimer.schedule(new DynamicServerWeightTask(), 0,
serverWeightTaskTimerInterval);
// do a initial run 执行一次ServerWeight对象的maintainWeights方法,用于初始化服务器权重信息。
ServerWeight sw = new ServerWeight();
sw.maintainWeights();
// 在系统关闭时会执行这段代码,会输出日志信息并取消serverWeightTimer定时任务
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
logger
.info("Stopping NFLoadBalancer-serverWeightTimer-"
+ name);
serverWeightTimer.cancel();
}
}));
}
class DynamicServerWeightTask extends TimerTask {
public void run() {
ServerWeight serverWeight = new ServerWeight();
try {
serverWeight.maintainWeights();
} catch (Exception e) {
logger.error("Error running DynamicServerWeightTask for {}", name, e);
}
}
}
从上述源码可见,在设置负载均衡策略对应的负载均衡器时,调用了initialize方法,而该方法创建了一个定时任务来计算权重(最终调用的serverWeight.maintainWeights()方法),每30秒执行一次。
权重计算
// 存储服务器的权重信息
private volatile List<Double> accumulatedWeights = new ArrayList<Double>();
class ServerWeight {
// 根据服务器的响应时间动态调整服务器的权重
public void maintainWeights() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) { // 负载均衡器实例lb
return;
}
// CAS 确保高并发情况 只有一个线程可以执行权重调整任务
if (!serverWeightAssignmentInProgress.compareAndSet(false, true)) {
return;
}
try {
logger.info("Weight adjusting job started");
AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;
LoadBalancerStats stats = nlb.getLoadBalancerStats();
if (stats == null) {
// 没有统计数据,无需操作
return;
}
double totalResponseTime = 0;
// find maximal 95% response time
for (Server server : nlb.getAllServers()) {
// 如果缓存中没有数据,会自动加载统计数据
ServerStats ss = stats.getSingleServerStat(server);
totalResponseTime += ss.getResponseTimeAvg();
}
// weight for each server is (sum of responseTime of all servers - responseTime)
// 每个服务器的权重为(所有服务器响应时间之和 - 该服务器平均响应时间)
// so that the longer the response time, the less the weight and the less likely to be chosen
// so,响应时间越长,权重越小,被选择的可能性也越小
Double weightSoFar = 0.0;
// create new list and hot swap the reference
List<Double> finalWeights = new ArrayList<Double>();
for (Server server : nlb.getAllServers()) {
ServerStats ss = stats.getSingleServerStat(server);
// 所有服务器响应时间之和 - 该服务器平均响应时间
double weight = totalResponseTime - ss.getResponseTimeAvg();
weightSoFar += weight;
finalWeights.add(weightSoFar);
}
setWeights(finalWeights);
} catch (Exception e) {
logger.error("Error calculating server weights", e);
} finally {
serverWeightAssignmentInProgress.set(false);
}
}
}
void setWeights(List<Double> weights) {
this.accumulatedWeights = weights;
}
通过源码可见,代码中维护一个用于存储权重的List集合accumulatedWeights,同时,通过maintainWeights方法做了权重计算,该计算主要分为两步,
- 第一步,根据LoadBalancerStatus中记录的每个实例的统计信息,累加所有实例的平均响应时间,得到总的响应时间totalResponseTime
- 第二步,为负载均衡器中维护的实例清单逐个计算权重(从第一个开始),计算规则为weightSoFar+totalResponseTime-实例的平均响应时间,其中weightSoFar的初始值为0。
举个例子,如果有ABCD4个实例,他们的平均响应时间是10、20、30、40,那么总的相应时间就是100,那么计算出4个实例的权重分别为:
A:100 - 10 = 90
B:90 +(100-20) = 170
C:170 +(100-30) = 240
D:240 +(100-40) = 300
权重区间是左开右闭,但是第一个和最后一个比较特殊,由于在后续选择实例时会用随机数从区间中获取,但是随机数最小值可以是0,但是不会到达随机数的最大值,因此第一个左边的0是闭区间,而最后一个的右侧是开区间,因此这4个实例对应的权重区间即为:
A:[0,90]
B:(90,170]
C:(170,240]
D:(240,300)
不难发现,区间的宽度就是总的平均响应时间-实例的平均响应时间,因此实例的平均响应时间越短,那么权重的区间就越大,那么被选中的几率就越大。
实例选择
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
// 获取当前权重列表的引用,以防它被其他线程更改
List<Double> currentWeights = accumulatedWeights;
if (Thread.interrupted()) {
return null;
}
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int serverIndex = 0;
// 最后一个是所有权重的总和
double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1);
// 没有服务器被选中且总权重未初始化
// 回退到使用轮询
if (maxTotalWeight < 0.001d || serverCount != currentWeights.size()) {
server = super.choose(getLoadBalancer(), key);
if(server == null) {
return server;
}
} else {
//生成一个介于0(包括)和maxTotalWeight(不包括)之间的随机权重
double randomWeight = random.nextDouble() * maxTotalWeight;
// 根据随机权重选择服务器索引
int n = 0;
for (Double d : currentWeights) {
if (d >= randomWeight) {
serverIndex = n;
break;
} else {
n++;
}
}
server = allList.get(serverIndex);
}
if (server == null) {
/* 临时情况. */
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// 下一个
server = null;
}
return server;
}
通过上述源码可见,其首先生成了一个 [0,最大权重值) 区间内的随机数,然后循环权重区间,如果该随机数在权限区间内,则就拿当前权重列表的索引去服务实例获取对应的服务。还是以上面的ABCD四个实例来说明,那么随机数就是从 [0,300) 的区间中获取,如果获取的随机数数230, currentWeights.size() = 4
for (Double d : currentWeights) {
if (d >= randomWeight) {
serverIndex = n;
break;
} else {
n++;
}
}
240 > 230,n =4,满足条件 那么该随机数在实例D的权重区间内,因此就会选择D实例
-
RetryRule
该策略实现了一个具备重试机制的实力选择功能。重下述源码可以看出,其选择服务实例使用的是轮询选择策略RoundRobinRule,然后在获取不到服务实例的情况下,则反复尝试获取,直到调用时间超过设置的阈值,则返回空。
IRule subRule = new RoundRobinRule(); // 创建一个轮询规则的实例,用于确定服务器选择的顺序
long maxRetryMillis = 500; //设置最大重试时间为500毫
/**
* 从负载均衡器中选择一个服务器
*
* @param lb 负载均衡器对象
* @param key 选择服务器的键
* @return 选定的服务器,如果没有可用服务器或超过重试时间则返回null
*/
public Server choose(ILoadBalancer lb, Object key) {
// 获取当前时间和最大重试时间的截止时间。
long requestTime = System.currentTimeMillis();
long deadline = requestTime + maxRetryMillis;
Server answer = null;
// 通过轮询规则选择一个服务器作为初始服务
answer = subRule.choose(key);
// 如果初始服务为null或者服务器不可用,并且当前时间未超过重试截止时间,则进行重试
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
InterruptTask task = new InterruptTask(deadline
- System.currentTimeMillis());
while (!Thread.interrupted()) { // 不断重试, 直到遇到可用的服务器或者超过重试截止时间
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
/* pause and retry hoping it's transient */
Thread.yield();
} else {
break;
}
}
task.cancel();
}
// 如果选择的服务器仍然为null或者不可用,则返回null;否则返回选定的服务器
if ((answer == null) || (!answer.isAlive())) {
return null;
} else {
return answer;
}
}
-
BestAvailableRule
该策略会选出负载最低的实例。
BestAvailableRule继承自ClientConfigEnabledRoundRobinRule,从choose方法看,会循环所有Server实例,过滤掉故障实例并选出负载最低的Server。同时我们可以发现,如果没有选择到Server的话,就会调用父类的choose方法,那么就会使用到上面说的 “通过继承该类,在子类中做一些其他的策略时,如果条件不满足,则会使用父类的策略 (轮询)” 。
public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule {
// 它继承了轮询规则的基本功能,可以根据负载均衡器中服务器的轮询顺序选择服务器
private LoadBalancerStats loadBalancerStats; //用于存储负载均衡器的统计信息
@Override
public Server choose(Object key) {
if (loadBalancerStats == null) {
return super.choose(key); //如果为空,则调用父类的选择方法
}
List<Server> serverList = getLoadBalancer().getAllServers();
int minimalConcurrentConnections = Integer.MAX_VALUE;
long currentTime = System.currentTimeMillis();
Server chosen = null;
for (Server server: serverList) {
ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
// 遍历所有服务器,找到当前并发连接数最小的可用服务器作为选择的服务器
if (!serverStats.isCircuitBreakerTripped(currentTime)) {
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
if (concurrentConnections < minimalConcurrentConnections) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) { //没有可用服务器(所有服务器的断路器均处于断开状态),则调用父类的选择方法
return super.choose(key);
} else {
return chosen;
}
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
if (lb instanceof AbstractLoadBalancer) {
loadBalancerStats = ((AbstractLoadBalancer) lb).getLoadBalancerStats();
}
}
}
-
ZoneAvoidanceRule
复合判断Server所在区域的性能和Server的可用性选择服务器;
ZoneAvoidanceRule同样继承自PredicateBasedRule,同时ZoneAvoidanceRule中没有choose方法,说明完全复用了父类中的策略(先过滤所有可用的实例,然后使用轮询从满足需要的实例清单中获取一个Server)。
同时通过ZoneAvoidanceRule的构造函数可见,使用的是CompositePredicate进行的过滤,CompositePredicate的构造函数传入了两个AbstractServerPredicate的子类,分别是主过滤条件ZoneAvoidancePredicate (主过滤条件ZoneAvoidancePredicate
可能用于根据服务器所在的区域或可用区来进行过滤和选择)和次过滤条件AvailabilityPredicate(次过滤条件AvailabilityPredicate
可能用于根据服务器的可用性状态或其他条件来进一步筛选服务器)
public ZoneAvoidanceRule() {
super();
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this);
compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}
private CompositePredicate createCompositePredicate(ZoneAvoidancePredicate p1, AvailabilityPredicate p2) {
return CompositePredicate.withPredicates(p1, p2)
.addFallbackPredicate(p2)
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
.build();
}
Ribbon使用
这里使用eureka的那一套服务。官方文档明确给出了警告:
这个自定义配置不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的。也就是不能放在主启动类所在的包及子包下
1. 修改order模块
2. 额外创建一个包
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = Myrule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
3. 创建配置类
指定负载均衡算法
@Configuration
public class Myrule {
@Bean
public IRule myRule(){
return new RandomRule();//定义为随机
}
}
4. 主启动类
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = Myrule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
访问CLOUD_PAYMENT_SERVICE的服务时,使用我们自定义的负载均衡算法
自定义的负载均衡算法
负载均衡算法 :
rest接口请求次数(reqNum) % 服务器集群数量(serverNum) = 实际调用服务器位置下标(index)
每次服务重启后,rest接口请求次数从1开始计数。
算法原理
例如:通过服务名称,可以获取到总服务实例个数为2个:8001与8002
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
例如:服务器下标(index)为0,请求由8001服务器处理;
服务器下标(index)为1,请求由8002服务器处理;
第1次请求,reqNum(1) % serverNum(2) = index(1),调用8001
第2次请求,reqNum(2) % serverNum(2) = index(0),调用8002
第3次请求,reqNum(3) % serverNum(2) = index(1),调用8001
第4次请求,reqNum(4) % serverNum(2) = index(0),调用8002
…
代码示例
演示ribbon的随机负载均衡
1. 添加方法
给pay模块(8001,8002),的controller方法添加一个方法,返回当前节点端口
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/lb")
public String getPayment(){
return serverPort;
}
2. 修改order模块
去掉@LoadBalanced
@Configuration
public class RestTemlateConfig {
@Bean
//@LoadBalanced //注释掉
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3. 自定义接口
public interface LoadBalance {
ServiceInstance instance(List<ServiceInstance> serviceInstances);
}
4. 接口实现类
@Component
public class MyLoadBalanceImpl implements LoadBalance {
private AtomicInteger atomicInteger = new AtomicInteger(0);
//这个方法是获取下一个要调用服务的id
public final int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1;
} while (!this.atomicInteger.compareAndSet(current, next));//调用CAS进行自旋锁,每次next+1
System.out.println("*******第几次访问,next:" + next);
return next;
}
@Override
public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
int size = serviceInstances.size();
int index = 0;
if (size > 0) {
//getAndIncrement() 拿到id,进行取余得到真正要调用服务的下标
index = getAndIncrement() % size;
}
return serviceInstances.get(index);
}
5. 修改controller
@Resource
private DiscoveryClient discoveryClient;
@Resource
private LoadBalance loadBalance;//自定义类
@GetMapping("/consumer/payment/lb")
public String getPaymentLB() {
//拿到指定服务下的所有服务
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null || instances.size() < 0) {
return null;
}
ServiceInstance instance = loadBalance.instance(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}