Spring Cloud系列之Ribbon

概述

负载均衡

负载均衡,即Load Balance,LB,通器常有两种实现手段,服务端和客户端。

负载均衡器,是带有负载均衡功能的实体(或载体),本文不做严格区分;即缩写LB,可表示负载均衡策略,也可以表示负载均衡器。

服务端LB的缺点,提供更强的流量控制权,但无法满足不同的消费者希望使用不同负载均衡策略的需求,而使用不同负载均衡策略的场景确实是存在的,客户端LB就提供这种灵活性。

客户端LB的缺点:配置不当,可能会导致服务提供者出现热点,或压根就获取不到任何服务。

另外还有一种分类:

  • 集中式LB:偏硬件。在服务的消费方和提供方之间使用独立的LB设施(硬件F5或软件Nginx),由该设施负责把访问请求通过某种策略转发到服务的提供方;
  • 进程内LB:偏软件。将LB逻辑集成到消费方,消费方从服务注册中心获取可用的服务地址,由消费方根据某种策略选择一个合适的服务提供方,如Ribbon。

本文源码基于ribbon-2.7.18版本。

Ribbon

Ribbon提供客户端的软负载均衡,Fegin和OpenFegin都是基于Ribbon实现。

Ribbon功能如下:

  • 与Eureka、Feign等集成
  • 基于Archalus完成运行时配置
  • 支持可插拔序列化

组件

Ribbon是Netflix发布的开源项目,主要功能是为REST客户端实现负载均衡。几个核心组件,除RestClient外,都是接口:

  1. ServerList:负载均衡使用的服务器列表。这个列表会缓存在负载均衡器中并定期更新。根据获取服务信息的方式不同分为:
    • 静态存储:从配置文件中获取服务节点列表并存储到本地;
    • 动态存储:从注册中心获取服务节点列表并存储到本地。
  2. ServerListFilter:服务器列表过滤器。接口,主要用于对Service Consumer获取到的服务器列表进行预过滤,过滤的结果也是ServerList。Ribbon提供多种过滤器的实现;
  3. IPing:探测服务实例是否存活的策略;
  4. IRule:负载均衡策略,其实现类表述的策略包括:轮询、随机、根据响应时间加权等;
  5. ILoadBalancer:负载均衡器。这也是一个接口,Ribbon为其提供多个实现,比如ZoneAwareLoadBalancer。而上层代码通过调用其API进行服务调用的负载均衡选择。一般ILoadBalancer的实现类中会引用一个IRule;
  6. ServerListUpdater:用于更新服务列表
  7. RestClient:服务调用器,负载均衡后Ribbon向Service Provider发起REST请求的工具;在ribbon-httpclient-2.3.0里已经废弃

Ribbon工作流程:

  • 优先选择在同一个Zone且负载较少的Eureka Server;
  • 定期从Eureka更新并过滤服务实例列表;
  • 根据用户指定的策略,在从Server取到的服务注册列表中选择一个实例的地址;
  • 通过RestClient进行服务调用。

实战

如果是Spring Cloud项目,引入如下依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>

此GAV是最后一个版本,已不再发布新版本。

如果是非Spring Cloud项目使用Ribbon:

<dependency>
	<groupId>com.netflix.ribbon</groupId>
	<artifactId>ribbon-loadbalancer</artifactId>
	<version>2.7.18</version>
</dependency>

不是很常见。

使用

主要是使用@RibbonClient和@LoadBalanced两个注解:

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

以及在启动类上添加:

@RibbonClient(name = "demo-service", configuration = MyRule.class)

其中MyRule是实现IRule的自定义的负载均衡算法。

配置

有注解配置和文件配置两种方式:

@Bean
public IRule rule() {
	return new BestAvailableRule();
}

application.yml配置:

ribbon:
  eager-load:
     #开启饥饿加载模式
    enabled: true
     #指定需要饥饿加载的服务名
  # http建立socket超时时间,毫秒
  ConnectTimeout: 2000
  # http读取响应socket超时时间
  ReadTimeout: 5000
  # 同一台实例最大重试次数,不包括首次调用
  MaxAutoRetries: 0
  # 重试负载均衡其他的实例最大重试次数,不包括首次server
  MaxAutoRetriesNextServer: 1
  # 是否所有操作都重试,POST请求注意多次提交错误。
  # 默认false,设定为false的话,只有get请求会重试
  OkToRetryOnAllOperations: false 

对应的配置类有:RibbonEagerLoadProperties和RibbonProperties。前者就2个配置,相对简单,且很好理解:

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

RibbonProperties用于存储与访问IClientConfig,IClientConfig是一个接口,其实现类AbstractDefaultClientConfigImpl已废弃;另一个实现类ReloadableClientConfig,提供一大堆get或get变形方法,传参是IClientConfigKey。IClientConfigKey也是一个接口,他的非匿名非内部类的实现类就一个,CommonClientConfigKey。这是一个抽象泛型类,支持各种类型的参数,可以看到这个类里面定义很多参数,设置参数的两种形式:

public static final IClientConfigKey<Boolean> OkToRetryOnAllOperations = new CommonClientConfigKey<Boolean>("OkToRetryOnAllOperations", false) {};

以及通过static静态代码块的形式:

public static final IClientConfigKey<Integer> ConnectionCleanerRepeatInterval;
static {
	ConnectionCleanerRepeatInterval = new CommonClientConfigKey<Integer>("ConnectionCleanerRepeatInterval", 30000) {};
}

构造方法是两个参数时,如上面两个例子,第一个参数表示配置Key,第二个参数表示Value默认值。

注解

Spring Cloud提供的@RibbonClient

@Configuration(proxyBeanMethods = false)
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClient {
	// value和name用于指定服务名
	String value() default "";
	String name() default "";
	Class<?>[] configuration() default {};
}

RibbonClients是数组形式的RibbonClient。

@RibbonClientName则

@Value("${ribbon.client.name}")
public @interface RibbonClientName {
}

Spring Cloud提供的@LoadBalanced是一个使用@Qualifier的空注解:

@Qualifier
public @interface LoadBalanced {
}

进阶

IRule策略

public interface IRule {
	/*
	 * choose one alive server from lb.allServers or
	 * lb.upServers according to key
	 */
	Server choose(Object key);
	void setLoadBalancer(ILoadBalancer lb);
	ILoadBalancer getLoadBalancer();    
}

自定义:可以实现IRule,也可以继承RoundRibbonRule。

自定义配置类如果放在@ComponentScan能扫描到的路径下,则会被所有的Ribbon客户端所使用,如果想某个客户端使用自定义配置类,则需要加以调整。

其类结构图
在这里插入图片描述

内置7种负载均衡策略:

  • RoundRobinRule:轮询策略,按照一定的顺序依次调用服务实例
  • WeightedResponseTimeRule:权重策略,根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。它的实现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大
  • RandomRule:随机策略,从服务提供者的列表中随机选择一个服务实例
  • BestAvailableRule:最小连接数策略,最小并发数策略,遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,会调用轮询策略进行选取
  • RetryRule:重试策略,按照轮询策略来获取服务,如果获取的服务实例为null或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回null
  • AvailabilityFilteringRule:可用性敏感策略,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例
  • ZoneAvoidanceRule:区域敏感策略,根据服务所在区域的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似

自动配置

直接看LoadBalancerAutoConfiguration源码:

@AutoConfiguration
@Conditional(BlockingRestClassesPresentCondition.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
public class LoadBalancerAutoConfiguration {
	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();
	
	@Autowired(required = false)
	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
	
	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
			ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
			for (RestTemplate restTemplate : restTemplates) {
				for (RestTemplateCustomizer customizer : customizers) {
					customizer.customize(restTemplate);
				}
			}
		});
	}

	@Bean
	@ConditionalOnMissingBean
	public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
		return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
	}
}

RestTemplateCustomizer的注入在LoadBalancerInterceptorConfig类里面:

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

看到LoadBalancerInterceptor,这是Ribbon中的默认拦截器,当调用RestTemplate的getObject方法时,就会调用拦截器中的方法:

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

继续看RibbonLoadBalancerClient的execute方法:

public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
	ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
	Server server = getServer(loadBalancer, hint);
	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);
}

getLoadBalancer方法用于获取LB,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");
}

最终会调用ILoadBalancer.chooseServer方法。
ILoadBalancer是一个顶层接口:

public interface ILoadBalancer {
	// 添加服务器
	void addServers(List<Server> newServers);
	// 选择服务器
	Server chooseServer(Object key);
	// 下线某个服务器(不是立马下线,而是直到下一个ping周期才下线)
	void markServerDown(Server server);
	@Deprecated
	public List<Server> getServerList(boolean availableOnly);
    // 返回所有在线的和可达的
    public List<Server> getReachableServers();
	// 返回所有已知的服务器,包括可达的和不可达的
	public List<Server> getAllServers();
}

其实现类图如下:
在这里插入图片描述
回到前面chooseServer方法,具体选择哪个实现类的方法:
在这里插入图片描述
可以通过在每个实现类里都加上断点来调试加以判断。

也可以推理,肯定不是NoOpLoadBalancer。当然,这需要一点点源码阅读经验,在众多源码里,NopXxxNoOpXxx表示空实现,如SLF4J里的slf4j-nop这个JAR包只有一个类,即实现org.slf4j.spi.SLF4JServiceProvider的NOPServiceProvider。又或者Spring Security里PasswordEncoder的实现类NoOpPasswordEncoder里根本就没有任何密码加密策略,密码直接明文存储。看NoOpLoadBalancer的源码,几个方法都是空实现,便能得以验证。

但,不管是BaseLoadBalancer还是ZoneAwareLoadBalancer,都需要被初始化。是在RibbonClientConfiguration类中被加载的:

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
	if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
		return this.propertiesFactory.get(ILoadBalancer.class, config, name);
	}
	// 几大核心组件
	return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, serverListFilter, serverListUpdater);
}

可知使用ZoneAwareLoadBalancer。通过IDEA定位到构造函数,有个super方法,即执行父类DynamicServerListLoadBalancer的构造函数,最后调用里面的restOfInit方法:

void restOfInit(IClientConfig clientConfig) {
    boolean primeConnection = this.isEnablePrimingConnections();
    // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
    this.setEnablePrimingConnections(false);
    // 调用PollingServerListUpdater.start方法
    enableAndInitLearnNewServersFeature();
    // 执行ping逻辑等
    updateListOfServers();
    if (primeConnection && this.getPrimeConnections() != null) {
        this.getPrimeConnections().primeConnections(getReachableServers());
    }
    this.setEnablePrimingConnections(primeConnection);
}

请求拦截

基于上述源码分析,得出总结:

  • Ribbon拦截所有标注@LoadBalanced注解的RestTemplate;
  • 将Ribbon默认的拦截器LoadBalancerInterceptor添加到RestTemplate的执行逻辑中,当RestTemplate每次发送HTTP请求时,都会被Ribbon拦截
  • 拦截后,Ribbon会创建一个ILoadBalancer实例
  • ILoadBalancer实例会使用RibbonClientConfiguration完成自动配置:IRule,IPing,ServerList
  • Ribbon会从服务列表中选择一个服务,将请求转发给这个服务

ServerListUpdater

ServerListUpdater是一个接口,实现类为PollingServerListUpdater:

public class PollingServerListUpdater implements ServerListUpdater {
	// 用于构造函数传参
	private static long LISTOFSERVERS_CACHE_UPDATE_DELAY = 1000; // msecs;
	private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs;

	@Override
	public synchronized void start(final UpdateAction updateAction) {
		if (isActive.compareAndSet(false, true)) {
			final Runnable wrapperRunnable = () ->  {
				if (!isActive.get()) {
		            if (scheduledFuture != null) {
		                scheduledFuture.cancel(true);
		            }
		            return;
		        }
		        try {
		            updateAction.doUpdate();
		        } catch (Exception e) {
					// just logging
				}
			};
			scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
		            wrapperRunnable,
		            initialDelayMs, // 1000
		            refreshIntervalMs, // 30000
		            TimeUnit.MILLISECONDS
			);
		} else {
			// just logging
		}
	}
}

30秒更新一次服务器信息。

拓展

Ribbon、Feign、Nginx三者负载均衡区别

不要笑,这是一道面试官的题目。

前面提过,Nginx是集中式LB。重点比较Ribbon和Feign,实际上Feign基于Ribbon。

参考

posted @ 2024-08-20 10:06  johnny233  阅读(35)  评论(0编辑  收藏  举报