16. 客户端负载均衡器:Ribbon

Ribbon 是一个客户端负载均衡器,它为你提供了对 HTTP 和 TCP 客户端行为的大量控制。Feign 已经使用了 Ribbon,因此,如果你使用 @FeignClient,则此部分也适用。

Ribbon 中的一个核心概念是命名客户端。每个负载均衡器都是一组组件的一部分,这些组件共同工作以按需联系远程服务器,并且该组有一个作为应用程序开发人员提供的名称(例如,通过使用 @FeignClient 注解)。根据需要,Spring Cloud 使用 RibbonClientConfiguration 为每个命名客户端创建一个新的集成作为 ApplicationContext。它包含(除其他外)ILoadBalancer、RestClient 和 ServerListFilter。

16.1 如何包含 Ribbon

要在项目中包含 Ribbon,请使用 group ID 为 org.springframework.cloud 和 artifact ID 为 spring-cloud-starter-netflix-ribbon 的 starter。了解有关使用当前 Spring Cloud Release Train 构建系统设置的详细信息请参阅 Spring Cloud 项目页面。

16.2 自定义 Ribbon 客户端

你可以通过使用 <client>.ribbon.* 中的外部属性来配置功能区客户端的某些位,这与本机使用 Netflix API 类似,只是你可以使用 Spring Boot 配置文件。本机选项可以作为 CommonClientConfigKey(ribbon-core 的一部分)中的静态字段进行检查。

Spring Cloud 还允许你使用 @RibbonClient 声明附加配置(在 RibbonClientConfiguration 之上),从而完全控制客户端,如下例所示:

@Configuration
@RibbonClient(name = "custom", configuration = CustomConfiguration.class)
public class TestConfiguration {
}

在这种情况下,客户端由已经在 RibbonClientConfiguration 中的组件以及任何 CustomConfiguration(后者通常会覆盖前者)组成。

[Warning] Warning

CustomConfiguration 类必须是 @Configuration 类,但请注意它不在主应用程序上下文的 @ComponentScan 中。否则,它将由所有 @RibbonClients 共享。如果使用 @ComponentScan(或 @SpringBootApplication),则需要采取措施避免将其包含在内(例如,可以将其放在单独的、不重叠的包中,或者在 @ComponentScan 中指定要显式扫描的包)。

下表展示了 Spring Cloud Netflix 默认为 Ribbon 提供的 bean:

Bean 类型 Bean 名称 类名称

IClientConfig

ribbonClientConfig

DefaultClientConfigImpl

IRule

ribbonRule

ZoneAvoidanceRule

IPing

ribbonPing

DummyPing

ServerList<Server>

ribbonServerList

ConfigurationBasedServerList

ServerListFilter<Server>

ribbonServerListFilter

ZonePreferenceServerListFilter

ILoadBalancer

ribbonLoadBalancer

ZoneAwareLoadBalancer

ServerListUpdater

ribbonServerListUpdater

PollingServerListUpdater

创建其中一种类型的 bean 并将其放置在 @RibbonClient 配置中(如上面的 FooConfiguration),可以覆盖所描述的每个 bean,如下例所示:

@Configuration
protected static class FooConfiguration {
	@Bean
	public ZonePreferenceServerListFilter serverListFilter() {
		ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
		filter.setZone("myTestZone");
		return filter;
	}

	@Bean
	public IPing ribbonPing() {
		return new PingUrl();
	}
}

前面示例中的 include 语句将 NoOpPing 替换为 PingUrl,并提供自定义的 serverListFilter。

16.3 自定义所有 Ribbon 客户端的默认值

通过使用 @RibbonClients 注解并注册默认配置,可以为所有 Ribbon 客户端提供默认配置,如下例所示:

@RibbonClients(defaultConfiguration = DefaultRibbonConfig.class)
public class RibbonClientDefaultConfigurationTestsConfig {

	public static class BazServiceList extends ConfigurationBasedServerList {
		public BazServiceList(IClientConfig config) {
			super.initWithNiwsConfig(config);
		}
	}
}

@Configuration
class DefaultRibbonConfig {

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

	@Bean
	public IPing ribbonPing() {
		return new PingUrl();
	}

	@Bean
	public ServerList<Server> ribbonServerList(IClientConfig config) {
		return new RibbonClientDefaultConfigurationTestsConfig.BazServiceList(config);
	}

	@Bean
	public ServerListSubsetFilter serverListFilter() {
		ServerListSubsetFilter filter = new ServerListSubsetFilter();
		return filter;
	}

}

16.4 通过设置属性自定义 Ribbon 客户端

从 1.2.0 版本开始,Spring Cloud Netflix 现在支持与 Ribbon 文档兼容的通过属性自定义 Ribbon 客户端。

这允许你在不同环境中启动时更改行为。

以下列表展示支持的属性:

  • <clientName>.ribbon.NFLoadBalancerClassName: 应该实现 ILoadBalancer
  • <clientName>.ribbon.NFLoadBalancerRuleClassName: 应该实现 IRule
  • <clientName>.ribbon.NFLoadBalancerPingClassName: 应该实现 IPing
  • <clientName>.ribbon.NIWSServerListClassName: 应该实现 ServerList
  • <clientName>.ribbon.NIWSServerListFilterClassName: 应该实现 ServerListFilter
[Note] Note

这些属性中定义的类优先于使用 @RibbonClient(configuration=MyRibbonConfig.class) 和 Spring Cloud Netflix 提供的默认值定义的 bean。

要为名为 users 的服务名设置 IRule,可以设置以下属性:

application.yml. 

users:
  ribbon:
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

有关 Ribbon 提供的实现,请参阅 Ribbon 文档。

16.5  使用 Ribbon 与 Eureka

当 Eureka 与 Ribbon(即两者都在类路径上)一起使用时,ribbonServerList 将被 DiscoveryEnabledNIWSServerList 的扩展覆盖,该扩展填充 Eureka 中的服务器列表。它还用 NIWSDiscoveryPing 替换了 IPing 接口,NIWSDiscoveryPing 委托给 Eureka 来确定服务器是否启动。默认情况下安装的 ServerList 是 DomainExtractingServerList。它的目的是在不使用 AWS AMI 元数据(这是 Netflix 所依赖的)的情况下为负载均衡器提供元数据。默认情况下,使用实例元数据中提供的 “zone” 信息构建服务器列表(因此,在远程客户端上,设置 eureka.instance.metadataMap.zone)。如果缺少,并且设置了 approximateZoneFromHostname 标志,则可以使用服务器主机名中的域名作为区域的代理。一旦区域信息可用,就可以在 ServerListFilter 中使用它。默认情况下,它用于在与客户端相同的区域中定位服务器,因为默认值是 ZonePreferenceServerListFilter。默认情况下,以与远程实例相同的方式确定客户端的区域(即,通过 eureka.instance.metadataMap.zone)。

[Note] Note

设置客户机区域的传统 “archaius” 方法是通过名为 "@zone" 的配置属性。如果它可用,Spring Cloud 会优先使用它而不是所有其他设置(请注意,该键必须在 YAML 配置中引用)。

[Note] Note

如果没有其他区域数据源,则根据客户端配置(而不是实例配置)进行猜测。我们取 eureka.client.availabilityZones,它是一个从区域名到区域列表的映射,并拉出实例自己区域的第一个区域(即 eureka.client.region,默认为 "us-east-1",以与本机 Netflix 兼容)。

16.6 示例: 如何在没有 Eureka 的情况下使用 Ribbon

Eureka 是一种抽象远程服务器发现的方便方法,这样你就不必在客户端中硬编码它们的 URL。但是,如果你不喜欢用 Eureka,Ribbon 和 Feign 也可以。假设你已经为 "stores" 声明了 @RibbonClient,而 Eureka 没有使用(甚至在类路径上也没有)。功能区客户端默认为已配置的服务器列表。你可以提供如下配置:

application.yml. 

stores:
  ribbon:
    listOfServers: example.com,google.com

16.7 示例: 在 Ribbon 中禁用 Eureka

将 ribbon.eureka.enabled 属性设置为 false 将在 Ribbon 中显式的禁用 Eureka,如下例所示:

application.yml. 

ribbon:
  eureka:
   enabled: false

16.8 直接使用 Ribbon API

你还可以直接使用 LoadBalancerClient,如下例所示:

public class MyClass {
    @Autowired
    private LoadBalancerClient loadBalancer;

    public void doStuff() {
        ServiceInstance instance = loadBalancer.choose("stores");
        URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
        // ... do something with the URI
    }
}

16.9 Ribbon 配置缓存

每个名为 client 的 Ribbon 都有一个 Spring Cloud 维护的相应子应用程序上下文。此应用程序上下文在向命名客户端发出第一个请求时被延迟加载。通过指定功能区客户端的名称,可以将这种延迟加载行为更改为在启动时急切地加载这些子应用程序上下文,如下例所示:

application.yml. 

ribbon:
  eager-load:
    enabled: true
    clients: client1, client2, client3

16.10 如何配置 Hystrix 线程池

如果将 zuul.ribbonIsolationStrategy 更改为 THREAD,那么 Hystrix 的线程隔离策略将用于所有路由。在这种情况下,HystrixThreadPoolKey 将设置 RibbonCommand 作为默认值。这意味着所有路由的 HystrixCommands 都在同一个 Hystrix 线程池中执行。可以使用以下配置更改此行为:

application.yml. 

zuul:
  threadPool:
    useSeparateThreadPools: true

前面的示例导致在每个路由的 Hystrix 线程池中执行 HystrixCommands。

在这种情况下,默认的 HystrixThreadPoolKey 与每个路由的 service ID 相同。要向 HystrixThreadPoolKey 添加前缀,请将 zuul.threadPool.threadPoolKeyPrefix 设置为要添加的值,如下例所示:

application.yml. 

zuul:
  threadPool:
    useSeparateThreadPools: true
    threadPoolKeyPrefix: zuulgw

16.11 如何为 Ribbon 的 IRule 提供关键字

如果你需要提供自己的 IRule 实现来处理特殊的路由需求,例如 “canary” 测试,请将一些信息传递给 IRule 的 choose 方法。

com.netflix.loadbalancer.IRule.java. 

public interface IRule{
    public Server choose(Object key);
         :

你可以提供一些 IRule 实现所使用的信息来选择目标服务器,如下面的示例所示:

RequestContext.getCurrentContext()
              .set(FilterConstants.LOAD_BALANCER_KEY, "canary-test");

如果将具有 FilterConstants.LOAD_BALANCER_KEY 键的任何对象放入 RequestContext 中,则会将其传递给 IRule 实现的 choose 方法。在执行 RibbonRoutingFilter 之前,必须执行前面示例中展示的代码。Zuul 的预过滤器是最好的选择。你可以通过过滤选中的 RequestContext 访问 HTTP 头和查询参数,因此它可以用于确定传递到 Ribbon 的 LOAD_BALANCER_KEY。如果不在 RequestContext 中使用 LOAD_BALANCER_KEY 放置任何值,则会将空值作为 choose 方法的参数传递。