作者:京东物流 刘达
一、Sentinel是什么?
Sentinel是从阿里技术体系内诞生并由相关社区从微服务到云原生阶段持续孵化的流量治理组件,在服务熔断限流以及秒级/分钟级监控方面提供了开箱即用的解决方案,此外作为支持云原生的重要探索,还提供了GO语言实现。Sentinel目前拥有着活跃的开源社区,从1.8.x版本开始,通过深度参与到SpringCloudAlibaba套件的支持,实现与Java主流开发框架SpringBoot及SpringCloud的兼容,进而成为了Java技术栈开源熔断限流的事实标准。虽然其2.x版本持续难产(可能是适配OpenSergo规范的影响),不过好在1.8.x并没停止迭代,今年以来已经在1.8.7(JDK8)的基础之上做了1.8.8(JDK17)的升级,完成了主流JDK版本的支持,基本能够满足Javaer的开发需求。
考虑到一般部署环境以JDK8居多,而且1.8.8除了JDK的升级外,没有做实质的功能改造,所以本篇文章主要基于1.8.7版本展开,即在JDK8的环境下对Sentinel的能力进行讨论。
二、Sentinel能带来什么?
就如前文所介绍的,Sentinel提供了完善的熔断、限流以及秒级/分钟级监控方案。最核心的一点,就是它把应用规则的目标统一抽象成了一个概念——资源。按照官方说明,资源可以是Java应用程序中的任何内容,比如服务、接口方法或者一段代码。只要用Sentinel提供的资源创建及释放API包裹起来,就可以称之为一个资源。资源的抽象为Sentinel的使用提供了极大的灵活性,这让开发者可以把代码链路中的任何一个部分都做到流量治理规则中,当然为了提高代码的可读性以及规范性,我们还是推荐以服务入口(Http\RPC)、接口或者接口的实现方法作为最小资源单元。
同时,Sentinel提供了丰富的控制规则:
区分来源、限流类型(QPS/线程数)、支持上下游链路、多种流控效果(拒绝、排队、慢启动)
热点限流是对流量控制的补充,对Top-K的热点参数进行限流处理
可以理解为应用级的流量控制策略,包含负载、单机平均RT,单机QPS等。不过,因为粒度较粗且部分指标的获取在容器环境下存在适配问题,所以社区中大部分人对这个的理解是还有待完善,成熟的案例比较少。
在熔断降级方面,则是直接对标的Hystrix框架,一是在实现机制上做了优化,相对Hystrix基于线程池的隔离控制策略,通过并发控制机制减少了线程池的创建和占用。另外,在配置上也更加简化,相信有过Hystrix使用经验,都会对其繁杂的注解配置项感到困惑,而当引入Sentinel时则无需担心这些。
如果仅仅是这些,Sentinel的定位也仅限于一个组件包,类似Hystrix一样。那为何本文开篇说Sentinel是一套解决方案呢?因为它还做到了另外的三点,接下来继续进行说明:
第一点:包容性
Sentinel通过对Adapter模块组持续迭代,目前已经兼容了主流的一系列调用协议及组件,比如dubbo2,dubbo3,httpclient,okhttp,grpc,sofa,quarkus,webflux,webmvc等。此外还完成了对Java开源网关的支持,包括zuul1,zuul2,springcloud-gateway。并且作为java应用流量控制的标准框架,很多其他开源框架也在项目内部做了相关的兼容。
第二点:界面化
Sentinel提供了方便易用的Dashboard,支持常用的一系列操作。比如应用的注册发现,规则参数的配置、查看,接口簇点链路的自发现和树状展示,秒级的监控视图等等,虽然开源方案展示的数据以内存或者对应用节点信息的拉取为依托,但这已经提供了非常强的指导性。其对应的商业化版本AHAS已经在阿里云中得到广泛应用。
第三点:扩展性
虽然core包提供的是基于应用内存的数据存取策略,但是sentinel同时对控制规则数据的拉取链路做了通用的抽象,并衍生出了一系列datasource包。包含apollo、consul、etcd、nacos、redis、zk等,他们具备的共同点就是客户端能够及时的获取到配置数据变更事件,而Sentinel依赖的控制规则就是以配置数据的形式维护在这些中间件中,并实现对客户端的分发。
前文介绍了这么多,那引入Sentinel能带来什么改变呢?
我觉得,首先,限流策略更加丰富,能够更好的应对日趋复杂的限流需求;其次,对于资源的流量管理实现了统一,从单一入口维护应用的所有流量控制规则,无需从网关、JSF等分别对应用的入口进行配置;限流的维度更加细化,从入口级下沉到了应用内资源;在熔断的支持上,除了JSF客户端调用外,应用内部或作为客户端对外部的任何协议调用都可以增加熔断的管理,服务的稳定性也会随之提升。
三、Sentinel的重点实现
概念之后再来说说Sentinel中的关键实现。这一部分准备分三块来说明。第一是它的核心规则控制链路,第二它的datasource实现结构,第三Dashboard与应用的交互逻辑。此处主要是专注在架构实现上,像业内熟知的一些控制算法比如令牌桶等,不再赘述。
1、核心规则控制链路
为了更好的理解这条链路的结构,此处引入一下官方图
上图即Sentinel的默认ProcessorSlotChain所配置的链路结构,默认实现依托DefaultProcessorSlotChain。
从代码上看是一个标准的链表结构,链表中装配的节点对象类型为AbstractLinkedProcessorSlot,也就是图中这些Slot的父类,我们或可以称之为Slot模版类。通过代码的查看,会发现它声明了两个方法,entry、exit分别作为Slot的入口和出口,并且在方法入参中携带了当前调用链路的上下文信息。通过官方图例能够看出,请求在进入处理链条后,按先后顺序会经过三类Slot,前置的NodeSelectorSlot、ClusterBuilderSlot用于调用链路的提取,中间StatisticSlot是通用的滑动窗口计数功能,比如RT、通过数、拒绝数、异常数等,后置的ParamFlowSlot、SystemSlot等则为真正的规则验证链条。如下图所示,
在上图中,会发现在模版类中有额外的两个方法,fireEntry和fireExit,这两个方法在AbstractLinkedProcessSlot中均有默认的实现,主要是用于在当前节点entry或exit方法执行过程中触发对next节点的调度,这就使得Slot链表触发顺序和完成顺序并不一定相同。这样的好处是在基础链路上提供了充足的灵活性,上游Slot可以按需触发下游Slot并获取执行结果。例如此处用于统计计数的StatisticSlot和用于规则验证的一系列Slot列表之前的关系,正常通过和规则拦截的调用都能被StatisticSlot抓取到进行统计。
2、Datasource实现
作为配置文件的读取来源,Sentinel提供了标准实现框架,并以此为基础,对主流的开源分布式配置中心也做了充分的适配,还是以类结构图作为切入点
可以看到,Sentinel提供的Datasource规范中,通过loadConfig,readSource,getProperty,close四个方法,覆盖了数据源的读取、加载、查询和释放整个流程。落地到具体实现方案,它提供了两个大的方向,前者是支持热更新,后者是不可变的配置来源,如jar包中的文件。
热更新模式下,基于数据源的特性,又细分了两种,我把它总结为轮询模式和订阅模式。像Eureka、本地文件这种,不支持数据变更事件的订阅,Sentinel提供的方案是按照一定的时间间隔,定期的去拉取最新数据比对是否需要更新,这种就是前面讲的轮询模式。还有一种,依托于越来越成熟的各种开源分布式配置中间件,应用通过SDK集成,能够方便的订阅指定的配置项,并实时接收数据变更事件进行处理逻辑,相对于轮询模式配置生效的更加及时,是目前线上案例中最常用的模式。
对于不可变模式,Sentinel也仅提供了一种实现,仅限于无配置变更需求的场景,支持一次性加载,好处是轻量依赖项少,但更新需要重启。
3、Dashboard与应用的交互
核心链路和数据来源之后,再来说说Sentinel关于控制面的实现。
首先,要说明的是,即便是到最新的版本,Sentinel Dashboard也不是必须的。尤其是在1.6.x以前,datasource和dashboard这些模块都还未完善,项目引用Sentinel的方式主要是以硬编码为主,控制规则是借助RuleManager的静态方法进行初始化,调用链路中通过SphU或SphO实现资源的创建,使用Tracer去显式的记录异常(@SentinelResource是在1.8.x才趋于完善)。现在,官方文档在大部分基础用例中也是以这些形式为主。那Dashboard的作用是什么?
Dashboard为我们提供了相对完整的控制面,基本涵盖了Sentinel的所有功能,包含各种规则的查看和配置、节点的自动注册发现以及健康状态监控、簇点链路的采集等,在可观测方面提供了秒级的监控视图,而在后台为这些功能的数据提供了基于内存的实现,为我们实现硬编码到控制台的转变提供了明确的方向。接下来从三个角度说明Dashboard的重点实现,分别是节点信息的发现、规则数据的维护、秒级监控的采集。逻辑链路如下所示:
这张链路图基本把Sentinel从客户端到Dashboard服务端的交互过程做了明确的展示。
A. 首先,Sentinel控制台对客户端的发现机制并不依赖于其他中间件,是独立的相对轻量的实现。逻辑上很简单,即在控制台暴露了一个MachineRegistryController,客户端配置Dashboard地址后,定时的上报心跳数据。
B. Sentinel对应用的Metrics数据采集是以定时拉取的方式,应用SDK会暴露http入口,用于提供给Dashboard服务侧进行调用。Dashboard中相关的规则配置也是以类似的方式进行,额外增加了数据的写入动作用于更新规则。
C. sentinel-core本身对Dashboard侧的交互没有依赖,而是通过transport包中以集成Spiloader的方式追加相关的初始化动作,实现像注册心跳、远端控制等附加能力,即sentinel-core仍然是独立可用的。
SlotChain、Dashboard和Datasource是一套组合拳,共同构成了Sentinel的流控解决方案。SlotChain完成了核心的规则控制逻辑,而Datasource则是解决了配置数据怎么更新怎么同步的问题,最后Dashboard则是将配置数据的界面化维护以及运行时数据的展示入口提供给了开发者,让线上的落地成为了可能。
四、Sentinel源码包结构
从工程结构上看,Sentinel以sentinel-core为基础,衍生了一系列的模块:
1、sentinel-dashboard:sentinel控制台,提供简易的登录方式,功能上支持节点注册展示,限流、熔断等流控规则的查看与下发,节点Metric信息的采集和聚合展示等。
2、sentinel-transport:内部通信模块,实现客户端节点的心跳注册以及消息接收功能。
3、sentinel-logging:sentinel为了和应用日志做隔离,单独指定的日志输出问题,这个模块提供了基于slf4j的默认实现,同时可以作为参考实现,以SPI的方式定制其他日志输出通道。
4、sentinel-adapter:协议适配模块,基本兼容了所有开源的协议,一是在资源生成时指定prefix,另外就是依赖调用链路中的FiIter做协议解析,以支持抽取流控规则依赖的参数,比如origin等。
5、sentinel-extension:这个模块组则是作为一版扩展实现,不过大部分主要是依赖各种分布式配置型中间件实现的datasource模块,实现规则数据的存取。
6、sentinel-cluster:主要是提供集群限流的部分简易实现,目前看这种基于token分发中心的集群限流方案在性能上损耗相对单机型还是要大一些。
五、Sentinel的落地问题及改造方案
在上文中,对Sentinel的逻辑架构以及源码已经做了详细介绍。如果在我们生产环境中落地,有哪些问题?
我们知道,Sentinel提供的默认实现,数据的读取以内存为主要媒介。Dashboard基于节点上报的心跳信息做汇总,收集到AppManagement中,用于左侧菜单栏的应用展示,并开始根据心跳上传时间判断各应用内节点的在线离线状态,当单个应用内所有节点都离线就对该应用进行隐藏。规则数据的读取以及Metric统计信息是通过远程调用节点的通信入口进行。由此可以衍生出下面四个场景的落地方案:
1、规则数据的读写
在第三部分中也介绍到,Sentinel对于规则数据的同步机制,提供了一组datasource模块,主要以各分布式配置中间件为中心,实现数据的写入和变更监听,达到动态变更规则数据的目的。如果开发者并不想引入这些额外的中间件,那么就需要根据自己的需求来扩展datasource模块。
接入新的配置源,首先从Dashboard开始,我们可以参考FlowControllerV2中提供的Nacos配置样例,对应于各个规则,实现基于appName的DynamicRuleProvider<T>和DynamicRulePublisher<T>的读写入口类,将规则数据以Json的形式写入新的配置源中,当然为了更大的扩展性和业务隔离性,我们可以在appName的参数基础上,再追加类似Datacenter这种参数,以区分不同的业务群,避免单个Dashboard中应用过量造成处理压力。
另外,就要考虑sentinel-datasource-{CustomizedDatasource}的开发,基于sentinel-datasource-extension这个数据源监听模版,参考sentinel-datasource-nacos中NacosDataSource实现对应的AbstractDataSource<String, T>,到此为止只是实现了Datasource类的定义和实现。而真正打通Dashboard到节点侧的数据链路,还需要对Datasource的调用和初始化流程。Sentinel包中的demo工程,有针对于各个datasource的初始化样例。
而我们如果想真正落地到项目中,仅仅这些还是不够的,而是需要把Datasource对各个规则的加载和监听封装成独立的Starter,这样才能最大限度降低springboot集成的复杂度。对于这个想法,我们则需要转向另一个开源项目:spring-cloud-alibaba-sentinel-datasource和spring-cloud-starter-alibaba-sentinel。
前者将不同Datasource和对应的配置节点做了关联,如下
①DatasourceFactoryBean的声明
public class NacosDataSourceFactoryBean implements FactoryBean<NacosDataSource>
②Properties初始化参数中携带对应的FactoryBean
public NacosDataSourceProperties() { super(NacosDataSourceFactoryBean.class.getName());}
后者,则是在初始化SentinelDataSourceHandler的过程中,根据SentinelProperties中各Datasource子节点配置与否决定是否初始化到Bean实例,如下
通过这几部分的构建,我们就可以实现自己的Sentinel-Starter,实现基于自定义数据源做规则数据的运维。
2、Metric统计数据及日志的托管
目前Metric信息是采用从Dashboard定时拉取再从应用维度聚合的方式进行。节点中的Metrics信息是相对完整的,我们可以从两方面来进行收集:
第一种方式,节点主动上报,收集至统一的存储,再以Grafana的形式做数据展示。具体方式的话,可以选用直接上传到定制的OAP服务,也可以扩展sentinel-log模块的实现做基于日志的上传(因为sentinel的设计机制中,自身的状态日志和业务日志是分离的)。不过,这会造成一定的性能损耗,落地到项目中需要进行详细的压测和评估。
第二种方式,由中控进行定时采样,时间间隔可以自定义,也就是以拉的方式进行,采样率可以在中控进行统一的动态控制,合理配置能够更容易达到性能损耗和观测性的平衡。
此处不再细说,方案比较多,见仁见智。
3、Dashboard的适配
Dashboard面向线上业务场景,需要解决的有以下三点:
①数据存储问题:上文中规则数据的Datasource扩展不再讨论,除此以外,Sentinel中节点、应用、簇点链路都是基于内存存储的,这就有一个问题——重启丢失。因为这些信息都是基于节点心跳的,数据丢失后意味着重新收集,那么之前在这些数据上做的规则配置就会丢失查看的入口。另外,应用的展示是以应用内节点的在线状态为准的,当应用内所有节点都离线,也会造成应用列表中缺失。所以解决方案就是需要将应用、节点保存至持久化存储中,例如Mysql,而簇点链路信息由于树状结构的特殊性,可以直接放到Redis中存储以及更新。
②权限问题:Dashboard通过轻量的自定义Filter实现的最简单的登录控制。落地的话就需要首先接入ERP统一登录,另外还需要做应用权限的管理,避免权限穿透和误操作问题。
③业务划分:需要按照业务进行应用层面的划分。实现多数据中心的支持,避免中心节点集成应用过多。
4、协议的适配
相比前几点,此处的改动是最轻量的。当开发使用的调用协议不在Sentinel官方兼容,那么就需要对协议做适配性开发。按照sentinel-adapter中对其他协议的适配方式,我们需要改造的大体可以总结为两点,第一,常规调用上下文参数的解析,比如调用来源等;第二,定义协议入口的资源前缀,与其他资源做区分。
六、小结
本文围绕Sentinel展开,从其基本概念到核心实现,再到探讨开源方案的挑战和如何在实际项目中应用和改进Sentinel。Sentinel以其高度的灵活性和可扩展性赢得了广大开发者的青睐,不论是新系统的开发还是现有系统的维护,Sentinel都能为我们提供极具价值的支持。
参考内容:
【1】https://sentinelguard.io/zh-cn/