(三)使用Ribbon实现客户端侧负载均衡
1.Ribbon 简介
Ribbon 是Netflix 发布的基于HTTP和TCP的客户端负载均衡器,将我们服务间的REST模板请求进行拦截封装,转发到合适的服务提供者实例上。
1.1 Ribbon + Eureka 架构
Spring Cloud 中,Ribbon与Eureka 配合使用时,Ribbon可自动通过Eureka Client获取服务提供者的地址列表,并基于某种负载均衡算法,请求其中一个服务提供实例。
Ribbon + Eureka 负载均衡架构如下图所示:

1.2 Ribbon 负载均衡器组件架构

Rebbion 负载均衡器组件介绍:
- ServerList:服务提供者实例列表,当和Eureka结合时,可以通过Eureka Client动态获取可用的服务实例列表,也可以配置静态的服务列表。
- ServerListFilter:服务过滤器,通过该过滤器将会从serverList中过滤掉不符合规则的服务。
- IPing:心跳检查,定时监测ServerList服务列表中服务状态。
- IRule:均衡策略,将最终的ServerList按照某种负载均衡策略选择要使用的服务实例。
通过上面的组件架构流程我们可以为服务的每个请求选择一个可用的服务DiscoveryEnabledServer。
Spring Cloud为每个服务提供者的Rebbion 负载均衡器 维护了一个子应用上下文,我们可以为不同服务提供者配置不同的均衡策略,当然也可以配置全局的均衡策略(详见:配置项)。
2 深入剖析Ribbon
前面介绍了Ribbon是将我们服务间的REST请求通过封装转发,来实现负载均衡,那么具体是怎么实现的?
2.1 Ribbon实现REST请求负载均衡逻辑
当服务启动时,会为我们应用中每个有@LoadBalanced 的RestTemplate实例,注入一个Ribbon负载均衡器的拦截器(LoadBalancerInterceptor ),当服务在向某个服务提供者发起首次请求时,会初始化该服务提供者负载均衡器,这个加载过程也可以通过配置在服务启动时被加载完成(详见:《饥饿加载》章节)

2.2 LoadBalancerInterceptor 的实现

LoadBalancerClient:负载均衡器客户端,负载均衡入口,下一章节将详解
LoadBalancerRequestFactory:负载均衡的请求创建工厂
在集成了Ribbon负载均衡之后不可能在使用IP:PORT 这样的方式去发起请求,而是将IP:PORT换成了每个服务提供者的ServerName。
2.3 LoadBalancerClient的实现
Spring Cloud 集成了Ribbion,使用RibbonLoadBalancerClient 实现了LoadBalancerClient 以下主要接口:
1.execute(String serviceId, LoadBalancerRequest
主要流程:

代码实现:

2.URI reconstructURI(ServiceInstance instance, URI original);

前面在拦截器中介绍到,在集成了Ribbon负载均衡之后不可能在使用IP:PORT 这样的方式去发起请求,而是将IP:PORT换成了每个服务提供者的ServerName,但是最终还是转成IP:PORT发起REST请求,reconstructURI实现了从RibbonServer服务实例向URI的转化。
2.4ILoadBalancer负载均衡器的实现(结合Eureka)

接口组件:
-
ServerList:服务实例列表,当Ribbon与Eureka联合使用时,ServerList会被DiscoveryEnabledNIWSServerList重写扩展成从Eureka注册中心中获取服务实例列表,并注册。当Eureka Client定时从Eureka注册中心获取服务后会触发DynamicServerListLoadBalancer 更新事件更新ServerList。
1 )初始化Serverlist和启动监听Eureker Client发送获取服务(Get Register)的请求监听.

2 )注册监听:EurekaNotificationServerListUpdater.start(final UpdateAction updateAction)
if (eurekaClient == null) {
eurekaClient = eurekaClientProvider.get();
}
if (eurekaClient != null) {
eurekaClient.registerEventListener(updateListener);
}
- 更新事件:com.netflix.loadbalancer.DynamicServerListLoadBalancer.updateListOfServers()
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
-
ServerListFilter:服务过滤器,通过该过滤器将会从serverList中过滤掉不符合规则的服务。
org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter.getFilteredListOfServers()
@Override public List<Server> getFilteredListOfServers(List<Server> servers) { List<Server> output = super.getFilteredListOfServers(servers); if (this.zone != null && output.size() == servers.size()) { List<Server> local = new ArrayList<Server>(); for (Server server : output) { if (this.zone.equalsIgnoreCase(server.getZone())) { local.add(server); } } if (!local.isEmpty()) { return local; } } return output; } -
IPing:检测ServerList中的是否可用,当Ribbon与Eureka联合使用时,NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来确定服务端是否已经启动。
1)启动时开始定时任务(默认10s):
BaseLoadBalancer.setupPingTask()
void setupPingTask() { if (canSkipPing()) { return; } if (lbTimer != null) { lbTimer.cancel(); } lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true); lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000); forceQuickPing(); }2)检测NIWSDiscoveryPing.isAlive():
public boolean isAlive(Server server) { boolean isAlive = true; if (server!=null && server instanceof DiscoveryEnabledServer){ DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server; InstanceInfo instanceInfo = dServer.getInstanceInfo(); if (instanceInfo!=null){ InstanceStatus status = instanceInfo.getStatus(); if (status!=null){ isAlive = status.equals(InstanceStatus.UP); } } } return isAlive; } -
IRule:均衡策略,将最终的ServerList按照一定的策略选择最终要使用的服务实例。
2.5 IRule负载均衡策略
-
随机策略RandomRule
从ServerList中随机选择一个Server实例。
关键代码:
int index = rand.nextInt(serverCount); server = upList.get(index); -
轮询策略RoundRobinRule
轮询Serverlist选择下个Server实例
关键代码:
int nextServerIndex = incrementAndGetModulo(serverCount); server = allServers.get(nextServerIndex);incrementAndGetModulo():
private int incrementAndGetModulo(int modulo) { for (;;) { int current = nextServerCyclicCounter.get(); int next = (current + 1) % modulo; if (nextServerCyclicCounter.compareAndSet(current, next)) return next; } } -
权重策略WeightedResponseTimeRule
WeightedResponseTimeRule继承了RoundRobinRule,开始时没有权重列表,采用父类(RoundRobinRule)的轮询方式;启动一个定时任务(默认30s),定时任务会根据实例的响应时间来更新权重列表,choose方法中,用一个(0,1)的随机double数乘以最大的权重得到randomWeight,然后遍历权重列表,找出第一个比randomWeight大的实例下标,然后返回该实例。
具体实现,请参考类:com.netflix.loadbalancer.WeightedResponseTimeRule
-
请求数最少策略BestAvailableRule
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; } } -
AvailabilityFilteringRule
过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值),在使用RoundRobinRule 选择一个服务
-
ZoneAvoidanceRule(Ribbon集合Eureka时,默认IRule)
使用ZoneAvoidancePredicate过滤掉不可用的zone下的所有Server实例,再使用AvailabilityFiltering过滤掉过滤掉那些高并发的的后端server(active connections 超过配置的阈值),在轮询选择一个服务实例。
3 Ribbon实战:为服务消费者整合Ribbon
3.1需求场景
学生查询已下单股票列表时,需要去股票服务中获取股票详情,补全股票信息。

3.2编写一个消费者
1.创建一个ArtifactId是finace-training-student的Maven工程,并为项目添加以下依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-retry</artifactId>
<groupId>org.springframework.retry</groupId>
</exclusion>
</exclusions>
</dependency>
2.在配置文件application.yml中添加如下内容。
server:
port: 9000
spring:
application:
name: finace-training-student
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
instance:
prefer-ip-address: true
3.编写启动类,在启动类上添加@EnableDiscoveryClient注解,声明这是一个Eureka Client,RestTemplate加上ribbon注解@LoadBalanced
package com.myhexin.finace.training.server.main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication(scanBasePackages = "com.myhexin")
@EnableDiscoveryClient
public class ServierApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ServierApplication.class, args);
}
}
4.编写消费者调用代码:
package com.myhexin.finace.training.server.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.myhexin.finace.training.api.stock.dto.StockDTO;
@RestController
@RequestMapping(value = "/student", produces = "application/json;charset=UTF-8")
public class StudentController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/{id}/ownStcoks")
public List<StockDTO> ownStcoks(@PathVariable String id) {
String[] ownStockCodes = { "300033", "000001" };
List<StockDTO> ownStocks = new ArrayList<>(ownStockCodes.length);
for (String code : ownStockCodes) {
// 补全股票信息
//StockDTO stock = restTemplate.getForObject(this.getInstance("finace-training-stock") + "/stock/" + code, StockDTO.class);
StockDTO stock = restTemplate.getForObject("http://finace-training-stock" + "/stock/" + code, StockDTO.class);
ownStocks.add(stock);
}
return ownStocks;
}
private String getInstance(String serviceId) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
if (instances.isEmpty()) {
return null;
}
return instances.get(0).getUri().toString();
}
}
4 其他
4.1 配置项
-
全局默认配置
Spring Cloud Ribbon自动化配置了默认接口配置:
IClientConfig:Ribbon的客户端配置,默认采用DefaultClientConfigImpl。
IRule:Ribbon的负载均衡策略,默认采用ZoneAvoidanceRule,该策略能够在多区域环境下选出最佳区域的实例进行访问。
IPing:Ribbon的实例检查策略,默认采用DummyPing实现,检查实例状态为UP,则返回true。
ServerList:服务实例清单的维护机制,默认采用 ConfigurationBasedServerList实现。
ServerListFilter:服务实例清单过滤机制,默认采用ZonePrefenceServerListFilter,优先过滤出与请求调用方处于同区域的服务实例。 -
使用属性自定义Ribbon配置
从Spring Cloud Netflix 1.2.0 (即从Spring Cloud Camden RELEASE开始),Ribbon支持使用属性自定义(即可定义在appplication.yml中)。
配置前缀
.ribbon.属性; 是RibbonClient的名称,如果省略则表示全部配置。 属性:
NFLoadBalancerClassName:配置ILoadBalancer的实现类
NFLoadBalancerPingClassName:配置IPing的实现类
NFLoadBalancerRuleClassName:配置IRule的实现类
NIWSServerListClassName:配置ServerList的实现类
NIWSServerListFilterClassName:配置ServerListFilter的实现类
例如:
finace-training-stock: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule将finace-training-stock的Ribbon Client的负载均衡策略改为随机策略。
例如:
ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule将所有的Ribbon Client的负载均衡策略改为随机策略。
-
使用java代码自定义Ribbon配置
4.2 重试机制
由于Spring Cloud Eureka实现的服务治理机制强调了CAP原理中的AP,为了实现更高的服务可用性,牺牲了一定的一致性,在极端情况下它宁愿接受故障实例也不要丢掉“健康”实例,比如,当服务注册中心的网络繁盛故障断开时,由于所有的服务实例无法维持持续心跳,在强调AP的服务治理中会把所有服务实例都踢出掉,而Eureka则会因为超过85%的实例丢失心跳二回触发保护机制,注册中心江湖保留此时的所有节点,以实现服务间依然可以进行互相调用的场景,即使其中有部分故障节点,但这样做可以继续保障大多数服务正常消费。
所以服务调用的时候通常会加入一些重试机制。从Camden SR2版本开始,Spring Cloud整合了Spring Retry来增强RestTemplate的重试能力,对于开发者来说只需通过简单的配置,原来那些通过RestTemplate实现的服务访问就会自动根据配置来实现重试策略。
重试机制只有在引入了RetryTemplate才会生效。
<dependency>
<artifactId>spring-retry</artifactId>
<groupId>org.springframework.retry</groupId>
</dependency>

重试机制属性配置策略:
spring.cloud.loadbalancer.retry.enabled:该参数用来开启重试机制,它默认是关闭的
hystrix.command.default.execution.isolation.thread.timeoutInMillseconds:断路器的超时时间需要大于Ribbon的超时时间,不然不会触发重试。
具体可参考:
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.RetryAutoConfiguration
4.3 饥饿加载
前面Spring Cloud为每个服务提供者的Rebbion 负载均衡器 维护了一个子应用上下文,通过代码也分析出来了,这个上下文默认是懒加载。只有在第一次请求时,对应的上下文才会被加载,因此,首次请求往往会比较慢,从Spring Cloud Dalston开始,我们可以配置饥饿加载。
例如:
ribbon:
eager-load:
enable: true
clients: finace-training-stock,finace-training-student
在启动的时候就会加载 finace-training-stock和finace-training-student的Ribbon Client对应的子应用上下文,从而提高第一次的访问速度。
5 参考文献
[1] Ribbon的GitHub : https://github.com/Netflix/ribbon
[2] 周立. Spring Cloud与Docker微服务架构实战
[3] Spring Cloud中文网.https://www.springcloud.cc/spring-cloud-dalston.html#spring-cloud-ribbon

浙公网安备 33010602011771号