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

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

前言

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

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

项目环境

1.Ribbon 主要组件

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

在这里插入图片描述

组件的作用

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

下面我们通过 Ribbon 的使用场景来分别介绍这些组件,当我们需要通过 Ribbon 选择一个可用的服务实例信息,进行远程调用时,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 是关闭状态(这里不需要注册中心)

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

多次刷新,每次的返回结果的 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 来获取服务注册列表信息,具体的细节可以更加以上这个调用链路来进行追踪。

调试截图:

在这里插入图片描述

可以看到本示例中,在第一次调用 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 秒为周期重复执行

调试截图

在这里插入图片描述

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 <code>Server</code> 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 类关系图如下:

在这里插入图片描述

  • 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 测试效果如下:

在这里插入图片描述

或者

在这里插入图片描述

端口号随机出现。

除了通过配置文件的方式来指定自定义的负载均衡策略,也可以通过配置 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 的服务实例信息。

在这里插入图片描述

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

第一步 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

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

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

11.参考

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

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值