客户端负载均衡-Ribbon 源码分析

客户端负载均衡-Ribbon 源码分析

文章目录

    • 客户端负载均衡-Ribbon 源码分析
    • 前言
    • 项目环境
    • 1.Ribbon 主要组件
    • 2.组件作用和联系
    • 3.静态配置 ServerList 示例
    • 4.@LoadBalanced 原理分析
    • 5.Serverlist 如何获取&更新
      • 5.1 获取 Serverlist
      • 5.2 更新 Serverlist
        • 5.1 ServerListFiter
        • 5.2 ServerListUpdater
        • 5.3 IPing
    • 6.负载均衡策略
    • 7.自定义负载均衡策略
    • 8.负载均衡算法的使用场景
      • 8.1 定制跟业务更匹配的策略
      • 8.2 灰度发布
      • 8.3 多版本隔离
      • 8.4 故障隔离
    • 9.Ribbon 饥饿加载模式
    • 10.配置方式自定义 Ribbon Client
    • 11.参考

前言

上一章 客户端负载均衡-Ribbon 基础篇 主要介绍一些负载均衡、Ribbon 、RestTemplate 的基本概念和用法,RestTemplate 和 Ribbon 如何结合使用,对 Ribbon 有一个基本的印象。

本章我们对 Ribbon 进行详细的源码分析。

项目环境

  • Java 8
  • Spring Cloud Finchley
  • 项目地址:https://github.com/huajiexiewenfeng/deep-in-spring-cloud-netflix

1.Ribbon 主要组件

下图为 Ribbon 中的一些主要组件,以及一些相关的实现类。

客户端负载均衡-Ribbon 源码分析_第1张图片

组件的作用

组件 作用
ILoaderBalancer 定义一系列的操作接口,比如选择服务实例
IRule 算法策略,内置算法策略来为服务实例的选择提供服务
ServerList 负责服务实例信息的获取,可以获取配置文件中的,也可以从注册中心获取
ServerListFilter 过滤掉某些不想要的服务实例信息
ServerListUpdater 更新本地缓存的服务实例信息
IPing 对已有的服务实例进行可用性检查,保证选择的服务都是可用的

下面我们通过 Ribbon 的使用场景来分别介绍这些组件,当我们需要通过 Ribbon 选择一个可用的服务实例信息,进行远程调用时,Ribbon 会根据指定的算法从服务列表中选择一个服务实例进行返回。

2.组件作用和联系

在这个选择服务实例的过程中,服务实例信息是怎么来的呢?

客户端负载均衡-Ribbon 源码分析_第2张图片

ServerList :存储服务实例组件,存储分为静态和动态两种方式

  • 静态存储需要事先配置好固定的服务实例信息;
  • 动态存储需要从注册中心获取对应的服务实例信息。

ServerListFilter :在某些场景下我们可能需要过滤一部分服务实例信息,这个时候可以用 ServerListFilter 组件来实现过滤操作。

ServerListUpdater :Ribbon 会将服务实例在本地内存中存储一份,这样就不需要每次都去注册中心获取信息,这种场景的问题在于当服务实例增加或者减少后,本地怎么更新呢?这个时候就需要用到 ServerListUpdater 组件,ServerListUpdater 组件就是用于服务实例更新操作。

IPing:如果缓存到本地的服务实例信息已经无法提供服务了,IPing 可以检测服务实例信息是否可用。

IRule:Ribbon 会根据指定的算法来选择一个可用的实例信息,IRule 组件提供了很多种算法策略来选择实例信息。

ILoadBalancer:使用 Ribbon 的入口了,我们要选择一个可用的服务,怎么选择?问谁要这个服务?这时ILoadBalancer 就上场了,ILoadBalancer 中定义了软件负载均衡操作的接口,比如动态更新一组服务列表,根据指定算法从现有服务器列表中选择一个可用的服务等操作。

3.静态配置 ServerList 示例

我们再上一章的示例 客户端负载均衡-Ribbon 基础篇-5.RestTemplate & Ribbon 示例 基础上进行简单的修改

演示手动静态配置 ServerList

修改 ribbon-demo 配置文件 application.yaml

  • 关闭 ribbon.eureka.enabled = false
  • 增加 user-service.ribbon.listOfServers 配置
server:
  port: 8180
eureka:
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:8761/eureka/
spring:
  application:
    name: ribbon-demo
ribbon:
  eureka:
    enabled: false
user-service:
   ribbon:
     listOfServers: localhost:8181,localhost:8182

启动相关应用测试,可以看到 EurekaServer 是关闭状态(这里不需要注册中心)

客户端负载均衡-Ribbon 源码分析_第3张图片

测试负载均衡结果,浏览器输入 http://127.0.0.1:8180/ribbon/getUser

第一次刷新,结果如下图所示:

客户端负载均衡-Ribbon 源码分析_第4张图片

第二次刷新,结果如下图所示:

客户端负载均衡-Ribbon 源码分析_第5张图片

多次刷新,每次的返回结果的 port 端口号都不一样,说明静态配置有效。

4.@LoadBalanced 原理分析

手动配置 listOfServers 可以让我们在某些场景下更加方便的进行调试工作,在正式的使用中,所有的服务实例信息都是从注册中心拉取的,也就是从我们前面讲的 Eureka 中获取。

所以我们还是将示例还原从 Eureka 注册中心获取,分析下 @LoadBalanced 实现负载均衡的原理。

首先我们可以搜索源码,看看哪些地方用到了 @LoadBalance,这里我们可以找到 LoadBalancerAutoConfiguration,看名称是负载均衡器的自动配置类。

这里采用 @Autowired 集合注入的方式将所有标有 @LoadBalanced 注解的 RestTemplate 对象都注入到 restTemplates 集合中。

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
    
    @LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();
    ...

然后循环遍历标注 @LoadBalanced 的 restTemplates 集合对象,设置拦截器

@Bean 配置 SmartInitializingSingleton 对象

  • 此对象会在 Spring Bean 生命周期中的初始化完成阶段被调用,即 ApplicationContext Spring 应用上下文启动的完成阶段,具体代码位置 DefaultListableBeanFactory#preInstantiateSingletons。
  • 这样设计的好处是不会被其他操作所影响,因为已经是在 Spring Bean 生命周期创建的最后阶段。

@Bean 方法注入 RestTemplateCustomizer 对象

  • 这个对象是利用 @Bean 的方法注入将 RestTemplateCustomizer 对象注入到方法的入参 restTemplateCustomizers 中
	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
			final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        });
	}

继续往下探源码,最终会执行 LoadBalancerInterceptor#intercept 的拦截方法中

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;
	private LoadBalancerRequestFactory requestFactory;
    ...
	@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));
	}
}

然后调用 RibbonLoadBalancerClient#execute 方法

	@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer);
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
				serviceId), serverIntrospector(serviceId).getMetadata(server));

		return execute(serviceId, ribbonServer, request);
	}

关键方法 getServer(),此方法就是用于选择具体的服务实例,最终会交给 ILoadBalancer 类去选择服务实例。

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

IRule 接口通过传入的参数 “default” 选择默认的负载均衡策略,从 Serverlist 中选择一个实例返回。

public interface IRule{
    public Server choose(Object key);
    public void setLoadBalancer(ILoadBalancer lb);
    public ILoadBalancer getLoadBalancer();    
}

5.Serverlist 如何获取&更新

通过上面的过程我们已经了解到,@LoadBalancer 拦截和选择实例的过程,最后调用对应的服务实例,那么服务实例集合又是如何进行获取和更新的呢?

5.1 获取 Serverlist

调用链路如下:

DynamicServerListLoadBalancer#initWithNiwsConfig -> restOfInit -> updateListOfServers -> DiscoveryEnabledNIWSServerList#getUpdatedListOfServers -> obtainServersViaDiscovery -> eurekaClient.getInstancesByVipAddress

最终通过 EurekaClient 来获取服务注册列表信息,具体的细节可以更加以上这个调用链路来进行追踪。

调试截图:

客户端负载均衡-Ribbon 源码分析_第6张图片

可以看到本示例中,在第一次调用 http://127.0.0.1:8180/ribbon/getUser 接口的时候触发这个过程,从 Eureka 中获取了两个服务实例信息。

这个地方可以设置为启动的时候自动获取,在本章的 第9小节 中会介绍。

5.2 更新 Serverlist

从第二小节 组件的作用和联系 的图中,我们可以看到有三种方式可以对 Server 实例信息进行更新

  • ServerListFiter
  • ServerListUpdater
  • IPing
5.1 ServerListFiter

com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateListOfServers

在这个方法中有个判断

如果 filter 不为空,意思就是我们如果设置了 ServerListFiter,这里的 server 实例列表就会根据我们设置的 ServerListFiter 来进行过滤操作

            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
5.2 ServerListUpdater

ServerListUpdater 有下面两个实现类

  • PollingServerListUpdater:动态更新服务列表的默认策略,DynamicServerListLoadBalancer 负载均衡器默认实现就是它,它通过定时任务的方式进行服务列表的更新。
  • EurekanotificationServerListUpdater:也可服务于 DynamicServerListLoadBalancer 负载均衡器,它需要利用 Eureka 的事件监听器来触发服务列表的更新操作。

这里我们来看看 ServerListUpdater 默认情况下的调用链路

DynamicServerListLoadBalancer#initWithNiwsConfig -> restOfInit-> enableAndInitLearnNewServersFeature -> serverListUpdater.start(updateAction)

start() 方法源码如下:

    @Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    if (!isActive.get()) {
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {
                        updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };

            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }
  • updateAction.doUpdate() 实际上执行的 updateListOfServers 方法来更新 ServerList 服务实例列表
  • 同时启动一个定时线程来继续执行 wrapperRunnable 任务
    • initialDelayMs 更新服务实例在初始化之后延迟1秒后开始执行
    • refreshIntervalMs 以 30 秒为周期重复执行

调试截图

客户端负载均衡-Ribbon 源码分析_第7张图片

5.3 IPing

BaseLoadBalancer 构造方法-> setupPingTask() 方法

lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);

定时每 10 秒钟执行 PingTask 任务

    class PingTask extends TimerTask {
        public void run() {
            try {
            	new Pinger(pingStrategy).runPinger();
            } catch (Exception e) {
                logger.error("LoadBalancer [{}]: Error pinging", name, e);
            }
        }
    }

查看 Pinger 的 runPinger 方法,最终根据 pingerStrategy.pingServers(ping, allServers); 来获取服务的可用性,如果返回的结果与之前相同,则不向 EurekaClient 获取注册列表;如果不同,则通知 ServerStatusChangeListener 服务注册列表发生了变化,进行更新或者重新拉取。

代码较多这里就不贴出来了,源码位置如下:com.netflix.loadbalancer.BaseLoadBalancer.Pinger#runPinger

IPing 如何判断服务是否可用?

IPing 用于向 server 发送 ping 来判断该 server 是否有响应,从而判断该 server 是否可用。

public interface IPing {
    
    /**
     * Checks whether the given Server is "alive" i.e. should be
     * considered a candidate while loadbalancing
     * 
     */
    public boolean isAlive(Server server);
}

IPing 的实现类

  • PingUrl:真实的去 ping 某个 Url,判断其是否可用。
  • PingConstant:固定返回某服务是否可用,默认返回 true,即可用。
  • NoOpPing:不去 ping,直接返回 true,即可用。
  • NIWSDiscoveryPing:根据 DiscoveryEnabledServer 的 InstanceInfo 的状态来判断,如果为 InstanceStatus.UP,则可用,否则不可用。
  • DummyPing:直接返回 true,并实现了 initWithNiwsConfig 方法。

6.负载均衡策略

IRule 类关系图如下:

客户端负载均衡-Ribbon 源码分析_第8张图片

  • BestAvailableRule:选择最小请求数
  • ClientConfigEnabledRoundRobinRule:线性轮询机制,该策略较为特殊,我们一般不直接使用它,可使用它的子类
  • RandomRule:随机选择一个 server
  • RoundRobinRule:轮询选择一个 server
  • RetryRule:根据轮询的方式重试
  • WeightedResponseTimeRule:根据响应时间去分配一个 weight,weight 越低,被选择的可能性就越低
  • ZoneAvoidanceRule:根据 server 的 zone 区域和可用性来轮询选择

7.自定义负载均衡策略

自定义负载均衡算法有实现和继承两种方式

  • 实现 Irule 接口,实现 choose 方法的逻辑
  • 继承 AbstractLoadBalancerRule 类,实现 choose 方法的逻辑

这里我们使用第二种方式来实现一个随机算法,代码如下:

public class MyRule extends AbstractLoadBalancerRule {

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        String clientName = clientConfig.getClientName();
        System.out.println(clientName);
    }

    @Override
    public Server choose(Object key) {
        ILoadBalancer loadBalancer = getLoadBalancer();
        List<Server> allServers = loadBalancer.getAllServers();
        return allServers.get(new Random().nextInt(allServers.size()));
    }

}

在配置文件 application.yaml 中指定规则

# 指定user-service的负载策略
user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.csdn.ribbon.rule.MyRule

浏览器输入 http://127.0.0.1:8180/ribbon/getUser 测试效果如下:

客户端负载均衡-Ribbon 源码分析_第9张图片

或者

客户端负载均衡-Ribbon 源码分析_第10张图片

端口号随机出现。

除了通过配置文件的方式来指定自定义的负载均衡策略,也可以通过配置 Bean 的方式

新增两个配置 Bean 即可

BeanConfiguration:

public class BeanConfiguration {
	@Bean
	public IRule myRule() {
		return new MyRule();
	}
}

RibbonClientConfig:

@RibbonClient(name="user-service", configuration=BeanConfiguration.class)
public class RibbonClientConfig {
}

测试效果一样。

8.负载均衡算法的使用场景

8.1 定制跟业务更匹配的策略

这点是在开发过程中相关度比较大的,就是某些场景可能更适合轮询算法,但是单纯的轮询算法可能不是你想要的,这个时候就需要在轮询的基础上,加上一些你自己的逻辑,组成一个新的算法,让 Ribbon 使用这个算法来进行服务实例的选择。

8.2 灰度发布

灰度发布是能够平滑过渡的一种发布方式,在发布过程中,先发布一部分应用,让指定的用户使用刚发布的应用,等到测试没有问题后,再将其他的全部应用发布。如果新发布的有问题,只需要将这部分恢复即可,不用恢复所有的应用。

8.3 多版本隔离

多版本隔离跟灰度发布类似,为了兼容或者过度,某些应用会有多个版本,这个时候如何保证 1.0 版本的客户端不会调用到 1.1 版本的服务,就是我们需要考虑的问题。

8.4 故障隔离

当线上某个实例发生故障后,为了不影响用户,我们一般都会先留存证据,比如:线程信息、JVM 信息等,然后将这个实例重启或直接停止。然后线下根据一些信息分析故障原因,如果我能做到故障隔离,就可以直接将出问题的实例隔离,不让正常的用户请求访问到这个出问题的实例,只让指定的用户访问,这样就可以单独用特定的用户来对这个出问题的实例进行测试、故障分析等。

9.Ribbon 饥饿加载模式

从本章 第5小节 的调试过程中,我们发现了一个细节问题,就是 Ribbon 在进行客户端负载均衡时并不是在启动时就加载上下文,而是在第一次请求时才去创建,因此第一次调用会比较慢,有可能会引起调用超时。可以通过指定 Ribbon 客户端的名称,在启动时加载这些子应用程序上下文的方式,来避免这个问题。

增加配置信息如下,我们这里只需要更加载 user-service 服务:

ribbon:
  eager-load:
    enabled : true
    clients : user-service

配置完成后,在 DynamicServerListLoadBalancer#updateListOfServers 240 行打上断点,启动应用,可以看到会进入到此方法中来更新 ServerList 的服务实例信息。

客户端负载均衡-Ribbon 源码分析_第11张图片

我们来分析下这个参数是如何工作的

第一步 RibbonAutoConfiguration 自动配置类中,可以看到条件配置注解是 @ConditionalOnProperty(value = "ribbon.eager-load.enabled"),意思就是配置文件中如果有 ribbon.eager-load.enabled = true,这个 @Bean 才能生效

	@Bean
	@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
	public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
		return new RibbonApplicationContextInitializer(springClientFactory(),
				ribbonEagerLoadProperties.getClients());
	}

继续看这个 RibbonEagerLoadProperties 配置 Bean,可以看到这里面 还有一个 clients 属性

@ConfigurationProperties(prefix = "ribbon.eager-load")
public class RibbonEagerLoadProperties {
	private boolean enabled = false;
	private List<String> clients;

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public List<String> getClients() {
		return clients;
	}

	public void setClients(List<String> clients) {
		this.clients = clients;
	}
}

最后会调用 RibbonApplicationContextInitializer#initialize 方法,如果 clientNames 不为 null 才能执行后续操作,所以 clients 也需要配置才行。

	protected void initialize() {
		if (clientNames != null) {
			for (String clientName : clientNames) {
				this.springClientFactory.getContext(clientName);
			}
		}
	}

最终我们得到结论只要配置了下面两个参数,就可以在启动的时候提前进行 ServerList 的初始化

  • ribbon.eager-load.enabled = true
  • ribbon.eager-load.clients = user-service

10.配置方式自定义 Ribbon Client

描述 配置
负债均衡器操作接口 .ribbon.NFLoadBalancerClassName
负债均衡算法 .ribbon.NFLoadBalancerRuleClassName
服务器可用性检查 .ribbon.NFLoadBalancerPingClassName
服务器列表获取 .ribbon.NIWSServerListClassName
服务器列表过滤 .ribbon.NIWSServerListFilterClassName

从 1.2.0 版本开始,支持通过属性配置的方式来定义 Ribbon Client。配置格式也是标准的,clientName 就是服务名称,比如 user-service,当我们需要配置一个自定义算法的时候,那就是 user-service.ribbon.NFLoadBalancerRuleClassName = 算法类的路径。

11.参考

  • 《深入理解 Spring Cloud 与微服务架构》 方志朋

  • 《300分钟搞懂 Spring Cloud》尹吉欢

你可能感兴趣的:(Spring,Cloud,系列,负载均衡,Ribbon)