1.引言

什么是Ribbon

  • Ribbon是Netflix公司的一个负载均衡项目,可以在进行服务调用时做负载均衡;

2.Ribbon基本使用

Ribbon结合Http

  • Ribbon通常和Http请求结合,对Http请求进行负载均衡;最简单的使用如下,通过注入RestTemplate,并且打上@LoadBlanced注解,即可得到一个带有负载均衡效果的RestTemplate

    1. @Configuration
    2. public class HttpConfiguration {
    3. @Bean
    4. @LoadBalanced
    5. public RestTempttpTlate restTemplate() {
    6. return new RestTemplate();
    7. }
    8. }
  • 使用时,只需要把RestTemplate发送请求时的URL中的主机名host替换为服务提供方的名字;那么在调用时,就会自动进行负载均衡

    1. @RestController
    2. public class TestController {
    3. @Autowired
    4. private RestTemplate restTemplate;
    5. @GetMapping("/test")
    6. public String test() {
    7. //HelloServiceProvider为服务提供方名称
    8. return restTemplate.getForEntity("http://HelloServiceProvider/hello?info=okkk", String.class).getBody();
    9. }
    10. }
  • 当然,这里还有配置好注册中心,将服务提供方和消费方都注册到注册中心上

3.Ribbon和RestTemplate整合原理

原理概述

  • RestTemplate相信大家都有用过,相当于一个发送Http请求的工具;但是为何引入@LoadBlanced就可以实现负载均衡了呢?
  • 简单来说:

    • RestTemplate在发送请求过程中,会构造一条具有多个拦截器的执行链
    • Ribbon可以借助拦截器,在RestTemplate中加入一个LoadBalancerInterceptor拦截器;
    • 这样在发送请求时,请求经过拦截器,Ribbon就可以根据请求的URL中的主机名(即服务名, 下面的HelloServiceProvider),去注册中心拿到提供该服务的所有主机
    • 并且根据负载均衡策略,选择其中一个,然后把服务名替换为真正的IP,接着继续执行下一个拦截器,最终发送请求
      1. //restTempalte负载均衡发送请求示例,其中HelloServiceProvider为服务名,需要被替换为真实IP地址
      2. restTemplate.getForEntity("http://HelloServiceProvider/hello?info=okkk", String.class).getBody();

      源码解析

  • Spring的IOC容器在刷新过程中,会执行到如下过程;该过程是bean容器刷新比较后面的一步,这是大部分单例bean都已经实例化完成;会遍历所有实现了SmartInitializingSingleton接口的bean,并调用它的afterSingletonsInstantiated方法 ```java //DefaultListableBeanFactory#preInstantiateSingletons部分代码

for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); if (singletonInstance instanceof SmartInitializingSingleton) { final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance; if (System.getSecurityManager() != null) { AccessController.doPrivileged((PrivilegedAction) () -> { smartSingleton.afterSingletonsInstantiated(); return null; }, getAccessControlContext()); } else { //调用afterSingletonsInstantiated方法 smartSingleton.afterSingletonsInstantiated(); } } }

  1. - LoadBalancerAutoConfiguration类:SpringCloud中的一个自动配置类,负责负载均衡方面的配置,会向容器中注入如下几个Bean对象(截取部分源码); 其中第一个是SmartInitializingSingleton类型的对象,也就是上面所说的接口;所以它的方法会在上面的过程中被调用;具体方法内容看下面注释;(下面这两个Bean对象都是lambda表达式的形式,注意别看混了)
  2. ```java
  3. //LoadBalancerAutoConfiguration
  4. public class LoadBalancerAutoConfiguration {
  5. @Bean //参数传入的是restTemplateCustomizers,这个应该是有ApplicationContext进行注入所有的customizer
  6. public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
  7. final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
  8. return () -> restTemplateCustomizers.ifAvailable(customizers -> {
  9. //遍历所有RestTemplate对象
  10. for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
  11. for (RestTemplateCustomizer customizer : customizers) {
  12. //调用customizer#customize方法,对restTemplate做一些操作,具体看下面的Bean
  13. customizer.customize(restTemplate);
  14. }
  15. }
  16. });
  17. }
  18. //注入customizer对象
  19. @Bean
  20. @ConditionalOnMissingBean
  21. public RestTemplateCustomizer restTemplateCustomizer(
  22. final LoadBalancerInterceptor loadBalancerInterceptor) {
  23. return restTemplate -> {
  24. //这就是上面的Bean执行customize方法,拿到该restTemplate所有拦截器
  25. List<ClientHttpRequestInterceptor> list = new ArrayList<>(
  26. restTemplate.getInterceptors());
  27. //加入负载均衡拦截器
  28. list.add(loadBalancerInterceptor);
  29. //重新设置拦截器给restTemplate
  30. restTemplate.setInterceptors(list);
  31. };
  32. }
  33. }
  • 通过上面的步骤,restTemplate中就会被注入了LoadBalancerInterceptor拦截器;发送http请求时会经过拦截器,就可以将URL的服务名替换为负载均衡过后的真正IP地址了

4.负载均衡流程

核心类

  • LoadBalanceClient:上面说到Http请求发送时,会经过Ribbon的LoadBalancerInterceptor拦截器进行负载均衡,该拦截器会把请求交给LoadBalancerClient进行负载均衡;该类是负载均衡客户端,也可以看成负载均衡的入口;采用的实现类是RibbonLoadBalancerClient,看一下继承图

ribbon1.png

  • ILoadBalancerRibbonLoadBalancerClient内部的execute方法会以Http请求的服务名为key,找到ILoadBalancer对象,这个ILoadBalancer就是专门负责这个服务的负载均衡;继承图如下

    • DynamicServerListLoadBalance: 具有ServerList功能的ILoadBalance,ServerList后面会讲,它能够维护和更新本服务对应的所有主机信息
    • ZoneAwareLoadBalance: 该类是DynamicServerListLoadBalancer的子类,具有区域感知(ZoneAware)功能,它可以把所有主机根据请求状况进行区域划分,当主机请求超时时,它会把相关主机从活跃区域踢出,避免影响服务质量;详情可看类上的注释
      1. /**
      2. * Load balancer that can avoid a zone as a whole when choosing server.
      3. *<p>
      4. * The key metric used to measure the zone condition is Average Active Requests,
      5. which is aggregated per rest client per zone. It is the
      6. total outstanding requests in a zone divided by number of available targeted instances (excluding circuit breaker tripped instances).
      7. This metric is very effective when timeout occurs slowly on a bad zone.
      8. <p>
      9. The LoadBalancer will calculate and examine zone stats of all available zones. If the Average Active Requests for any zone has reached a configured threshold, this zone will be dropped from the active server list. In case more than one zone has reached the threshold, the zone with the most active requests per server will be dropped.
      10. Once the the worst zone is dropped, a zone will be chosen among the rest with the probability proportional to its number of instances.
      11. A server will be returned from the chosen zone with a given Rule (A Rule is a load balancing strategy, for example {@link AvailabilityFilteringRule})
      12. For each request, the steps above will be repeated. That is to say, each zone related load balancing decisions are made at real time with the up-to-date statistics aiding the choice.
      13. */
      ribbon2.png
  • IRule:代表负载均衡策略,ILoadBalancer的负载均衡由它进行处理,Ribbon内置了多种负载均衡策略,都实现了该接口

ribbon3.png

源码流程

  • 前面说了负载均衡入口是RestTemplate拦截器,所以看一下LoadBalancerInterceptor的intercept方法

    1. //LoadBalancerInterceptor#intercept
    2. @Override
    3. public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
    4. final ClientHttpRequestExecution execution) throws IOException {
    5. final URI originalUri = request.getURI();
    6. String serviceName = originalUri.getHost();
    7. Assert.state(serviceName != null,
    8. "Request URI does not contain a valid hostname: " + originalUri);
    9. //拿到loadBalancer,它是一个LoadBalancerClient类型对象,并调用execute方法
    10. return this.loadBalancer.execute(serviceName,
    11. this.requestFactory.createRequest(request, body, execution));
    12. }
  • 上述LoadBalancerClient实际用的实现类是RibbonLoadBalancerClient,先看一下其父接口LoadBalancerClient的方法 ```java public interface LoadBalancerClient extends ServiceInstanceChooser { //用于执行http负载均衡请求 T execute(String serviceId, LoadBalancerRequest request) throws IOException;

    T execute(String serviceId, ServiceInstance serviceInstance,

    1. LoadBalancerRequest<T> request) throws IOException;

    //重构URI,替换服务名 URI reconstructURI(ServiceInstance instance, URI original);

}

  1. - `RibbonLoadBalancerClient`execute方法如下
  2. ```java
  3. //RibbonLoadBalancerClient#execute
  4. public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
  5. throws IOException {
  6. //根据serviceId拿到ILoadBalancer,交由它进行负载均衡
  7. ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
  8. //根据loadBalancer拿到真正的服务提供者
  9. Server server = getServer(loadBalancer, hint);
  10. if (server == null) {
  11. throw new IllegalStateException("No instances available for " + serviceId);
  12. }
  13. //包装Server
  14. RibbonServer ribbonServer = new RibbonServer(serviceId, server,
  15. isSecure(server, serviceId),
  16. serverIntrospector(serviceId).getMetadata(server));
  17. //执行请求
  18. return execute(serviceId, ribbonServer, request);
  19. }
  20. //RibbonLoadBalancerClient#getLoadBalancer
  21. protected ILoadBalancer getLoadBalancer(String serviceId) {
  22. //从clientFactory中获得该服务对应的ILoadBalancer
  23. return this.clientFactory.getLoadBalancer(serviceId);
  24. }
  • 这里ILoadBalancer loadBalancer = getLoadBalancer(serviceId)这句代码是重点,它会根据当前URL中的serviceId,先从工厂中拿到对应的ApplicationContext(首次取没有则新建容器并refresh),接着再从IOC容器中拿到ILoadBalancer类型的bean;由于调用栈比较复杂,下面只摘取部分重点 ```java protected ILoadBalancer getLoadBalancer(String serviceId) { //从SpringClientFactory工厂中获取该serviceId对应的ILoadBalancer return this.clientFactory.getLoadBalancer(serviceId); }

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

public T getInstance(String name, Class type) { //获取服务名对应的ApplicationContext AnnotationConfigApplicationContext context = getContext(name); if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { //从ApplicationContext中后去ILoadBalancer类型的bean return context.getBean(type); } return null; }

protected AnnotationConfigApplicationContext getContext(String name) { //大致逻辑就是直接容map中拿,没有则创建一个并缓存 if (!this.contexts.containsKey(name)) { synchronized (this.contexts) { if (!this.contexts.containsKey(name)) { this.contexts.put(name, createContext(name)); } } } return this.contexts.get(name); }

  1. - 因此可以看出,Ribbon中,每个服务会对应一个`ApplicationContext`,内部维护了各种bean信息,包括负责负载均衡的`ILoadBalancer`;每个ApplicationContext内部都有自己的ILoadBalancer
  2. - getServer()方法最终会调用ILoadBalancerchooseServer方法,代码如下
  3. ```java
  4. //BaseLoadBalancer#chooseServer
  5. public Server chooseServer(Object key) {
  6. if (counter == null) {
  7. counter = createCounter();
  8. }
  9. counter.increment();
  10. //如果没有配置rule则返回
  11. if (rule == null) {
  12. return null;
  13. } else {
  14. try {
  15. //由rule执行真正的负载均衡
  16. return rule.choose(key);
  17. } catch (Exception e) {
  18. logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
  19. return null;
  20. }
  21. }
  22. }
  • 下面以RandomRule为例看一下choose方法,篇幅原因只保留部分代码,详情可自己debug

    1. //RandomRule#choose
    2. public Server choose(ILoadBalancer lb, Object key) {
    3. if (lb == null) {
    4. return null;
    5. }
    6. Server server = null;
    7. while (server == null) {
    8. //... 从ILoadBalancer中拿到该服务对应的所有Server
    9. List<Server> upList = lb.getReachableServers();
    10. List<Server> allList = lb.getAllServers();
    11. //... 产生随机数,并拿到对应Server
    12. int index = chooseRandomInt(serverCount);
    13. server = upList.get(index);
    14. //...
    15. if (server.isAlive()) {
    16. return (server);
    17. }
    18. //...
    19. }
    20. return server;
    21. }

    小结

  • 这样就完成了负载均衡过程,梳理一下大致流程

    • Http请求经过LoadBalancerInterceptor拦截器,它将调用LoadBalancerClient进行处理
    • LoadBalancerClient(实现类Ribbon``LoadBalancerClient)根据服务名拿到对应的ApplicationContext,并从容器中拿到ILoadBalancer(实际类ZoneAwareLoadBalancer
    • ILoadBalancer中获取服务提供者Server,具体是调用chooseServer方法,内部会使用IRule进行负载均衡,并返回合适的Server
    • IRule才是真正的负载均衡实现接口,Ribbon内置多种默认负载均衡策略
  • 以上流程讲述了从RestTemplate与Ribbon整合,到一次请求发起实现负载均衡的大致流程;但是还不涉及Ribbon如何维护注册中心中的服务及对应的主机列表操作;即Ribbon需要定时获取注册中心的服务信息,供负载均衡使用

5.服务列表拉取和刷新

核心类

  • ServerList:该接口用于表示向注册中心拉取服务信息

    1. public interface ServerList<T extends Server> {
    2. public List<T> getInitialListOfServers();
    3. public List<T> getUpdatedListOfServers();
    4. }

源码流程

  • 前面说到过,每个服务都有自己的ILoadBalancer(默认是ZoneAwareLoadBalancer),其父类为DynamicServerListLoadBalancer,它会负责服务对应的主机列表信息;
  • DynamicServerListLoadBalancer在创建过程中,会调用如下方法进行服务列表初始化 ```java public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
    1. ServerList<T> serverList, ServerListFilter<T> filter,
    2. ServerListUpdater serverListUpdater) {
    super(clientConfig, rule, ping); this.serverListImpl = serverList; this.filter = filter; this.serverListUpdater = serverListUpdater; if (filter instanceof AbstractServerListFilter) {
    1. ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
    } //调用该方法进行主机列表初始化 restOfInit(clientConfig); }

void restOfInit(IClientConfig clientConfig) { //… updateListOfServers(); //… }

//这是另一个构造方法,可能是使用自定义配置时 public DynamicServerListLoadBalancer(IClientConfig clientConfig) { initWithNiwsConfig(clientConfig); }

//DynamicServerListLoadBalancer#initWithNiwsConfig public void initWithNiwsConfig(IClientConfig clientConfig) { try { super.initWithNiwsConfig(clientConfig); //从配置文件中拿到NIWSServerListClassName对应的值,应该是拿到配置的ServerList实现类,负责维护服务信息 //当注册中心不同时,应该可以动态替换 String niwsServerListClassName = clientConfig.getPropertyAsString( CommonClientConfigKey.NIWSServerListClassName, DefaultClientConfigImpl.DEFAULT_SEVER_LIST_CLASS); //实例化ServerList ServerList niwsServerListImpl = (ServerList) ClientFactory .instantiateInstanceWithClientConfig(niwsServerListClassName, clientConfig); //保存 this.serverListImpl = niwsServerListImpl; //… //拿到ServerListUpdaterClassName配置的类 String serverListUpdaterClassName = clientConfig.getPropertyAsString( CommonClientConfigKey.ServerListUpdaterClassName, DefaultClientConfigImpl.DEFAULT_SERVER_LIST_UPDATER_CLASS ); //实例化 this.serverListUpdater = (ServerListUpdater) ClientFactory .instantiateInstanceWithClientConfig(serverListUpdaterClassName, clientConfig); //在该函数中,会进行服务初始化 restOfInit(clientConfig); } catch (Exception e) { throw new RuntimeException( “Exception while initializing NIWSDiscoveryLoadBalancer:”

  1. + clientConfig.getClientName()
  2. + ", niwsClientConfig:" + clientConfig, e);
  3. }

}

  1. - 接下来调用DynamicServerListLoadBalancer#restOfInit方法,最终会调用updateListOfServers方法进行列表更新
  2. ```java
  3. @VisibleForTesting
  4. public void updateListOfServers() {
  5. List<T> servers = new ArrayList<T>();
  6. //判断serverListImpl是否有配置
  7. if (serverListImpl != null) {
  8. //拉取所有的Servers
  9. servers = serverListImpl.getUpdatedListOfServers();
  10. LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
  11. getIdentifier(), servers);
  12. //如果由过滤规则,对Server进行过滤
  13. if (filter != null) {
  14. servers = filter.getFilteredListOfServers(servers);
  15. LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
  16. getIdentifier(), servers);
  17. }
  18. }
  19. //更新本地的ServerList
  20. updateAllServerList(servers);
  21. }
  22. @Override
  23. public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
  24. List<DiscoveryEnabledServer> servers = setZones(
  25. this.list.getUpdatedListOfServers()); //这里才是真正获取主机的方法, list是一个ServerList类型的变量,根据注册中心不同,具体实现不同
  26. return servers;
  27. }
  • 如果注册中心使用Eureka,那么ServerList使用的是DiscoveryEnabledNIWSServerList,如果是其他注册中心,可以替换,下面看一下它的getUpdatedListOfServers方法,层层调用后进入如下方法,篇幅原因省略部分代码
    1. //DiscoveryEnabledNIWSServerList#obtainServersViaDiscovery
    2. private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
    3. List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
    4. //...
    5. //拿到Eureka客户端,它可以进行服务拉取,注册等
    6. EurekaClient eurekaClient = eurekaClientProvider.get();
    7. if (vipAddresses!=null){
    8. for (String vipAddress : vipAddresses.split(",")) {
    9. // if targetRegion is null, it will be interpreted as the same region of client
    10. List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
    11. for (InstanceInfo ii : listOfInstanceInfo) {
    12. if (ii.getStatus().equals(InstanceStatus.UP)) {
    13. //... 封装服务并保存
    14. DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
    15. serverList.add(des);
    16. }
    17. }
    18. if (serverList.size()>0 && prioritizeVipAddressBasedServers){
    19. break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
    20. }
    21. }
    22. }
    23. return serverList;
    24. }

6.服务列表更新

  • Ribbon服务列表更新是通过定时任务来完成的;相关类是PollingServerListUpdater;可以看一下它的start方法

    1. public synchronized void start(final UpdateAction updateAction) {
    2. if (isActive.compareAndSet(false, true)) {
    3. //创建定时任务
    4. final Runnable wrapperRunnable = new Runnable() {
    5. @Override
    6. public void run() {
    7. //...
    8. try {
    9. //更新主机信息
    10. updateAction.doUpdate();
    11. lastUpdated = System.currentTimeMillis();
    12. } catch (Exception e) {
    13. logger.warn("Failed one update cycle", e);
    14. }
    15. }
    16. };
    17. //设置定时任务
    18. scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
    19. wrapperRunnable,
    20. initialDelayMs,
    21. refreshIntervalMs,
    22. TimeUnit.MILLISECONDS
    23. );
    24. }
    25. }
  • 另外,Ribbon还会定期去Ping每一个主机,保证主机存活

7.服务注册中心

  • Ribbon默认的服务注册中心是eureka,如果想用nacos做服务注册中心,可以直接添加nacos依赖即可;
  • nacos中包含了Ribbon,并且提供了NacosServerList来与注册中心进行信息拉取
  • 而Eureka中使用的是DiscoveryEnabledNIWSServerList
  • 以上纯属猜测,具体需要自己实践一下;但Ribbon的注册中心替换原理应该类似,通过提供ServerList来实现