Ribbon简介

什么是Ribbon?

Ribbon是springcloud下的客户端负载均衡器,消费者在通过服务别名调用服务时,需要通过Ribbon做负载均衡获取实际的服务调用地址,然后通过httpclient的方式进行本地RPC远程调用。

Ribbon原理

Ribbon负载均衡算法主要是轮询算法,分为以下几步:

  1. 根据服务别名,从eureka获取服务提供者的列表
  2. 将列表缓存到本地
  3. 根据具体策略获取服务提供者

Ribbon的核心是负载均衡管理,另还有5个大功能点。如下图:

源码分析

事前准备

  1. 先搭建一个SpringCloud的项目,也可以从我的github上下载。地址:https://github.com/mmcLine/spring-cloud-study

  2. 拷贝以下代码

@Configuration
public class RestTemplateConfiguration {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
 @Autowired
private RestTemplate restTemplate; @GetMapping("/testRibbon/{id}")
public User getTodayStatistic(@PathVariable("id") Integer id){
String url ="http://STUDY-USER/user/getUserById?id="+id;
return restTemplate.getForObject(url, User.class);
}

代码都准备好了,可以开始分析了。

  1. 执行调用

http://localhost:8005/trade/testRibbon/2

为什么这么就能调用到服务提供者的方法?

打断点,可以看到restTemplate里有两个拦截器,根据名字可以推断RetryLoadBalancerInterceptor是关键。

跟踪到RetryLoadBalancerInterceptor类

@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
//获取到service的name
final String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
//根据serviceName和LoadBalancerClient,LoadBalancedRetryPolicy里面包含了RibbonLoadBalancerContext和ServiceInstanceChooser
final LoadBalancedRetryPolicy retryPolicy = lbRetryFactory.createRetryPolicy(serviceName,
loadBalancer);
RetryTemplate template = createRetryTemplate(serviceName, request, retryPolicy);
//执行方法会进入到doExecute方法
return template.execute(context -> {
ServiceInstance serviceInstance = null;
if (context instanceof LoadBalancedRetryContext) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
serviceInstance = lbContext.getServiceInstance();
}
if (serviceInstance == null) {
serviceInstance = loadBalancer.choose(serviceName);
}
ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer.execute(
serviceName, serviceInstance,
requestFactory.createRequest(request, body, execution));
int statusCode = response.getRawStatusCode();
if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody());
response.close();
throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy);
}
return response;
}, new LoadBalancedRecoveryCallback<ClientHttpResponse, ClientHttpResponse>() {
//This is a special case, where both parameters to LoadBalancedRecoveryCallback are
//the same. In most cases they would be different.
@Override
protected ClientHttpResponse createResponse(ClientHttpResponse response, URI uri) {
return response;
}
});
}

doExecute方法:

protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,
RecoveryCallback<T> recoveryCallback, RetryState state)
throws E, ExhaustedRetryException {
//省略部分代码 /*
* We allow the whole loop to be skipped if the policy or context already
* forbid the first try. This is used in the case of external retry to allow a
* recovery in handleRetryExhausted without the callback processing (which
* would throw an exception).
*/
//执行逻辑的关键方法
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { }

继续跟踪canRetry方法

  @Override
public boolean canRetry(RetryContext context) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext)context;
if(lbContext.getRetryCount() == 0 && lbContext.getServiceInstance() == null) {
//We haven't even tried to make the request yet so return true so we do
//设置选中的服务提供者
lbContext.setServiceInstance(serviceInstanceChooser.choose(serviceName));
return true;
}
return policy.canRetryNextServer(lbContext);
}

我们跟踪serviceInstanceChooser.choose(serviceName)看看怎么通过serviceName选服务提供者的。

@Override
public ServiceInstance choose(String serviceId) {
//选择server
Server server = getServer(serviceId);
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
}

跟踪getServer方法

protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
//可以看出是loadBalancer在选择
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}

继续深入

 public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
//有一个调用次数在+1
counter.increment();
if (rule == null) {
return null;
} else {
try {
//委托给了IRule,所以Irule是负载均衡的关键,最后来总结
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}

查看Irule的实现

 public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
//lb.getAllServers里面是所有的服务提供者列表
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}

跟踪chooseRoundRobinAfterFiltering方法

public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
//拿到筛选后的servers
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
//incrementAndGetModulo方法拿到下标,然后根据list.get取到一个服务
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}

至此就拿到了具体的服务提供者。

但是到这里还有个问题?

  1. 怎么根据服务名拿到server的?

有一个ServerList接口是用于拿到服务列表的。我们使用的loadBalancer(ZoneAwareLoadBalancer)的父类DynamicServerListLoadBalancer类的构造方法里,有一个restOfinit方法

public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
ServerList<T> serverList, ServerListFilter<T> filter,
ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping);
this.serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if (filter instanceof AbstractServerListFilter) {
((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
}
restOfInit(clientConfig);
}

跟踪restOfInit方法

void restOfInit(IClientConfig clientConfig) {
boolean primeConnection = this.isEnablePrimingConnections();
// turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
this.setEnablePrimingConnections(false);
enableAndInitLearnNewServersFeature(); //用于获取所有的serverList
updateListOfServers();
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections()
.primeConnections(getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}

继续跟踪updateListOfServers方法

 public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
//查询serverList
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);
}

继续跟踪源码到obtainServersViaDiscovery方法,

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
//eurekaClientProvider.get()会去获取EurekaClient
if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
logger.warn("EurekaClient has not been initialized yet, returning an empty list");
return new ArrayList<DiscoveryEnabledServer>();
} EurekaClient eurekaClient = eurekaClientProvider.get();
//vipAddresses就是serviceName
if (vipAddresses!=null){
for (String vipAddress : vipAddresses.split(",")) {
// if targetRegion is null, it will be interpreted as the same region of client
//此处获取到服务的信息
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
for (InstanceInfo ii : listOfInstanceInfo) {
if (ii.getStatus().equals(InstanceStatus.UP)) { if(shouldUseOverridePort){
if(logger.isDebugEnabled()){
logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
} // copy is necessary since the InstanceInfo builder just uses the original reference,
// and we don't want to corrupt the global eureka copy of the object which may be
// used by other clients in our system
InstanceInfo copy = new InstanceInfo(ii); if(isSecure){
ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
}else{
ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
}
} DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);
des.setZone(DiscoveryClient.getZone(ii));
serverList.add(des);
}
}
if (serverList.size()>0 && prioritizeVipAddressBasedServers){
break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
}
}
}
return serverList;
}

综合上面可以看出,最终是通过eurekaClient去拿到服务列表的。

那么如果服务列表发生变化怎么刷新呢?

是通过CacheRefreshThread在定时线程池里面执行,核心拉取方法是fetchRegistry

Iping

Iping是用于探测服务列表中的服务是否正常,如果不正常,则从eureka拉取服务列表并更新。

在BaseLoadBalancer里面有一个setupPingTask方法,启动定时任务,30秒一次定时向EurekaClient发送“ping”

public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
IPing ping, IPingStrategy pingStrategy) { logger.debug("LoadBalancer [{}]: initialized", name); this.name = name;
this.ping = ping;
this.pingStrategy = pingStrategy;
setRule(rule);
setupPingTask();
lbStats = stats;
init();
}

Iping的具体逻辑在PingTask类里。

Irule总结:

Irule是负载均衡的规则:

我这里默认是使用的是ZoneAvoidanceRule,还有很多种策略:

  • RandomRule: 随机
  • RoundRobinRule: 轮询
  • RetryRule: 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
  • WeightedResponseTimeRule: 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
  • BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
  • ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器

properties配置方式如下:

STUDY-USER是服务名

STUDY-USER.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

超详细的Ribbon源码解析的更多相关文章

  1. 超详细的Eureka源码解析

    Eureka简介 Eureka是什么? Eureka是基于REST(Representational State Transfer)服务,主要以AWS云服务为支撑,提供服务发现并实现负载均衡和故障转移 ...

  2. 超详细Go语言源码目录说明

    开源项目「go home」聚焦Go语言技术栈与面试题,以协助Gopher登上更大的舞台,欢迎go home~ 导读 学习Go语言源码的第一步就是了解先了解它的目录结构,你对它的源码目录了解多少呢?今天 ...

  3. 详细的String源码解析

    我们常常把String类型的字符串作为HashMap的key,为什么要这样做呢? 因为String是不可变的,一旦初始化就不再改变了,如果被修改将会是一个新对象. @Test public void ...

  4. spring cloud ribbon源码解析(二)

    在上一篇文章中主要梳理了ribbon的执行过程,这篇主要讲讲ribbon的负载均衡,ribbon的负载均衡是通过ILoadBalancer来实现的,对ILoadBalancer有以下几个类 1.Abs ...

  5. Ribbon源码解析

    SpringCloud中的Ribbon开源项目,提供了客户端的负载均衡算法.这篇文章,我们来介绍下他是如何实现的.为了方便理解,我们以客户端调用的流程来介绍,其中会穿插介绍相关源代码. 简单回顾下Ri ...

  6. spring cloud ribbon源码解析(一)

    我们知道spring cloud中restTemplate可以通过服务名调接口,加入@loadBalanced标签就实现了负载均衡的功能,那么spring cloud内部是如何实现的呢? 通过@loa ...

  7. SpringCloud服务调用源码解析汇总

    相信我,你会收藏这篇文章的,本篇文章涉及Ribbon.Hystrix.Feign三个组件的源码解析 Ribbon架构剖析 这篇文章介绍了Ribbon的基础架构,也就是下图涉及到的6大组件: Ribbo ...

  8. ribbon源码(1) 概述

    ribbon的核心功能是提供客户端在进行网络请求时负载均衡的能力.主要有以下几个模块: 负载均衡器模块 负载均衡器模块提供了负载均衡能力,详细参见ribbon源码之负载均衡器. 配置模块 配置模块管理 ...

  9. Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例

    概要  前面,我们已经学习了ArrayList,并了解了fail-fast机制.这一章我们接着学习List的实现类——LinkedList.和学习ArrayList一样,接下来呢,我们先对Linked ...

随机推荐

  1. Linux centos7 scp命令

    1. 命令简介 scp(secure copy) 是 linux 系统下基于 ssh 登陆进行安全的远程文件拷贝命令,可以在两台 Linux 主机进行复制操作 # 语法 scp [-1246BCpqr ...

  2. 第一次实战:XX漫画的XSS盲打

    第一次实战:XX漫画的XSS盲打 XSS盲打 盲打是一种惯称的说法,就是不知道有没有XSS漏洞存在的情况下,不顾一切的输入XSS代码在留言啊投诉窗口啊之类的地方,尽可能多的尝试XSS的语句,就叫盲打. ...

  3. Nginx反向代理之巨坑underscores_in_headers

    一.背景 因为项目需求,在做Windows的相关的事情:基本架构就是Nginx--> Nginx --> IIS,在Linux机器上通过Nginx做反向代理到Windows的IIS:然后遇 ...

  4. Kubernetes-kubectl介绍

    前言 本篇是Kubernetes第三篇,大家一定要把环境搭建起来,看是解决不了问题的,必须实战.本篇重要介绍kubectl的使用. Kubernetes系列文章: Kubernetes介绍 Kuber ...

  5. java 线程状态 详解

    线程被创建后,有一个生命周期,下图是线程的生命周期详解. java api java.lang.Thread.State 这个枚举中给出了六种线程状态,分别是: 线程状态 导致状态发生条件 NEW(新 ...

  6. 从零开始学习SQL SERVER(1)--- 了解SQL

    SQL是什么 SQL (发音为 sequal ['  sikwəl ' ]) SQL指 Structured Query Language 结构化查询语言,是用于访问和处理数据库的标准的计算机语言. ...

  7. activemq启动错误UnsupportedClassVers rg/apache/activemq/console/Main:Unsupported major.minor version52.0

    ActiveMQ与java的JDK是有版本对应匹配的. 下面提供一个匹配图: MQ版本号 Build-Jdk 依赖JDKapache-activemq-5.0.0 1.5.0_12 1.5+apach ...

  8. Android通过WebView实现新闻界面的加载

    原文链接:Android实现WebView加载网页及网页美化(简易新闻 四)_Tobey_r1的博客-CSDN博客 效果展示: 我是按照原文作者的步骤一步步来的,中间没有遇到什么问题.主要是界面中有很 ...

  9. 项目需求分析与建议——NABCD模型

    特点一:旧物再利用N:需求:在我们的校园生活中,会遇到许多自己用不到的东西例如,学过的课本.废置的闲置物品等,这些"废物"往往占据着许多空间却不能够发挥自身的价值,通过我们的校园二 ...

  10. 利用 g4l 完整备份和还原Linux系统

    前言: 1.Windows中Ghost由于一系列原因,有不支持分区格式,因此可能无法完整备份Linux. 2.g4l = Ghost for Linux 1.下载g4l https://sourcef ...