我们添加了@LoadBalanced 注解,即可实现负载均衡功能,这是什么原理呢?

1. 负载均衡原理

SpringCloud 底层其实是利用了一个名为 Ribbon 的组件,来实现负载均衡功能的。

image-20210713224517686

那么我们发出的请求明明是 http://userservice/user/1,怎么变成了 http://localhost:8081 的呢?

2. 源码跟踪

为什么我们只输入了 service 名称就可以访问了呢?之前还要获取 ip 和端口。

显然有人帮我们根据 service 名称,获取到了服务实例的 ip 和端口。它就是 LoadBalancerInterceptor,这个类会对 RestTemplate 的请求进行拦截,然后从 Eureka 中根据服务 id 获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务 id。

我们进行源码跟踪:

2.1. LoadBalancerIntercepor

image.png

可以看到这里的 intercept 方法,拦截了用户的 HttpRequest 请求,然后做了几件事:

  • request.getURI()获取请求 uri,本例中就是 http://user-service/user/8
  • originalUri.getHost()获取 uri 路径的主机名,其实就是服务 id,user-service
  • this.loadBalancer.execute():处理服务 id,和用户请求。

这里的 this.loadBalancerLoadBalancerClient 类型,我们继续跟入。

2.2. LoadBalancerClient

继续跟入 execute 方法:

image.png

代码是这样的:

  • getLoadBalancer(serviceId):根据服务 id 获取 ILoadBalancer,而 ILoadBalancer 会拿着服务 id 去 eureka 中获取服务列表并保存起来
  • getServer(loadBalancer):利用内置的负载均衡算法,从服务列表中选择一个 在本例中,可以看到获取了 8082 端口的服务

放行后,再次访问并跟踪,发现获取的是 8081:

image.png

果然实现了负载均衡。

2.3. 负载均衡策略 IRule

在刚才的代码中,可以看到获取服务使通过一个 getServer 方法来做负载均衡:

image.png

我们继续跟入:

image.png

继续跟踪源码 chooseServer 方法,发现这么一段代码:

image.png

我们看看这个 rule 是谁:

image.png

这里的 rule 默认值是一个 RoundRobinRule,看类的介绍:

image.png

这不就是轮询的意思嘛。
到这里,整个负载均衡的流程我们就清楚了。

2.4. 总结

%%
▶49.🏡⭐️◼️【🌈费曼无敌🌈⭐️♨️♨️♨️⭐️】◼️⭐️-point-20230312-1455%%
❕ ^pxs57b

SpringCloudRibbon 的底层采用了一个拦截器拦截了 RestTemplate 发出的请求,对地址做了修改。用一幅图来总结一下:

image.png

基本流程如下:

  1. LoadBalancerIntercepor 拦截我们的 RestTemplate 请求 http://userservice/user/1
  2. RibbonLoadBalancerClient 会从请求 url 中获取服务名称,也就是 user-service
  3. DynamicServerListLoadBalancer 根据 user-service 到 eureka 拉取服务列表
  4. Eureka 返回列表,localhost:8081、localhost:8082
  5. IRule 利用内置负载均衡规则,从列表中选择一个,例如 localhost:8081
  6. RibbonLoadBalancerClient 修改请求地址,用 localhost:8081 替代 userservice,得到 http://localhost:8081/user/1,发起真实请求

3. 负载均衡策略

3.1. 负载均衡策略

负载均衡的规则都定义在 IRule 接口中,而 IRule 有很多不同的实现类:

image.png

不同规则的含义如下:

image.png

默认的实现就是 ZoneAvoidanceRule,是一种轮询方案

3.2. 详细介绍

IRule
这是所有负载均衡策略的父接口,里边的核心方法就是 choose 方法,用来选择一个服务实例。
AbstractLoadBalancerRule
AbstractLoadBalancerRule 是一个抽象类,里边主要定义了一个 ILoadBalancer,这里定义它的目的主要是辅助负责均衡策略选取合适的服务端实例。

3.2.1. RandomRule

看名字就知道,这种负载均衡策略就是随机选择一个服务实例,看源码我们知道,在 RandomRule 的无参构造方法中初始化了一个 Random 对象,然后在它重写的 choose 方法又调用了 choose(ILoadBalancer lb, Object key) 这个重载的 choose 方法,在这个重载的 choose 方法中,==每次利用 random 对象生成一个不大于服务实例总数的随机数==,并将该数作为下标所以获取一个服务实例。

3.2.2. RoundRobinRule

RoundRobinRule 这种负载均衡策略叫做线性轮询负载均衡策略。这个类的 choose(ILoadBalancer lb, Object key) 函数整体逻辑是这样的:开启一个计数器 count,在 while 循环中遍历服务清单,获取清单之前先通过 incrementAndGetModulo 方法获取一个下标,这个下标是一个不断自增长的数先加 1 然后和服务清单总数取模之后获取到的(所以这个下标从来不会越界),拿着下标再去服务清单列表中取服务,每次循环计数器都会加 1,如果连续 10 次都没有取到服务,则会报一个警告 No available alive servers after 10 tries from load balancer: XXXX

3.2.3. RetryRule(在轮询的基础上进行重试)

看名字就知道这种负载均衡策略带有重试功能。首先 RetryRule 中又定义了一个 subRule,它的实现类是 RoundRobinRule,然后在 RetryRule 的 choose(ILoadBalancer lb, Object key) 方法中,每次还是采用 RoundRobinRule 中的 choose 规则来选择一个服务实例,如果选到的实例正常就返回,如果选择的服务实例为 null 或者已经失效,则在失效时间 deadline 之前不断的进行重试(重试时获取服务的策略还是 RoundRobinRule 中定义的策略),如果超过了 deadline 还是没取到则会返回一个 null。

3.2.4. WeightedResponseTimeRule

(权重 —nacos 的 NacosRule ,Nacos 还扩展了一个自己的基于配置的权重扩展)
WeightedResponseTimeRule 是 RoundRobinRule 的一个子类,在 WeightedResponseTimeRule 中对 RoundRobinRule 的功能进行了扩展,
WeightedResponseTimeRule 中会根据每一个实例的运行情况来给计算出该实例的一个权重,然后在挑选实例的时候则根据权重进行挑选,这样能
够实现更优的实例调用。WeightedResponseTimeRule 中有一个名叫 DynamicServerWeightTask 的定时任务,默认情况下每隔 30 秒会计算一次
各个服务实例的权重,权重的计算规则也很简单,如果一个服务的平均响应时间越短则权重越大,那么该服务实例被选中执行任务的概率也就越大

3.2.5. ClientConfigEnabledRoundRobinRule

ClientConfigEnabledRoundRobinRule 选择策略的实现很简单,内部定义了 RoundRobinRule,choose 方法还是采用了 RoundRobinRule 的
choose 方法,所以它的选择策略和 RoundRobinRule 的选择策略一致,不赘述。

3.2.6. BestAvailableRule

BestAvailableRule 继承自 ClientConfigEnabledRoundRobinRule,它在 ClientConfigEnabledRoundRobinRule 的基础上主要增加了根据
loadBalancerStats 中保存的服务实例的状态信息来过滤掉失效的服务实例的功能,然后顺便找出并发请求最小的服务实例来使用。然而
loadBalancerStats 有可能为 null,如果 loadBalancerStats 为 null,则 BestAvailableRule 将采用它的父类即
ClientConfigEnabledRoundRobinRule 的服务选取策略(线性轮询)。

3.2.7. ZoneAvoidanceRule

默认规则,复合判断 server 所在区域的性能和 server 的可用性选择服务器
ZoneAvoidanceRule 是 PredicateBasedRule 的一个实现类,只不过这里多一个过滤条件,ZoneAvoidanceRule 中的过滤条件是以
ZoneAvoidancePredicate 为主过滤条件和以
AvailabilityPredicate 为次过滤条件组成的一个叫做 CompositePredicate 的组合过滤条件,过滤成功之后,继续采用线性轮询
(RoundRobinRule) 的方式从过滤结果中选择一个出来。
AvailabilityFilteringRule(先过滤掉故障实例,再选择并发较小的实例)
 过滤掉一直连接失败的被标记为 circuit tripped 的后端 Server,并过滤掉那些高并发的后端 Server 或者使用一个 AvailabilityPredicate 来
包含过滤 server 的逻辑,其实就是检查 status 里记录的各个 Server 的运行状态

3.3. 自定义负载均衡策略

通过定义 IRule 实现可以修改负载均衡规则,有两种方式:

  1. 代码方式:在 order-service 中的 OrderApplication 类中,定义一个新的 IRule:
    这是一种全局配置,不推荐

    1
    2
    3
    4
    @Bean  
    public IRule randomRule(){
       return new RandomRule();
    }
  2. 配置文件方式:在 order-service 的 application.yml 文件中,添加新的配置也可以修改规则:
    按服务配置,更灵活

    1
    2
    3
    userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务  
    ribbon:
      NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

注意一般用默认的负载均衡规则,不做修改

4. 饥饿加载

%%
▶48.🏡⭐️◼️【🌈费曼无敌🌈⭐️♨️♨️♨️⭐️】◼️⭐️-point-20230312-1358%%
❕ ^x4qqqw

Ribbon 默认是采用懒加载,即第一次访问时才会去创建 LoadBalanceClient,请求时间会很长。
image.png

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

1
2
3
4
ribbon:    
eager-load:  
  enabled: true  
  clients: userservice

image.png

5. 重试机制

- [ ] 🚩 - 重试机制 - 🏡 2023-03-12 18:41 #todo

6. RestTemplate

6.1. 是什么

RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 Http 服务的方法, 能够大大提高客户端的编写效率。

调用 RestTemplate 的默认构造函数,RestTemplate 对象在底层通过使用 java.net 包下的实现创建 HTTP 请求,可以通过使用 ClientHttpRequestFactory 指定不同的 HTTP 请求方式。

ClientHttpRequestFactory 接口主要提供了两种实现方式

  1. 一种是 SimpleClientHttpRequestFactory,使用 J2SE 提供的方式(既 java.net 包提供的方式)创建底层的 Http 请求连接。
  2. 一种方式是使用 HttpComponentsClientHttpRequestFactory 方式,底层使用 HttpClient 访问远程的 Http 服务,使用 HttpClient 可以配置连接池和证书等信息。

RestTemplate 的核心之一 Http Client。

目前通过 RestTemplate 的源码可知,RestTemplate 可支持多种 Http Client 的 http 的访问,如下所示:

  • 基于 JDK HttpURLConnection 的 SimpleClientHttpRequestFactory,默认。
  • 基于 Apache HttpComponents Client 的 HttpComponentsClientHttpRequestFactory
  • 基于 OkHttp3 的 OkHttpClientHttpRequestFactory。
  • 基于 Netty4 的 Netty4ClientHttpRequestFactory。

其中 HttpURLConnection 和 HttpClient 为原生的网络访问类,OkHttp3 采用了 OkHttp3 的框架,Netty4 采用了 Netty 框架。

6.2. 配置

6.2.1. RestTempate 的访问的超时设置

例如,我用的是 Httpclient 的连接池,RestTemplate 的超时设置依赖 HttpClient 的内部的三个超时时间设置。

HttpClient 内部有三个超时时间设置:连接池获取可用连接超时,连接超时,读取数据超时:

1.setConnectionRequestTimeout 从连接池中获取可用连接超时:设置从 connect Manager 获取 Connection 超时时间,单位毫秒。

HttpClient 中的要用连接时尝试从连接池中获取,若是在等待了一定的时间后还没有获取到可用连接(比如连接池中没有空闲连接了)则会抛出获取连接超时异常。

2.连接目标超时 connectionTimeout,单位毫秒。

指的是连接目标 url 的连接超时时间,即客服端发送请求到与目标 url 建立起连接的最大时间。如果在该时间范围内还没有建立起连接,则就抛出 connectionTimeOut 异常。

如测试的时候,将 url 改为一个不存在的 url:“http://test.com” ,超时时间 3000ms 过后,系统报出异常:   org.apache.commons.httpclient.ConnectTimeoutException:The host did not accept the connection within timeout of 3000 ms

3.等待响应超时(读取数据超时)socketTimeout ,单位毫秒。

连接上一个 url 后,获取 response 的返回等待时间 ,即在与目标 url 建立连接后,等待放回 response 的最大时间,在规定时间内没有返回响应的话就抛出 SocketTimeout。

测试时,将 socketTimeout 设置很短,会报等待响应超时。

我遇到的问题,restTemplate 请求到一个高可用的服务时,返回的超时时间是设置值的 2 倍,是因为负载均衡器返回的重定向,导致 httpClient 底层认为没有超时,又请求一次,如果负载均衡器下有两个节点,就耗费 connectionTimeout 的双倍时间。

6.3. 连接池

https://zhuanlan.zhihu.com/p/384627133

7. 实战经验

8. 参考与感谢

8.1. 黑马程序员

微服务开发框架 SpringCloud+RabbitMQ+Docker+Redis+ 搜索 + 分布式微服务全技术栈课程 ^4ps1v8

8.1.1. 视频

https://www.bilibili.com/video/BV1LQ4y127n4?p=17&spm_id_from=pageDriver&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204

8.1.2. 资料

[[SpringCloud01]]

8.2. 其他

image-20210925135730907