Sentinel
的功能都是针对所有的请求资源,今天就来继续说一下Sentinel
的来源访问控制。很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel
的黑白名单控制的功能。黑白名单根据资源的请求来源(origin
)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
调用方信息通过
ContextUtil.enter(resourceName, origin)
方法中的origin
参数传入。
黑白名单规则(AuthorityRule
)非常简单,主要有以下配置项:
resource
:资源名,即限流规则的作用对象limitApp
:对应的黑名单/白名单,不同 origin
用 , 分隔,如 appA
,appB
strategy
:限制模式,AUTHORITY_WHITE
为白名单模式,AUTHORITY_BLACK
为黑名单模式,默认为白名单模式比如我们希望控制对资源 test
的访问设置白名单,只有来源为 appA
和 appB
的请求才可通过,则可以配置如下白名单规则:
1 | public class AuthorityDemo { |
1 |
|
1 |
|
【后面的话】最后是我自己实践自定义调用链的源码 。
Sentinel
的功能都是针对接口的,今天就来继续说一下Sentinel的热点参数限流。何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K
数据,并对其访问进行限制。比如:
ID
为参数,统计一段时间内最常购买的商品 ID
并进行限制ID
为参数,针对一段时间内频繁访问的用户 ID
进行限制热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel
利用 LRU
策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。
要使用热点参数限流功能,需要引入以下依赖:
1 | <dependency> |
然后为对应的资源配置热点参数限流规则,并在 entry
的时候传入相应的参数,即可使热点参数限流生效。
注:若自行扩展并注册了自己实现的
SlotChainBuilder
,并希望使用热点参数限流功能,则可以在chain
里面合适的地方插入ParamFlowSlot
。
那么如何传入对应的参数以便 Sentinel
统计呢?我们可以通过 SphU
类里面几个 entry
重载方法来传入:
1 | public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException |
其中最后的一串 args
就是要传入的参数,有多个就按照次序依次传入。比如要传入两个参数 paramA
和 paramB
,则可以:
1 | // paramA in index 0, paramB in index 1. |
注意
:若 entry
的时候传入了热点参数,那么 exit
的时候也一定要带上对应的参数(exit(count, args)
),否则可能会有统计错误。正确的示例:
1 | Entry entry = null; |
对于 @SentinelResource
注解方式定义的资源,若注解作用的方法上有参数,Sentinel
会将它们作为参数传入 SphU.entry(res, args)
。比如以下的方法里面 uid
和 type
会分别作为第一个和第二个参数传入 Sentinel API
,从而可以用于热点规则判断:
1 |
|
热点参数规则(ParamFlowRule
)类似于流量控制规则(FlowRule
):
属性 | 说明 | 默认值 |
---|---|---|
resource | 资源名,必填 | |
count | 限流阈值,必填 | |
grade | 限流模式 | QPS 模式 |
durationInSec | 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 | 1s |
controlBehavior | 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 | 快速失败 |
maxQueueingTimeMs | 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 | 0ms |
paramIdx | 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置 | |
paramFlowItemList | 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型 | |
clusterMode | 是否是集群参数流控规则 | false |
clusterConfig | 集群流控相关配置 |
我们可以通过 ParamFlowRuleManager
的 loadRules
方法更新热点参数规则,下面是一个示例:
1 | ParamFlowRule rule = new ParamFlowRule(resourceName) |
1 | public class ParamFlowQpsDemo { |
【后面的话】最后是我自己实践的源码 ,包括流量控制和初始规则加载等等。
Sentinel
的功能都是针对单机的,今天就来继续说一下Sentinel的集群流量控制。为什么要使用集群流控呢?假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控的方式。
另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。
集群流控中共有两种身份:
Sentinel 1.4.0 开始引入了集群流控模块,主要包含以下几部分:
sentinel-cluster-common-default
: 公共模块,包含公共接口和实体sentinel-cluster-client-default
: 默认集群流控 client 模块,使用 Netty 进行通信,提供接口方便序列化协议扩展sentinel-cluster-server-default
: 默认集群流控 server 模块,使用 Netty 进行通信,提供接口方便序列化协议扩展;同时提供扩展接口对接规则判断的具体实现(TokenService),默认实现是复用 sentinel-core 的相关逻辑注意:集群流控模块要求 JDK 版本最低为 1.7。
FlowRule
添加了两个字段用于集群限流相关配置:
1 | private boolean clusterMode; // 标识是否为集群限流配置 |
其中用一个专门的 ClusterFlowConfig
代表集群限流相关配置项,以与现有规则配置项分开:
1 | // 全局唯一的规则 ID,由集群限流管控端分配. |
flowId
代表全局唯一的规则 ID
,Sentinel
集群限流服务端通过此 ID
来区分各个规则,因此务必保持全局唯一。一般 flowId
由统一的管控端进行分配,或写入至 DB
时生成。thresholdType
代表集群限流阈值模式。其中单机均摊模式下配置的阈值等同于单机能够承受的限额,token server
会根据客户端对应的 namespace
(默认为 project.name
定义的应用名)下的连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30);而全局模式下配置的阈值等同于整个集群的总阈值。ParamFlowRule
热点参数限流相关的集群配置与 FlowRule
相似。
在集群流控的场景下,推荐使用动态规则源来动态地管理规则。
对于客户端,按照原有的方式来向 FlowRuleManager 和 ParamFlowRuleManager 注册动态规则源,例如:
1 | ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser); |
对于集群流控 token server
,由于集群限流服务端有作用域(namespace)的概念,因此我们需要注册一个自动根据 namespace
生成动态规则源的 PropertySupplier
:
1 | // Supplier 类型:接受 namespace,返回生成的动态规则源,类型为 SentinelProperty<List<FlowRule>> |
然后每当集群限流服务端 namespace set
产生变更时,Sentinel
会自动针对新加入的 namespace
生成动态规则源并进行自动监听,并删除旧的不需要的规则源。
要想使用集群限流服务端,必须引入集群限流 server 相关依赖:
1 | <dependency> |
Sentinel
集群限流服务端有两种启动方式:
token server
进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter
给集群提供流控服务。token server
与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server
和 client
可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server
的总 QPS
,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。我们可以使用 API
将在 embedded
模式下转换集群流控身份:
1 | http://<ip>:<port>/setClusterMode?mode=<xxx> |
其中 mode
为 0
代表 client
,1
代表 server
,-1
代表关闭。注意应用端需要引入集群限流客户端或服务端的相应依赖。
在独立模式下,我们可以直接创建对应的 ClusterTokenServer
实例并在 main
函数中通过 start
方法启动 Token Server
。
集群限流服务端注册动态配置源来动态地进行配置。配置类型有以下几种:
namespace set
: 集群限流服务端服务的作用域(命名空间),可以设置为自己服务的应用名。集群限流 client
在连接到 token server
后会上报自己的命名空间(默认为 project.name
配置的应用名),token server
会根据上报的命名空间名称统计连接数。transport config
: 集群限流服务端通信相关配置,如 server port
flow config
: 集群限流服务端限流相关配置,如滑动窗口统计时长、格子数目、最大允许总 QPS等我们可以通过 ClusterServerConfigManager
的各个 registerXxxProperty
方法来注册相关的配置源。
从 1.4.1
版本开始,Sentinel
支持给 token server
配置最大允许的总 QPS(maxAllowedQps)
,来对 token server
的资源使用进行限制,防止在嵌入模式下影响应用本身。
下图是Token Server 分配配置的示意图:
以下通用接口位于 sentinel-core
中:
以下通用接口位于 sentinel-cluster-common-default
:
集群流控 Client
端通信相关扩展接口:
集群流控 Server
端通信相关扩展接口:
集群流控 Server
端请求处理扩展接口:
【后面的话】最后是我自己实践的源码 ,包括流量控制和初始规则加载等等。
Sentinel
的流量控制,今天就来继续说一下Sentinel的系统自适应保护。Sentinel 系统自适应保护从整体维度对应用入口流量进行控制,结合应用的 Load、总体平均 RT、入口QPS 和线程数等几个维度的监控指标,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
在开始之前,先回顾一下 Sentinel
做系统自适应保护的目的:
长期以来,系统自适应保护的思路是根据硬指标,即系统的负载 (load1) 来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当load开始好转,则恢复流量的进入。这个思路给我们带来了不可避免的两个问题:
load
好转的一个动作,也需要1秒之后才能继续调整,这样就浪费了系统的处理能力。所以我们看到的曲线,总是会有抖动。load
仍然很高,通过率的恢复仍然不高。TCP BBR
的思想给了我们一个很大的启发。我们应该根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统 load)来做限流。最终我们追求的目标是 在系统不被拖垮的情况下,提高系统的吞吐率,而不是 load 一定要到低于某个阈值
。如果我们还是按照固有的思维,超过特定的 load 就禁止流量进入,系统 load 恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照果来调节因,都无法取得良好的效果。
Sentinel
在系统自适应保护的做法是,用 load1 作为启动控制流量的值,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体Load、RT、入口QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效
。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的阈值类型:
Load
(仅对 Linux/Unix-like
机器生效):当系统 load1
超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt
计算得出。设定参考值一般是 CPU cores * 2.5
。CPU usage
(1.5.0+ 版本):当系统 CPU
使用率超过阈值即触发系统保护(取值范围 0.0-1.0
)。RT
:当单台机器上所有入口流量的平均RT
达到阈值即触发系统保护,单位是毫秒。线程数
:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。入口 QPS
:当单台机器上所有入口流量的 QPS
达到阈值即触发系统保护。先用经典图来镇楼:
我们把系统处理请求的过程想象为一个水管,到来的请求是往这个水管灌水,当系统处理顺畅的时候,请求不需要排队,直接从水管中穿过,这个请求的RT是最短的;反之,当请求堆积的时候,那么处理请求的时间则会变为:排队时间 + 最短处理时间。
我们用 T 来表示(水管内部的水量),用RT来表示请求的处理时间,用P来表示进来的请求数,那么一个请求从进入水管道到从水管出来,这个水管会存在 P * RT
个请求。换一句话来说,当 T ≈ QPS * Avg(RT)
的时候,我们可以认为系统的处理能力和允许进入的请求个数达到了平衡,系统的负载不会进一步恶化。
接下来的问题是,水管的水位是可以达到了一个平衡点,但是这个平衡点只能保证水管的水位不再继续增高,但是还面临一个问题,就是在达到平衡点之前,这个水管里已经堆积了多少水。如果之前水管的水已经在一个量级了,那么这个时候系统允许通过的水量可能只能缓慢通过,RT会大,之前堆积在水管里的水会滞留;反之,如果之前的水管水位偏低,那么又会浪费了系统的处理能力。
然而,和 TCP BBR 的不一样的地方在于,还需要用一个系统负载的值(load1)来激发这套机制启动。
这种系统自适应算法对于低 load 的请求,它的效果是一个“兜底”的角色。
对于不是应用本身造成的 load 高的情况(如其它进程导致的不稳定的情况),效果不明显
。
1 | public class SystemGuardDemo { |
【后面的话】最后是我自己实践的源码 ,包括流量控制和初始规则加载等等。
Sentinel
的流量控制,今天就来继续说一下Sentinel的熔断降级。除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用
进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
Sentinel 1.8.0 及以上版本对熔断降级特性进行了全新的改进升级,我们可以选择最新版本体验降级规则熔断。
Sentinel 提供以下几种熔断策略:
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT
(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态
),若接下来的一个请求响应时间小于设置的慢调用 RT
则结束熔断,若大于设置的慢调用 RT
则会再次被熔断。1 | public class SlowRatioCircuitBreakerDemo { |
ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态
),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%
。ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态
),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。注意异常降级仅针对业务异常,对 Sentinel
限流降级本身的异常(BlockException
)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex)
记录业务异常。示例:
1 | Entry entry = null; |
开源整合模块,如
Sentinel Dubbo Adapter
,Sentinel Web Servlet Filter
或@SentinelResource
注解会自动统计业务异常,无需手动调用。但是如果你的程序发生异常的异常被处理过,或者异常时并不会抛出异常,则需要你自己手动调用Tracer.trace(ex)
来记录业务异常。否则你的异常比例
和异常数
将不会生效。
熔断降级规则(DegradeRule)包含下面几个重要的属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
Sentinel
支持注册自定义的事件监听器监听熔断器状态变换事件(state change event)。示例:
1 | EventObserverRegistry.getInstance().addStateChangeObserver("logging", |
【后面的话】最后是我自己实践的源码 ,包括流量控制和初始规则加载等等。
另外在使用API
去加载规则的时候,发现存在规则不生效的时候,通过调试发现:Sentinel
在加载规则到内存中的时候会校验规则的合法性,如果规则不合法,该规则将不被加载。
具体可以查看com.alibaba.csp.sentinel.property#configLoad
方法的实现类中参数校验方法,下面贴出DegradeRule
的校验方法
1 |
|
Sentinel
的基本原理,今天就来具体说一下Sentinel
的流量控制。壹、概述
FlowSlot
会根据预设的规则,结合前面 NodeSelectorSlot
、ClusterNodeBuilderSlot
、StatistcSlot
统计出来的实时信息进行流量控制。
限流的直接表现是在执行 Entry nodeA = SphU.entry(资源名字)
的时候抛出 FlowException
异常。FlowException
是 BlockException
的子类,您可以捕捉 BlockException
来自定义被限流之后的处理逻辑。
同一个资源可以对应多条限流规则。FlowSlot
会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。
一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
resource
:资源名,即限流规则的作用对象 count
: 限流阈值grade
: 限流阈值类型,QPS 或线程数strategy
: 根据调用关系选择策略贰、基于QPS/并发数的流量控制
流量控制主要有两种统计类型,一种是统计线程数
,另外一种则是统计 QPS
。类型由 FlowRule.grade
字段来定义。其中,0
代表根据并发数量来限流,1
代表根据 QPS 来进行流量控制。其中线程数
、QPS
值,都是由 StatisticSlot
实时统计获取的。
可以通过下面的命令查看实时统计信息:
1 | curl http://localhost:8719/cnode?id=resourceName |
8719
端口可以通过配置文件修改
输出内容格式如下:
1 | idx id thread pass blocked success total Rt 1m-pass 1m-block 1m-all exeption |
其中:
线程数限流用于保护业务线程数不被耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对高线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离),或者使用信号量来控制同时请求的个数(信号量隔离)。这种隔离方案虽然能够控制线程数量,但无法控制请求排队时间。当请求过多时排队也是无益的,直接拒绝能够迅速降低系统压力。Sentinel线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程个数,如果超出阈值,新的请求会被立即拒绝。
1 | public class FlowThreadDemo { |
当 QPS
超过某个阈值的时候,则采取措施进行流量控制。流量控制的手段包括下面 3 种,对应 FlowRule
中的 controlBehavior
字段:
1、直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT
)方式。该方式是默认的流量控制方式,当QPS
超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException
。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
2、冷启动(RuleConstant.CONTROL_BEHAVIOR_WARM_UP
)方式。该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
3、匀速器(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式。这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
叁、基于调用关系的流量控制
调用关系包括调用方、被调用方;方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel
通过 NodeSelectorSlot
建立不同资源间的调用的关系,并且通过 ClusterNodeBuilderSlot
记录每个资源的实时统计信息。
有了调用链路的统计信息,我们可以衍生出多种流量控制手段。
ContextUtil.enter(resourceName, origin)
方法中的 origin
参数标明了调用方身份。这些信息会在 ClusterBuilderSlot
中被统计。可通过以下命令来展示不同的调用方对同一个资源的调用数据:
1 | curl http://localhost:8719/origin?id=nodeA |
调用数据示例:
1 | id: nodeA |
上面这个命令展示了资源名为 nodeA
的资源被两个不同的调用方调用的统计。
限流规则中的 limitApp
字段用于根据调用方进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:
default
:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。{some_origin_name}
:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA
配置了一条针对调用者caller1
的规则,那么当且仅当来自 caller1
对 NodeA
的请求才会触发流量控制。other
:表示针对除 {some_origin_name}
以外的其余调用方的流量进行流量控制。例如,资源NodeA
配置了一条针对调用者 caller1
的限流规则,同时又配置了一条调用者为 other
的规则,那么任意来自非 caller1
对 NodeA
的调用,都不能超过 other
这条规则定义的阈值。同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default
NodeSelectorSlot
中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root
的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:
1 | machine-root |
上图中来自入口 Entrance1
和 Entrance2
的请求都调用到了资源 NodeA
,Sentinel
允许只根据某个入口的统计信息对资源限流。比如我们可以设置 FlowRule.strategy
为 RuleConstant.CHAIN
,同时设置 FlowRule.ref_identity
为 Entrance1
来表示只有从入口 Entrance1
的调用才会记录到 NodeA
的限流统计当中,而对来自 Entrance2
的调用漠不关心。
调用链的入口是通过 API
方法 ContextUtil.enter(name)
定义的。
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db
和 write_db
这两个资源分别代表数据库读写,我们可以给 read_db
设置限流规则来达到写优先的目的:设置 FlowRule.strategy
为 RuleConstant.RELATE
同时设置 FlowRule.ref_identity
为 write_db
。这样当写库操作过于频繁时,读数据的请求会被限流。
【后面的话】最后是我自己实践的源码 ,包括流量控制和初始规则加载等等。
另外在使用API
去加载规则的时候,发现存在规则不生效的时候,通过调试发现:Sentinel
在加载规则到内存中的时候会校验规则的合法性,如果规则不合法,该规则将不被加载。
具体可以查看com.alibaba.csp.sentinel.property#configLoad
方法的实现类中参数校验方法,下面贴出FlowRule
的校验方法
1 |
|
Sentinel
,今天就来具体说一下Sentinel
的基本原理。在 Sentinel
里面,所有的资源都对应一个资源名称以及一个 Entry
。Entry
可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 API
显式创建;每一个 Entry
创建的时候,同时也会创建一系列功能插槽(slot chain)。这些插槽有不同的职责,例如:
NodeSelectorSlot
负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级; ClusterBuilderSlot
则用于存储资源的统计信息以及调用者信息,例如该资源的 RT
, QPS
, thread count
等等,这些信息将用作为多维度限流,降级的依据;StatisticSlot
则用于记录、统计不同纬度的 runtime
指标监控信息;FlowSlot
则用于根据预设的限流规则以及前面 slot
统计的状态,来进行流量控制;AuthoritySlot
则根据配置的黑白名单和调用来源信息,来做黑白名单控制;DegradeSlot
则通过统计信息以及预设的规则,来做熔断降级;SystemSlot
则通过系统的状态,例如 load1
等,来控制总的入口流量;总体的框架如下:
Sentinel
将 ProcessorSlot
作为 SPI
接口进行扩展(1.7.2 版本以前 SlotChainBuilder
作为 SPI
),使得 Slot Chain
具备了扩展的能力。您可以自行加入自定义的 slot
并编排 slot
间的顺序,从而可以给 Sentinel
添加自定义的功能。
下面介绍一下各个 slot
的功能。
这个 slot
主要负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级。
1 | ContextUtil.enter("entrance1", "appA"); |
上述代码通过 ContextUtil.enter()
创建了一个名为 entrance1
的上下文,同时指定调用发起者为 appA
;接着通过 SphU.entry()
请求一个 token
,如果该方法顺利执行没有抛 BlockException
,表明 token
请求成功。
以上代码将在内存中生成以下结构:
1 | machine-root |
注意:每个
DefaultNode
由资源ID
和输入名称来标识。换句话说,一个资源ID
可以有多个不同入口的DefaultNode
。
1 | ContextUtil.enter("entrance1", "appA"); |
以上代码将在内存中生成以下结构:
1 | machine-root |
上面的结构可以通过调用 curl http://localhost:8719/tree?type=root
来显示:
1 | EntranceNode: machine-root(t:0 pq:1 bq:0 tq:1 rt:0 prq:1 1mp:0 1mb:0 1mt:0) |
此插槽用于构建资源的 ClusterNode
以及调用来源节点。ClusterNode
保持资源运行统计信息(响应时间、QPS、block 数目、线程数、异常数等)以及原始调用者统计信息列表。来源调用者的名字由 ContextUtil.enter(contextName,origin)
中的 origin
标记。可通过如下命令查看某个资源不同调用者的访问情况:curl http://localhost:8719/origin?id=caller
:
1 | id: nodeA |
StatisticSlot
是 Sentinel
的核心功能插槽之一,用于统计实时的调用数据。
clusterNode
:资源唯一标识的 ClusterNode
的 runtime
统计 origin
:根据来自不同调用者的统计信息defaultnode
: 根据上下文条目名称和资源 ID
的 runtime
统计Sentinel
底层采用高性能的滑动窗口数据结构 LeapArray
来统计实时的秒级指标数据,可以很好地支撑写多于读的高并发场景。
这个 slot
主要根据预设的资源的统计信息,按照固定的次序,依次生效。如果一个资源对应两条或者多条流控规则,则会根据如下次序依次检验,直到全部通过或者有一个规则生效为止:
这个 slot
主要针对资源的平均响应时间(RT)以及异常比率,来决定资源是否在接下来的时间被自动熔断掉。
这个 slot
会根据对于当前系统的整体情况,对入口资源的调用进行动态调配。其原理是让入口的流量和当前系统的预计容量达到一个动态平衡。
注意系统规则只对入口流量起作用(调用类型为 EntryType.IN
),对出口流量无效。可通过 SphU.entry(res, entryType)
指定调用类型,如果不指定,默认是EntryType.OUT
。
Sentinel
的核心骨架,将不同的 Slot
按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain
其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。核心结构:
目前的设计是 one slot chain per resource
,因为某些 slot
是 per resource
的(比如 NodeSelectorSlot
)。
Context
代表调用链路上下文,贯穿一次调用链路中的所有 Entry
。Context
维持着入口节点(entranceNode)、本次调用链路的 curNode
、调用来源(origin)等信息。Context
名称即为调用链路入口名称。
Context
维持的方式:通过 ThreadLocal
传递,只有在入口 enter
的时候生效。由于 Context
是通过 ThreadLocal
传递的,因此对于异步调用链路,线程切换的时候会丢掉 Context
,因此需要手动通过 ContextUtil.runOnContext(context, f)
来变换 context
。
每一次资源调用都会创建一个 Entry
。Entry
包含了资源名、curNode(当前统计节点)、originNode(来源统计节点)等信息。
CtEntry
为普通的 Entry
,在调用 SphU.entry(xxx)
的时候创建。特性:Linked entry within current context(内部维护着 parent 和 child)
需要注意的一点:CtEntry
构造函数中会做调用链的变换,即将当前 Entry
接到传入 Context
的调用链路上(setUpEntryFor)。
资源调用结束时需要 entry.exit()
。exit
操作会过一遍 slot chain exit
,恢复调用栈,exit context
然后清空 entry
中的 context
防止重复调用。
Sentinel
里面的各种种类的统计节点:
StatisticNode
:最为基础的统计节点,包含秒级和分钟级两个滑动窗口结构。DefaultNode
:链路节点,用于统计调用链路上某个资源的数据,维持树状结构。ClusterNode
:簇点,用于统计每个资源全局的数据(不区分调用链路),以及存放该资源的按来源区分的调用数据(类型为 StatisticNode
)。特别地,Constants.ENTRY_NODE
节点用于统计全局的入口资源数据。EntranceNode
:入口节点,特殊的链路节点,对应某个 Context
入口的所有调用数据。Constants.ROOT
节点也是入口节点。构建的时机:
EntranceNode
:在 ContextUtil.enter(xxx)
的时候就创建了,然后塞到 Context
里面。NodeSelectorSlot
:根据 context
创建 DefaultNode
,然后 set curNode to context
。ClusterBuilderSlot
:首先根据 resourceName
创建 ClusterNode
,并且 set clusterNode to defaultNode
;然后再根据 origin
创建来源节点(类型为 StatisticNode
),并且 set originNode to curEntry
。几种 Node
的维度(数目):
ClusterNode
的维度是 resource
DefaultNode
的维度是 resource * context
,存在每个 NodeSelectorSlot
的 map
里面EntranceNode
的维度是 context
,存在 ContextUtil
类的 contextNameNodeMap
里面StatisticNode
)的维度是 resource * origin
,存在每个 ClusterNode
的 originCountMap
里面StatisticSlot
是 Sentinel
最为重要的类之一,用于根据规则判断结果进行相应的统计操作。
entry
的时候:依次执行后面的判断 slot
。每个 slot
触发流控的话会抛出异常(BlockException
的子类)。若有 BlockException 抛出,则记录 block 数据;若无异常抛出则算作可通过(pass),记录 pass 数据。
exit
的时候:若无 error(无论是业务异常还是流控异常)
,记录 complete(success)
以及 RT
,线程数-1
。
记录数据的维度:线程数+1
、记录当前 DefaultNode
数据、记录对应的 originNode
数据(若存在 origin
)、累计 IN
统计数据(若流量类型为 IN
)。
【后面的话】最后是我自己实践自定义调用链的源码 。
前面说过基于Guava的限流的解决方案,但是这个方案只适用于单体应用,所以这边我们就可用借助第三方中间件来实现,这里就使用Redis来实现,进一步实现集群限流的功能。主要参考Redis官方的伪代码:https://redis.io/commands/incr
我们在使用Redis的分布式锁的时候,大家都知道是依靠了setnx的指令,在CAS(Compare and swap)的操作的时候,同时给指定的key设置了过期实践(expire),我们在限流的主要目的就是为了在单位时间内,有且仅有N数量的请求能够访问我的代码程序。所以依靠setnx可以很轻松的做到这方面的功能。
比如我们需要在10秒内限定20个请求,那么我们在setnx的时候可以设置过期时间10,当请求的setnx数量达到20时候即达到了限流效果。代码比较简单就不做展示了。
当然这种做法的弊端是很多的,比如当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题。
其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。
而我们如果用Redis的list数据结构可以轻而易举的实现该功能,我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求
1 | public Response limitFlow(){ |
通过上述代码可以做到滑动窗口的效果,并且能保证每N秒内至多M个请求,缺点就是zset的数据结构会越来越大。实现方式相对也是比较简单的。
令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。
依靠上述的思想,我们可以结合Redis的List数据结构很轻易的做到这样的代码,只是简单实现依靠List的leftPop来获取令牌
1 | // 输出令牌 |
再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成
1 | // 10S的速率往令牌桶中添加UUID,只为保证唯一性 |
Guava 是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法。
Guava 的好处
下面就使用Guava 中提供的并发相关的工具中的RateLimiter
来实现一个限流的功能。
1 | dependency> |
1 |
|
1 | package com.eelve.limiting.guava.aspect; |
1 | package com.eelve.limiting.guava.configuration; |
通过上面的代码我们就可用对
/get
接口实现限流了,但是也有明显的缺点,就是规则被写死,所以下面我们通过注解方式实现。
1 | package com.eelve.limiting.guava.annotation; |
1 | package com.eelve.limiting.guava.aspect; |
1 | /** |
通过上面的代码我们就实现了零活的通过注解的方式实现了限流功能,并且我们还可以在
Around
通知的时候灵活实现。包括过滤某些异常等等。
【后面的话】除了前面我们使用的RateLimiter
之外,Guava
还提供了专门针对超时的SimpleTimeLimiter
组件,有兴趣的也可以尝试一下。另外以上的源码都可用在 limiting 中找到。
Sentinel
有了简单的了解之后,下面就Spring Boot
单体应用集成Sentinel
做一下简单的讨论。实际上官方已经提供了 Spring Cloud Alibaba Sentinel ,然后在配合 控制台
就可以方便使用熔断能力。但是存在部分不想引入控制台
的场景,此文就由此而来。Sentinel
在官方提供了API
用于动态修改熔断的规则,针对每种规则都有独有的loadRules
方法:
1 | /** |
1 | /** |
Sentiunel
还有一个缺点,就是熔断规则只缓存在内存中,当应用重启之后,规则就消失了。所以解决方法就是可以考虑讲规则持久化,官方也有相应的实现的方案:动态规则扩展 。我这里实现的方案则是将规则存在数据库中,并提供API方式修改规则。
sentinel-annotation-aspectj
提供注解支持功能,并且其中包含了sentinel-core
所以就不需要单独再引入了。
1 | <dependency> |
包括流控规则和降级规则的实体类
1 | package com.eelve.limiting.sentinel.entity; |
1 | package com.eelve.limiting.sentinel.entity; |
主要是提供规则更新的工具类
1 | package com.eelve.limiting.sentinel.enums; |
1 | package com.eelve.limiting.sentinel.util; |
主要是提供接口给前端用于规则更新,并且包括更新内存中的熔断规则。
1 | package com.eelve.limiting.sentinel.controller; |
1 | package com.eelve.limiting.sentinel.controller; |
规则初始化可以使用
Sentinel
提供的SPI
机制,实现com.alibaba.csp.sentinel.init#InitFunc
接口,在接口被第一次调用时初始化,不过需要单独引入sentinel-datasource-extension
。当然我们也可以直接Spring
提供的CommandLineRunner
或ApplicationRunner
在项目启动是从数据库中加载规则。
1 | package com.eelve.limiting.sentinel.config; |
至此简单的
Spring Boot
单体应用接入Sentinel
的熔断能力的后端开发就完成了。然后前端再开发相应的页面,就可以给用户真正的使用了。
【后面的话】以上的接口有一点缺陷就是需要用户填写具体的熔断资源名称,但是用户实际上是有可能填写错误,从而导致熔断规则不生效。为此这里给出的解决方案是,在应用启动过程中扫描所有添加 @SentinelResource
注解的资源,然后再开放接口提供给前端,然后用户再填写熔断资源名称的时候就可以通过下拉来选择具体的资源名称了。
1 | package com.eelve.limiting.sentinel.config; |
1 | package com.eelve.limiting.sentinel.controller; |
只有Controller层和Service层的直接第一层方法才能通过注解触发,如果是方法再调用普通方法需要勇SphO或者SphU原生写法
1 | private void extractedSphO(Integer num) { |
Sentinel
来记录以下。随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel
是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助您保障微服务的稳定性。
资源是 Sentinel
的关键概念。它可以是 Java
应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。只要通过 Sentinel API
定义的代码,就是资源,能够被 Sentinel
保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:
流量控制有以下几个角度:
QPS
、线程池、系统负载等;Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel
的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。当调用链路中某个资源出现不稳定,例如,表现为 timeout
,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。
降级有以下几个角度:
和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
除了对并发线程数进行控制以外,Sentinel
还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
Sentinel同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。
针对这个情况,Sentinel
提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
API
,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。Sentinel
提供开放的接口,方便您定义及改变规则。Sentinel
提供实时的监控系统,方便您快速了解目前系统的状态。SphU
包含了try-catch
风格的API
。用这种方式,当资源发生了限流之后会抛出BlockException
。这个时候可以捕捉异常,进行限流之后的逻辑处理。示例代码如下:
1 | // 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。 |
注意:
SphU.entry(xxx)
需要与entry.exit()
方法成对出现,匹配调用,否则会导致调用链记录异常,抛出ErrorEntryFreeException
异常。
SphO
提供 if-else
风格的 API
。用这种方式,当资源发生了限流之后会返回 false
,这个时候可以根据返回值,进行限流之后的逻辑处理。示例代码如下:
1 | // 资源名可使用任意有业务语义的字符串 |
Sentinel
支持异步调用链路的统计。在异步调用中,需要通过 SphU.asyncEntry(xxx)
方法定义资源,并通常需要在异步的回调函数中调用 exit
方法。以下是一个简单的示例:
1 | try { |
SphU.asyncEntry(xxx)
不会影响当前(调用线程)的 Context
,因此以下两个 entry
在调用链上是平级关系(处于同一层),而不是嵌套关系:
1 | // 调用链类似于: |
若在异步回调中需要嵌套其它的资源调用(无论是 entry
还是 asyncEntry
),只需要借助Sentinel
提供的上下文切换功能,在对应的地方通过 ContextUtil.runOnContext(context, f)
进行 Context
变换,将对应资源调用处的 Context
切换为生成的异步 Context
,即可维持正确的调用链路关系。示例如下:
1 | public void handleResult(String result) { |
此时的调用链就类似于:
1 | -parent |
Sentinel
提供了 @SentinelResource
注解用于定义资源,并提供了 AspectJ
的扩展用于自动定义资源、处理 BlockException
等。使用 Sentinel Annotation AspectJ Extension
的时候需要引入以下依赖:
1 | <dependency> |
注意:注解方式埋点不支持 private 方法。
@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback
配置项。 @SentinelResource
注解包含以下属性:
value
:资源名称,必需项(不能为空)entryType
:entry
类型,可选项(默认为 EntryType.OUT
)blockHandler
/ blockHandlerClass
: blockHandler
对应处理 BlockException
的函数名称,可选项。blockHandler
函数访问范围需要是 public
,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException
。blockHandler
函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass
为对应的类的 Class
对象,注意对应的函数必需为 static
函数,否则无法解析。fallback
:fallback
函数名称,可选项,用于在抛出异常的时候提供 fallback
处理逻辑。 fallback
函数可以针对所有类型的异常(除了 exceptionsToIgnore
里面排除掉的异常类型)进行处理。fallback
函数签名和位置要求:Throwable
类型的参数用于接收对应的异常。fallback
函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass
为对应的类的 Class
对象,注意对应的函数必需为 static
函数,否则无法解析。defaultFallback
(since 1.6.0):默认的 fallback
函数名称,可选项,通常用于通用的 fallback
逻辑(即可以用于很多服务或方法)。默认 fallback
函数可以针对所以类型的异常(除了 exceptionsToIgnore
里面排除掉的异常类型)进行处理。若同时配置了 fallback
和 defaultFallback
,则只有 fallback
会生效。defaultFallback
函数签名要求:Throwable
类型的参数用于接收对应的异常。defaultFallback
函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass
为对应的类的 Class
对象,注意对应的函数必需为 static
函数,否则无法解析。exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback
逻辑中,而是会原样抛出。注:1.6.0 之前的版本
fallback
函数只针对降级异常(DegradeException
)进行处理,不能针对业务异常进行处理。
特别地,若 blockHandler
和 fallback
都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。若未配置 blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出。
Sentinel
的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel
也提供相关 API
,供您来定制自己的规则策略。
Sentinel
支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则 和 热点参数规则。
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,资源名是限流规则的作用对象 | |
count | 限流阈值 | |
grade | 限流阈值类型,QPS 或线程数模式 | QPS 模式 |
limitApp | 流控针对的调用来源 | default,代表不区分调用来源 |
strategy | 调用关系限流策略:直接、链路、关联 | 根据资源本身(直接) |
controlBehavior | 流控效果(直接拒绝 / 排队等待 / 慢启动模式),不支持按调用关系限流 | 直接拒绝 |
同一个资源可以同时有多个限流规则。
理解上面规则的定义之后,我们可以通过调用 FlowRuleManager.loadRules()
方法来用硬编码的方式定义流量控制规则,比如:
1 | private static void initFlowQpsRule() { |
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
同一个资源可以同时有多个降级规则
理解上面规则的定义之后,我们可以通过调用 DegradeRuleManager.loadRules()
方法来用硬编码的方式定义流量控制规则。
1 | private static void initDegradeRule() { |
Sentinel
系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load
、CPU
使用率、总体平均 RT
、入口 QPS
和并发线程数
等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
Field | 说明 | 默认值 |
---|---|---|
highestSystemLoad | load1 触发值,用于触发自适应控制阶段 | -1 (不生效) |
avgRt | 所有入口流量的平均响应时间 | -1 (不生效) |
maxThread | 入口流量的最大并发数 | -1 (不生效) |
qps | 所有入口资源的 QPS | -1 (不生效) |
highestCpuUsage | 当前系统的 CPU 使用率(0.0-1.0) | -1 (不生效) |
理解上面规则的定义之后,我们可以通过调用 SystemRuleManager.loadRules()
方法来用硬编码的方式定义流量控制规则
1 | private void initSystemProtectionRule() { |
很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel
的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin
)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
授权规则,即黑白名单规则(AuthorityRule)非常简单,主要有以下配置项:
resource
:资源名,即限流规则的作用对象limitApp
:对应的黑名单/白名单,不同 origin
用 , 分隔,如 appA
,appB
strategy
:限制模式,AUTHORITY_WHITE
为白名单模式,AUTHORITY_BLACK
为黑名单模式,默认为白名单模式【后面的话】在使用API
去加载规则的时候,发现存在规则不生效的时候,通过调试发现:Sentinel
在加载规则到内存中的时候会校验规则的合法性,如果规则不合法,该规则将不被加载。
具体可以查看com.alibaba.csp.sentinel.property#configLoad
方法的实现类中参数校验方法,下面贴出FlowRule
和 Degrade
的校验方法
1 |
|
1 |
|
最后是我自己实现的 demo 。
Sentinel | Hystrix | resilience4j | 使用Guava实现 | |
---|---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 | |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 | |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) | Ring Bit Buffer | 令牌桶 |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 | |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 | |
基于注解的支持 | 支持 | 支持 | 支持 | 支持 |
单机限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter | 基于 QPS |
集群流控 | 支持 | 不支持 | 不支持 | |
流量整形 | 支持预热模式与匀速排队控制效果 | 不支持 | 简单的 Rate Limiter 模式 | |
系统自适应保护 | 支持 | 不支持 | 不支持 | |
热点识别/防护 | 支持 | 不支持 | 不支持 | |
Service Mesh 支持 | 支持 Envoy/Istio | 不支持 | 不支持 | |
控制台 | 提供开箱即用的控制台,可配置规则、实时监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 | |
是否支持默认规则 | 不支持,需要针对每个接口配置规则 | 支持 | 支持 | |
是否支持过滤异常 | 注解单个接口支持 | 注解和全局默认配置 | 注解和全局默认配置 |
1 | <dependency> |
@SentinelResource(value = “allInfos”,fallback = “errorReturn”)
1 |
|
1 |
|
默认过滤拦截所有Controller接口
1 | /** |
注意也可以不配置限流或者熔断方法。通过全局异常去捕获UndeclaredThrowableException或者BlockException避免大量的开发量
1 | spring: |
接入配置中心如:zookeeper等等,并对规则采用推模式
1 | <dependency> |
@HystrixCommand(fallbackMethod = “timeOutError”)
1 |
|
1 |
|
1 | /** |
1 | hystrix: |
曲线:用来记录2分钟内流量的相对变化,我们可以通过它来观察到流量的上升和下降趋势。
集群监控需要用到注册中心
1 | <dependency> |
可以按需要引入:bulkhead,ratelimiter,timelimiter等
1 |
|
1 | /** |
1 | resilience4j.circuitbreaker: |
配置的规则可以被代码覆盖
如grafana等
个人建议的话,比较推荐sentinel,它提供了很多接口便于开发者自己拓展,同时我觉得他的规则动态更新也比较方便。最后是相关示例代码:单体应用示例代码
我的站点使用halo
搭建的,主要涉及到的中间件有:Nginx
、Mysql
等;日常运行产生的数据有站点运行数据和资源数据,所以站点迁移也会从这些方面着手。
其实Nginx
的迁移很简单,只需要在新的服务器中安装即可,然后迁移nginx.conf
配置文件。我的站点还用到https
,所有在安装的时候要注意安装相应的模块以及证书的迁移。
1 | ./configure --prefix=/usr/local/nginx --add-module=../ngx_cache_purge-1.3/ --with-http_stub_status_module --with-http_ssl_module --with-http_flv_module --with-http_gzip_static_module |
在新的服务器安装Mysql
服务,然后导入sql文件即可:
1 | mysqldump -u$db_user -p$db_password $db_name | gzip > /home/firbackup/halodb.sql.gz |
对于资源数据,主要是halo
产生的主题以及上传的文章的图片等等。就直接采用压缩打包,然后发送到新服务器再解压即可。
1 | tar czvf /home/firbackup/halo.tar.gz /root/.halo |
1 | #如果在同一个内网,记得使用内网ip,速度会更快哦 |
然后再解压到/root/.halo
文件夹即可
配置域名解析和相应的安全策略以及安装JDK
之后,你就可以重新启动halo
服务就好,到这里站点迁移工作就完成了。
日常备份也就是应用的配置文件以及应用产生的必要数据的备份。我这边的方案是定时打包压缩之后发送到邮箱中。下面给出具体脚本:
mailx
1 | yum -y install mailx |
1 | vim /etc/mail.rc |
如果在测试执行脚本,发现发送报错的话,那就是证书有问题,只需要在上面提到的/root/.certs/
文件夹中放置163邮箱
的证书即可。
1 | Resolving host smtp.163.com . . . done. |
1 |
|
1 | [root@fir /home]#crontab -e |
到这里你只需要去邮箱中下载备份的数据就好了。另外邮箱发送附件是有大小限制的,每个邮箱的具体情况不一。另外对于文章中的图片数据可以上传到又拍云
等云存储中即可。最后一句话道路千万条,数据备份第一条
。
【前情提要】最近参加了几次面试,面试的感受是简历上写的东西一定是都烂熟于心,另外知识要成体系,引导面试官跟着你走,而不是被面试官牵着走。另外hr最常问的一个问题是:你为什么从上一份工作离职。下面简单记录一下我碰到的面试题。
Use the WSL 2 based engine
Redis v4.0
之后有了 Module(模块/插件)
功能,Redis Modules
让 Redis
可以使用外部模块扩展其功能 。布隆过滤器就是其中的Module
。详情可以查看Redis
官方对 Redis Modules
的介绍 :https://redis.io/modules
另外,官网推荐了一个RedisBloom
作为Redis
布隆过滤器的Module
,地址:https://github.com/RedisBloom/RedisBloom. 其他还有:
RedisBloom
提供了多种语言的客户端支持,包括:Python
、Java
、JavaScript
和 PHP
。
如果我们需要体验Redis
中的布隆过滤器非常简单,通过 Docker 就可以了!这里我们使用这个仓库下的镜像:https://hub.docker.com/r/redislabs/rebloom/
下面是具体命令:
1 | cc@Chirius:/mnt/c/Users/Chirius$ docker run -p 9379:6379 --name redis-redisbloom redislabs/rebloom:latest |
根据提示修改内存参数等,注意使用root用户:
1 | cc@Chirius:/mnt/c/Users/Chirius$ cd ~ |
然后再重启容器,就可以启动成功了,然后进行体验
注意: key:布隆过滤器的名称,item : 添加的元素。
BF.ADD
:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:BF.ADD {key} {item}
。BF.MADD
: 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式BF.ADD与之相同,只不过它允许多个输入并返回多个值。格式:BF.MADD {key} {item} [item ...]
。**BF.EXISTS **
: 确定元素是否在布隆过滤器中存在。格式:BF.EXISTS {key} {item}
。BF.MEXISTS
: 确定一个或者多个元素是否在布隆过滤器中存在格式:BF.MEXISTS {key} {item} [item ...]
。另外,BF.RESERVE
命令需要单独介绍一下:
这个命令的格式如下:
1 | BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion] 。 |
下面简单介绍一下每个参数的具体含义:
key
:布隆过滤器的名称error_rate
:误报的期望概率。这应该是介于0到1之间的十进制值。例如,对于期望的误报率0.1%(1000中为1),error_rate
应该设置为0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU使用率越高。capacity
: 过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。可选参数:
expansion
。默认扩展值为2。这意味着每个后续子过滤器将是前一个子过滤器的两倍。1 | cc@Chirius:/mnt/c/Users/Chirius$ docker exec -it redis-redisbloom bash |
【后面的话】布隆过滤器主要用来解决缓存穿透(大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层)
。一般MySQL 默认的最大连接数在 150 左右,这个可以通过show variables like '%max_connections%';
命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般3000
个并发请求就能打死大部分数据库了。布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的就是判断key
是否合法。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走具体的业务的流程。
1 | wsl --set-version Ubuntu-18.04 2 |
其中Ubuntu-18.04
为你安装的WSL的发行版本,可以通过 wsl -l -v
来查看安装的WSL的发行版本详细信息。
另外我在升级的过程中遇到了WSL 2 需要更新其内核组件
问题。解决方法也很简单,从微软下载WSL2 Linux内核的升级包, 下载完成之后直接一路安装即可,之后WSL2就可以成功升级了。
最后如果想要将默认的WSL发行版设置成WSL2,可以使用下面命令
1 | wsl --set-default-version 2 |
先更新,再安装xfce4
和xrdp
1 | $ sudo apt update |
由于xrdp
安装好后默认配置使用的是和Windows远程桌面相同的3389
端口,为了防止和Windows系统远程桌面冲突,建议修改成其他的端口
1 | $ sudo vim /etc/xrdp/xrdp.ini |
注意这一步很重要,如果不设置的话会导致后面远程桌面连接上闪退
1 | $ vim ~/.xsession |
在Windows系统中运行mstsc命令打开远程桌面连接,地址输入localhost:13389
注意这里的端口号应当与上面修改配置中一致
输入linux系统的用户名和密码,就可以登陆成功了
【后面的话】如果在日常使用中遇到WSL异常,一般为网络端口占用问题导致,一般可以通过重置网络修复,使用管理员身份运行cmd,重置端口,然后重启:netsh winsock reset
1 | 参考的对象类型不支持尝试的操作。 |
1 | Microsoft Windows [版本 10.0.19041.388] |
Tomcat
中IllegalArgumentException
的报错,所以在这里记录一下。在用Get请求是当URL中包含特殊字符,比如:<
、>
、(
、)
、{
、}
、|
等时,Tomcat会报出以下错误:
1 | java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986 |
因为Tomcat严格按照 RFC 3986规范进行访问解析,而 RFC 3986规范定义了Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符(RFC3986中指定了以下字符为保留字符:! * ’ ( ) ; : @ & = + $ , / ? # [ ])。传入的参数中有”{“不在RFC3986中的保留字段中,所以会报参数异常错。而且这个错误你在应用中处理不到,因为根本都还没有进入应用,在Tomcat中就已经报错了,而且就连你在Tomcat中配置错误页面也没有用。
Tomcat 7.0.76, 8.0.42, 8.5.12 这些版本之后可以定义requestTargetAllow 属性来允许禁止的字符。在tomcat的 catalina.properties文件中添加这一句:
1 | tomcat.util.http.parser.HttpParser.requestTargetAllow=|{} |
如果某些版本的Tomcat已经参照3.1
中的方法修改之后,还是不生效的话。从官网的文档中我们可以查看到如下提示:tomcat.util.http.parser.HttpParser. requestTargetAllow(This system property is deprecated. Use the relaxedPathChars and relaxedQueryChars attributes of the Connector instead)
所有我们在Tomcat配置文件中:$CATALINA_HOME/conf/server.xml添加relaxedQueryChars
属性添加到Connector元素:
1 | <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" URIEncoding="UTF-8" relaxedQueryChars="[]|{}^\`"<>" redirectPort="8443" /> |
在SpringBootApplication的的main方法中增加
1 | System.setProperty("tomcat.util.http.parser.HttpParser.requestTargetAllow","|{}"); |
另外在Springboot 2.0 之后的版本,可以自定义WebServerFactoryCustomizer
,添加特殊字符的支持:
1 | import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; |
分别在Github和Gitee平台上配置SSH公钥,便于使用git协议拉取和提交推送代码的时候需要输入密码。
我这里以我的git@github.com:eelve/fly.git仓库为示例来说明。使用工具或者命令拉取git@github.com:eelve/fly.git
1 | git clone git@github.com:eelve/fly.git |
进入拉取的仓库文件夹下,找到.git
的隐藏文件夹,打开config
文件
1 | [core] |
修改成如下配置
1 | [core] |
添加一个remote远程仓库,并添加远程仓库地址,修改的部分如下
1 | url = git@gitee.com:eelve/fly.git |
1 | [remote "gitee"] |
使用其他工具或者执行git push
命令推送,我这里没有其他分支我这里就省略了分支名称等等
1 | git push |
然后查看gitee上面的仓库,可以看到,就已经成功推送上去了。
【后面的话】完成上述工作之后,就能够做到一次推送,两个仓库都有代码了。
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
1 | package com.eelve.springboot.war; |
【后面的话】使用maven打包(clean package),生成的war包可以用于传统的部署方式(外部tomcat),也可以直接使用java -jar 的方式运行。