本期目录
- 1. 负载均衡原理
- 1.1 总体流程
- 1.2 源码解析
- 2. 负载均衡策略
- 2.1 负载均衡策略继承关系
- 2.2 负载均衡策略描述
- 1)ZoneAvoidanceRule
- 2)AvailabilityFilteringRule
- 2.3 修改负载均衡策略方式
- 1)全局修改
- 2)局部修改
- 3. 饥饿加载
- 3.1 背景
- 3.2 启用饥饿加载
- 3.3 对比测试
1. 负载均衡原理
1.1 总体流程
- 上一章的案例中,订单微服务
order-service
发起的远程调用 URLhttp://userservice/user/1
并不是一个真实的 URL 。在浏览器中无法访问该 URL 。 - 这是因为,这个 HTTP 请求被 Ribbon 拦截,由 Ribbon 解析 URL 中的
/userservice
,根据注册服务名称前往 Eureka 的注册列表中查询这个服务名称下的所有服务实例的真实 IP 地址和端口。然后 Ribbon 根据 Eureka 返回的服务实例名单,采用某种负载均衡策略从中选取一个服务实例进行调用。
1.2 源码解析
-
订单微服务
OrderApplication.java
发起的 HTTP 远程调用请求,首先被 Spring Cloud 的 commons 包下的LoadBalancerInterceptor.java
负载均衡拦截器拦截下来。LoadBalancerInterceptor.java
实现了 Spring Web 包下的ClientHttpRequestInterceptor.java
客户端 HTTP 请求拦截器接口,它拦截所有客户端的 HTTP 请求。而RestTemplate.java
正是发起客户端 HTTP 请求,因此会被这个拦截器拦截。ClientHttpRequestInterceptor.java
接口定义了一个方法intercept()
。@FunctionalInterface public interface ClientHttpRequestInterceptor { ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException; }
-
再回来查看这个接口的实现类
LoadBalancerInterceptor.java
是如何实现intercept()
方法的。 -
LoadBalancerInterceptor.java
:@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, this.requestFactory.createRequest(request, body, execution)); }
在这个方法上打断点进行逐行 Debug 。在 Postman 中发起一个查询订单的请求
http://localhost:8080/order/101
。 -
可以看到
LoadBalancerInterceptor.java
的职责是拦截RestTemplate
发起的 HTTP 请求;然后取出这个 HTTP 请求的 URL 中的微服务名称userservice
;最后,把截取到的微服务名称userservice
输出给this.loadBalancer
的execute()
方法来处理。鼠标悬停在
this.loadBalancer
上可以看到,它是RibbonLoadBalancerClient.java
的对象,到这里,我们终于见到 Ribbon 负载均衡器本体了。我们点进this.loadBalancer
的execute()
方法中去看。 -
点击后就进入了
RibbonLoadBalancerClient.java
的execute()
方法。再点进,就进入了真正的RibbonLoadBalancerClient.java
的execute()
方法。在这里,微服务名称被称为
serviceId
,输入到getLoadBalancer()
方法中,此方法根据微服务名称向 Eureka 拉取服务列表,返回一个名为DynamicServerListLoadBalancer
的动态服务器列表负载均衡器。点开查看其属性
allServerList
,两个用户微服务UserApplication
的 IP 地址和端口赫然在列。说明此时 Ribbon 已经成功向 Eureka 拿到用户微服务的服务实例列表了。接下来,就应该在拉取到的服务列表中,采用负载均衡策略选取出一个服务实例。这就是下一行代码中
getServer()
方法做的事。我们点进这个方法查看。点进
getServer()
方法中,可以看到其调用了chooseServer()
方法,顾名思义该方法用于选取服务实例。继续点进这个方法。 -
就进入了区域感知负载均衡器
ZoneAwareLoadBalancer.java
的chooseServer()
方法。发现它调用了父类BaseLoadBalancer.java
的chooseServer()
方法。继续点进这个方法。 -
点进来
BaseLoadBalancer.java
后往下走,我们就看到了调用rule.choose()
方法。rule
是负载均衡策略,点击rule
属性查看其是什么类的对象。可以看到,
rule
是策略接口IRule.java
的对象。默认的负载均衡策略是ZoneAvoidanceRule
(区域回避策略,在下一节详细讲解) 。按 Ctrl + H 查看
IRule.java
接口的所有实现类。其中有我们熟悉的RoundRobinRule
轮询策略和RandomRule
随机策略,这都是之前在 Nginx 负载均衡策略中学过的。 -
看到这里,不再深挖了,点顺序执行往回走,走回
RibbonLoadBalancerClient.java
的execute()
方法中。发现getServer()
方法使用ZoneAvoidanceRule
负载均衡策略选取到了端口为 8082 的用户微服务UserApplication
,接下来就可以把之前的远程调用 URL 中的服务名称/userservice
替换成 8082 的 IP 地址和端口号/localhost:8082
了,去发起真实的 HTTP 远程调用请求http://localhost:8082/user/userId
。 -
Ribbon 负载均衡源码就解析到这里。总结全流程如下图所示:
2. 负载均衡策略
2.1 负载均衡策略继承关系
- Ribbon 的负载均衡策略是一个叫
IRule.java
的接口来定义的,其每个子接口都是一种策略。
- 上一节 Ribbon 源码中,我们聊到的默认策略是
ZoneAvoidanceRule
,就在上图的最底层右侧。初次接触这个词可能很陌生,我们不妨往上看它的爷爷ClientConfigEnableRoundRobinRule
,顾名思义,从后面几个单词RoundRobin
中我们也不难看出,这是轮询策略的一种,只是在轮询的基础上作了加强。那么作为孙子的ZoneAvoidanceRule
,我们就可以大胆推测,它也是轮询策略的一种。具体看下表。
2.2 负载均衡策略描述
- 下表中罗列了 Ribbon 中常见的负载均衡策略。
1)ZoneAvoidanceRule
- 其中第 4 行,就是默认策略
ZoneAvoidanceRule
。我们在配置服务注册时,是可以设置微服务的Zone
值 (Zone
值可以理解为一个地区代码,比如服务器机房在北京、上海、深圳等) 。设置Zone
值后,Ribbon 作轮询时,会优先选择跟自己在同一个Zone
内的微服务 (即就近原则) ,然后再在多个微服务实例中作轮询。 - 因此,这个
Zone
值往往只有大厂才会设置,因为大厂需要在多地搭建机房提升容灾能力和并发能力,中小公司往往所有的服务器都在一个机房,因此没有必要配置Zone
而是直接轮询。
2)AvailabilityFilteringRule
-
表中第 2 行,中文名是可用性筛选策略。会排除网络环境差和并发数高的服务,在剩下的服务中去做轮询。
-
其他策略都很好理解,一般情况下选择默认的
ZoneAvoidanceRule
即可。
2.3 修改负载均衡策略方式
1)全局修改
-
消费者服务远程调用任何其他提供者微服务都起作用。例如,订单微服务
order-service
远程调用用户微服务user-service
时起作用,远程调用购物车微服务时也起作用。 -
全局修改步骤:在订单微服务
order-service
中的启动类OrderApplication.java
定义一个新的 IRule 。 -
OrderApplication.java
:// 全局修改负载均衡策略为随机策略 @Bean public IRule randomRule() { return new RandomRule(); }
2)局部修改
-
消费者远程调用时,只在调用某个提供者微服务时起作用。例如,局部修改负载均衡策略成
RandomRule
时,订单微服务order-service
远程调用用户微服务user-service
时起作用,远程调用任何其他提供者微服务不起作用 (还是默认策略) 。 -
局部修改负载均衡策略步骤:在订单微服务
order-service
中的配置文件application.yml
中添加 IRule 负载均衡策略。 -
application.yml
:userservice: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡策略
3. 饥饿加载
3.1 背景
- 懒加载,又称懒汉式加载,思想与单例设计模式中的懒汉式相同。Ribbon 默认采用懒加载,即第一次访问到来时才会去创建
LoadBalanceClient
,好处是启动时间快,坏处是用户第一次访问的请求时间耗时长。 - 饥饿加载,又称饿汉式加载 ,思想与单例设计模式中的饿汉式相同。会在项目启动时就创建,好处是降低用户第一次访问的请求耗时,坏处是服务启动时间变长。
- 在实际企业开发中,企业是宁愿服务启动耗时长一些,也要保障用户体验的。因此我们把 Ribbon 的加载方式修改成饥饿加载。
3.2 启用饥饿加载
-
打开订单微服务
order-service
中的配置文件application.yml
。 -
application.yml
:ribbon: eager-load: enabled: true # 开启饥饿加载 clients: - userservice # 指定只对用户微服务userservice启用饥饿加载 - xxservice # 指定多个
3.3 对比测试
-
采用懒加载,重启订单微服务
order-service
后,发送第一次请求http://localhost:8080/order/101
。耗时要 1639 ms ,这就非常离谱了,用户能明显感知到卡顿延迟。 -
修改成饿汉式加载后,重启订单微服务
order-service
后,服务一启动就作服务发现。虽然服务启动需要更长时间,但能显著缩短用户第一次访问的耗时。修改后访问耗时只需要 1639 ms , (从 550 ms 提升到 20 ms) 。