基于 Alibaba Sentinel 实现的分布式限流中间件服务以及遇到的坑和注意事项
2024年3月7日 l101784 minutes
基于 Alibaba Sentinel 实现的分布式限流中间件服务。主要对服务提供者提供限流、系统保护,对服务调用者提供熔断降级、限流排队等待效果。
实现目标:
- 作为服务提供者,保护自己不被打死,服务可以慢不可以挂。
- 作为客户端及时限速和熔断,防止对服务提供方包含 Http、数据库、MQ 等造成太大压力,防止把糟糕的情况变得更糟。
- 以用户、租户、对象等更细粒度进行流量精细控制。
- 服务预热,应用新发布上线,缓存尚未完全建立,防止流量一下子把服务打死。
- 能够根据 Prometheus、ClickHouse、Elasticsearch 提供的监控指标,动态生成规则,自适应调整规则。
概述
Sentinel 的基础知识请参考官方文档描述,这里单独介绍一些与我们定制相关的内容。
限流简单来说就三个点:资源、规则、效果。
资源:就是一个字符串,这个字符串可以自己定义、可以用注解自动生成、可以通过拦截器按规则生成。
规则:Sentinel 定义的一系列限流保护规则,比如流量控制规则、自适应保护规则。
效果:实际上“效果”也是“规则”定义的一部分。任何一条请求,命中某些资源规则后产生的效果,比如直接抛出异常、匀速等待。
Sentinel 全局注意事项和使用限制
使用开源默认 Sentinel 组件,有一些坑,或者说需要关注的注意事项:
- 单个进程内资源数量阈值是 6000,多出的资源规则将不会生效(因为是懒加载,资源先到先得),也不提示错误而是直接忽略,资源数量太多建议使用热点参数控制。
- 对于限流的链路模式,context 阈值是 2000,所以默认的 WEB_CONTEXT_UNIFY 为 true,如果需要链路限流需要把这个改为 false。
- 自定义时,资源名中不要带
|
线, 这个日志中要用,日志以此作为分割符。 - Sentinel 支持按来源限流,注意
origin
数量不能太多,否则会导致内存暴涨。 - 一个资源可以有多个规则,一条请求能否通过,取决于规则里阈值最小的限制条件。
- 限流的目的是保护系统,计数计量并不准确,所以不要拿限流做计量或配额控制。
- 增加限流一定程度上通过时间换空间,降低了 CPU、内存负载,对 K8S HPA 策略会有一定影响。后续我们也会考虑根据 Sentinel 限流指标进行扩缩容。
- 限流中如果有增加等待效果会使接口变慢,各调用链需要关注调用超时和事务配置。
- 目前 sentinel-web-servlet 和 sentinel-spring-webmvc-adapter 均不支持热点参数限流。为了支持热点参数需要自行扩展。
- sentinel-web-servlet 和 sentinel-spring-webmvc-adapter 会将每个到来的不同的 URL 都作为不同的资源处理,因此对于 REST 风格的 API,需要自行实现 UrlCleaner 接口清洗一下资源(比如将满足 /foo/:id 的 URL 都归到 /foo/* 资源下)。否则会导致资源数量过多,超出资源数量阈值(目前是 6000)时多出的资源的规则将不会生效。
- Java 中
sentinel-time-tick-thread
线程会额外多占用约 1-2% CPU,详细代码参考com.alibaba.csp.sentinel.util.TimeUtil
。
一些文档中尚未更新但是大家可能关心的内容:
- 从 1.8.7 版本开始资源匹配支持正则表达式,使用的是 Java 自带的正则引擎。
- 从 1.8.7 版本对 QPS 限流做了优化,支持 QPS 大于 1000 了。
- 热点参数限流已经支持按照线程数限流,文档尚未更新,文档中说只支持 QPS。
接入指导
总体架构图。
我们所有组件,规则加载都是由 Datasource 组件统一加载,配置是懒加载的,在第一次访问的时候加载,如果需要定义规则请在配置中心定义,这是由 Sentinel 在第一次初始化的时候(参考源码:com.alibaba.csp.sentinel.Env.java)通过 SPI 加载的。
注意:如果你有自编码使用 Sentinel SDK 自带的 XxxRuleManager.loadRules 加载规则,会被远端配置中心覆盖掉,远端配置变更自动刷新后会以远端配置为准,把 XxxRuleManager.loadRules 加载的规则覆盖掉。
底层限流策略实现
Sentinel 底层限流策略共有 2 种,另外有 2 个 WARMUP 的变体,总共 4 个。参考源码 com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController
。
- 基于绝对值的 DefaultController,只要总值校验通过即可,比如 QPS 10,1s 内可以前 500ms 通过 10 个,后 500ms 通过 0 个,只要在这 1s 内没超标就行。
- 基于频率的 ThrottlingController,漏桶算法,比如 QPS 10,要求以每 100ms 一个的固定频率执行。如果前 500ms 有 10 个请求,最多通过 5 个,其他的都要排队。
关于这几种策略支持的范围,有兴趣的可以查看源码 com.alibaba.csp.sentinel.slots.block.flow.FlowRuleUtil#generateRater
,核心代码如下。
根据以上代码,可以确定限流各个参数适用的范围,比如 “按照并发线程数限流,实现匀速等待效果” 就做不到了。
规则参数详解
Sentinel 规则的资源名字匹配支持正则表达式,但是不知道为什么文档里从未提及,可能是考虑到性能。如果要为某个规则启用正则,需主动设置 xxRule.setRegex(true),另外注意用的是 Java 正则匹配,不要和 Spring Path 的正则匹配混了。比如 Java 里 .*
代表任意匹配,Spring *
表达任意匹配。
系统自适应保护规则
参数示例:
- highestSystemLoad:当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的
maxQps * minRt
计算得出。设定参考值一般是CPU cores * 2.5
。 - highestCpuUsage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
- avgRt :当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- maxThread:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- qps:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
以上参数默认是 -1
,代表无限制。
注意:
- 在 K8S 环境下,Sentinel 读取当前指标值时,highestSystemLoad 获取的是宿主机的 load1,不是 Pod 的。参考:https://github.com/alibaba/Sentinel/issues/2260
- Sentinel 读取当前指标值时,获取的 CPU 指标取的是 Pod Cpu 和宿主机 CPU 的最大值,也就是说如果
宿主机 CPU 占用太高,Pod CPU 很低
,会误伤,会触发限流。 - highestSystemLoad 相当于要不要自适应的开关,达到条件后会计算下是否还能承受流量,不行才拒绝。这就是所谓的“自适应”。除 highestSystemLoad 外,其他几个参数是达到阈值就拒绝。
- 规则中的几个参数,可以在一条规则里全部设置,也可以分多个规则配置不同参数,也可以只设置某个,Sentinel 会自行合并参数计算。
流量控制
参数示例:
- resource:资源名,即限流规则的作用对象
- count: 限流阈值
- grade: 限流阈值类型,QPS(RuleConstant.FLOW_GRADE_QPS = 1) 或线程数(RuleConstant.FLOW_GRADE_THREAD = 0)。
- controlBehavior:限流效果,有直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT = 0)、冷启动(RuleConstant.CONTROL_BEHAVIOR_WARM_UP=1)、匀速器(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER=2)、冷启动-匀速器(RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER=3)。
- warmUpPeriodSec:冷启动时间,单位秒,默认 10s。
- maxQueueingTimeMs:最大排队等待时长,默认 500ms。(仅在匀速排队模式 + QPS 下生效)
- limitApp: 按来源限流,默认 default 表示忽略来源,所有来源都限流。如果 limitApp 为 null,将忽略此条规则,不限流直接放行。
- strategy: 根据调用关系选择策略:根据调用方限流(STRATEGY_DIRECT=0),根据调用链路入口限流(STRATEGY_CHAIN=1),具有关系的资源流量控制(STRATEGY_RELATE=2)。
注意:
- 匀速器模式的时候根据实际情况设置 maxQueueingTimeMs,处理好排队超时情况,如果太长客户端可能超时,如果太短直接抛出超时异常了,达不到匀速的效果。
热点参数限流
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。 Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。
参数示例:
- 默认参数参考流量控制规则的解释。
- paramIdx:热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的 args 参数索引位置,从 0 开始。
- durationInSec:统计窗口时间长度(单位为秒),默认 1s。
- paramFlowItemList:参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型。
- burstCount: 为应对突发流量"额外允许"的流量,在原 count 的基础上再额外加上这个值,相当于保底。默认为 0,仅在
快速失败|Warm UP
+ QPS 下生效。(Java 文档中未提及,代码中支持)
注意:
- 可以通过 paramFlowItemList 设置例外项,比如为 VIP 单独设置限流阈值。
- 每个参数索引 (paramIdx) 对应的不同值最多统计 4000(ParameterMetric.BASE_PARAM_MAX_CAPACITY)个。
- 在统计窗口时间长度(durationInSec)内最多允许统计 20 万个。可以理解为 LRU 的 Top N。
来源访问控制
参数示例:
- resource:资源名,即限流规则的作用对象
- limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB
- strategy:限制模式,AUTHORITY_WHITE=0 为白名单模式,AUTHORITY_BLACK=1 为黑名单模式,默认为白名单模式。
熔断降级
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
- 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
- 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
- grade:熔断策略,支持慢调用比例/异常比例/异常数策略。默认慢调用比例。
- count:慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值。
- timeWindow:熔断时长,单位为秒。
- minRequestAmount:熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断。默认 5。
- statIntervalMs:统计时长(单位为 ms),如 60*1000 代表分钟级。默认 1000ms。
- slowRatioThreshold:慢调用比例阈值,仅慢调用比例模式有效。
注意事项:
- 熔断降级规则在服务端时,Spring 的全局异常处理器一般会消化掉异常转换成一个合法的 Response,会导致熔断规则中的异常数规则失效,我们在 Server 端并不准备支持,参考 Issue 。
对象类型作为热点参数
在热点参数限流规则说明中,有单独提到一点 “参数只支持基本类型和字符串类型”。
很多场景下,我们的接口是 POST 类型,请求参数是一个 Request Body,能不能以此作为热点参数呢。
能。实现方式是对此对象实现 com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowArgument
接口,在接口中通过 paramFlowKey 返回真正作为热点的数据。
以上例子将使用 name 作为真正的热点参数。
当然,若需要配置例外项或者使用集群维度流控,则传入的参数只支持基本类型,要不然例外项的规则里没法配置。
集群限流
集群限流功能就不复制粘贴了,这里着重提几个注意事项:
- 集群 Token Server 不支持高可用,生产环境需自己做高可用改造。
- 客户端获取 Token 失败会降级到本地限流。
- 集群限流只是解决限流问题,不解决流量不均衡问题,这是网络层面的问题。常见的如 http 长连接情况下,客户端和服务器连接保持,一个客户端所有请求可能会发给同一个服务器节点,单机限流阈值是 10 QPS,部署了 3 个节点,理论上集群的总 QPS 可以达到 30,但是实际上由于流量不均匀导致集群总 QPS 还没有达到 30 就已经触发限流了。
关联限流
在 Sentinel 限流规则中,有一个大家容易忽略的属性:流控模式之关联模式。可实现类似于进程内的“背压”模型。
限流规则可以选择三种流控模式:
- 直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式。
- 关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流。
- 链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流。
典型应用场景:
- 数据库读写竞争:比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,限制某一方。
- 保证优先级任务:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是有限支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。
- 阻止请求放大:比如接口 A 本身流量很小,但是一次接口 A 又调用了其他 10 个接口,相当于请求放大了 10 倍,如果任由 A 随意调用,后端服务则无法承担压力,需要对 A 进行限流。
举例:
比如 http 接口 /create 作为入口流量,调用一次,需要对数据库(我们暂定数据库操作生产资源名是 /database)执行 10 次操作,如果不限制 /create,就会把数据库压崩溃。 那么可以在数据库资源/database 到达阈值 N 时,对/create 进行限流。注意这里:阈值是数据库资源 /database 的阈值,限流的却是资源 /create,数据库资源 /database 并不触发限流。
注意使用限制:关联限流仅限于“流量控制”规则,不支持热点参数限流规则。
日志和监控模块
我们自定义了日志模块,在 block 和 metrics 日志中增加 trace id、user id 等更多参数,通过 fluent-bit 收集到 ClickHouse 中。
可在 Granfana 中以 ClickHouse 作为数据源配置自己需要的视图,并结合告警组件配置告警,比如应用 1 分钟 block 次数超过 10 次触发告警。
自定义实现
如果各个 adapter 模块不能满足你的要求,可以自己编码实现,一个典型的示例代码如下,按需选择。
FAQ
Q:Sentinel 资源生成时如何忽略某些资源。
A:自定义 UrlCleaner,对想忽略的资源返回空字符。
Q:Sentinel DataSource adapter 和 XxxRuleManager.loadRules 两种加载规则的方式能不能同时使用。
A:不能。比如 Nacos DataSource adapter,远端配置有变更后自动刷新,会以远端配置为准,覆盖掉 XxxRuleManager.loadRules 主动加载的规则。
Q:对于限流的冷启动效果,冷启动结束进入稳定状态后,还会不会重新回到冷启动阶段。
A:会,一段时间流量较小或无流量后会回到冷启动阶段。通俗来讲就是会先判断一下“冷不冷”,很久没有流量或者流量很小,不就很冷。Sentinel 固定速率产生令牌再消费,服务第一次启动时,或者接口很久没有被访问,都会导致当前时间与上次生产令牌的时间相差甚远,所以第一次生产令牌将会生产 maxPermits 个令牌,直接将令牌桶装满。由于令牌桶已满,接下来 N 秒就是冷启动阶段。具体查看参考资料里的冷启动算法详解。
Q: 很多开发通过错误码来处理流程,而非通过异常。这种写法,导致 Sentinel 不能拦截到异常,无法触发降级。对于这种情况,有没有什么好的处理方法。
A: 实际上 Sentinel 是通过 Tracer.trace(e) 来统计业务异常的,因此可以收到错误码就调用此函数来统计业务异常。
Q: 我的服务响应有时快有时慢,为了尽可能保证服务可用,我能不能向未来“借”指标,或者某些接口比较重要能否优先处理。
A:QPS 模式下能。但是各个 adapter 都不支持,需要自己实现,参考 com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController#canPass(com.alibaba.csp.sentinel.node.Node, int, boolean) 和 com.alibaba.csp.sentinel.SphU#entryWithPriority(java.lang.String, com.alibaba.csp.sentinel.EntryType),核心在于需要 prioritized 为 true。
参考资料
- 令牌桶算法在 Sentinel 中的应用:https://blog.51cto.com/morris131/6506314
- Sentinel 中的冷启动限流算法:https://cloud.tencent.com/developer/article/1674916