Spring Cloud(四):客户端负载均衡 Ribbon
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 依赖
1
2
3
4
5<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
<version>2.2.2</version>
</dependency>负载均衡调用演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class ConsumerController {
/**
* 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>() {
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负载均衡官方示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45public 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>() {
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 依赖包
1 | <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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* RestTemplate配置类
*/
public class RestTemplateConfig {
public RestTemplate restTemplateOne() {
//设置超时时间,毫秒
return new RestTemplateBuilder().setConnectTimeout(Duration.ofMillis(1000)).setReadTimeout(Duration.ofMillis(1000)).build();
}
public RestTemplate restTemplateTwo(){
//设置超时时间,毫秒
return new RestTemplateBuilder().setConnectTimeout(Duration.ofMillis(1000)).setReadTimeout(Duration.ofMillis(1000)).build();
}
}远程服务创建接口,并使用不同端口运行多个实例。
1
sakila-service1 =
1
2
3
4
5
6
7
8
9
10
11
12
public class HomeController {
private String port;
public String serverPort(){
return port;
}
}客户端应用通过 Eureka 调用远程服务接口。
实际是通过 Eureka 服务发现,使用 Eureka 实例名的链接来调用,而不是调用指定端口的服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ConsumerController {
private RestTemplate restTemplateOne;
private RestTemplate restTemplateTwo;
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;
}
}多次调用远程服务接口的结果:
1
2
3
4
5
6调用结果: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 客户端初始化,配置如下:
1 | #开启饥饿加载模式 |
负载均衡策略
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() 方法,在方法内部定义服务选择逻辑。
1 | public class MyLoadBalancerRule implements IRule { |
在 Spring Cloud 中,可通过配置的方式使用自定义的负载策略,sakila-service 是调用的服务名称,如下,
1 | com.xxx.xxx.ILoadBalancer = |
或者自定义配置,将其注册为 IRule 类型的 Bean。
自定义 Ribbon 客户端
可以在 Spring Boot 配置文件中,使用 <client>.ribbon.*
来配置 Ribbon 客户端。Spring Cloud 还允许使用 @RibbonClient 声明其它配置(在 RibbonClientConfiguration 上)来完全控制客户端。如下示例:
1 |
|
这样,客户端由 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 注解的配置类中。
1 |
|
自定义所有默认配置
为使用 @RibbonClients 注解并注册默认配置的所有 Ribbon 客户端提供默认配置,如下所示:
1 |
|
属性自定义 Ribbon 客户端
从版本1.2.0开始,Spring Cloud Netflix 现在支持设置与 Ribbon 文档兼容的属性来自定义Ribbon客户端。
这样可以在不同的启动环境中使用相应的配置。
支持的属性列表:
1 | <clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer |
注意: 这些属性中定义的类的使用优先于 @RibbonClient(configuration = MyRibbonConfig.class) 定义的 bean 以及 Spring Cloud Netflix 提供的默认值。
要为名称为 users 的服务的设置 IRule,可以设置如下属性:
1 | #application.yml |
在 Ribbon 中禁用 Eureka
将 Ribbon.eureka.enabled 属性设置为 false 将显式禁用在 Ribbon 中使用 eureka,就不能使用服务名去调用接口了,必须硬编码指定服务器地址。
1 | false = |
直接使用 Ribbon API
也可以直接使用 LoadBalancerClient。Ribbon 自动配置类 RibbonAutoConfiguration 注册了 LoadBalancerClient Bean。
1 | public class MyClass { |
无 Eureka 使用 Ribbon
Eureka 是一种抽象远程服务发现的便捷方式(组件),这样就无需在客户端对远程服务的 URL 进行硬编码。
如果不想使用 Eureka ,Ribbon 和 Feign 仍可使用。假设已为 stores 声明了 @RibbonClient,并且未使用 Eureka (甚至未引入 Eureka 包),Ribbon 客户端默认为已配置的服务器列表。可使用如下配置:
1 | localhost:8081,localhost:8082,example.com,google.com = |
无 Eureka 作为服务发现,该配置的前缀是服务名称,配置后即可使用服务名称来调用接口。
客户端请求重试机制
集群环境,是多个节点提供相同的服务,若某个节点故障,该节点就无法提供服务,但 Eureka 服务列表还没更新,客户端拿到的可能是已经故障的服务信息,就发导致请求失败。
重试机制就是当 Ribbon 发现请求的服务不可到达时,重新请求另外的服务。
RetryRule 重试
利用 Ribbon 自带的重试策略进行重试,只需要指定某个服务的负载策略为重试策略即可:
1
com.netflix.loadbalancer.RetryRule =
Spring Retry 重试
还可通过基础 Spring Retry 来执行重试操作,需要引入 spring-retry 和 spring-boot-starter-aop 依赖包。
1
2
3
4
5
6
7# 请求重试
## 对当前实例重试次数,默认0
1 =
## 切换实例的重试次数,默认1
1 =
## 对所有操作请求都进行重试,默认false
true =
Ribbon 超时设置
- 客户端默认配置在 com.netflix.client.config.DefaultClientConfigImpl 配置类上。
- 对配置类的调用大多是在 com.netflix.loadbalancer.LoadBalancerContext 类上。
1 | # 超时时间 |
Spring Cloud(四):客户端负载均衡 Ribbon
http://blog.gxitsky.com/2019/03/03/SpringCloud-04-client-loadbalancer-ribbon-1/