SpringCloud之Ribbon原理
一、Ribbon是什么
Ribbon是SpringCloudNetflix家族中的一款功能为客户端负载均衡的组件,提供超时、重试等可配置。它是由Eureka捆绑基于SpringBoot自动装配引入项目中,一种方式是利用RestTemplate和@LoadBalanced注解整合,另一种是使用feign工具,它会自动开启负载均衡。本文主要介绍第一种方式。
二、项目搭建
-
项目概览
-
项目介绍
这里创建了eureka-server作为eureka注册中心提供服务注册功能,order-service和user-service作为两个eureka-client服务,其中order-service会调用user-service服务。通过这个简单项目,我们一起debug源码看看负载均衡是怎么实现的?
三、Ribbon调用源码剖析
- 服务调用流程图
- 启动服务在eureka管理台可以看到注册情况:
-
代码增加RestTemplate和@LoadBalanced展示
/** * 创建RestTemplate并注入Spring容器 * 增加@LoadBalanced注解实现负载均衡 */ @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
-
源码初始化RestTemplate分析流程
/** * Annotation to mark a RestTemplate or WebClient bean to be configured to use a * LoadBalancerClient. * @author Spencer Gibb */ @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
由LoadBalanced注解可以看到注释说到:通过注解标记RestTemplate使用LoadBalancerClient去代理使用,结合开始说的自动装配我们看到ribbon包下面:
spring-cloud-netflix-ribbon
中的RibbonAutoConfiguration
、spring-cloud-netflix-eureka-client
中的RibbonEurekaAutoConfiguration
。/** * Spring configuration for configuring Ribbon defaults to be Eureka based if Eureka * client is enabled. * * @author Dave Syer * @author Biju Kunjummen */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties @ConditionalOnRibbonAndEurekaEnabled @AutoConfigureAfter(RibbonAutoConfiguration.class) @RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class) public class RibbonEurekaAutoConfiguration { }
/** * Auto configuration for Ribbon (client side load balancing). * * @author Spencer Gibb * @author Dave Syer * @author Biju Kunjummen */ @Configuration @Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class) @RibbonClients @AutoConfigureAfter( name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration") @AutoConfigureBefore({ LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class }) @EnableConfigurationProperties({ RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class }) public class RibbonAutoConfiguration { ... @Bean @ConditionalOnMissingBean(LoadBalancerClient.class) public LoadBalancerClient loadBalancerClient() { return new RibbonLoadBalancerClient(springClientFactory()); } }
通过上面两个类找到自动装配类LoadBalancerAutoConfiguration,在这个类里面可以看到一个变量restTemplates:
@LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList();
在上面的 @LoadBalanced 里面有一个 @Qualifier 注解,是说当Spring无法判断哪个bean应该被注入时(有可能存在多个类型相同的对象),@Qualifier 注解通过指定 value, 有助于消除歧义bean的自动注入。在这里如果@LoadBalanced 和 @Bean 一起用,那么就相当于给这个bean打上了标记,然后 @LoadBalanced 和 @Autowired 一起用的时候就会把项目所有被 @LoadBalanced 注解修饰的对象注入到 restTemplates 这个list对象中, 等待后续逻辑处理。
LoadBalancerAutoConfiguration类里有三个对象
SmartInitializingSingleton
,LoadBalancerInterceptor
,RestTemplateCustomizer
:
- LoadBalancerInterceptor :ribbon拦截器的具体处理逻辑。
- RestTemplateCustomizer :向所有的RestTemplate都塞入一个loadBalancerInterceptor,让其具备有负载均衡的能力。
- SmartInitializingSingleton - spring的拓展接口,就是它打通了spring与 RestTemplateCustomizer 的交互,能让 spring 启动的时候顺便执行了RestTemplateCustomizer 的逻辑。
-
LoadBalancerInterceptor 逻辑
@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, this.requestFactory.createRequest(request, body, execution)); }
请求进来被这个类拦截后断点:
可以看到loadBalancer的实现类是RibbonLoadBalancerClient(由上面RibbonAutoConfiguration引入)。
- 进入RibbonLoadBalancerClient
经过一些操作后,看到serviceId是userServer,也就是我们的服务节点名,getServer(...)方法使用均衡算法拿到可用的服务实例(从几个中选择一个,达到均衡的作用),返回server,即server是localhost:8663,这是具体的服务节点ip。loadBalancer是默认返回的一个负载均衡器:ZoneAwareLoadBalancer。
-
getServer(...)方法进入:
protected Server getServer(ILoadBalancer loadBalancer, Object hint) { if (loadBalancer == null) { return null; } // Use 'default' on a null hint, or just pass it on? return loadBalancer.chooseServer(hint != null ? hint : "default"); }
-
再进入ZoneAwareLoadBalancer类的chooseServer方法:
@Override public Server chooseServer(Object key) { if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { logger.debug("Zone aware logic disabled or there is only one zone"); return super.chooseServer(key); }
进入父类方法
public Server chooseServer(Object key) { if (counter == null) { counter = createCounter(); } counter.increment(); if (rule == null) { return null; } else { try { return rule.choose(key); } catch (Exception e) { logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e); return null; } } }
-
进入rule.choose(key)方法
/** * Get a server by calling {@link AbstractServerPredicate#chooseRandomlyAfterFiltering(java.util.List, Object)}. * The performance for this method is O(n) where n is number of servers to be filtered. */ @Override public Server choose(Object key) { ILoadBalancer lb = getLoadBalancer(); Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); if (server.isPresent()) { return server.get(); } else { return null; } }
- 图上看到会从已经获取到user-service的两个服务列表中选一个服务,抽象函数getPredicate来获取AbstractServerPredicate 对象的实现,而在 choose 函数中, 通过AbstractServerPredicate 的chooseRoundRobinAfterFiltering函数来选出具体的服务实例。从该函数的命名我们也大致能猜出它的基础逻辑:先通过子类中实现的Predicate逻辑来过滤一部分服务实例,然后再以轮询的方式从过滤后的实例清单中选出一个。
四、总结
- 创建RestTemplate
- 添加了ribbon依赖后,会在项目启动的时候自动往RestTemplate中添加LoadBalancerInterceptor拦截器
- 用户根据RestTemplate发起请求时,会将请求转发到LoadBalancerInterceptor去执行,该拦截器会根据指定的负载均衡方式获取该次请求对应的应用服务端IP、port
- 根据获取到的IP、port重新封装请求,发送HTTP请求,返回具体响应。