3.3 Netflix Ribbon负载均衡
Netflix Ribbon是 Netflix开源的客户端负载均衡组件,在 Spring Cloud LoadBalancer出现之前,它是Spring Cloud生态里唯一的负载均衡组件。目前市场上绝大多数的Spring Cloud应用还是使用Ribbon作为其负载均衡组件。
3.3.1 RibbonLoadBalancerClient
Netflix Ribbon对应的LoadBalancerClient实现类为RibbonLoadBalancerClient,定义如下:
上述代码中:
① SpringClientFactory 是一个与 LoadBalancerClientFactory 作用类似的工厂类,其内部维护着一个 Map,这个 Map用于保存各个服务的 ApplicationContext(Map的 key表示服务名)。每个ApplicationContext内部维护对应服务的一些配置和Bean。
②SpringClientFactory在RibbonAutoConfiguration自动化配置类中被构造,可以通过构造器注入的方式注入。
③reconstructURI方法与Spring Cloud LoadBalancer中的BlockingLoadBalancerClient实现完全不一样。BlockingLoadBalancerClient 直接委托给 LoadBalancerUriTools#reconstructURI 方法实现,其内部使用 ServiceInstance 进行相应的属性替换;而 RibbonLoadBalancerClient 内部基于新的类 com.netflix.loadbalancer.Server(表示一个服务器实例,内部有 host、port、schema、zone等属性)来实现。
④RibbonServer是RibbonLoadBalancerClient的内部类,其实现了ServiceInstance接口,内部维护着一个Server属性,同时还有其他3个属性:serviceId:String(服务名)、secure:boolean(是否使用HTTPS)和metadata:Map<String,String>(服务器实例的元数据)。
⑤基于ServiceInstance构造Server。
⑥ServerIntrospector接口可以根据Server调用isSecure和getMetadata方法获取secure和metadata 信息(Server 类跟 ServiceInstance 没有直接关系,通过 ServerIntrospector 接口得到的这两个属性跟ServiceInstance接口中定义的isSecure和getMetadata方法返回的结果匹配)。
⑦ 使用RibbonLoadBalancerContext#reconstructURIWithServer方法基于Server和老的URI重新构造新的 URI。这里的 RibbonLoadBalancerContext 是服务名对应的 ApplicationContext 里存在的实例。
⑧ choose 方法返回负载均衡策略得到的最终实例,将负载均衡的操作委托给ILoadBalancer 接口的实现类,默认的实现为 ZoneAwareLoadBalancer。这里的 ILoadBalancer是服务名对应的ApplicationContext里存在的实例。
⑨ 返回的服务实例 ServiceInstance 使用 RibbonServer,secure 和 metadata 属性使用ServerIntrospector获取。
⑩服务调用的过程与choose方法选择服务实例的步骤一致,根据ILoadBalancer得到最终的实例,再调用另外一个execute重载方法。
⑪ 每次服务调用都会使用 RibbonStatsRecorder 内部的 ServerStats 对象进行数据统计。每个实例都有独立的ServerStats对象。
⑫真正的服务调用操作,使用LoadBalancerRequest完成。
⑬服务调用成功,进行状态记录。
⑭服务调用发生IOException异常,进行异常状态记录。
⑮服务调用发生Exception异常,进行异常状态记录。
SCL和Netflix Ribbon对应功能的对比如表3-1所示。
表3-1
接下来对Netflix Ribbon的核心类进行分析。
3.3.2 RibbonServer和Server
Spring Cloud LoadBalancer 对服务实例的处理封装到 ServiceInstance中,Netflix Ribbon在ServiceInstance的基础上引入了Server的概念。
Server 表示一个服务器实例。比如,一台阿里云 ECS,ID 为 iZ2ze1orftocdqwA12F1,IP为123.57.68.101,注册到注册中心的服务名是my-service,端口为8080,这些值都可以被设置到Server对应的属性中。
Server类定义如下:
RibbonServer是RibbonLoadBalancerClient中的内部类,其内部持有Server属性,实现ServiceInstance接口。ServiceInstance接口中定义的方法使用Server对应的属性实现,getServiceId()、isSecure()和getMetadata()这 3个方法直接使用serviceId、secure和 metadata属性返回。
secure 和 metadata 属性由 ServerIntrospector 接口返回,各个注册中心实现各自的ServerIntrospector接口。
3.3.3 Serverlntrospector
ServerIntrospector 接口基于 Server 对象确定服务实例是否是 HTTPS 协议,以及获取服务实例中的元数据信息,其定义如下:
各个注册中心都有具体的实现类。比如 Nacos 注册中心提供的 NacosServerIntrospector;Eureka注册中心提供的 EurekaServerIntrospector;Consul注册中心提供的 ConsulServerIntrospector;Zookeeper 注册中心提供的 ZookeeperServerIntrospector。其中,NacosServerIntrospector 的实现源码如下:
上述代码中:
① NacosServerIntrospector 获取元数据只针对 NacosServer 类型的 Server 做处理,直接返回NacosServer里的metadata元数据。
②如果不是NacosServer,调用父类DefaultServerIntrospector的getMetadata方法,返回空Map。
③ NacosServerIntrospector 只针对 NacosServer 类型判断是否是 HTTPS 协议,可根据getMetadata获取元数据map里的secure属性来实现。
④ 如果不是NacosServer,调用父类DefaultServerIntrospector的isSecure方法,根据配置文件配置的端口,默认为443和8443(配置key为ribbon.secure-ports)。
3.3.4 lLoadBalancer
RibbonLoadBalancerClient内部执行负载均衡请求的时候会先根据 ILoadBalancer#chooseServer方法得到最终的Server实例,然后根据LoadBalancerRequest执行真正的服务调用。
ILoadBalancer的chooseServer方法内部将负载均衡操作交给IRule接口完成,定义如下:
ILoadBalancer 接口是Netflix Ribbon用于实现负载均衡的核心接口,其内部有一套完善的机制用于实现负载均衡:
①维护所有的Server列表,可以添加、删除Server或更新Server的状态。
②监听机制。当Server列表(ServerListChangeListener)或Server状态(ServerStatus-ChangeListener)发生变化的时候,会产生相应的事件。
③可自定义的负载均衡策略IRule。
④可自定义的服务实例健康状态检查方式IPing(针对单个Server如何检查是否健康)。
⑤ 可自定义的服务实例健康状态检查策略 IPingStrategy(针对所有的 Server 列表如何检查,比如,可以过滤特定的Server,可以并行检查)。
⑥ 可自定义的 Server 列表过滤器(ServerListFilter),可以基于 Server 列表过滤出新的Server列表。
⑦可自定义的Server列表获取方式(ServerList),用于获取注册中心服务对应的实例。
⑧可自定义的Server列表更新机制(ServerListUpdater),默认会使用一个调度线程池每30s从注册中心获取一次实例信息。
⑨负载均衡器中各个服务实例当前的统计信息(LoadBalancerStats)。
Netflix Ribbon提供了@RibbonClient 注解,用于进行自定义的配置操作,上述所有可自定义的组件均可替换:
@RibbonClient注解与@LoadBalancerClient注解的作用几乎一样,同样拥有 value:String、name:String和configuration:Class[]这3个属性。name和value属性表示同一个含义,即服务名,且只能设置其中一个属性。上述代码中,nacos-provider-lb 对应的是服务名,每个服务名拥有单独的自定义配置。
提示:@RibbonClients 注解的 defaultConfiguration 属性表示默认的配置类,所有的RibbonLoadBalancerClient都会使用这些配置类里的配置。
configuration属性表示配置类,配置类中返回的 Bean会替换 RibbonClientConfiguration配置类中已经存在的Bean(前文提到SpringClientFactory内部维护着一个Map,这个Map用于保存各个服务的 ApplicationContext。每个 ApplicationContext构造的时候都会加上 RibbonClient-Configuration配置类)。比如RibbonClientConfiguration配置类中负载均衡策略为ZoneAvoidanceRule (一种基于可用区zone和过滤规则的负载均衡策略):
默认的IPing规则及Server列表更新机制定义如下:
3.3.5 ServerList
ServerList接口是一个获取Server列表的规范,其内部定义了两个方法:
各个注册中心都有 ServerList 实现类。比如,Nacos 注册中心提供的 NacosServerList;Eureka注册中心提供的ConfigurationBasedServerList;Consul注册中心提供的ConsulServerList;Zookeeper注册中心提供的ZookeeperServerList。其中,NacosServerList的实现源码如下:
Spring Cloud Alibaba Nacos Discovery 内部定义了 NacosRibbonClientConfiguration 自动化配置类,默认会使用NacosServerIntrospector和NacosServerList替换RibbonClientConfiguration中默认的Default-ServerIntrospector和ConfigurationBasedServerList:
3.3.6 ServerListUpdater
Server列表更新机制ServerListUpdater对于服务调用非常重要,它决定着Server列表是否更新及时,如果Server列表还保留着已经下线的Server,那么调用会直接报错。
默认的ServerListUpdater为PollingServerListUpdater,内部使用调度线程池默认每隔30s进行一个UpdateAction操作:
上述代码中:
①确定是否已经开启调度线程池。
②如果开关还未启动,取消调度线程池任务。
③ Runnable 内部调用 updateAction 的 doUpdate 方法,开始更新操作。具体的更新在updateListOfServers方法中。
④开启调度线程池,默认的时间间隔是3000 ms。
⑤ DynamicServerListLoadBalancer 内部维护着一个 UpdateAction 属性,其内部会调用updateListOfServers方法。
⑥调用ServerList的getUpdatedListOfServers方法获取最新的Server列表。
⑦如果ServerListFilter属性存在,进行Server过滤。
⑧更新Server列表,同时记录统计信息。
使用 ServerListUpdater 一定要注意合理配置参数,想象有这么一个场景(IPing 默认为DummyPing(DummyPing的 isAlive方法直接返回 true,不做健康检查)):用户注册一个服务且只有两个实例,客户端进行调用,其中1个实例撤销注册,这时最坏的情况下客户端要等待30s才会知道有一个实例已经撤销,在这30s内,如果有请求进来,就会发生调用报错。
3.3.7 ServerStats
ServerStats内部记录了每个Server的统计信息和一些配置信息。
① 总请求数(totalRequests)。
② 请求错误数量(successiveConnectionFailureCount)。注意:这里的请求错误数量会一直递增,如果某个请求成功,会重置数量为0。
③ 请求错误阈值(connectionFailureThreshold),默认为3个。当请求错误数达到该阈值时,该实例会进入blackOut(停电,不对外提供服务)状态。
④ 熔断时间因子(circuitTrippedTimeoutFactor),默认为10。
请求错误数与请求错误阈值差的绝对值(超过16就取16)左移1位,再乘以熔断时间因子,得到熔断时间:
⑤ 熔断最大时间(maxCircuitTrippedTimeout),默认为30s。
当根据熔断时间因子得到的熔断时间大于熔断最大时间时,取熔断最大时间:
⑥ 总熔断停电时间(totalCircuitBreakerBlackOutPeriod)。
⑦ 最近一次连接建立的时间戳(lastAccessedTimestamp)。
⑧ 第一次发生请求的时间戳(firstConnectionTimestamp)。
⑨ 响应时间(RT),包括每次请求最大RT、最小RT、平均RT和标准差RT。
⑩ 最近一次连接失败时间戳(lastConnectionFailedTimestamp)。
⑪ 当前活跃请求数(activeRequestsCount)即连接刚建立到请求结束之间算的活跃请求数。
⑫ 活跃请求的计算超时时间(activeRequestsCountTimeout)。如果某个请求超过60s后才响应,会将活跃请求数置为0。
⑬ 最近一次请求的时间戳(lastActiveRequestsCountChangeTimestamp),即连接刚建立会更新,请求结束会更新。
LoadBalancerStats内部维护着每个Server和对应的ServerStats。ServerStats所有对外暴露的操作全部通过LoadBalancerStats完成,LoadBalancerStats还会汇总所有的ServerStats里的统计数据。
RibbonClientConfiguration 中默认的负载均衡策略是 ZoneAvoidanceRule。这个 Rule 内部有服务实例的过滤规则,其中有一个规则是 AvailabilityPredicate,这个规则内部会判断 Server的状态来判断是否过滤掉这个Server:
shouldSkipServer方法内部会根据ServerStats内部的熔断状态选择是否过滤掉Server。
3.3.8 Netflix Ribbon配置项总结
Netflix Ribbon的配置项如表3-2所示,合理搭配能够避免一些问题。
表3-2
续表
续表
续表
由于 Ribbon 相关的配置对于不同的 HTTP 客户端会有不同的作用,本书不再一一说明。如果读者对具体参数的内容感兴趣,可以参考这些类或接口:CommonClientConfigKey、HttpClientConfiguration、OkHttpRibbonConfiguration、HttpClientRibbonConfiguration。
3.3.9 Ribbon缓存时间
Netflix Ribbon提供的配置项ribbon.ServerListRefreshInterval表示客户端主动与注册中心拉取最新的服务实例数据的时间间隔。这个时间间隔的默认配置值是30s,意味着:如果注册中心里某个服务对应的实例已经下线,但是客户端的刷新时间间隔还未达到默认值,那么客户端就不会触发主动拉取服务实例数据的逻辑,从而导致客户端订阅到已经下线的实例,此时发起服务根据负载均衡策略得到的实例是已经下线的实例,那么服务调用会发送超时异常(这个实例已经下线,发起连接会引起网络连接超时)。
提示:单击 Nacos Dashboard 提供的服务实例“下线”按钮后并不会立即生效,这是因为Ribbon的刷新时间间隔还未达到所导致的结果,并不是Nacos的问题。
模拟Ribbon缓存时间使用默认值在实例下线后发生调用错误的过程如下:
(1)Ribbon刷新时间没有配置,默认为30s。在这个模拟调用错误的过程中,假设刷新时间是每分钟的第0s和第30s刷新。
(2)第5s的时候,服务对应的实例IP为192.168.1.1,出现故障被迫下线。
(3)IP为192.168.1.1的实例下线后,注册中心发现这个IP心跳检测异常,在15s摘除该实例。
(4)由于第30s才主动刷新服务实例,在5~30s这段时间内调用到这个IP的请求都会报错(在 5~15s之间的调用错误是注册中心心跳机制的问题导致这个故障实例没下线引起的,在15~30s时的调用错误是Ribbon客户端没有主动拉取服务注册数据导致客户端获取到故障实例引起的)。
Ribbon 主动拉取注册中心数据关键的代码在 PollingServerListUpdater 类上,这是ServerListUpdater接口的默认实现类。我们在3.3.6节中已分析过这个类。
很明显,PollingServerListUpdater这个 ServerListUpdater接口的默认实现类实现的逻辑并不优雅。Eureka 提供了 ServerListUpdater接口的另一个实现类 EurekaNotificationServerListUpdater,其内部基于事件通知的方式主动去拉取数据。
EurekaNotificationServerListUpdater 内部的核心代码(接收到 CacheRefreshedEvent 事件后通过线程池提交主动拉取配置数据的任务)如下: