Ribbon 是 Netflix 开源的内置了软件负载均衡器的进程间通信(远程调用)库。支持负载均衡、容错处理、异步和响应式模型中的多协议(HTTP、TCP、UDP)支持、缓存和批处理。
目前行业常见的负载均衡方案分两种:一种是集中式负载均衡,在消费者与服务提供方中间使用独立的代理方式进行负载,有根据 IP 的硬件负载(如 F5,Array), 有软件的负载(如 Nginx,LVS等);另一种是客户端自己做负载均衡,根据自己对目标的请求做负载,Ribbon 就是属于客户端侧的负载均衡。
后续要讲到的 Rest 客户端 Feign 也是基于 Ribbon 实现的。
Spring Cloiud - Client Side Load Balancer: Ribbon 文档,Netflix Ribbon 官方文档,Ribbon 负载均衡文档,
Netflix Ribbon
Ribbon 模块
- ribbon:在 ribbon 模块和 Hystrix 基础之上,集成了 负载均衡、容错处理、缓存/批处理 的 APIs。
- ribbon-loadbalancer:负载均衡模块,可以独立使用,也可和其它模块一起使用。
- ribbon-eureka:基于 Eureka 封装的模块,可快速方便地与 Eureka 集成,为云提供动态服务器列表 APIs。
- ribbon-transport:使用具有负载均衡功能的 RxNetty 进行客户端传输,支持 HTTP、TCP和UDP协议。
- ribbon-httpclient:基于 Apache HttpClient 集成了负载均衡的 Rest 客户端。
- ribbon-example:Ribbon 使用代码示例。
- ribbon-core:Ribbon 核心功能和代码,客户端 APIs 配置和其它 APIs 定义。
Ribbon 单独使用
使用原生 Ribbon 实现客户负载均衡
添加 Ribbon 依赖
<dependency> <groupId>com.netflix.ribbon</groupId> <artifactId>ribbon</artifactId> <version>2.2.2</version> </dependency>
负载均衡调用演示
@RestController @RequestMapping("/consumer1") public class ConsumerController { /** * ribbon 单独使用负载均衡 */ @GetMapping("/ribbon") public String ribbon() { ArrayList<Server> serverList = Lists.newArrayList( new Server("localhost", 8001), new Server("localhost", 8002)); //构建负载实例 ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList); //调用5次来测试结果 for (int i = 0; i < 5; i++) { String result = LoadBalancerCommand.<String>builder() .withLoadBalancer(loadBalancer) .build() .submit(new ServerOperation<String>() { @Override public Observable<String> call(Server server) { // String address = "http://" + server.getHost() + ":" + server.getPort(); String address = "http://" + server.getHost() + ":" + server.getPort() + "/service/home"; System.out.println("调用地址:" + address); String body = ""; try { URL url = new URL(address); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.connect(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { InputStream is = connection.getInputStream(); StringBuffer sb = new StringBuffer(); byte[] buffer = new byte[1024]; int len = 0; while ((len = is.read(buffer)) != -1) { String str = new String(buffer, Charset.forName("utf-8")); sb.append(str); } body = sb.toString(); is.close(); connection.disconnect(); } return Observable.just(body); } catch (Exception e) { return Observable.error(e); } } }).toBlocking().first(); System.out.println("调用结果:" + result); } return null; } } //调用结果 调用地址:http://localhost:8002/service/home 调用结果:DESKTOP-G05G2748002SAKILA-SERVICE2 调用地址:http://localhost:8001/service/home 调用结果:DESKTOP-G05G2748001SAKILA-SERVICE1 调用地址:http://localhost:8002/service/home 调用结果:DESKTOP-G05G2748002SAKILA-SERVICE2 调用地址:http://localhost:8001/service/home 调用结果:DESKTOP-G05G2748001SAKILA-SERVICE1 调用地址:http://localhost:8002/service/home 调用结果:DESKTOP-G05G2748002SAKILA-SERVICE2
负载均衡官方示例
public class URLConnectionLoadBalancer { private final ILoadBalancer loadBalancer; // retry handler that does not retry on same server, but on a different server private final RetryHandler retryHandler = new DefaultLoadBalancerRetryHandler(0, 1, true); public URLConnectionLoadBalancer(List<Server> serverList) { loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList); } public String call(final String path) throws Exception { return LoadBalancerCommand.<String>builder() .withLoadBalancer(loadBalancer) .build() .submit(new ServerOperation<String>() { @Override public Observable<String> call(Server server) { URL url; try { url = new URL("http://" + server.getHost() + ":" + server.getPort() + path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); return Observable.just(conn.getResponseMessage()); } catch (Exception e) { return Observable.error(e); } } }).toBlocking().first(); } public LoadBalancerStats getLoadBalancerStats() { return ((BaseLoadBalancer) loadBalancer).getLoadBalancerStats(); } public static void main(String[] args) throws Exception { URLConnectionLoadBalancer urlLoadBalancer = new URLConnectionLoadBalancer(Lists.newArrayList( new Server("www.taobao.com", 80), new Server("www.baidu.com", 80), new Server("www.jd.com", 80))); for (int i = 0; i < 6; i++) { System.out.println(urlLoadBalancer.call("/")); } System.out.println("=== Load balancer stats ==="); System.out.println(urlLoadBalancer.getLoadBalancerStats()); } }
Spring Cloud Ribbon
Spring Cloud 根据 RibbonClientConfiguration 为每个命名的客户端创建了一个新的集成作为 ApplicationContext 。它包含 ILoadBalancer、一个 RestClient、一个 ServerListFilter。
引入 Ribbon 依赖
在 pom.xm 文件添加 spring-cloud-starter-netflix-ribbon 依赖包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
在 Eureka 中使用 Ribbon
当 Eureka 与 Ribbon 一起使用时(即两者都在类路径上,即都引入了两者的依赖包),ribbonServerList 将被DiscoveryEnabledNIWSServerList 的扩展覆盖,该扩展为 Eureka 的服务器列表。
还用 NIWSDiscoveryPing 替换 IPing 接口,NIWSDiscoveryPing 委托 Eureka 确定服务是否启动。
默认情况下安装的 ServerList 是 DomainExtractingServerList,DomainExtractingServerList 实现的是 Ribbon 负载均衡模块 loadbalancer 里的 ServerList<DiscoveryEnabledServer>
接口,目的是在不使用 AWS AMI 元数据(这是 Netflix 所依赖的) 的情况下为负载均衡器提供元数据。
默认情况下,Server List 使用实例元数据提供的 zone 信息构建(在远程客户端上,设置 eureka.instance.metadatamap.zone)。如果缺少该标志,但设置开启了 approximateZoneFromHostname ,则可以使用服务器主机名的域名作为 zone 的代理(映射)。一旦 zone 信息可用,就可以 ServletListFilter 中使用它。
默认情况下,这些 zone 元数据用于定位与客户端在相同区域的服务器,默认是在 ZonePreferenceServerListFilter 过滤器中处理 zone 信息。默认情况下,确定客户端所在的区域与远程实例的方式相同,即通过 eureka.instance.metadataMap.zone 确定。(此两条:即根据客户端定位服务器和根据服务器定位客户端所在的区域)。
注意:如何使用了外部配置:archaius ,通过 @zone 注解属性设置客户端所在区域。如果配置可用,Spring Cloud 会优先使用它。
RestTemplate 集成 Ribbon
RestTemplate 实际上是在 Eureak 基础上整合 Ribbon;应用作为 eureka-client 注册到 eureka-server 服务器。
客户端应用引入Spring Cloud eureka-client 和 ribbon 包。
客户端应用使用 RestTemplate 调用远程服务,在 RestTemplate Bean 上添加负载均衡注解 @LoadBalanced。
/** * RestTemplate配置类 */ @Configuration public class RestTemplateConfig { @Bean(name = "restTemplateOne") public RestTemplate restTemplateOne() { //设置超时时间,毫秒 return new RestTemplateBuilder().setConnectTimeout(Duration.ofMillis(1000)).setReadTimeout(Duration.ofMillis(1000)).build(); } @Bean(name = "restTemplateTwo") @LoadBalanced public RestTemplate restTemplateTwo(){ //设置超时时间,毫秒 return new RestTemplateBuilder().setConnectTimeout(Duration.ofMillis(1000)).setReadTimeout(Duration.ofMillis(1000)).build(); } }
远程服务创建接口,并使用不同端口运行多个实例。
spring.application.name=sakila-service1
@RestController @RequestMapping("/service") public class HomeController { @Value("${server.port}") private String port; @GetMapping("/port") public String serverPort(){ return port; } }
客户端应用通过 Eureka 调用远程服务接口。
实际是通过 Eureka 服务发现,使用 Eureka 实例名的链接来调用,而不是调用指定端口的服务。@RestController @RequestMapping("/consumer1") public class ConsumerController { @Autowired private RestTemplate restTemplateOne; @Autowired private RestTemplate restTemplateTwo; @GetMapping("/home") public String callHome() { //直接调用服务接口 String url1 = "http://localhost:8001/service/home"; String str1 = restTemplateOne.getForObject(url1, String.class); //通过Eureka来调用服务接口 String url2 = "http://sakila-service1/service/port"; String str2 = restTemplateTwo.getForObject(url2, String.class); System.out.println("调用结果:" + str2); return "调用结果:" + str2; } }
多次调用远程服务接口的结果:
调用结果:8001 调用结果:8002 调用结果:8001 调用结果:8002 调用结果:8001 调用结果:8002
@LoadBalanced 原理
在 RestTemplate 上加了一个 @LoadBalanced 注解就可以负载均衡。这是因为 Spring Cloud 做了大量的底层封装,做了很多简化。
内部的主要逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行了替换,或者根据具体的负载策略选择服务地址,然后去调用,这就是 @LoadBalanced 的原理。
Spring Web 为 HttpClient 提供了 Request 拦载器 ClientHttpRequestInterceptor,位于 spring-web jar 包下。
在 spring-cloud-commons 包中提供了负载均衡自动配置类 LoadBalancerAutoConfiguration ,里面维护了一个 @LoadBalanced 注解的 RestTemplate 列表,里面的静态类 LoadBalancerInterceptorConfig 注册了 负载均衡拦截器 LoadBalancerInterceptor,RestTemplateCustomizer 来添加拦截器列表。
负载均衡拦截器 LoadBalancerInterceptor 实现了 ClientHttpRequestInterceptor,主要逻辑在 intercept() 方法中,执行交给了 LoadBalancerClient,通过 LoadBalancerRequestFactory 来构建一个 LoadBalancerRequest 对象,createRequest 方法中通过 ServiceRequestWrapper 来执行替换 URI 的逻辑,核心是通过 reconstructURI() 方法实现,该方法的业务实现是在 RibbonLoadBalancerClient 类中 。
Spring Cloud Ribbon 配置
Ribbon 饥饿加载
在进行服务调用时,如果网络不好,第一次调用会超时,这是因为 Ribbon 客户端在第一次请求的时候初始化,如果超时时间比较短的话,初始化 Client 的时间再加上请求接口的时间,就会导致第一次请求超时。
spring-cloud-netflix-ribbon 提供了饥饿加载模式,在服务启动时就执行 Ribbon 客户端初始化,配置如下:
#开启饥饿加载模式
ribbon.eager-load.enabled=true
#指定需要饥饿加载的服务名,允许多个,用逗号隔开
ribbon.eager-load.clients=
负载均衡策略
Ribbon 默认的负载策略是轮询,同时也提供了很多其他的策略能够让用户根据业务需求来选择。负载均衡策略的根接口是 com.netflix.loadbalancer.IRule。
BestAvailableRule
选择最小并发请求的服务器,每个客户端都会获得一个随机的服务器列表,如果服务器被标记为错误,则跳过。AvailabilityFilteringRule
用于过滤连接一直失败或读取失败而被标记为 circuit breaker tripped 状态的服务;或持有超过可配置限制的活动连接(默认值为 Integer.MAX_VALUE),即过滤掉高并发的后端服务。实际就是检查获取到的服务列表里,各个 Server 的 Status 。ZoneAvoidanceRule
根据区域和可用性来过滤服务器。使用 ZoneAvoidancePredicate 来判断 Zone 的使用是否达到阀值,过滤出最差 Zone 中的所有服务器; AvailabilityPredicate 用于过滤出并发连接过多的服务。RandomRule
随机分配流量,即随机选择一个 Server。RetryRule
向现有负载均衡策略添加重试机制。ResponseTimeWeightedRule
该策略已过期,同见 WeightedResponseTimeRule。WeightedResponseTimeRule
根据响应时间为每个服务器动态分配权重(Weight)分,然后台加权循环的方式使用该策略。响应时间越长,权重越低,被选中可能性越低。
自定义负载均衡策略
创建一个实现负载均衡策略 IRule 接口的类,重写 choose() 方法,在方法内部定义服务选择逻辑。
public class MyLoadBalancerRule implements IRule {
private ILoadBalancer iLoadBalancer;
@Override
public Server choose(Object key) {
List<Server> serverList = iLoadBalancer.getAllServers();
for (Server server : serverList) {
//这里定义选择服务策略
System.out.println(server.getHostPort());
}
return serverList.get(0);
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
}
@Override
public ILoadBalancer getLoadBalancer() {
return null;
}
}
在 Spring Cloud 中,可通过配置的方式使用自定义的负载策略,sakila-service 是调用的服务名称,如下,
sakila-service.ribbon.NFLoadBalancerRuleClassName = com.xxx.xxx.ILoadBalancer
//或使用已提供的其它策略
sakila-service.ribbon.NFLoadBalancerRuleClassName = com.netflix.loadbalancer.RandomRule
或者自定义配置,将其注册为 IRule 类型的 Bean。
自定义 Ribbon 客户端
可以在 Spring Boot 配置文件中,使用 <client>.ribbon.*
来配置 Ribbon 客户端。Spring Cloud 还允许使用 @RibbonClient 声明其它配置(在 RibbonClientConfiguration 上)来完全控制客户端。如下示例:
@Configuration
@RibbonClient(name = "custom", configuration = CustomConfiguration.class)
public class TestConfiguration {
}
这样,客户端由 RibbonClientConfiguration 中已存在的组件和 CustomConfiguration(通常会覆盖前者) 中的任何组件组成。
注意: CustomConfiguration 类必须是 @Configuration 注解的类;不能存在于 主 application context 的 @ComponentScan 中。否则,将由所有 @RibbonClients 共享。
Spring Cloud Netflix 默认为 Ribbon 提供的 Bean 包含 :
Bean Type | Bean Name | Class Name |
---|---|---|
IClientConfig | ribbonClientConfig | DefaultClientConfigImpl |
IRule | ribbonRule | ZoneAvoidanceRule |
IPing | ribbonPing | DummyPing |
ServerList<Server> |
ribbonServerList | ConfigurationBasedServerList |
ServerListFilter<Server> |
ribbonServerListFilter | ZonePreferenceServerListFilter |
ILoadBalancer | ribbonLoadBalancer | ZoneAwareLoadBalancer |
ServerListUpdater | ribbonServerListUpdater | PollingServerListUpdater |
也可以自定义这些 Bean 来覆盖默认的,并将这些自定义的 Bean 放置在 @RibbonClient 注解的配置类中。
@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();
}
}
自定义所有默认配置
为使用 @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;
}
}
属性自定义 Ribbon 客户端
从版本1.2.0开始,Spring Cloud Netflix 现在支持设置与 Ribbon 文档兼容的属性来自定义Ribbon客户端。
这样可以在不同的启动环境中使用相应的配置。
支持的属性列表:
<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter
注意: 这些属性中定义的类的使用优先于 @RibbonClient(configuration = MyRibbonConfig.class) 定义的 bean 以及 Spring Cloud Netflix 提供的默认值。
要为名称为 users 的服务的设置 IRule,可以设置如下属性:
#application.yml
users:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
在 Ribbon 中禁用 Eureka
将 Ribbon.eureka.enabled 属性设置为 false 将显式禁用在 Ribbon 中使用 eureka,就不能使用服务名去调用接口了,必须硬编码指定服务器地址。
ribbon.eureka.enabled=false
直接使用 Ribbon API
也可以直接使用 LoadBalancerClient。Ribbon 自动配置类 RibbonAutoConfiguration 注册了 LoadBalancerClient Bean。
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
}
}
无 Eureka 使用 Ribbon
Eureka 是一种抽象远程服务发现的便捷方式(组件),这样就无需在客户端对远程服务的 URL 进行硬编码。
如果不想使用 Eureka ,Ribbon 和 Feign 仍可使用。假设已为 stores 声明了 @RibbonClient,并且未使用 Eureka (甚至未引入 Eureka 包),Ribbon 客户端默认为已配置的服务器列表。可使用如下配置:
stores.ribbon.listOfServers=localhost:8081,localhost:8082,example.com,google.com
无 Eureka 作为服务发现,该配置的前缀是服务名称,配置后即可使用服务名称来调用接口。
客户端请求重试机制
集群环境,是多个节点提供相同的服务,若某个节点故障,该节点就无法提供服务,但 Eureka 服务列表还没更新,客户端拿到的可能是已经故障的服务信息,就发导致请求失败。
重试机制就是当 Ribbon 发现请求的服务不可到达时,重新请求另外的服务。
RetryRule 重试
利用 Ribbon 自带的重试策略进行重试,只需要指定某个服务的负载策略为重试策略即可:
<clientName>.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
Spring Retry 重试
还可通过基础 Spring Retry 来执行重试操作,需要引入 spring-retry 和 spring-boot-starter-aop 依赖包。
# 请求重试 ## 对当前实例重试次数,默认0 ribbon.maxAutoRetries=1 ## 切换实例的重试次数,默认1 ribbon.maxAutoRetriesNextServer=1 ## 对所有操作请求都进行重试,默认false ribbon.okToRetryOnAllOperations=true
Ribbon 超时设置
- 客户端默认配置在 com.netflix.client.config.DefaultClientConfigImpl 配置类上。
- 对配置类的调用大多是在 com.netflix.loadbalancer.LoadBalancerContext 类上。
# 超时时间
## 请求连接超时时间
ribbon.connectTimeout=2000
## 请求处理超时时间
ribbon.readTimeout=5000
注意:本文归作者所有,未经作者允许,不得转载