客户端负载均衡:Spring Cloud Ribbon
Spring Cloud Ribbon的简介
Spring Cloud Ribbon 是一个基于HTTP和TCP的客户端负载均衡工具,基于Netflix Ribbon实现,可以让我们将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用,是一个工具框架,无需部署
负载均衡:通过维护一个下挂可用的服务端清单(服务端的清单来自服务注册中心),通过心跳监测剔除故障鼓舞节点,保证清单都是可以正常访问的服务端清单,当客户端发送请求到负载均衡服务器,负载均衡服务器按照某种算法(线性轮询,权重,流量负载等)从维护的服务端清单取出一台服务端的地址 ,进行转发
硬件负载均衡:F5
软件负载均衡 : Nginx
spring Cloud Ribbon的使用
1.服务提供者只启动多个服务实例并注册到一个或者多个想关联的服务注册中心
源码解析:在RestTemplate类存在当前工程中且Spring容器中有LoadBalancerClient的Bean中,自动化配置类中创建了LoadBalancerInterceptor和RestTemplateCustomizer的bean,并且维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表并初始化,通过调用RestTemplateCustomizer的实例来给需要的客户端负载均衡的RestTemplate增加LoadBalancerInterceptor
为什么增加了@LoadBalancerInterceptor拦截器就能使普通的RestTemplate变成客户端的负载均衡呢?
当一个被@LoadBalanced修饰的RestTemplate对象向外发起HTTP请求的时候,会被拦截器中的intercept方法拦截,由于我们使用RestTemplate的时候采用服务名作为host,所以从HttpRequest的URI对象中可以通过getHost直接拿到服务名,然后调用execute方法根据服务名来选择实例并发起实际的请求
在execute方法中,首先通过getServer根据传入的服务名serviceId去获取具体的服务实例,在getServer方法中调用的是IloadBalancer的chooseServer函数获取负载均衡策略分配到的服务实例对象Server之后,将其内容包装成RibbonServer对象(除了存储服务实例的信息外,增加了服务名serviceId,是否使用https等其他信息),然后使用这个对象回调LoadBalancerTnterceptor的请求拦截器中的LoadBalabcerRequest的apply(final ServiceInstance instance)函数,向一个实际的具体的服务实例发起请求,从而实现了一开始以服务名为host的URI的请求到host:post形式的实际访问地址的转换
具体的转换主要是在RibbonLoadBalancerClient中实现的reconstructURI来组织请求的服务实例地址,通过serviceInstance实例对象中serviceId,从SpringClinetFactory类的clientFactory
2.服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现服务的接口调用
负载均衡器
Spring Cloud Ribbon是通过ILoadBalancer接口实现负载均衡的,有哪些具体的实现类实现客户端的负载均衡呢?
AbstractLoadBalancer
是ILoadBalancer接口的抽象实现,在该抽象类中定义了一个关于服务实例的分组枚举类ServerGroup,包含
public abstract class AbstractLoadBalancer implements ILoadBalancer {
public AbstractLoadBalancer() {
}
public Server chooseServer() {
return this.chooseServer((Object)null);
}
public abstract List<Server> getServerList(AbstractLoadBalancer.ServerGroup var1); //根据分组获取服务列表
public abstract LoadBalancerStats getLoadBalancerStats();//LoadBalancerStats对象被用来存储负载均衡器中各个服务实例当前的属性和统计信息
public static enum ServerGroup {
ALL, //所有服务实例
STATUS_UP, //正常服务实例
STATUS_NOT_UP; //停止服务的实例
private ServerGroup() {
}
}
}
BaseLoadBalancer
定义了负载均衡处理规则IRule对象,从BaseLoadBalancer中chooseServer(Object key)的实现中我们可以知道,负载均衡器实际将服务实例选择的任务委托给IRule实例中的choose函数,默认初始化RoundRobinRule为IRule的实现对象,RoundRobinRule实现了最基本且最常用的线性负载均衡规则,还有一些其他方法负载均衡器的基本操作,标记某个实例暂停任务,可用的服务列表等等
DynamicServerListLoadBalancer
DynamicServerListLoadBalancer类继承自BaseLoadBalancer类,实现了服务实例清单在运行期的动态更新能力,还具备了对服务实例清单的过滤功能,也就是说我们可以通过过滤器来选择性的获取一匹服务实例清单。
ZoneAwareLoadBalancer
负载均衡策略
Ribbon实现的负载均衡器中的服务实例的选择策略主要是通过IRule接口来实现的
AbstractLoadBalancerRule
负载均衡策略的抽象类,类中定义一个负载均衡器ILoadBalancer对象,能够在具体选择服务策略的时候获取负载均衡器中维护的信息作为分配依据
RandomRule
该策略实现了从服务实例清单随机选择一个服务实例的功能,从传入的负载均衡器中获取可用的实例列表upList和所有实例列表allList,通过rand.nextInt(serverCount)函数获取随机数作为upList的索引值返回具体实例
RoundRobinRule
该策略实现了按照线性轮询的方式一次选择每个服务实例,通过一个count计数变量,每次循环之后累加,如果一直选择不到server超过10次,就结束尝试,打印警告信息
RetryRule
该策略定义一个具备重试机制的实例选择功能,在内部定义一个IRule对象,默认使用了RoundRobinRule,在choose方法中则实现了对内部定义的策略进行返回尝试的策略,若期间能选择到具体的服务实例则返回没选择不到则根据设置的尝试结束时间为阀值(MaxRetryMillis参数定义的值+choose方法开始执行的时间戳),超过阀值就返回null
WeightedResponseTimeRule
该策略是对RoundRobinRule的扩展,增加了根据实例运行情况来计算权重,并根据权重来挑选任务,以达到更优的分配,主要包括3个核心内容
定时任务
该策略在初始化的时候会通过serverWeightTimer.shcedule(new DynamicServerWeightTask(),0,serverWeighrTaskTimerInterval)来启动一个定时任务,用来为每个服务实例计算权重,该任务默认30s执行一次
计算权重
权重的对象存储在List<Double> accumulatedWeight = new ArrayList<Double>(),该List中每个权重值所处的位置对应了负载均衡器维护的服务实例清单中的所有实例在清单中的位置
如何计算权重,在maintainWeight()函数中实现
- 根据LoadBalancerStats中记录的每个实例的统计信息,累加所有实例的平均响应时间,得到总平均响应时间totalResponseTime
- 为负载均衡器中维护的实例清单逐个计算权重,weightSoFar+ totalResponseTime – 实例的平均响应时间(其中weightSoFar初始化为0),每计算好一个权重需要累加到weightSoFar上供下一次计算使用
例如:ADCD 4个实例,平均响应时间是10,40,80,100,则总响应时间为230,那么每个实例的权重为总响应时间与实例自身的平均响应时间的差累计而得,这里的权重值只是表示各个实例权重区间的上限,并非某个实例的优先级,下面4个实例构建了4个不同的区间,区间的下限是上个实例区间上限
实例A:(230-10) +0=220,权重区间:[0,220]
实例B:(230-40) +220=410, 权重区间:(220,410]
实例C:(230-80) +410 =560, 权重区间:(410,560]
实例D:(230-100) +560=690, 权重区间:(560,690)
不难发现,实际上每个区间的宽度就是:总的平均响应时间- 实例的平均响应时间,每个实例平均响应时间越短,权重区间的宽度越大,越容易被选中
实例选择
实例选择算法主要分为以下:
- 生成一个[0,最大权重值)的随机数(随机数可以取0)
- 遍历权重列表,比较权重值与随机数的大小,如果权重值大于等于随机数,就拿当前权重列表的索引值去服务实例列表中获取具体的实例
ClientConfigEnabledRoundRobinRule
该策略一般我们不直接使用,因为它本身没有实现什么特殊的处理逻辑,在它的内部定义了一个RoundRobinRule策略,而choose函数的实现也正是使用了RoundRobinRule的线性轮询机制,所以使用中和RoundRobinRule相同
一般我们通过继承该策略,在子类中做一些高级策略的通常会存在一些无法实施的情况,可以用弗雷的实现作为备选。后面提高的高级策略就是基于ClientConfigEnabledRoundRobinRule的扩展
BestAvailableRule
该策略继承自ClinetConfigEnabledRoundRobinRule,在视线中注入负载均衡器的统计对象LoadBalancerStats,同事在具体的choose中利用LoadBalancerStats保存的实例统计信息来选择满足要求的实例,主要是通过遍历负载均衡器中维护的所有服务实例,并过滤故障的实例,找出并发请求书最小的一个,所以该策略的特性是选出最空闲的实例,由于算法核心以及是统计对象LoadBalancerStats,当其为空的时候,该策略无法执行,则会采用父类的轮询策略
PredicateBasedRule
该策略是一个抽象策略,也继承在ClientConfigEnabledRoundRobinRule,从其命名中可以猜出这是一个基于Predicate实现的策略,Predicate是Google Guava Collection工具对集合进行过滤的条件接口
主要是先通过子类实现的Predicate逻辑过滤一部分服务实例,然后再议线性轮询的方式从过滤后的实例清单中选出一个
AvailablityFilterRule
该策略继承PredicateBasedRule,即也是先过滤后轮询的策略,过滤主要是判断服务实例的2项内容
1.是否故障,即断路器是否生效已断开
2.实例的并发请求数大于阀值,默认是2的32次方-1,该配置可通过<clientNmae>.<nameSpce>.ActiveConnectionsLimit来修改
这2项内容只要有一个满足apply就返回false(代表该节点存在故障或负载太高),否则返回true
在choose方法中,也做了改进优化,父类的实现作为备用 ,它是以线性的方式选择一个实例,接着用过滤条件来判断该实例是否满足要求,若满足就直接使用该实例,若不满足要求就选择下一个实例,并检查是否,重复10次还没有找到则采用父类的方案(父类是先遍历所有节点进行过滤,在过滤的集合中选择实例)
ZoneAvoidanceRule
该策略也是PredicateBasedRule的具体实现类,在实现的时候并没有想AvailabilityFilterRule那样重写choose函数,所以还是遵循父类的过滤主逻辑:先过滤清单在轮询选择
它有2个过滤条件:
主过滤条件对所有实例过滤并返回过滤的实例清单
依次使用过滤条件列表中的过滤条件对主过滤条件的结果进行过滤,每次过滤之后都需要判断下面2个条件,只有有一个符合就不在过滤,将当前结果返回共线性轮询算法
- 过滤后的实例总数>= 最小过滤实例数(minimalFilterServers,默认为1)
- 过滤后的实例比例 > 最小过滤百分比(minimalFilterPercentage,默认为0)
Ribbon使用时的配置详解
自动化配置
引入Spring Cloud Ribbon依赖后,就能自动化构建下面这些接口的实现
IClientConfig:Ribbon的客户端配置。默认使用DefaultClientConfigImpl
IRule:Ribbon的负载均衡策略默认采用ZoneAvoidanceRule,能够在多区域的环境下选择最佳区域的实例进行访问
IPing:Ribbon的实例检查策略,默认采用NoOpPing实现,该策略是不会检查实例是否可用,始终返回true,默认所有服务实例都是可用的
ServerList<Server>:服务实例清单的维护机制,默认采用ConfigurationBasedServerList实现
ServerListFilter<Server>:服务实例清单过滤机制,默认采用ZonePreferenceServerListFilter实现,该策略优先过滤出请求调用方出于同区域的服务实例
ILoadBalancer:负责均衡器,默认采用ZoneAwareLoadBalancer实现,具备区域感知能力
这些自动化配置在没有引入Spring Cloud Eureka的配置,在同时引入Eureka和Ribbon,自动化配置有所不同
通过这些配置就会轻松实现客户端负载均衡,同事针对个性化需求,我们只要在Spring Boot项目中创建相应的实现就能覆盖这些默认的配置实现
如:创建PingUrl实例,NoOpPing就不会创建
@Configuration
public class RibbonConfiguration {
@Bean
public IPing ribbonPing(IClientConfig config){
return new PingUrl();
}
}
另外,也可以通过@RibbonClient注解来实现更细粒度的客户端配置,如为hello-Service服务使用helloServiceConfiguration中的配置
@Configuration
@RibbonClient(name = "hello-Service",configuration = helloServiceConfiguration.class)
public class RibbonConfiguration {
}
Camden版本对RibbonClient配置的优化
在之前介绍的Brixton版本中,对RibbonClient的IPing、IRule等接口实现进行话定制的方法主要是创建一个独立的Configuration类来定义IPing,IRule等接口的具体实现bean,但是当有大量的这类配置的时候,对于各个RibbonClient的指定配置信息将分散在这些配置类的注解定义中,这使得管理和修改非常不便
在Camden版本中,Spring Cloud Ribbon对RibbonClient定义个性化配置方法做了进一步优化,可以直接通过<ClientName>.ribbon.<key>=<value>的形式配置
如我们上面的将hello-Service服务客户端的IPing接口实现替换成PingUrl,只需要在application.proerties配置以下即可
Hello-service.ribbon.NFLoadBalancerPingClassName = com。Netflix.loadbalancer.PingUrl
其中hello-service为服务名,NFLoadBalancerPingClassName参数用来指定具体的IPing接口的实现类,在Camden版本中Spring Cloud Ribbon新增一个PropertiesFactory类来动态的位RibbonClient创建这些接口的实现
public class PropertiesFactory { @Autowired private Environment environment; private Map<Class, String> classToProperty = new HashMap(); public PropertiesFactory() { this.classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName"); this.classToProperty.put(IPing.class, "NFLoadBalancerPingClassName"); this.classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName"); this.classToProperty.put(ServerList.class, "NIWSServerListClassName"); this.classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName"); }
…
从代码中可以看出除了NFLoadBalncerPingClassName参数,还提供了其他介个接口的动态配置实现,我们可以通过配置更加方便的位RibbonClient指定定制化实现
"NFLoadBalancerClassName" 配置ILoadBalancer接口 "NFLoadBalancerPingClassName" 配置IPing接口 "NFLoadBalancerRuleClassName"配置IRule接口 "NIWSServerListClassName"配置ServerList接口 "NIWSServerListFilterClassName"配置ServerListFilter接口
参数配置
对于Ribbon参数配置有2种方式,全局配置和指定客户端配置
- 全局配置方式很简单,只需要通过ribbon.<key>=<value>格式进行配置即可,其中key代表Ribbon客户端配置的参数名,value表示参数的值
如配置Ribbon创建连接的超时时间:ribbon.ConnectTimeout =250
- 指定客户端的配置方式采用<client>.ribbon.<key>=<value>的格式进行配置,client代表客户端的名称
与Eureka的结合
在应用中同时引入Spring Cloud Ribbon 和Spring Cloud Eureka,会触发Eureka中实现的对Ribbon的自动化配置,这时ServerList维护机制将被DiscoveryEnabledNIWSServerList的实例所覆盖,该实现会将服务清单列表交给Eureka的服务治理机制来进行维护;IPing的实现将被NIWSDiscoveryPing的实例覆盖,实例检查也交给服务治理框架来维护
我们可以通过eureka实例的元数据配置实现区域化的实例配置方案,可以将处于不同机房的实例配置成不同的区域值,只需要在服务实例的元数据中添加zone参数指定自己所在的区域即可
eureka.instance.metadataMap.zone=shanghai
当然如果想禁用Eureka对Ribbon服务实例的维护实现,只需要添加以下,那么我们对服务实例的维护又将回归到使用<client>.ribbon.listOfServers参数配置方式实现
ribbon.eureka.enabled=false
hello-service.ribbon.listOfServers=localhost:8001,localhost:8002
重试机制
由于Spring Cloud Eureka实现的服务治理机制强调了CAP原理的AP,即可用性与可靠性,他与Zookeeper这类强调CP(一致性和可靠性)的服务治理框架的区别就是Eureka为了实现服务的可用性,牺牲一致性
,极端情况下,宁愿接受故障实例也不要丢掉健康实例,比如当服务注册中心网络故障,所有服务实例无法维持心跳,在强调AP的服务治理中会把服务实例都剔除掉,而Eureka会因为超过85%的实例丢失心跳而出发保护机制,注册中心将保护节点,以实现服务之间可以互相调用,即使部分故障,也能继续保持大多数服务正常消费
正因为如此,在服务调用到故障实例的时候,我们希望增强对这类问题的容错,所以我们在实现服务调用的时候会加入一些重试机制,在Camden SR2版本开始,Spring Cloud整合了Spring Retry来增强RestTemplate的重试能力,对于我们来说只要简单的配置,原先根据RestTemplate实现的服务访问就会自动根据配置来实现重试策略
spring.cloud.loadbalancer.retry.enabled=true #开启重试机制,默认是关闭的 hystrix.commond.default.execution.isolation.thread.timeoutInMillisecond=1000 #断路器的超时时间需要大于Ribbon的超时时间,不然不会触发重试 hello-service.ribbon.ConnectTimeout=250 #请求连接的超时时间 hello-service.ribbon.ReadTimeOut=1000 #请求处理的超时时间 hello-service.ribbon.OkToRetryOnAllOperations=true #对所有操作都进行重试 hello-service.ribbon.MaxAutoRetriesNextServer=2 #切换实例的重试次数 hello-service.ribbon.MaxAutoRetries=1 #当前实例的重试次数
如上配置,当访问到故障请求的时候,它会在尝试当问一次当前实例(MaxAutoRetries),如果不行,换一个实例进行访问,如果还是不行,在换一次(2次:由MaxAutoRetriesNextServer配置),依然不行则返回失败