客户端负载均衡 spring cloud ribbon
- Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于Netflix Ribbon实现
客户端负载均衡
- 服务端负载均衡
- 硬件负载均衡:通过在服务器节点安装专门用于负载均衡的设备
- 软件负载均衡:通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求分发工作
- 客户端负载均衡和服务端负载均衡最大的不同点是服务清单所存储的位置
- 客户端负载均衡:客户端节点维护着自己要访问的服务端清单,这些服务清单来自于服务注册中心。客户端负载均衡中也需要心跳去维护服务端清单的健康性,但需要与服务注册中心配合完成。
- 客户端负载均衡步骤:
- 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心
- 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用
RestTemplate详解
GET请求
- getForEntity:返回ResponseEntity,对象是spring对http请求响应的封装,主要存储了http的几个重要元素。有三种不同重载实现
- getForObject:getForEntity的进一步封装,通过HttpMessageConverterExtractor对http的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容,提供三种重载实现
POST请求
- postForEntity函数:调用后返回ReponseEntity,三种重载方法
- postForObject函数:作用是简化postForEntity的后续处理,通过直接将请求响应的body内容包装成对象来返回使用
- postForLocation函数:实现了以post请求提交资源,并返回新资源的uri
PUT请求
- 三种不同重载方法
- 函数为void类型,所以没有返回内容,也没有其他函数定义的responseType参数
DELETE请求
- 三种不同重载方法
- 在rest请求时,将delete请求的唯一标识拼接在url中,delete请求不需要request的body信息,url指定delete请求位置,URLVariables绑定url中的参数即可。
源码分析
-
@LoadBalanced注解用来给RestTemplate做标记,以使用负载均衡的客户端来配置它
-
LoadBalancerClient具备能力
- ServiceInstance choose(String serviceId):根据传入的服务名,从负载均衡器中挑选一个对应服务的实例
- T execute(String serviceId, LoadBalancerRequest request) throws IOException;使用从负载均衡器中挑选出的服务实例来执行请求内容
- URI reconstructURI(ServiceInstance instance, URI original);为系统构建一个合适的host:port形式的uri,在分布式系统中,使用逻辑上的服务名称作为host来构建uri(替代服务实例的host:port形式)进行请求
-
LoadBalancerClient所属包loadbalancer中的接口/类
-
LoadBalancerAutoConfiguration:为实现客户端负载均衡器的自动化配置类
-
类头上注解
- @ConditionalOnClass(RestTemplate.class):RestTemplate类必须存在于当前工程的环境中
- @ConditionalOnBean(LoadBalancerClient.class):在 spring的bean工程中必须有LoadBalancerClient的实现bean
-
自动化配置类中,主要流程
- 创建了一个LoadBalancerInterceptor的bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡
- 创建了一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器
- 当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起http请求时,会被LoadBalancerInterceptor类的intercept函数所拦截,
- 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。
一个被@LoadBalanced注解修饰的RestTemplate对象向外发起http请求,会被LoadBalancerInterceptor类的intercept函数所拦截。直接从httpRequest的URI对象中通过getHost拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际请求。
- 具体实现类execute
- 通过getServer根据传入的服务名serviceId去获得具体的服务实例
- 获取具体服务实例使用的是Ribbon自身的chooseServer函数
- addServers:向负载均衡器中维护的实例列表增加服务实例
- chooseServer:通过某种策略,从负载均衡器中挑选一个具体的服务实例
- markServerDown:用来通知和标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前都会认为服务实例均是正常服务的
- getReachableServers:获取当前正常服务的实例列表
- getAllServers:获取所有已知的服务实例列表,包括正常服务和停止服务的实例
- 获取具体服务实例使用的是Ribbon自身的chooseServer函数
- BaseLoadBalancer类实现了基础的负载均衡
- DynamicServerListLoadBalancer:在负载均衡的策略上做一些功能的扩展
- ZoneAwareLoadBalancer:在负载均衡的策略上做一些功能的扩展
- 通过getServer根据传入的服务名serviceId去获得具体的服务实例
通过ZoneAwareLoadBalancer的chooseServer函数获取了负载均衡策略分配到的服务实例对象server之后,将其包装成RibbonServer对象,然后使用该对象回到LoadBalancerInterceptor请求拦截器中LoadBalancerRequest的apply函数(传入serviceRequestWrapper对象(重写了getURI,通过调用LoadBalancerClient接口的reconstructURI函数来重新构建一个URI进行访问)),向一个实际的具体服务实例发起请求,从而实现一开始以服务名为host的URI请求到host:post形式的实际访问地址的转换。
reconstructURI:
- 通过serviceInstance实例获取对象的serviceId
- 从SpringClientFactory类的clientFactory对象中获取对应serviceId的负载均衡上下文RibbonLoadBalancerContext对象
- SpringClientFactory用来创建客户端负载均衡器的工厂类,为每一个不同名的ribbon客户端生成不同的spring上下文。
- RibbonLoadBalancerContext是LoadBalancerContext的子类,该子类用于存储一些被负载均衡器使用的上下文内容和API操作
- 根据serviceInstance中的信息来构建具体服务实例信息的server对象,并使用RibbonLoadBalancerContext对象的reconstructURIWithServer函数来构建服务实例URI
-
-
负载均衡器
ILoadBalancer接口的实现类实现客户端负载均衡
-
AbstractLoadBalancer
- 是ILoadBalancer接口的抽象实现。定义了关于服务实例的分组枚举类ServerGroup
- ALL:所有服务实例
- STATUS_UP:正常服务的实例
- STATUS_NOT_UP:停止服务的实例
- chooseServer函数:通过调用接口中的chooseServer(Object key)实现,表示在选择具体服务实例时忽略key的条件判断
- 抽象函数:
- getServerList:定义了根据分组类型来获取不同的服务实例的列表
- getLoadBalancerStats:定义了获取LoadBalancerStats对象的方法。LoadBalancerStats被用来存储负载均衡器中各个服务实例当前属性和统计信息
- 是ILoadBalancer接口的抽象实现。定义了关于服务实例的分组枚举类ServerGroup
-
BaseLoadBalancer
- Ribbon负载均衡的基础实现类,定义了很多关于负载均衡器相关的基础内容
- 定义并维护两个存储服务实例server对象的列表,一个用于存储所有服务实例的清单,一个用于存储正常服务的实例清单
- 定义了用来存储负载均衡器各服务实例属性和统计信息的LoadBalancerStats对象
- 定义了检查服务实例是否正常服务的IPing对象,默认为null,需要在构造时注入他的具体实现
- 定义了检查服务实例操作的执行策略对象IPingStrategy,该策略采用线性遍历ping服务实例的方式检查。速度不理想或server列表过大时,可能影响系统性能。实现IPingStrategy重写pingServers函数扩展ping执行策略。
- 定义了负载均衡的处理规则IRule对象,将服务实例选择任务委托给了IRule实例中的choose函数实现,这里默认初始化了RoundRobinRule为IRule的实现对象,RoundRobinRule实现了最基本且常用的线性负载均衡规则。
- 启动ping任务:直接启动一个用于定时检查server是否健康的任务,默认执行间隔为10秒
- 实现了ILoadBalancer接口定义的负载均衡器应具备的基本操作
- addServers:向负载均衡器中增加新的服务实例列表
- chooseServer:挑选一个具体服务实例
- markServerDown:标记某个服务实例暂停服务
- getReachableServers:获取可用的服务实例列表
- getAllServers:获取所有的服务实例列表
-
DynamicServerListLoadBalancer
- 继承与BaseLoadBalancer类,是对基础负载均衡器的扩展。实现了服务实例清单在运行期的动态更新能力。具备对服务实例清单的过滤功能。
- ServerList
- 定义了两个抽象方法
- getInitialListOfServers:用于获取初始化的服务实例清单
- getUpdatedListOfServers:用于获取更新的服务实例清单
- 定义了两个抽象方法
- DomainExtractingServerList实例内部定义了serverList list,对getInitialListOfServers和getUpdatedListOfServers具体实现,创建DomainExtractingServerList时,构造函数传入DiscoveryEnabledNIWSServerList。
- DiscoveryEnabledNIWSServerList通过私有函数obtainServersViaDiscovery通过服务发现机制来实现服务实例的获取的。
- 从服务注册中心获取到具体服务实例InstanceInfo列表
- 对服务实例遍历,将状态为up的实例转换成DiscoveryEnabledServer对象
- 将实例组织成列表返回
- DiscoveryEnabledNIWSServerList通过私有函数obtainServersViaDiscovery通过服务发现机制来实现服务实例的获取的。
- ServerListUpdater
- 两个实现类
- PollingServerListUpdater:动态服务列表更新的默认策略。DynamicServerListLoadBalancer默认为这个,通过定时任务方式进行服务列表更新
- EurekaNotificationServerListUpdater:也可服务于DynamicServerListLoadBalancer,但触发机制需要利用Eureka的事件监听器来驱动服务列表的更新操作
- PollingServerListUpdater
- start函数:先创建了一个Runnable线程实现,在该实现中调用了上面提到的具体更新服务实例列表的方法doUpdate,最后为这个Runable线程实现启动一个定时任务执行。
- 两个实现类
- ServerListFilter
- 定义了一个方法List getFilteredListOfServers(List servers)用于实现对服务实例列表的过滤,通过传入的服务实例清单,根据一些规则返回过滤后的服务实例清单
- 过滤器
- AbstractServerListFilter:抽象过滤器,定义了过滤时需要的一个重要依据对象LoadBalancerStats(存储了关于负载均衡器的一些属性和统计信息等)
- ZoneAffinityServerListFilter:基于区域感知的方式实现服务实例的过滤,即会根据提供服务的实例所处的区域与消费者自身的所处区域进行比较,过滤掉那些不是同处一个区域的实例。
- DefaultNIWSServerListFilter:继承自ZoneAffinityServerListFilter,默认的NIWS过滤器
- ServerListSubsetFilter:继承自ZoneAffinityServerListFilter,适用于拥有大规模服务器集群的系统,他可以产生一个“区域感知”结果的子集列表,还能通过比较服务实例的通信失败数量和并发连接数来判定该服务是否健康来选择性的从服务实例列表中剔除哪些相对不健康的实例
- 获取区域感知的过滤结果,作为候选的服务实例清单
- 从当前消费者维护的服务实例子集中剔除那些相对不够健康的实例,标准
- 服务实例的并发连接数超过客户端配置的值,默认为0
- 服务实例的失败数超过客户端配置的值,默认为0
- 如果按符合上面任一规则的服务实例剔除后,剔除比例小于客户端默认配置的百分比
- 完成剔除后,清单已经少了至少10%的服务实例,最后通过随机的方式从候选清单中选出一批实例加入到清单中,以保持服务实例子集与原来数量一致,默认实例子集数量20
- ZonePreferenceServerListFilter:spring cloud整合时新增的过滤器,实现了通过配置或者Eureka实例元数据的所属区域来过滤出同区域的服务实例。
-
ZoneAwareLoadBalancer
- 是对DynamicServerListLoadBalancer的扩展。采用在BaseLoadBalancer中实现的算法、使用RoundRobinRule规则,以线性轮询的方式来选择调用的服务实例。
- 重写了setServerListForZones函数。函数的调用位于更新服务实例清单函数setServersList的最后,在父类DynamicServerListLoadBalancer的作用是根据按区域Zone分组的实例列表,为负载均衡器中的LoadBalancerStats对象创建ZoneStats并放入Map集合中,每个区域Zone对应一个ZoneStats,用于存储每个Zone的一些状态和统计信息。
- 创建balancers对象存储么个zone区域对应的负载均衡器
- 具体的负载均衡器创建是通过在第一个循环调用getLoadBalancer函数完成。同时在创建负载均衡器的时候会创建它的规则,创建完负载均衡器后马上调用setServersList函数为其设置zone区域的实例清单
- 第二个循环是对zone区域中实例清单的检查,看看是否有zone区域下已经没有实例了,是的话就将balancers中对应智能的区域实例列表清空,该操作作用是为了后续选择节点时,防止过时的zone区域统计信息干扰具体实例的选择算法。
- 挑选服务实例,实现对区域的识别,当zone区域的个数大于1的时候
- 调用ZoneAvoidanceRule中的静态方法createSnapshot(),为当前负载均衡器中所有的zone区域分别创建快照,保存在map中
- 调用ZoneAvoidanceRule中的静态方法getAvailableZones()来获取可用的Zone区域集合,在该函数中会通过Zone区域快照中的统计数据来实现可用区的挑选
- 首先它会剔除符合这些规则的zone区域,所属实例为0的zone区域,zone区域内实例的平均负载小于0,或者实例故障率大于等于阈值
- 然后根据zone区域的实例平均负载计算出最差的zone区域,最差指实例平均负载最高的zone区域
- 如果上面的过程中没有符合提出要求的区域,同时实例最大平均负载小于阈值,直接返回所有zone区域为可用区域。最坏zone区域集合中随机选择一个,将它从可用zone区域集合中剔除
- 获得的可用zone区域不为空,并且个数小于zone区域总数,就随机选择一个zone区域
- 确定某个zone区域后,则获取了对应zone区域的服务均衡器,并调用chooseServer选择具体的服务实例。
- 重写了setServerListForZones函数。函数的调用位于更新服务实例清单函数setServersList的最后,在父类DynamicServerListLoadBalancer的作用是根据按区域Zone分组的实例列表,为负载均衡器中的LoadBalancerStats对象创建ZoneStats并放入Map集合中,每个区域Zone对应一个ZoneStats,用于存储每个Zone的一些状态和统计信息。
- 是对DynamicServerListLoadBalancer的扩展。采用在BaseLoadBalancer中实现的算法、使用RoundRobinRule规则,以线性轮询的方式来选择调用的服务实例。
负载均衡策略
IRule接口的各个实现
- AbstractLoadBalancerRule
- 负载均衡抽象类,定义了负载均衡器ILoadBalancer对象,该对象能够在具体实现选择服务策略时,获取到一些负载均衡器中维护的信息来作为分配依据,并设计一些算法来实现针对特定场景的高效策略
- RandomRule
- 从服务实例清单中随机选一个服务实例
- choose具体实现:
- 会使用传入的负载均衡器来获得可用实例列表uplist和所有实例列表allList
- 通过rand.nextInt函数来获取一个随机数
- 将该随机数作为uplist的索引值来返回具体实例
- 具体的选择逻辑在一个while循环内,根据选择逻辑的实现,正常情况下每次选择出一个服务实例,如果出现死循环获取不到服务实例时,很有可能存在并发bug
- RoundRobinRule
- 实现了按照线性轮询的方式一次选择每个服务实例的功能
- 具体实现:线性轮询增加了一个count计数变量,变量会在每次循环之后累加,即如果一直选择不到server超过10次,就会结束尝试,并打印警告信息,线性轮询的实现则是通过AtomicInteger实现,每次进行实例选择时通过调用incrementAndGetModulo函数实现递增。
- RetryRule
- 实现了一个具备重试机制的实例选择功能。
- 默认使用RoundRobinRule实例,而在choose方法中则实现了对内部定义的策略进行反复尝试的策略,若期间能够选择到具体的服务实例就返回,若选择不到就根据设置的尝试结束时间为阈值,当超过该阈值后就返回null
- WeightedResponseTimeRule
- 对RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,达到最优的分配效果
- 定时任务
- 初始化时会通过serverWeightTimer.schedule(…)启动一个定时任务,用来为每个服务实例计算权重,该任务默认30秒执行一次
- 权重计算
- 用于存储权重的对象List 该list中每个权重值所处的位置对应了负载均衡器维护的服务实例清单中所有实例在清单中的位置。
- 维护实例权重计算过程maintainWeights
- 根据LoadBalanceerStats中记录的每个实例的统计信息吗,累加所有实例的平均响应时间,得到总平均响应时间totalResponseTime,该值会用于后续的计算。
- 为负载均衡器中维护的实例清单逐个计算权重,计算规则为weightSoFar+totalResponseTime-实例的平均响应时间,
- 这里的权重值只是表示了各实例权重区间的上限,并非某个实例的优先级,所以不是数值越大被选中的概率就越大
- 用于存储权重的对象List 该list中每个权重值所处的位置对应了负载均衡器维护的服务实例清单中所有实例在清单中的位置。
- 实例选择
- 生成一个[0,最大权重值)区间内的随机数
- 遍历权重列表,比较权重值与随机数的大小,如果权重值大于等于随机数 ,就拿当前权重列表的索引值去服务实例列表中获取具体的实例,
- 定时任务
- 对RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,达到最优的分配效果
- ClientConfigEnabledRoundRobinRule
- 内部定义了一个RoundRobinRule策略,choose函数使用了RoundRobinRule线性轮询机制
- BestAvailableRule
- 选出最空闲的实例
- 继承自ClientConfigEnabledRoundRobinRule,在实现中它注入了负载均衡器的统计对象LoadBalancerStats,在choose中利用LoadBalancerStats保存的实例统计信息来选择满足要求的实例
- 通过遍历负载均衡器中维护的所有服务实例,会过滤掉故障的实例,并找出并发请求数最小的一个
- PredicateBasedRule
- 先通过子类中实现的Predicate逻辑来过滤一部分服务实例
- 以线性轮询的方式从过滤后的实例清单中选出一个
- 过滤功能getEligibleServers函数
- 遍历服务清单,使用this.apply方法来判断实例是否需要保留,如果是就添加到结果列表中
- AvailabilityFilteringRule
- 继承了先过滤清单,再轮询选择的基本处理逻辑,过滤条件使用了AvailabilityPredicate
- 过滤逻辑shouldSkipServer。只满足下面一项apply就返回false,都不满足就返回true
- 是否故障,即断路器是否生效已断开
- 实例的并发请求数大于阈值
- 以线性方式选择一个实例,接着用过滤条件来判断该实例是否满足要求,如此循环,10次如果没有找到符合要求的实例,就采用父类的实现方案。
- 该策略通过线性抽样的方式直接尝试寻找可用且较空闲的实例来使用。
- ZoneAvoidanceRule
- 完全遵循了父类的过滤主逻辑,先过滤清单,再轮询选择,
- 在获取过滤结果的实现函数getEligibleServers
- 使用主过滤条件对所有实例过滤并返回过滤后的实例清单
- 依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤
- 每次过滤之后都需要判断,只要符合一个就不再进行过滤,将当前结果返回供线性轮询算法选择
- 过滤后的实例总数>=最小过滤实例数
- 过滤后的实例比例>最小过滤百分比
配置详解
自动化配置
在引入spring cloud ribbon依赖后,就能自动化构建接口实现:
- IClientConfig:Ribbon客户端配置,默认采用com.netflix.client.config.DefaultClientConfigImpl实现
- IRule:Ribbon负载均衡策略,默认采用com.netfix.loadbalancer.ZoneAvoidanceRule实现,该策略能够咋在多区域环境下选出最佳区域的实例进行访问
- IPing:Ribbon实例检查策略,默认采用com.netflix.loadbalancer.NoOpPing实现,该检查策略是一个特殊实现,实际上它并不会检查实例是否可用,而是始终返回true,默认认为所有服务实例都是可用的。
- ServerList:服务实例清单的维护机制,默认采用com.netflix.loadbalancer.ConfigurationBasedServerList实现。
- ServerListFilter:服务实例清单过滤机制,默认org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter实现,该策略能够优先过滤出与请求调用方处于同区域的服务实例
- ILoadBalancer:负载均衡器,默认采用com.netflix.loadbalancer.ZoneAwareLoadBalancer实现,具备区域感知能力
Camden版本对RibbonClient配置的优化
参数配置
对ribbon的参数配置
- 全局配置
- ribbon.=
- key:Ribbon客户端配置的参数名
- value:对应参数的值
- 指定客户端配置
- .ribbon.=
- key:Ribbon客户端配置的参数名;value:对应参数的值