扫码关注公众号:芋道源码

发送: 百事可乐
获取永久解锁本站全部文章的链接

《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://thoreauz.com/2017/08/11/language/java/spring%20cloud/Ribbon%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/ 「二道涯」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

Ribbon是一个客户端负载均衡软件,通过注册到Eureka上的服务名,获取服务列表,缓存到本地,选择负载均衡算法,发送http请求。 在spring cloud可以通过简单配置,即可完成客户端负载均衡,用法如下:配置,注入,请求

 @Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();

}
@Autowired
private RestTemplate restTemplate;

public Object getUser(Long id) {
return restTemplate.getForEntity("http://CLOUD-SERVICE-USER/user/persionalInfo?id=" + id, Object.class).getBody();
}

带着问题看源码:

  1. Ribbon怎么和RestTemplate整合
  2. 怎么样自定义负载均衡算法
  3. 底层http客户端怎么修改

common 包

在spring-cloud-common中提供了这样一个接口LoadBalancerClient,注释说明是客户端负载均衡器。

public interface LoadBalancerClient extends ServiceInstanceChooser {
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
/**
* 使用来自LoadBalancer的ServiceInstance,对起执行请求
* @param serviceId
* @param 服务实例
* @param Request。允许在执行前后添加metric
* @return 回调结果
*/
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
/**
* 创建一个真正的URL包含主机和端口:
* http://myservice/path/to/service --> http://host:port/path/to/service
* @param 服务实例
* @param 源url,是一个包含serviceId或者dns的URL
* @return 重新构造的URL
*/
URI reconstructURI(ServiceInstance instance, URI original);
}

此接口提供了三个方法,继承ServiceInstanceChooser,我们再来看下这个接口:

public interface ServiceInstanceChooser {
ServiceInstance choose(String serviceId);
}

这个接口只提供了一个方法:通过serviceId获取需要访问的实例。而Ribbon则实现了LoadBalancerClient。

img

下面分析这个类怎么具体实现接口中的choose方法:

choose实现

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

第一行返回一个server,server包含了服务的地址端口和状态区域等信息,通过serviceId获取这个服务是关键。

Server{
public static final String UNKNOWN_ZONE = "UNKNOWN";
private String host;
private int port = 80;
private volatile String id;
private volatile boolean isAliveFlag;
private String zone = "UNKNOWN";
private volatile boolean readyToServe = true;
}

再看获取服务的方法:

protected Server getServer(String serviceId) {
return getServer(getLoadBalancer(serviceId));
}
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}

获取ILoadBalancer对象,再传递给另一个同名方法。下面分析ILoadBalancer及其实现成了重点。

img

追查ILoadBalancer的实现,最终落实到DynamicServerListLoadBalancer,下面从接口开始分析这个实现类。 先看几个抽象类和接口中需要实现的方法:

public interface ILoadBalancer {
void addServers(List<Server> var1); // 初始化服务列表
Server chooseServer(Object var1); // 从列表中选择一个服务
void markServerDown(Server var1); // 标记服务为down
List<Server> getReachableServers(); // 获取up和reachable的服务
List<Server> getAllServers(); //获取所有服务(reachable and unreachable)
}
public abstract List<Server> getServerList(ServerGroup serverGroup);
public abstract LoadBalancerStats getLoadBalancerStats();
public void primeCompleted(Server s, Throwable lastException);
public abstract void initWithNiwsConfig(IClientConfig clientConfig);

接着再看下面调用链:下面列出的代码中省略了很多逻辑。

public ILoadBalancer getLoadBalancer(String name) {
return getInstance(name, ILoadBalancer.class);
}

//类:DynamicServerListLoadBalancer
@Override
public <C> C getInstance(String name, Class<C> type) {
C instance = super.getInstance(name, type);
if (instance != null) {
return instance;
}
IClientConfig config = getInstance(name, IClientConfig.class);
return instantiateWithConfig(getContext(name), type, config);
}

//类:DynamicServerListLoadBalancer
protected Server getServer(ILoadBalancer loadBalancer) {
return loadBalancer.chooseServer("default");
}

//类:ZoneAwareLoadBalancer
@Override
public Server chooseServer(Object key) {
return zoneLoadBalancer.chooseServer(key);
}

//类:BaseLoadBalancer
public Server chooseServer(Object key) {
return rule.choose(key); // 重要方法 rule = new RoundRobinRule()
}

总之,逻辑是这样的:

  1. 获取负载均衡器(LoadBalancer)的实例(getInstance)。
  2. 负载均衡器通过IRule实现的算法逻辑实现选择。
  3. 最后获得一个Server(服务)

负载均衡的算法主要通过ribbon提供的一些列算法实现IRule,默认是轮询。 img Ribbon提供了一些其他算法:

  • RetryRule:根据轮询的方式重试
  • RandomRule: 随机
  • WeightedResponseTimeRule:根据响应时间去分配权重
  • ZoneAvoidanceRule:区域可用性

至于切换算法的配置,主要集中在初始化时的配置类里面IClientConfig,而这个bean的信息通过对restTemplate添加注解@LoadBalanced完成。具体的负载均衡算法可以通过bean注入,如下。

@Bean
public IRule ribbonRule() {
return new RandomRule();//这里配置策略,和配置文件对应
}

这就解决了我在文章开头的第二个问题。如果需要自定义算法,实现IRule,通过bean的配置完成注入。至于服务列表维护,此文跳过。

restTemplate整合

上文提到通过注解即可开启restTemplate,那是如何做到的?

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
}

这是一个自动配置类,首先维护了一个restTemplates列表,并通过RestTemplateCustomizer对这些他们添加一个拦截器,如下。

@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}

而拦截器的主要就是把serviceId取出来,再使用负载均衡器发起http请求。

@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}

又回到了common中提到的四个方法中的第一个。至此,很明了,就是通过注入给restTemplate注入一个拦截器达到使用loadBalancer的目的。

RestTemplate 本身并没有做 HTTP 底层的实现,而是利用了现有的第三方库:

  1. HttpURLConnection
  2. Apache httpclient
  3. ok http
  4. netty

不同的底层实现,只需要定义直接的RequestFactory,实现ClientHttpRequestFactory。在spring cloud给出了http client切换成okhttp的配置。

666. 彩蛋

如果你对 Ribbon 感兴趣,欢迎加入我的知识星球一起交流。

知识星球

文章目录
  1. 1. common 包
    1. 1.1. choose实现
  2. 2. restTemplate整合
  • 666. 彩蛋