共计 10422 个字符,预计需要花费 27 分钟才能阅读完成。
前面介绍了 sentinel-core 的流程,提到在进行流控判断时,会判断当前是本地限流,还是集群限流,若是集群模式,则会走另一个分支,这节便对集群模式做分析。
一. 基本概念
namespace:限流作用于,用于区分一个规则作用于什么范围
flowId:代表全局唯一的规则 ID,Sentinel 集群限流服务端通过此 ID 来区分各个规则,因此务必保持全局唯一。一般 flowId 由统一的管控端进行分配,或写入至 DB 时生成。
thresholdType:代表集群限流阈值模式。其中单机均摊模式下配置的阈值等同于单机能够承受的限额,token server 会根据客户端对应的 namespace(默认为 project.name 定义的应用名)下的连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30);而全局模式下配置的阈值等同于整个集群的总阈值。
二. 通信框架
sentinel-cluster 基于 netty 提供了一套远程通信框架,分为客户端和服务,其使用了 jdk 自带的 SPI,提供了一些接口的默认实现。如下图为 sentinel-cluster-client 客户端模块的默认实现类。
InitFunc 的加载是通过 InitExecutor 加载的,InitExecutor 在 sentinel-core 模块中。InitExecutor 会在全局访问内加载所有 InitFunc 的实现类,并调用其 init 方法完成初始化。该模块中配置的 InitFunc 实现类为 DefaultClusterClientInitFunc,该类会初始化通信协议中各种类型的编码和解码处理类。编解码器将调用注册工厂 RequestDataWriterRegistry 和 ResponseDataDecodeRegistry 的方法进行注册,供后续使用。系统提供了 PING,FLOW(流控)和 PARAM_FLOW(热点参数流控)三种编解码器。
上图为 sentinel-cluster 的通信协议格式,请求和响应中有个 4 个字节的消息 id 和 1 个字节的消息类型,剩下的就是消息体,对于响应格式,有 1 个字节的状态信息。需要说明的是,在初始化 Netty 客户端时,增加了两个 filter:
也就是说在发送一个消息时,会自动加上长度为 2 个字节的消息长度头部,在读取时也会自动省略 2 个字节的消息长度头部。
为了解析上面的消息格式,在提供了注册方法之上,sentinel 还提供了 ClientEntityCodeProvider,统一了报文的处理。
如上,该类在 static 静态代码块中进行了初始化,使用 SPI,获取 RequestEntityWriter 和 ResponseEntityDecoder 的实现类,这两种实现类也在该模块中指定了默认实现:DefaultResponseEntityDecoder 和 DefaultRequestEntityWriter。即处理过程为
ClientEntityCodecProvider->ResponseEntityDecoder->ResponseDataDecodeRegisty-> EntityDecoder
ClientEntityCodecProvider->RequestEntityWriter->RequestDataWriterRegisty-> EntityWriter
系统还提供了 TokenClientHandler 类,用于响应数据流,进行相应的处理
如上只列出了比较重要的属性和方法。该类继承了 ChannelInboundHandlerAdapter 并实现了对应的方法,currentState 属性用于标记客户端当前的状态,disconnectCallback 则用于负责在断线时进行重连。TokenClientHandler 实现 channelActive 方法,会在连接建立时会发送 PING 请求给服务端;实现 channelUnregistered 方法,会在连接断开时调用 disconnectCallback,在一定时间后进行重连,等待时间跟失败次数有关;实现 channelRead 方法,会在有响应数据时,接收响应内容,并进行处理,处理流程如下:
在经过 Netty 处理解析为消息类型对象后,会判断该响应的类型,如果是 PING 消息的响应,则直接输出日志,否则将从 TokenClientPromiseHolder 中根据消息 id 设置对应的响应内容,以便消息发送线程能够获得响应。
上面提到的 TokenClientPromiseHolder 用于缓存请求消息。如下图,发送消息后,会获取对应的 ChannelPromise 对象,并根据消息存于 TokenClientPromiseHolder 中。ChannelPromise 会等待 Netty 请求响应回来,对应的流程如上面 InBound 流程。在请求正常响应后,会根据消息 id 再从 TokenClientPromiseHolder 中获取对应的响应结果。
Cluster 模块的核心接口为 TokenService,ClusterTokenServer 和 ClusterTokenClient。其中 ClusterTokenClient 内部主要类为 NettyTransportClient,在上面已经进行了说明,下面说下其他两个接口。TokenService,ClusterTokenServer 在模块中的关系如下图:
其中接口都由 SPI 给出了默认的实现,如下:
下面对涉及到的接口和类进行说明。
TokenService:token 服务接口,提供了 requestToken 和 requestParamToken 方法,分别表示获取流控令牌和获取热点参数令牌。提供的默认实现为 DefaultTokenService,会在 TokenServiceProvider 初始化时使用 SPI 进行加载。
ClusterTokenServer:服务端上层接口,提供了 start 和 stop 方法用于服务端的启动和停止。
NettyTransportServer:ClusterTokenServer 的 netty 实现,同客户端对应,有如下的 pipeline 配置
其中编解码器的处理同客户端类似,只是增加了服务端的处理器:TokenServerHandler。TokenServerHandler 继承自 ChannelInboundHandlerAdapter 用以在连接建立和有数据交互时进行相应的处理:
- 实现 channelActive:在连接建立时将其缓存起来
- 实现 channelInactive:在连接断开时移除缓存
- 实现 channelRead:在有数据到来时,进行处理。这里会使用 RequestProcessorProvider 加载的 RequestProcessor 实现类,根据请求的类型 (type 字段) 选择相应的处理类进行处理。系统现在提供的处理类有 FlowRequestProcessor 和 ParamFlowRequestProcessor,这两者最后都将通过 TokenServiceProvider 获得 DefaultTokenService 对象,调用其来完成请求。
SentinelDefaultTokenServer:包装了 NettyTransportServer 方法,增加了 ServerTransportConfigObserver 用于监听服务端配置项的更改,从而更新自身。
EmbeddedClusterTokenServer:继承自 TokenService 和 ClusterTokenServer,用于内嵌服务端模式,默认实现为 DefaultEmbeddedClusterTokenServer。
DefaultEmbeddedClusterTokenServer:主要组合了 DefaultTokenService 和 SentinelDefaultTokenServer 对象用以实现接口方法。
结合上面服务端的实现,可以得到客户端请求一个 token 的流程如下:
- 客户端调用 DefaultClusterTokenClient 的 requestToken 方法获取 token,其内部会委托 NettyTransportClient 编码后发给服务端
- 服务端 NettyTransportServer 收到请求后,由 TokenServerHandler 的 channelRead 方法处理这里会根据请求内容中的 type,委托给对应的消息处理处理,如 FlowRequestProcessor
- FlowRequestProcessor 会调用 TokenServiceProvider 获取对应的 TokenService 实现类,默认为 DefaultTokenService。然后委托为该类进行处理。
三. 统计逻辑
由上可知,cluster 模式下,token 的获取是由 DefaultTokenService 来负责的,分为两种:普通流控和热点参数流控。二者的实现基本一致,这里只对普通流控做讲解,即 DefaultTokenService 中的 requestToken 方法,如下为处理流程。
当请求 requestToken 方法时,请求参数包括:
ruleId:规则 id
acquireCount:需要获取的 token 数
prioritized:是否支持优先
- DefaultTokenService 会先根据 ruleId,使用 ClusterFlowRuleManager 获得对应的 FlowRule 规则对象。ClusterFlowRuleManager 会在更新规则或者加载规则时根据 ruleId 缓存在 Map 中,且分配唯一一个 ClusterMetric。
- 获得对应的 FlowRule 对象后,会调用 ClusterFlowRuleChecker,判断是否能够获取所需要的 token
- ClusterFlowRuleChecker 会先根据规则 Id 获得该规则所对应的 namespace,然后判断该 namespace 在全局状态下是否超过流控,该步骤主要由 GlobalRequestLimiter 提供,该类存储着各个 namespace 对应的 RequestLimiter 对象。RequestLimiter 继承自 LeapArray,只提供了 QPS 一个维度的滑动窗口实现,默认实现为一秒内 10 个格子,如下图。全局流控主要使用 RequestLimiter 的 tryPass 方法,计算当前 qps 是否大于规则设定的全局 qps。
- 全局流控通过后,会根据 ClusterMetricStatistics 获取 ruleId 对应的 ClusterMetric,以获取 ruleId 对应的统计维度。首先会判断当前时间是否有可用的 token,这里会根据规则设定的 thresholdType,区分设定的阈值模式,如果是全局模式,直接根据设定的值进行限流,如果是单机均摊模式,会将该值乘上已有的额客户端数达到设定的阈值。如果有则更新统计信息并返回成功,如果没有且不支持优先,则直接返回获取失败。如果支持优先,则尝试从下一个格子借用 token(注:本地模式的借用会从后面的格子借用,只要不超过最大的等待时间),如果借用成功则更新统计信息并返回成功,否则返回失败。ClusterMetric 的结构如下,继承自 ClusterMetriceLeapArray,该滑动窗口提供了 cluster 模式下多种模式的统计数据,还支持请求优先。
四. 服务端启动模式
Sentinel 服务端启动模式可以分为 Alone 独立模式和 Embedded 嵌入模式。
独立模式(Alone),即作为独立的 token server 进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。
- 在独立模式下,我们可以直接创建对应的 ClusterTokenServer 实例并在 main 函数中通过 start 方法启动 Token Server。
- 嵌入模式(Embedded),即作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。
系统提供了 HTTP API 用于在 embedded 模式下转换集群流控身份:
http://<ip>:<port>/setClusterMode?mode=<xxx>
其中 mode 为 0 代表 client,1 代表 server,-1 代表关闭。
该请求会由 ModifyClusterModeCommandHandler 处理并最终调用 ClusterStateManager.applyState 方法来设置当前节点的状态。需要说明的是,嵌入模式可以不用显示启动服务端,而是由上面的 applyState 模式来设置,该方法会在内部启动服务。当然也可以不显示启动客户端,同样通过上面的方法,可以将当前节点设置为客户端模式。在将当前节点设置为客户端时,会先获取当前嵌入模式下的服务端对象,如果不为空,则停止该对象,并启动服务端;反之在设置服务端时,会先获取客户端对象,如果不为空,则先停掉,再启动嵌入模式下服务端对象。应用启动接入 dashboard 后,可以通过管理台来控制各节点的角色,或者通过从配置中心加载规则来更改规则。
五.Handler
sentinel-transport-common 中定义了一套 handler 接口,用于对外提供 HTTP 接口同系统交互,从而能够获取系统数据或者对应用节点下发命令。
common 模块提供了如下几个基本接口:
- CommandCenter:命令中心,作为服务启动,定义了 start 和 stop 方法,主要提供 handler 的初始化和注册服务。
- HeartbeatSender:心跳发送接口,用于给控制台 dashboard 定时发送心跳
- CommandHandler:请求处理接口,请求对象为 CommandRequest,响应对象为 CommandResponse
- CommandMapping:注解,用于为 Handler 添加元数据,包括处理器名 (URL 路径名) 和描述
针对上面的接口,common 模块提供了相对应的 Provider 类,用于以 SPI 的方式加载默认 / 自定义的实现,如上图,包括:
- CommandCenterProvider:根据 SPI,加载设定的实现,如果有多个实现,则根据 Order 注解,选择优先级最高的一个
- HeartbeatSenderProvider:根据 SPI,加载设定的实现,如果有多个实现,则根据 Order 注解,选择优先级最高的一个
- CommandHandlerProvider:会加载所有的 Handler 实现类,不同模块提供的 Handler 实现只要以 SPI 的方式,在 META-INF 中提供对应的全限定名就会被该类扫描并使用。实现类需要增加 CommandMapping 注解以指定 URL。
如下为 common 模块提供的 Handler 实现
上图中 common 的 SPI 接口中还有一个 InitFunc 实现,包括 CommandCenterInitFunc 和 HeartbeatSenderInitFunc 两个实现类,这两个类实现了 InitFunc 接口,会在 InitExecutor 被调用时初始化所有的 InitFunc 实现。对应的作用为:
CommandCenterInitFunc:使用 CommandCenterProvider 获取对应的 CommandCenter 实现,依次执行 beforeStart 和 start 方法,以启动服务。即只要加载了 sentinel-transport-common 模块并通过 SPI 提供 CommandCenter 的实现,便会在 InitFunc 被调用时启动服务。
HeartbeatSenderInitFun:HeartbeatSenderProvider 获取对应的 HeartbeatSender 实现,启动定时器,每隔 5 秒执行一次 sendHeartbeat 方法。即只要加载了 sentinel-transport-common 模块并通过 SPI 提供 HeartbeatSender 的实现,便会在 InitFunc 被调用时启动心跳定时器。
上面提到,只要提供了 CommandCenter 和 HeartbeatSender 的实现,并通过 SPI 注册对应的实现,并会自动启动对应的服务,而位于 sentinel-transport-simple-http 和 sentinel-transport-netty 的模块为这两个接口提供了默认实现。
sentinel-transport-simple-http 提供的实现为 SimpleHttpCommandCenter 和 SimpleHttpHeartbeatSender。
SimpleHttpCommandCenter:基于 socket,以阻塞模式提供了简单的 http 服务器,会在启动前通过 CommandHandlerProvider 缓存所有的 Handler 对象,当请求进来时新开线程处理,并在线程中调用对应的 Handler 进行处理并返回
SimpleHttpHeartbeatSender:使用内建的 SimpleHttpRequest 向 dashboard 发送 Http 心跳请求
sentinel-transport-netty 提供的实现为 NettyHttpCommandCenter 和 HttpHeartbeatSender。
NettyHttpCommandCenter:基于 netty,以服务端模式启动,会在启动前通过 CommandHandlerProvider 缓存所有的 Handler 对象,内建的 HttpServerHandler 对象会在请求进来时获取解码后的对象,并根据请求类型调用对应的 Handler 进行处理并返回
HttpHeartbeatSender:使用 httpclient 客户端想 dashboard 发送 Http 心跳请求
综上,sentinel-cluster-server-default 模块提供了如下的 Handelr 实现,用于给 dashboard 提供集群信息并接受从 dashboard 发送过来的命令。
其中 Fetch 开头的为读取消息,Modify 开头的为修改系统消息。
六. 集群管理接口
Sentinel 预留了诸多管理接口,用于动态加载规则或者配置,然后更新本地的状态,这里对涉及到 cluster 模式下的几个管理接口进行说明。在这之前,先介绍下 demo 中以 Nacos 为配置中心的接入方式。
接入 Nacos 涉及到另外两个模块,sentinel-datasource-extension 和 sentinel-datasource-nacos。Extension 模块定义了 ReadableDataSource 接口,用于从数据源读取数据,返回配置数据 SentinelProperty。Extension 模块提供了一个抽象类实现 AbstractDataSource,实现了 loadConfig 方法。该类引入了 Converter 接口和 DynamicSentinelProperty 类,Converter 接口用于将数据源中读取的数据结构转换为 SentienlProperty 存储的数据格式;DynameicSentinelProperty 类为 SentinelPorperty 的默认实现,该类能够添加多个 PropertyListener 监听器,在添加时触发监听器的 configLoad 方法进行监听器的初次动作,并在数据发生变更时,逐个通知监听器,调用监听器的 configLoad 方法,提醒监听器进行更新。AbstractDataSource 实现了 loadConfig 方法,该方法会调用 readSource 方法,从数据源读取原始数据,并调用 Converter 进行数据转换。
Nacos 模块提供了 NacosDataSource 实现,继承自 AbstractDataSource,以接入 Nacos 配置中心。NacosDataSource 在初始化时会在 Nacos 上申请一个配置集,并添加监听器,然后执行一遍 loadConfig,从配置中心加载一遍配置并,更新 property 中的值并通知配置集上的监听器。Nacos 上的监听器会在配置发生变化时,调用 Convert 记性处理,并更新配置集,同时通知配置集上的监听器。
由上可知,可以通过使用 DynamicSentinelProperty 动态配置集上的监听器,配合数据眼监听配置变化,从而让系统做出相应的动作。事实上,sentinel 内置的大部分管理接口都是这样处理的,如下为集群相关的主要管理接口,均以 Manager 结尾。这些管理接口的结构都同 FlowRuleManager 一样,内部维护这一个或者多个配置源,并在配置源上设置了监听器,当配置源有数据变化时,会调用配置源的 updateValue 方法,更新配置源数据并且通知监听器。
- FlowRuleManager
这个在讲解 sentinel-core 模块时有介绍过,主要是存储本地限流规则集 SentinelProperty<List<FlowRule>>。该规则集上有 FlowPropertyListener 监听器,会在规则发生变更时重新构建,加载规则。
- ParamFlowRuleManager
同 FlowRuleManager,主要用于热点参数限流规则管理。
- ClusterClientConfigManager
集群客户端配置管理,主要管理:
1) 集群客户端配置,用于设定客户端超时时间,配置集为 SentinelProperty<ClusterClientConfig> 和监听器 ClientConfigPropertyListener。会在规则发生变更时,更新客户端的请求超时时间
2) 集群服务端信息配置,用于设定服务端的 ip 和端口信息,配置集为 SentinelProperty<ClusterClientAssignConfig> 和监听器 ClientAssignPropertyListener。会在规则发送变更时,更新本地配置,并通知 ServerChangeObserver 观察者服务端节点发送了变化,由之前的内容可以看到,DefaultClusterTokenClient 为该接口的观察者,会在服务端信息发送变更时先断开同之前的链接,再同心的服务端节点建立新的链接。
- ClusterServerConfigManager
集群服务端配置管理,主要管理:
1) 集群服务端传输配置,用于设定服务端端口和 idle 时间,配置集为 SentinelProperty<ServerTransportConfig> 和监听器 ServerGlobalTransportPropertyListener。会在规则发生变更时,更新本地配置,并通知 ServerTransportConfigObserver 观察者配置发生了变化。由之前的内容可以看到,SentinelDefaultTokenServer 为该接口的观察者,会在服务端信息发送变更时,停止自身应用,再重新启动。
2) 集群服务端全局流控配置,用于设定全局流控配置项,包括滑动窗口实现大小,窗口格子数,允许通过的最大 qps 等。配置集为 SentinelProperty<ServerFlowConfig> 和监听器 ServerGlobalFlowPropertyListener,会在规则更新时重新设置这些配置内容。
3) 集群服务端 namespace 集合配置,用于设定集群中的 namespace 集合,配置集为 SentinelProperty<Set<String>> 和监听器 ServerNamespaceSetPropertyListener,会在配置发生变更时移除老 namesapce 的配置,并重新载入新 namesapce 的配置,包括对应的全局限流器 GlobalRequestLimiter,集群限流规则,集群热点限流规则。
- ClusterFlowRuleManager
集群限流规则配置管理,主要管理:
1) 集群规则配置,用于设定集群规则,配置集为 SentinelProperty<List<FlowRule>> 和监听器 FlowRulePropertyListener,会在配置发生变更时,移除对应 namespace 下的缓存的配置,并重新构建对应的规则。对于一个新的 flowId,会为其分配一个对应的 ClusterMetricStatistics 统计节点。
- ClusterParamFlowRuleManager
集群热点限流规则配置管理,同 ClusterFlowRuleManager
- ClusterStateManager
集群全局状态管理,主要管理:
1) 本机角色配置,配置集为 SentinelProperty<Integer> 和监听器 ClusterStatePropertyListener,会在规则发生变更时,调整本机的角色。角色包括:服务端,客户端和非集群模式。若规则为非集群模式,则会停止相关的客户端或者服务端;若设置为服务端模式,则会使用嵌入模式启动服务,若之前为客户端则会关闭客户端连接;若设置为客户端模式,则会启动客户端连接,若之前为服务端则会停止服务。
上述几个管理接口都可以接入配置中心如 Nacos,以通过配置中心和管理台来改变各配置项。
个人公众号:啊驼