14-系统可用性

一、系统可用性常见策略及实现

(一)服务不可用问题和基本对策

​ 服务访问失败原因有很多,例如分布式环境的固有原因、服务自身失败、服务依赖失败等。

​ 其中分布式环境的固有原因是指在分布式环境中存在的的网络连接等问题,服务自身失败是自己代码编写的问题,这里着重解决服务依赖失败的问题。

​ 如下图所示,服务B访问服务A,当A服务不可用时,服务B就会进行重试加大流量,从而导致线程资源持续消耗,最终会耗尽线程引发服务B不可用,从而引发雪崩。


​ 对于服务依赖失败的基本应对策略有超时与重试、异步解耦等。

​ 例如对于服务提供者,可以采取快速失败的方式,一旦自身服务发生错误,那么应该快速返回合理的处理结果,例如返回一个默认值;

​ 对于服务消费者,可以采用超时与重试的策略,重点关注不要被服务提供者所产生的错误影响到自身服务的可用性。超时指如果服务未能在这个时间内响应,将回复一个失败消息;重试指为了降低网络瞬态异常所造成的的网络通信问题,可以使用重试机制。

​ 而异步解耦则是把服务依赖失败的影响分摊到消息中间件从而降低服务失败的概率。

​ 那么对于服务失败,对应的处理策略有集群容错、服务隔离、请求限流、服务降级。

​ 集群容错:

​ 常见集群容错机制:Failover、Failfast、Failsafe、Failback、Forking、Broadcast

​ Failover Cluster:失败自动切换,当出现失败,重试集群其它服务器 。通常用于读操作,但重试会带来更长延迟。一般都会设置重试次数。

​ Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

​ Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

​ Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

​ Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。一般会设置最大并行数。

​ Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

​ 服务隔离:

​ 服务隔离是指当请求进来后,在某种资源商将请求进行隔离,使其不能相互影响。常见的隔离资源有线程隔离、进程隔离、集群隔离、机房隔离、读写隔离。

​ 线程隔离一般使用代码实现,例如Hystrix、Sentinel等框架都实现了线程隔离;进程隔离其实就是服务本身的隔离。

(二)请求限流思想和设计方法

​ 所谓限流即流量限制,限流的目的是在遇到流量高峰期或者流量突增时,把流量速率限制在系统所能接受的合理范围之内,不至于让系统被高流量击垮。

​ 为了保证在业务高峰期线上系统也能保证一定的弹性和稳定性,限流就是最常采用的方案之一。

​ 请求限流类型主要可以分为流量控制和流量整形。流量控制是控制单位时间内的流量以及并发流量,流量整形是指让流量可以更平稳的通过,例如使用漏桶算法和令牌桶算法。流量控制和流量整形是从理论的角度进行表述,但是从具体实现上,并不会做区分。

​ 流量控制可以分为单位时间内调用量限流和并发调用限流。

​ 单位时间内调用量限流一般使用计数器,这是单位时间内调用量限流的基本思想,非常简单,就是控制一定时间内的请求量大小,如果这个请求量在合理的访问阈值内,我们就认为能起到限流的效果。

​ 计数器(Counter)统计单位时间内某个服务的访问量。一旦访问量超过了所设定的阈值,则该单位时间段内不允许服务继续响应请求,或者把接下来的请求放入队列中等待到下一个单位时间段继续访问;但是计数器存在临界值的问题。

​ 滑动窗口:为了解决普通计数器的临界问题,我们可以采用滑动窗口来进行限流。在滑动窗口中,我们把单位时间设置为一个时间窗口,然后把时间窗口进行划分。比方说我们可以把10秒这个单位时间划分成10个时间格,这样每格代表1秒钟。然后每过1秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器。

​ 当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确

​ 流量整形:

​ 漏桶算法:漏桶算法有点像我们生活中用到的漏斗,液体倒进去以后,总是从下端的小口中以固定速率流出,也就是说不管突发流量有多大,漏桶都保证了流量的常速率输出。

​ 如果我们把服务调用量看做是液体,那么不管服务请求的变化多么剧烈,通过漏桶算法进行整形之后只会输出固定的服务请求。

​ 令牌桶算法:令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,请求处理过程需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。如果某一时间点上桶类存放着很多令牌,那么也就可以在这一时间点上响应很多的请求,因此服务请求的输入和输出都可以是变速的。

​ 令牌桶算法和漏桶算法的主要区别在于漏桶算法能够强行限制数据的传输速率,而令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。

(三)服务降级思想和设计方法

​ 服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务进行有策略的快速失败处理,以此避免服务之间的调用依赖影响到其他核心服务。

​ 服务降级的前提是服务分级,如下图所示,可以将服务分为三级,在主动做业务降级时,可以根据实际需要,对不同等级的服务进行降级,例如先降级三级系统,如果还无法满足业务需要,还可以继续降级二级系统。

​ 降级可以是有计划的执行,也可以是被动触发。如电商网站在双十一期间对部分非核心业务进行手工降级就属于前者,而后者则包括系统运行时为了控制异常的影响范围可以在程序级别实现自动服务降级。

​ 服务熔断是服务降级的常见实现方式,而具体的技术组件就是熔断器(Circuit Breaker)。熔断器的概念来源于电路系统,当流经该熔断器的电流过大时就会自动切断电路。在分布式系统中,也存在类似现实世界中的服务熔断器,当某个异常条件被触发,直接熔断整个服务,而不是一直等到该服务超时。

​ 熔断器设计思想是在每次发起请求时都会先经过熔断器,获取熔断器的状态,如果可以发起请求,就调用服务提供者;如果当前状态需要熔断,则直接返回降级结果,不再发起远程调用。

​ 熔断器有三种状态:Closed、Open、Half-Open

​ Closed: 熔断器关闭状态,不对服务调用进行限制,但会对调用失败次数进行积累,到了阈值或一定比例时则启动熔断机制。

​ Open:熔断器打开状态,此时对服务的调用将直接返回错误,不执行真正的网络调用。同时,熔断器设计了一个时钟选项,当时钟达到了一定时间时会进入半熔断状态。

​ Half-Open:半熔断状态,允许一定量的服务请求,如果调用都成功或达到一定比例则认为调用链路已恢复,关闭熔断器;否则认为调用链路仍然存在问题,又回到熔断器打开状态。

​ 当远程调用发生异常时,服务回退(Fallback)并不是直接抛出该异常,而是产生一个另外的处理机制来应对该异常,相当于执行了另一条路径上的代码或返回一个默认处理结果,而这条路径上的代码或这个默认处理结果并一定满足业务逻辑的实现需求,而只是告知服务的消费者当前调用中所存在的问题。

​ 熔断器在执行熔断时通过调用回退方法为客户端请求提供反馈信息。

​ 目前主流的熔断器实现有Sentinel、Hystrix、resilience4j,以下是三种主流熔断器的对比,Sentinel 从限流策略、流量整形、动态规则配置等场景,都是支持的最好的。

二、基于Sentinel实现流量控制

(一)Sentinel核心概念和工作流程

​ 作为一款由阿里巴巴开源的高可用流量管理框架,Sentinel提供了面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,采用多个维度来帮助开发者保障微服务的稳定性。

​ 1、Sentinel 业务架构

​ Sentinel 总体业务架构如下图所示,主要包含了四部分,分别是控制台、核心功能、动态规则配置、远程调用方式集成。

​ 控制台包括实时监控、机器发现、规则配置

​ 核心功能包括流量控制、速率控制、调用关系限流、线程数隔离、集群限流等等

​ 动态规则配置集成了如Zookeeper、Nacos、Apollo等配置中心

​ 远程调用方式集成主要集成了 Spring Cloud、Dubbo、gRPC、Service Mesh 等

​ 2、Sentinel 开源生态

​ Sentinel 也有非常强大的开源生态,主要包括流量入口、云原生、远程调用、配置中心,例如流量入口的zuul、nginx、gateway等,云原生的 K8S、envoy、Istio等,远程调用的GRPC、Dubbo、Spring Cloud等,配置中心的Apollo、etcd、Nacos等。

​ 下图中的箭头指向表示依赖的关系,其中 Sentinel 需要依赖配置中心,流量入口、云原生、远程调用则是依赖 Sentinel。

​ 3、Sentinel 技术架构

​ Sentinel 模块结构如下图所示,最主要的是 sentinel core,其提供了 Sentinel 最核心的功能,在基于 Sentinel core 的基础上,Sentinel 做了 Sentinel extension、sentinel transport、sentinel cluster、sentinel adapter 扩展。

​ Sentinel Core:这是 Sentinel 的核心模块,负责流量控制、熔断降级等核心功能。其他模块都在这个核心模块的基础上构建。

​ Sentinel Extension:该模块用于扩展 Sentinel 功能,包括数据源管理、AOP(面向切面编程)、冷启动等。数据源管理允许 Sentinel 从不同的配置中心(如 Redis、Zookeeper、Nacos、Apollo 等)获取配置信息,实时更新规则和限流策略。

​ Sentinel Adapter:这个模块用于适配不同的流量入口,使得 Sentinel 能够与不同的应用、框架(如 Zuul、Web、gRPC、Dubbo 等)集成,实现流量监控和控制。

​ Sentinel Transport:该模块用于与 Sentinel Dashboard 控制台通信,将 Sentinel 的运行时信息传输到 Sentinel Dashboard 控制台,以便于监控和管理。

​ Sentinel Dashboard:这是一个用于监控和管理 Sentinel 集群的 Web 控制台。它通过 Sentinel Transport 模块接收运行时数据,允许用户查看流量数据、规则信息,实时调整和管理限流、熔断策略等。

​ Sentinel 并没有独立的 "Sentinel Cluster" 模块,但是支持多个 Sentinel 实例组成集群,协同工作以提供高可用性和扩展性。这些实例一起利用 Sentinel 的各种模块和功能来确保流量控制和系统稳定性。

​ 4、Sentinel 核心概念

​ (1)资源

​ 资源可以是 Java 应用程序中的任何内容,如一个方法、一段代码、一个服务等,一般指一个具体的接口

​ (2)规则

​ 规则是围绕资源的实时指标数据设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则等

​ (3)指标数据

​ Sentinel 以资源为维度统计指标数据,这些指标包括每秒请求数、请求平均耗时、每秒异常总数等,基于这些指标数据,Sentinel 判断是否触发相应的规则。

​ (4)调用树:

​ Sentinel 在记录方法调用的时候,会将调用树进行分级和分类,以便更好地展示方法之间的调用关系和依赖关系。如下代码所示,对外提供了一个 web 访问入口,形成的调用树如下图所示。

​ ROOT 节点代表整个调用树的根节点。在 Sentinel 的调用树中,所有的调用关系都会汇聚到 ROOT 节点,它代表了整个系统的入口,也就是系统的顶层。

​ sentinel_spring_web_context 这个节点通常出现在 Web 应用中,代表了 Spring Web 上下文。在 Web 应用中,方法的调用往往是由请求触发的。当一个请求进入应用并触发了方法调用时,Sentinel 会记录这个调用,并将其放置在 "sentinel_spring_web_context" 节点下,以便更好地展示与 Web 请求相关的方法调用关系

​ 然后是端点入口的processBusiness方法,在processBusiness方法中分别调用了process1 和 process2 两个方法,则会在 processBusiness 节点下面生成两个子树 process1 和 process2,如果后续还有调用,则会在process1 和 process2下生成子树。

@PostMapping(value = "/") 
public Result processBusiness(Request request) {
    process1(request); 
  	process2(request);
}

​ (5)ProcessorSlot(处理器插槽)

​ 处理器插槽是Sentinel提供的插件,负责执行具体的资源指标数据的统计、限流、熔断降级、系统自适应保护等工作。

​ 一组处理器插槽表现为有序的处理器插槽链表(ProcessorSlotChain),Sentinel在执行方法之前根据ProcessorSlotChain调度处理器插槽完成资源指标数据的统计、限流、熔断降级等。

​ 同时我们也可以自定义 ProcessorSlot,添加自定义的功能。

​ ProcessorSlot定义如下代码所示,主要分为两组,一组是 entry 和 exit,用来启动各个节点对该资源本次访问的数据度量的开始和结束,另外一组是fireEntry和fireExit,用来表示该Slot的entry或exit方法已经执行完毕, 可以将entry对象传递给下一个Slot。

public interface ProcessorSlot<T> {
    // 执行entry 方法来启动各个节点对该资源本次访问的数据度量的开始
    void entry(Context context, ResourceWrapper resourceWrapper, T param, int count, boolean prioritized, Object... args) throws Throwable; 
    // 表示该Slot的entry或exit方法已经执行完毕, 可以将entry对象传递给下一个Slot
    void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable; 
    // 执行entry 方法来启动各个节点对该资源本次访问的数据度量的结束
    void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args); 
    // 表示该Slot的entry或exit方法已经执行完毕, 可以将entry对象传递给下一个Slot
    void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args); }

​ Sentinel中的ProcessorSlot 如下图所示

​ ProcessorSlotChain 的类结构如下图所示,首先是 ProcessorSlot 接口,实现类是 AbstractLinkedProcessorSlot抽象类,子类是 处理器插槽链表 ProcessorSlotChain、基于统计和降级规则金平匹配校验以决定是否降级的 DegradeSlot、根据预设资源进行限流的 FlowSlot,可以看到 ProcessorSlotChain 也是 ProcessorSlot 的一种;而ProcessorSlotChain 的实现类则是 DefaultProcessorSlotChain。

​ ProcessorSlotChain的代码如下所示,分别提供了在头结点和尾结点添加一个 ProcessorSlot 的 addFirst 方法和 addLast 方法。

​ 子类 DefaultProcessorSlotChain 则是实现了这两个方法。

public abstract class ProcessorSlotChain extends AbstractLinkedProcessorSlot<Object> { 
    
    public abstract void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor); 
    
    public abstract void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor); 
}

public class DefaultProcessorSlotChain extends ProcessorSlotChain { 
    
    public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) { 
        protocolProcessor.setNext(this.first.getNext()); 
        this.first.setNext(protocolProcessor); 
        if (this.end == this.first) { 
            this.end = protocolProcessor; 
        } 
    }
    
    public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) { 
        this.end.setNext(protocolProcessor); 
        this.end = protocolProcessor; 
    } 
}

​ 5、指标数据统计

​ (1)ResourceWrapper

​ 在资源数据统计中,ResourceWrapper 是一个关键的类,用于对资源进行封装和管理。它的作用是为了将被保护的资源进行包装,以便在流量控制、熔断降级等场景中进行统一的管理和监控,主要作用包括资源封装和标识、规则匹配、统计指标收集、规则实时更新等。

​ 资源统计类 ResourceWrapper,属性有资源名称name、流量类型 entryType、资源类型 resourceType。

​ 流量类型是一个枚举类,枚举类型有流入流量和流出流量。

​ 资源类型也是一个枚举类,枚举类型有默认值(可以是接口、方法、代码段等)、web应用的接口、使用Dubbo框架实现的RPC接口、API Gateway 网关接口、数据库 SQL 操作,

public abstract class ResourceWrapper { 
    // 资源名称
    protected final String name; 
    // 流量类型
    protected final EntryType entryType; 
    // 资源类型
    protected final int resourceType; 
}

public enum EntryType { 
    IN, //流入流量 
    OUT; //流出流量 
}

public final class ResourceTypeConstants { 
    public static final int COMMON = 0; //默认,可以是接口、一个方法、一段代码。 
    public static final int COMMON_WEB = 1; //Web应用的接口 
    public static final int COMMON_RPC = 2; //使用Dubbo框架实现的RPC接口 
    public static final int COMMON_API_GATEWAY = 3; //API Gateway网关接口 
    public static final int COMMON_DB_SQL = 4; //数据库SQL操作 
}

​ (2)Node

​ Node 定义了统计资源实时指标数据的方法,不同实现类被用在不同维度为资源统计实时指标数据。

public interface Node {
    // 获取总请求数
    long totalRequest();
    // 获取通过的请求数
    long totalPass();
    // 获取成功的请求数
    long totalSuccess();
    // 获取被阻塞的请求数
    long blockRequest();
    // 获取异常的请求数
    long totalException();
    // 获取通过的 QPS(每秒通过的请求数)
    double passQps();
    // 获取被阻塞的 QPS(每秒被阻塞的请求数)
    double blockQps();
    // 获取总的 QPS(每秒总请求数)
    double totalQps();
    // 获取成功的 QPS(每秒成功的请求数)
    double successQps();
    // 获取最大成功 QPS(每秒最大成功请求数)
    double maxSuccessQps();
    // 获取异常的 QPS(每秒异常的请求数)
    double exceptionQps();
    // 获取平均响应时间
    double avgRt();
    // 获取最小响应时间
    double minRt();
    // 获取当前线程数
    int curThreadNum();
    // 获取之前的被阻塞 QPS
    double previousBlockQps();
    // 获取之前的通过 QPS
    double previousPassQps();
    // 获取时间窗口内的度量指标
    Map<Long, MetricNode> metrics();
    // 获取指定时间范围内的原始度量指标
    List<MetricNode> rawMetricsInMin(Predicate<Long> var1);
    // 添加通过的请求数量
    void addPassRequest(int var1);
    // 添加响应时间和成功的请求数量
    void addRtAndSuccess(long var1, int var3);
    // 增加被阻塞的 QPS
    void increaseBlockQps(int var1);
    // 增加异常的 QPS
    void increaseExceptionQps(int var1);
    // 增加线程数
    void increaseThreadNum();
    // 减少线程数
    void decreaseThreadNum();
    // 重置节点的度量指标
    void reset();
}

​ Node 的类结构如下所示,首先是 Node 接口,实现类是 StatisticNode,其封装了实现实时指标数据统计;其有两个子类,分别是 DefaultNode 和 ClusterNode,DefaultNode 是统计同一资源单不同调用链入口的实时指标数据,此外DefaultNode 还有一个子类 EntranceNode,用于表示调用链的入口节,而 ClusterNode 则是统计每个资源的全局指标数据。

​ 统计服务节点状态类 StatisticNode 的源码如下:

​ rollingCounterInSecond 和 rollingCounterInMinute 这两个成员变量是用于秒级和分钟级滑动窗口统计的指标对象。它们的具体类型 Metric 可能包含一些方法用于记录成功请求数量、响应时间等信息。这样的滑动窗口用于收集和计算指定时间段内的各种指标,以便监控服务的性能和状态。

​ curThreadNum:这是一个 LongAdder 对象,用于统计当前活跃的线程数。

​ lastFetchTime:这个变量记录最后一次获取统计信息的时间,可能在其他地方使用。

​ addRtAndSuccess(long rt, int successCount):这个方法用于添加响应时间和成功请求数量。它将这些信息同时添加到秒级和分钟级滑动窗口的指标对象中,以便进行后续的统计和分析。

​ totalRequest():这个方法返回统计的请求总数,包括通过和被阻塞的请求数量。它通过从分钟级滑动窗口的指标对象中获取通过和阻塞请求数量的和来计算。

​ blockRequest():这个方法返回统计的被阻塞的请求数量,通过从分钟级滑动窗口的指标对象中获取阻塞请求数量来计算。

​ 总体来说,这段代码的作用是提供了一种机制来收集和统计服务节点的状态信息,包括响应时间、成功请求数量、通过请求数量、被阻塞的请求数量等,以便进行监控和性能分析。它使用滑动窗口的方式来收集和计算这些指标,可以在监控和运维中起到重要作用。

public class StatisticNode implements Node { 
    // 秒级/分钟级滑动窗口
    private transient volatile Metric rollingCounterInSecond; 
    private transient Metric rollingCounterInMinute; 
    private LongAdder curThreadNum; 
    private long lastFetchTime; 
    
    // 响应时间统计
    public void addRtAndSuccess(long rt, int successCount) { 
        this.rollingCounterInSecond.addSuccess(successCount); 
        this.rollingCounterInSecond.addRT(rt); 
        this.rollingCounterInMinute.addSuccess(successCount); 
        this.rollingCounterInMinute.addRT(rt); 
    }
    
    // 请求总数统计
    public long totalRequest() { 
        return this.rollingCounterInMinute.pass() + this.rollingCounterInMinute.block(); 
    }
    
    // 请求总数统计
    public long blockRequest() { 
        return this.rollingCounterInMinute.block(); 
    } 
}

​ (3)Entry

​ 在调用链上,一个资源对应一个Entry实例,其是一个抽象类,实现了AutoCloseable接口,意味着它可以在 try-with-resources 语句块中使用。

​ curNode:表示当前资源的 DefaultNode 实例,用于记录资源的统计信息。

​ originNode:表示资源相对于当前调用来源的 StatisticNode 实例,用于记录资源在调用链中的统计信息。

​ CtEntry 类:这是 Entry 类的子类,用于扩展资源记录的功能。它包含以下成员变量:

​ parent:表示当前资源的父资源的 Entry 实例,可能在调用链中表示调用的上一层资源。

​ child:表示当前资源的子资源的 Entry 实例,可能在调用链中表示调用的下一层资源。

​ chain:表示当前资源的 ProcessorSlotChain 实例,用于管理资源在调用链中的处理流程。

​ context:表示调用链上的 Context 实例,可能包含与当前调用相关的上下文信息。

public abstract class Entry implements AutoCloseable { 
    // 当前资源的DefaultNode实例
    private Node curNode; 
    // 资源相对当前调用来源的StatisticNode实例
    private Node originNode; 
    ... 
}

class CtEntry extends Entry { 
    protected Entry parent = null; 
    // 当前资源的父子Entry
    protected Entry child = null; 
    // 当前资源的ProcessorSlotChain实例
    protected ProcessorSlot<Object> chain; 
    // 调用链上的Context实例
    protected Context context; 
    ... 
}

​ (4)Context

​ Context即调用链上下文,贯穿整条调用链。在响应式编程项目中,Context隐藏在订阅过程中传递,而在非响应式编程项目中,Context可通过ThreadLocal在调用链上传递。

​ 优势1:Context可用于减少方法的参数个数,具体做法是将一些调用链上被多个方法使用的参数提取到Context中,使调用链上方法的执行强依赖Context,即Context作为这些方法执行所依赖的环境。

​ 优势2:Context还可用提升框架的扩展性。在调用链入口处将新增参数写入Context,使调用链上的任何方法都可以从Context中获取该参数

public class Context { 
    // 名称、入口节点、调用链上当前资源的Entry 实例及调用来源等信息
    private final String name; 
    private DefaultNode entranceNode; 
    private Entry curEntry; 
    private String origin; 
    private final boolean async; 
}

​ 6、Sentinel工作流程:

​ 定义规则:根据业务需求定义规则,包括限流规则、熔断规则和降级规则等

​ 注册资源:将需要限流或熔断的资源(如接口、方法等)注册到Sentinel中

​ 调用链路拦截:在业务代码中调用需要进行限流或熔断的资源时,Sentinel会拦截请求,进行规则匹配和统计

​ 执行处理:根据规则匹配的结果,Sentinel 可以进行限流、熔断和降级等处理,保障系统的稳定性和可用性

public class SentinelDemo { 
    public static void main(String[] args) { 
        // 配置规则
        initRules(); 
        String resourceName = "resource"; 
        Entry entry = null; 
        try {
            // 注册资源
            entry = SphU.entry(resourceName); 
            // 执行业务逻辑
            doBusinessLogic(); 
        } catch (BlockException e) { 
            // 处理限流
            handleBlockException(); 
        } finally { 
            if (entry != null) { 
                // 退出数据度量
                entry.exit(); 
            } 
        } 
    }
    ... 
}

(二)Sentinel基本限流机制

​ 限流的开发步骤可以分为三步:定义资源、设置限流规则、验证限流效果,定义资源是通过代码嵌入和注解集成的,设置限流规则是指定流量统计类型和控制行为,验证限流效果是通过测试工具执行验证。

​ 1、定义资源

​ 定义资源可以使用代码嵌入或使用注解,使用代码嵌入可以使用SphU包含try-catch风格的API,也可以使用SphO提供if-else风格的API。

​ 使用代码嵌入:SphU包含try-catch风格的API,如果被限流会抛出BlockException

try (Entry entry = SphU.entry("resourceName")) { 
    // 被保护的业务逻辑 
    // do something here... 
} catch (BlockException ex) { 
    // 资源访问阻止,被限流或被降级 
    // 在此处进行相应的处理操作 
}

​ 使用代码嵌入:SphO提供if-else风格的API,如果创建成功如何处理,创建失败如何处理

if (SphO.entry("resourceName")) { 
    // 务必保证finally会被执行 
    try {
        //被保护的业务逻辑 
        // do something here... 
    } finally { 
        SphO.exit(); 
    } 
} else { 
    // 资源访问阻止,被限流或被降级 
    // 在此处进行相应的处理操作 
}

​ 使用注解:在开发中实际不太建议使用编码的方式,而是建议使用@SentinelResource注解的方式

public @interface SentinelResource { 
    // 资源名称,非空
    String value() default ""; 
    EntryType entryType() default EntryType.OUT; 
    int resourceType() default 0; 
    
    String blockHandler() default ""; 
    // 对应处理BlockException的方法或类
    Class<?>[] blockHandlerClass() default {}; 
    
    String fallback() default ""; 
    String defaultFallback() default ""; 
    // 用于在抛出异常的时候提供fallback处理逻辑
    Class<?>[] fallbackClass() default {}; 
    
    Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class}; 
    Class<? extends Throwable>[] exceptionsToIgnore() default {}; 
}

​ 如下代码是使用@SentinelResource来定义名为test和hello的资源,在test资源中,定义了异常处理类和处理方法,在hello中,定义了处理异常的方法exceptionHandler和降级方法helloFallback。

​ blockHandler的作用是处理异常,函数签名与原函数一致或加一个 Throwable 类型的参数;fallback方法是处理回退方法,即降级方法,要有返回值,函数签名与原函数一致或加一个 Throwable 类型的参数。

public class TestService { 
    // 对应的 handleException函数需要位于 ExceptionUtil类中, 并且必须为 static 函数
    @SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class}) 
		public void test() {
        System.out.println("Test"); 
    }
                          
    @SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback") 
    public String hello(long s) { 
        return String.format("Hello at %d", s); 
    }
    
    // Fallback函数,函数签名与原函数一致或加一个 Throwable 类型的参数
    public String helloFallback(long s) { 
        return String.format("Hello fall back %d", s); 
    }
    
    // Block异常处理函数,参数最后多一个 BlockException,其余与原函数一致	
    public String exceptionHandler(long s, BlockException ex) { 
        ex.printStackTrace(); 
        return "Error occurred at " + s; 
    } 
}

​ 2、设置限流规则

​ 限流规则主要是FlowRule类,其继承了AbstractRule类,在该类中定义了很多限流规则,例如限流阈值类型、基于调用关系的限流策略、流量控制效果、冷启动时长、流量整形控制器等。

public class FlowRule extends AbstractRule { 
    // 限流阈值类型
    private int grade = RuleConstant.FLOW_GRADE_QPS; 
    private double count; 
    // 基于调用关系的限流策略
    private int strategy = RuleConstant.STRATEGY_DIRECT; 
    private String refResource; 
    // 流量控制效果
    private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT; 
    // 冷启动时长
    private int warmUpPeriodSec = 10; 
    private int maxQueueingTimeMs = 500; 
    private boolean clusterMode; 
    private ClusterFlowConfig clusterConfig; 
    // 流量整形控制器
    private TrafficShapingController controller; 
    ... 
}

​ 上面代码中的很多值都是枚举类,以下是对于这么枚举类的说明:

​ grade:限流阈值类型,有两个值:FLOW_GRADE_THREAD:并发线程数:相当于线程隔离机制; FLOW_GRADE_QPS:即QPS,每秒查询数

​ strategy:基于调用关系的限流策略,有三个值:

​ STRATEGY_DIRECT:直接流控:当前资源访问量达到某个阈值时后续请求将被直接拦截

​ STRATEGY_RELATE:关联流控:关联资源的访问量达到某个阈值时对当前资源进行限流

​ STRATEGY_CHAIN:链路流控:指定链路的访问量大于某个阈值时对当前资源进行限流

​ controlBehavior:流量控制效果,具体的值如下所示

​ warmUpPeriodSec:冷启动时长。下图是对冷启动思想的概要描述,由于在系统刚启动时,系统性能并没有达到最大值,而是随着系统的运行,才会慢慢达到最大值,因此冷启动的思想也是在系统刚启动时设置一个小的限流阈值,而随着系统的运行,慢慢加大阈值,直到最大阈值,而实现的思路就是使用令牌桶算法,即系统刚启动时使用一个较低的、稳定的产生令牌的时间间隔,随着时间推移,慢慢减小产生令牌的时间间隔,最终使其达到令牌桶可放令牌的最大值。

​ 而设置限流规则则可以使用 FlowRuleManager,代码如下所示,首先设置资源的名称、限流类型、限流策略、流量控制效果等,最后将限流规则加入限流规则集合,并使用FlowRuleManager.loadRules(rules);让其生效。

private static void initFlowRules() { 
    List<FlowRule> rules = new ArrayList<>(); 
    FlowRule rule = new FlowRule(); 
    // 资源名 
    rule.setResource("myResource"); 
    // 限流类型 
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS); 
    // 限流阈值 
    rule.setCount(20); 
    // 限流策略 
    rule.setStrategy(RuleConstant.STRATEGY_CHAIN); 
    //流量控制效果 
    rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); 
    rule.setClusterMode(false);
    // 添加FlowRule到执行流程中 
    rules.add(rule); 
    FlowRuleManager.loadRules(rules); 
}

(三)Sentinel热点参数限流

​ 参数限流是指根据方法调用传递的参数实现限流,或者根据接口的请求参数限流,而热点参数限流是指对访问频繁的参数进行限流,例如对频繁访问的IP地址进行限流等。

​ 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

​ 与围绕资源实现限流不同,热点参数限流是围绕资源的参数的不同取值来限流的,它不需要统计资源指标数据,而需要统计不同参数取值的指标数据。Sentinel利用LRU策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

​ 如下三个请求为例,明显参数 a 的访问频次更高,就可以使用热点参数限流对 a 参数进行限流。

http://localhost:8080/test?a=10 访问100次 
http://localhost:8080/test?b=10 访问10次 
http://localhost:8080/test?c=10 访问3次

​ 热点参数限流规则如下所示,也是有资源名、限流阈值、限流模式、统计滑动窗口阈值等,但是有两个比较特殊的属性,一个是paramIdx,表示热点参数的索引,对应 Sphu.entry方法中的参数,从0开始,其中Sphu.entry方法的第二个参数是一个可变长度的参数列表;第二个是 paramFlowitemList,即参数例外项,可以针对指定的参数单独设置限流阈值,不受之前的count限制。

​ 下面代码定义了一个热点参数限流资源,SphU.entry(resourceName, EntryType.IN, 1, id) 表示对该资源下标为1的参数进行限流,即对 name 参数进行限流。

@RestController 
public class ParamController { 
    final String resourceName = "test"; 
    
    @GetMapping("/param") 
    public String test(@PathParam("id") String id, @PathParam("name") String name) { 
        Entry entry = null; 
        try {
            // 使用entry带参数的重载方法定义资源
            // 处理不同的热点数据,最后一个参数是一个可变入参
            entry = SphU.entry(resourceName, EntryType.IN, 1, id); 
            return "success"; 
        } catch (BlockException e) { 
            e.printStackTrace(); 
            return "block exception"; 
        } finally { 
            if (entry != null) { 
                entry.exit(); 
            } 
        } 
    } 
}

​ 设置热点参数限流规则和其他的限流规则设置基本一致,不过是使用ParamFlowRuleManager.loadRules进行加载生效。

public static void initRule(){ 
    ParamFlowRule paramFlowRule = new ParamFlowRule(); 
    paramFlowRule.setResource("test"); 
    paramFlowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); 
    paramFlowRule.setCount(3); 
    // 允许的最大突发请求
    paramFlowRule.setBurstCount(10); 
    paramFlowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); 
    paramFlowRule.setDurationInSec(1); 
    // 热点参数索引
    paramFlowRule.setParamIdx(0); 
    List<ParamFlowRule> paramFlowRules = new ArrayList<>(); 
    paramFlowRules.add(paramFlowRule); 
    // 通过ParamFlowRuleManager加载规则
    ParamFlowRuleManager.loadRules(paramFlowRules); 
}

@SentinelResource(value = "test") 
@GetMapping("/hello") 
public String test(@PathParam("id") Integer id){ 
    return "success"; 
}

(四)客服系统案例演进

​ Sentinel主要有核心库和控制台两部分组成:

​ 核心库:不依赖其他任何框架或者库,能够在任何Java环境上运行,并且能与Spring Cloud、Dubbo等开源框架进行整合

​ 控制台:基于Spring Boot开发,独立可以运行Jar包,不需要额外的Tomcat等容器

​ 在Spring Cloud Alibaba集成Sentinel比较简单,主要就是配置

​ 首先引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

​ 然后集成控制台

## 集成控制台
spring: 
  cloud: 
    sentinel: 
      transport: 
        dashboard: 127.0.0.1:8088

​ 如果需要对调用链路做限流,则还需要在feign客户端开启 sentinel

## 集成Feign
feign: 
  sentinel: 
      enabled: true

​ 配置完成就可以进行验证和压测。

​ Sentinel 的配置和使用可以参考:Sentinel 流量防卫兵

​ 压测可以参考:压测介绍 && 搭建压测平台

三、基于Sentinel实现服务降级

(一)熔断器模型

​ 熔断器的思路就是有关闭、半开、全开三个状态,然后根据不同的指标对状态进行变更,而在远程请求时,则根据熔断器的判断是否可进行远程调用。

​ 下面是根据以上思路自己实现一个自定义的熔断器。

​ 首先是使用枚举类定义熔断器的三个状态:

// 熔断器状态
public enum State { 
    CLOSED, 
    OPEN, 
    HALF_OPEN 
}

​ 定义熔断器接口,提供熔断器需要提供的方法,首先是获取和设置熔断器状态,然后是对于请求成功和请求失败的处理方法,最后是做远程调用的方法,这些方法组成了一个熔断器最基本的方法。

// 熔断器定义
public interface CircuitBreaker { 
    // 请求成功,重置熔断器 
    void recordSuccess(); 
    // 请求失败,处理结果并根据需要更新状态 
    void recordFailure(String response); 
    // 获取熔断器当前状态 
    String getState(); 
    // 将熔断器设置到特定状态 
    void setState(State state); 
    // 对远程服务发起请求 
    String attemptRequest() throws RemoteServiceException; 
}

​ 然后定义熔断器接口的实现类,实现以上五个方法,以下是对每一个方法实现的具体代码

​ 首先是对于控制状态的设置,如果请求成功,则设置熔断器状态为关闭,同时设置失败条数为0,最后一次失败时间为当前时间加上设置的时间段;如果请求失败,将失败次数加一,最后一次失败时间为当前时间,将最后一次失败响应设置为本次的响应结果。

public class DefaultCircuitBreaker implements CircuitBreaker { 
    // 请求成功,设置熔断器状态
    @Override 
    public void recordSuccess() { 
        this.failureCount = 0; 
        this.lastFailureTime = System.nanoTime() + futureTime; 
        this.state = State.CLOSED; 
    }
    
    // 请求失败,更新统计数据
    @Override 
    public void recordFailure(String response) { 
        failureCount = failureCount + 1; 
        this.lastFailureTime = System.nanoTime(); 
        // 保存失败响应,作为熔断器打开状态下的默认返回值 
        this.lastFailureResponse = response; 
    } 
}

​ 然后是执行请求实现,在该方法中,首先判断熔断器状态,如果失败次数小于设置的阈值,则表明熔断器为关闭状态,否则,在判断当前时间与上一次失败的时间间隔是否已经超过了设置的重试时间阈值,如果超过了,则将熔断器状态改为半开,否则熔断器状态则为打开。

​ 获取熔断器状态后,如果状态为打开,则不进行远程调用,而是调用失败处理方法,如果是关闭或者半开,则作远程调用,调用成功,则走调用成功逻辑,如果调用失败,则走调用失败逻辑。

public class DefaultCircuitBreaker implements CircuitBreaker { 
    public String attemptRequest() throws RemoteServiceException { 
        // 发起请求,评估熔断器状态
        evaluateState(); 
        if (state == State.OPEN) { 
            return this.lastFailureResponse; 
        } else { 
            try {
                String response = service.call();
                recordSuccess(); 
                return response; 
            } catch (RemoteServiceException ex) { 
                recordFailure(ex.getMessage()); 
                throw ex; 
            } 
        } 
    }
    
    // 根据失败次数、重试时间更新熔断器状态
    protected void evaluateState() { 
        if (failureCount >= failureThreshold) { 
            if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) { 
                state = State.HALF_OPEN; 
            } else { 
                state = State.OPEN; 
            } 
        } else { 
            state = State.CLOSED; 
        } 
    } 
}

(二)Sentinel降级机制

​ 降级的开发步骤与限流的开发步骤基本上一致,分别是

​ 定义资源:通过代码嵌入和注解集成

​ 设置降级规则:指定熔断类型和控制行为

​ 编写降级逻辑:实现回退函数

​ 验证降级效果:通过测试工具执行验证

​ 1、降级规则

​ 如下代码是降级的规则类DegradeRule

public class DegradeRule extends AbstractRule { 
    // 熔断策略
    private int grade = RuleConstant.DEGRADE_GRADE_RT; 
    private double count; 
    // 熔断时长,单位为 s
    private int timeWindow; 
    // 熔断触发的最小请求数
    private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT; 
    // 慢调用比例阈值
    private double slowRatioThreshold = 1.0D; 
    // 统计时长
    private int statIntervalMs = 1000; 
    ... 
}

​ 首先是熔断策略grade,其是一个枚举类,具体值有以下几种:

​ DEGRADE_GRADE_RT:按平均响应耗时熔断

​ DEGRADE_GRADE_EXCEPTION_RATIO:按失败比率熔断

​ DEGRADE_GRADE_ EXCEPTION_COUNT:按失败次数熔断

​ 然后是统计值 count、熔断时长 timeWindow、慢调用比例slowRatioThreshold、统计时长statIntervalMs

​ minRequestAmount表示可触发熔断的最小请求数,在DEGRADE_GRADE_ EXCEPTION_COUNT(按失败次数熔断)时起作用。

​ slowRatioThreshold表示超过限流阈值的慢请求数量,在DEGRADE_GRADE_RT(按平均响应耗时熔断)时起作用。

​ 2、设置降级规则 - DegradeRuleManager

​ 设置降级规则使用DegradeRuleManager进行设置,整体流程和设置限流一致,最后使用DegradeRuleManager.loadRules进行加载。

​ 如下代码所示,在一分钟内,请求数超过2次,并且当异常数大于2之后请求会被熔断; 10s后断路器转换为半开状态,当再次请求又发生异常时会直接被熔断,之后重复。

private void initDegradeRule() { 
   List<DegradeRule> rules = new ArrayList<>(); 
   DegradeRule degradeRule = new DegradeRule(); 
   //设置熔断降级资源名 
   degradeRule.setResource("resourceName"); 
   //设置降级规则:异常数 
   degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT); 
   //阈值计数,这里是触发熔断异常数:2 
   degradeRule.setCount(2); 
   //可以触发熔断的最小请求数:2 
   degradeRule.setMinRequestAmount(2); 
   //统计时间间隔:1分钟 
   degradeRule.setStatIntervalMs(60*1000); 
   //熔断器打开时的恢复超时:10秒 
   degradeRule.setTimeWindow(10); 
   rules.add(degradeRule); 
   DegradeRuleManager.loadRules(rules); 
}

​ 3、编写降级逻辑

​ 主要是在资源上使用@SentinelResource来设置降级的实现类、方法等,以及对降级类和降级方法的编写。

​ 在编写回退方法时,回退方法尽可能返回一个能被服务处理的默认值。

public class BuyFallback { 
    // 回退方法 
    public static String buyFallback(@PathVariable String name, @PathVariable Integer count, Throwable throwable) { 
        ... 
    } 
}

public class BuyBlockHandler { 
    // 异常处理 
    public static String buyBlock(@PathVariable String name, @PathVariable Integer count, BlockException e) { 
        ... 
    } 
}

@SentinelResource(value = "buy", fallback = "buyFallback", fallbackClass = BuyFallBack.class, blockHandler = "buyBlock", blockHandlerClass = BuyBlockHandler.class, exceptionsToIgnore = NullPointerException.class )

​ CircuitBreaker

public interface CircuitBreaker { 
    // 获取降级规则
    DegradeRule getRule(); 
    // 判断是否熔断
    boolean tryPass(Context context); 
    // 获取当前熔断状态
    CircuitBreaker.State currentState(); 
    // 请求完成回调
    void onRequestComplete(Context context); 
    public static enum State { 
        OPEN, 
        HALF_OPEN, 
        CLOSED; 
    } 
}

​ 4、Sentinel降级类结构

​ Sentinel对于降级的实现首先是其接口 CircuitBreaker,其定义了Sentinel熔断器的主要方法,然后是接口的抽象实现类 AbstractCircuitBreaker,用于一些通用方法的实现,然后对于该抽象类有两个子类ResponseTimeCircuitBreaker 和 ExecptionCircuitBreaker,其中 ResponseCircuitBreaker 用来处理慢调用熔断,ExecptionCircuitBreaker用来处理异常调用熔断。

​ 以下是 CircuitBreaker 接口代码,主要是定义了熔断器的三个状态,获取降级规则、判断是否熔断、获取当前熔断状态、请求完成回调等方法。

public interface CircuitBreaker { 
  // 获取降级规则
  DegradeRule getRule(); 
  // 判断是否熔断
  boolean tryPass(Context context); 
  // 获取当前熔断状态
  CircuitBreaker.State currentState(); 
  // 请求完成回调
  void onRequestComplete(Context context); 
  
  public static enum State { 
    OPEN, 
    HALF_OPEN, 
    CLOSED; 
  } 
}

​ 以下是 AbstractCircuitBreaker的代码,其实现了 CircuitBreaker 接口,最主要的是提供了状态切换。

​ Close → Open:使用并发安全类将熔断器状态从关闭设置为打开,然后更新下一次重试时间,最后通知其他对此变更感兴趣的订阅者。

​ HalfOpen → Open:处理逻辑与从关闭到打开的处理逻辑一致

​ Open → HalfOpen:变更状态,其中 whenTerminate 方法是一个回调函数,它在当前 entry 终止时被调用。如果在 entry 上存在 blockError,表示请求被熔断,此时会将状态从 "HALF_OPEN" 再次转换回 "OPEN"

​ HalfOpen → Close:更新熔断器状态,然后调用resetStat方法进行重置

// Close → Open
protected boolean fromCloseToOpen(double snapshotValue) { 
    State prev = State.CLOSED; 
    if (this.currentState.compareAndSet(prev, State.OPEN)) { 
        this.updateNextRetryTimestamp(); 
        this.notifyObservers(prev, State.OPEN, snapshotValue); 
        return true; 
    } else { 
        return false; 
    } 
} 

// HalfOpen → Open
protected boolean fromHalfOpenToOpen(double snapshotValue) { 
    if (this.currentState.compareAndSet(State.HALF_OPEN, State.OPEN)) { 
        this.updateNextRetryTimestamp(); 
        this.notifyObservers(State.HALF_OPEN, State.OPEN, snapshotValue); 
        return true; 
    } else { 
        return false; 
    } 
} 

// Open → HalfOpen
protected boolean fromOpenToHalfOpen(Context context) { 
    if (this.currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) { 
        this.notifyObservers(State.OPEN, State.HALF_OPEN, (Double)null); 
        Entry entry = context.getCurEntry(); 
        entry.whenTerminate(new BiConsumer<Context, Entry>() {
            public void accept(Context context, Entry entry) { 
                if (entry.getBlockError() != null) {
                    //注册回调防止熔断器一直处于HalfOpen状态
   					AbstractCircuitBreaker.this.currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
                    AbstractCircuitBreaker.this.notifyObservers(State.HALF_OPEN, State.OPEN, 1.0D); 
                } 
            } 
        }); 
        return true; 
    } else { 
        return false; 
    } 
}

// HalfOpen → Close
protected boolean fromHalfOpenToClose() { 
    if (this.currentState.compareAndSet(State.HALF_OPEN, State.CLOSED)) { 
        this.resetStat(); 
        this.notifyObservers(State.HALF_OPEN, State.CLOSED, (Double)null); 
        return true; 
    } else { 
        return false; 
    } 
}

​ 然后是用来判断请求是否可以通过的方法 tryPass,如果熔断器状态是关闭则返回true,如果是半开,则返回false,如果是开启但是上一个请求距离现在已经过了重试间隔时间就开启半启动状态

public abstract class AbstractCircuitBreaker implements CircuitBreaker { 
    // 判断请求是否可以通过
    public boolean tryPass(Context context) { 
        if (this.currentState.get() == State.CLOSED) {
            return true; 
        } else if (this.currentState.get() != State.OPEN) { 
            return false; 
        } else { 
            // 如果断路器开启,但是上一个请求距离现在已经过了重试间隔时间就开启半启动状态
            return this.retryTimeoutArrived() && this.fromOpenToHalfOpen(context); 
        } 
    }
    abstract void resetStat(); 
}

​ ResponseTimeCircuitBreaker是对于慢调用的熔断器实现,继承了抽象熔断器类AbstractCircuitBreaker。它主要用于根据响应时间的统计来判断是否需要触发熔断操作。

​ resetStat() 方法中重置滑动窗口的统计数据,以便在每个时间窗口结束后重新开始统计。这里通过获取当前窗口的计数器对象,调用其 reset()方法来实现重置。

​ onRequestComplete(Context context) 方法在请求完成时被调用,用于统计慢请求的数量、总请求数,并根据统计结果判断是否需要触发熔断状态的改变。

​ 首先,从当前窗口获取慢请求计数器对象,然后从请求的上下文中获取当前请求的入口 Entry 对象。

​ 如果入口对象存在,则计算请求的响应时间 rt,并将慢请求数和总请求数递增。

​ 最后,调用 handleStateChangeWhenThresholdExceeded(rt) 方法来根据阈值判断是否需要改变熔断器的状态。

​ handleStateChangeWhenThresholdExceeded(long rt)方法是根据当前熔断器状态和响应时间的情况,决定是否进行状态转换。

​ 如果当前状态不是 "OPEN",则根据不同状态进行判断。

​ 如果状态是 "HALF_OPEN",则判断响应时间是否超过最大允许响应时间,如果超过则触发从半开到开启的状态转换,否则转换为关闭状态。

​ 如果状态是其他状态(如 "CLOSED"),则根据慢请求数和总请求数判断是否需要触发开启状态的转换。如果慢请求比例超过阈值,或者慢请求数与总请求数达到一定条件,就会触发熔断器从关闭到开启的转换。

public class ResponseTimeCircuitBreaker extends AbstractCircuitBreaker { 
    public void resetStat() {
        // 重置滑动窗口
        ((ResponseTimeCircuitBreaker.SlowRequestCounter)this.slidingCounter.currentWindow().value()).reset(); 
    }
    
    public void onRequestComplete(Context context) {
        ResponseTimeCircuitBreaker.SlowRequestCounter counter = (ResponseTimeCircuitBreaker.SlowRequestCounter)this.slidingCounter.currentWindow().value(); 
        Entry entry = context.getCurEntry(); 
        if (entry != null) { 
            long completeTime = entry.getCompleteTimestamp(); 
            if (completeTime <= 0L) { 
                completeTime = TimeUtil.currentTimeMillis(); 
            }
            long rt = completeTime - entry.getCreateTimestamp(); 
            if (rt > this.maxAllowedRt) { 
                // 统计慢请求总数和总请求数
                counter.slowCount.add(1L);
            }
            counter.totalCount.add(1L); 
            // 根据当前时间窗口统计的指标数据是否 达到阈值来改变熔断器的状态
            this.handleStateChangeWhenThresholdExceeded(rt); 
        } 
    } 
    
    private void handleStateChangeWhenThresholdExceeded(long rt) { 
        if (this.currentState.get() != State.OPEN) { 
            if (this.currentState.get() == State.HALF_OPEN) { 
                // 半开状态,判断RT是否超过最大RT
                if (rt > this.maxAllowedRt) { 
                    this.fromHalfOpenToOpen(1.0D); 
                } else { 
                    this.fromHalfOpenToClose(); 
                } 
            } else { 
                long totalCount = 0L; 
                ... 
                if (totalCount >= (long)this.minRequestAmount) { 
                    // 关闭状态,判断慢请求数是否达到触发熔断条件
                    double currentRatio = (double)slowCount * 1.0D / (double)totalCount; 
                    if (currentRatio > this.maxSlowRequestRatio) { 
                        this.transformToOpen(currentRatio); 
                    }
                    if (Double.compare(currentRatio, this.maxSlowRequestRatio) == 0 && Double.compare(this.maxSlowRequestRatio, 1.0D) == 0) { 
                        this.transformToOpen(currentRatio); 
                    } 
                } 
            } 
        } 
    }
}

​ DegradeSlot:

@SpiOrder(-1000) 
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> { 
    @Override 
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { 
        // 在触发后续Slot前执行熔断的检查
        performChecking(context, resourceWrapper); 
        ... 
    }
    
    void performChecking(Context context, ResourceWrapper r) throws BlockException { 
        List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName()); 
        for (CircuitBreaker cb : circuitBreakers) { 
            // 执行状态检查,熔断是否关闭或者打开
            f (!cb.tryPass(context)) { 
                throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule()); 
            } 
        } 
    }
    ... 
}

(三)Spring Cloud Circuit Breaker抽象

​ Spring Cloud Circuit Breaker 是对熔断器的抽象,在最多的时候,分别集成了 Hystrix、Sentinel、Resilience4j、Spring Retry,现在最新版本已经将 Hystrix移除。

​ Spring Cloud Circuit Breaker 的定义如下代码所示,首先是CircuitBreaker接口,定义了run方法,其次是抽象类CircuitBreakerFactory,提供了创建熔断器的方法create。

public interface CircuitBreaker { 
    default <T> T run(Supplier<T> toRun) {
        return run(toRun, throwable -> { 
            throw new NoFallbackAvailableException("No fallback available.", throwable);
        }); 
    };
    // 执行业务方法,包含回退方法
    <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback); 
}

public abstract class CircuitBreakerFactory<CONF, CONFB extends ConfigBuilder<CONF>> extends AbstractCircuitBreakerFactory<CONF, CONFB> { 
    // 创建熔断器
    public abstract CircuitBreaker create(String id); 
    public CircuitBreaker create(String id, String groupName) { 
        return create(id); 
    } 
}

​ SentinelCircuitBreakerFactory 是 Spring Cloud Circuit Breaker集成Sentinel的工厂类,其继承了CircuitBreakerFactory类,实现了create方法,主要是使用配置信息创建了一个SentinelCircuitBreaker。配置信息主要包括资源名称和熔断规则集合等。

public class SentinelCircuitBreakerFactory extends CircuitBreakerFactory<SentinelCircuitBreakerConfiguration, SentinelConfigBuilder> { 
    public CircuitBreaker create(String id) { 
        SentinelCircuitBreakerConfiguration conf = (SentinelCircuitBreakerConfiguration) this.getConfigurations().computeIfAbsent(id, this.defaultConfiguration); 
        // 基于配置创建Sentinel熔断器
        return new SentinelCircuitBreaker(id, conf.getEntryType(), conf.getRules()); 
    }
    
    private Function<String, SentinelCircuitBreakerConfiguration> defaultConfiguration = (id) -> { 
        return (new SentinelConfigBuilder()).resourceName(id).entryType(EntryType.OUT) .rules(new ArrayList()).build(); 
    }; 
}

// Sentinel熔断器配置
public static class SentinelCircuitBreakerConfiguration { 
    private String resourceName; 
    private EntryType entryType; 
    private List<DegradeRule> rules; 
}

​ 在SentinelCircuitBreaker中通过Sentinel原生方法理SphU.entry执行降级处,同时提供了加载熔断器规则的方法applyToSentinelRuleManager。

public class SentinelCircuitBreaker implements CircuitBreaker { 
    private void applyToSentinelRuleManager() { 
        if (this.rules != null && !this.rules.isEmpty()) { 
            ... 
            // 加载熔断器规则
            DegradeRuleManager.loadRules(new ArrayList(ruleSet)); 
        } 
    }
    
    public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) { 
        Entry entry = null; 
        Object result; 
        try {
            // 通过Sentinel原生方法执行降级处理
            entry = SphU.entry(this.resourceName, this.entryType); 
            Object object = toRun.get(); 
            return object; 
        } catch (BlockException blockException) { 
            result = fallback.apply(blockException); 
        }
        ... 
        return result; 
    } 
}

四、Sentinel限流和降级扩展

(一)Sentinel扩展点分析

​ Sentinel 扩展点是使用JDK 原生 SPI实现的。

​ JDK SPI机制实现过程:

​ 设计一个服务接口,并提供对应的实现类,可以融扩展需衲共多种实现类

​ 在 META-INF/services目录中创建一个以服务接口命名的文件,配置实现该服务接口的具体实现类

​ 外部程序通过 META-INF/services/ 目录下的配置文件找到具体的实现类名并实例化

​ 如下代码是Sentinel与SPI的集成:

​ 首先是SPI的实现方法SpiLoader,其会从META-INF/services/ 中获取接口名称的文件,然后从文件中读取实现类的全限定名,然后使用反射机制生成对应的实现类;

​ 然后是DefaultSlotChainBuilder,其集成了 SPI 机制,首先调用SpiLoader并传入接口名称ProcessorSlot,然后就获取了SPI中配置的实现类,然后循环每一个ProcessorSlot将其加入ProcessorSlotChain并返回。

public class DefaultSlotChainBuilder implements SlotChainBuilder { 
    public ProcessorSlotChain build() { 
        ProcessorSlotChain chain = new DefaultProcessorSlotChain(); 
        // 基于SPI机制加载ProcessorSlot,读取 配置文件在/META-INF/services/接 口全限定命名的文件
        List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted(); 
        Iterator slots = sortedSlotList.iterator(); 
        while(slots.hasNext()) { 
            ProcessorSlot slot = (ProcessorSlot)slots.next(); 
            if (!(slot instanceof AbstractLinkedProcessorSlot)) { 
                ... 
            } else { 
                chain.addLast((AbstractLinkedProcessorSlot)slot); 
            } 
        }
        return chain; 
    } 
}

public final class SpiLoader<S> { 
    public List<S> loadInstanceListSorted() { 
        this.load(); 
        return this.createInstanceList(this.sortedClassList); 
    }
    public void load() { 
        if (this.loaded.compareAndSet(false, true)) { 
            // 根据SPI配置文件地址加载SPI定义
            String fullFileName = "META-INF/services/" + this.service.getName(); 
            ClassLoader classLoader; 
            try {
                urls = classLoader.getResources(fullFileName);    
            }
            ... 
        } 
    }
    
    private List<S> createInstanceList(List<Class<? extends S>> clazzList) { 
        ... 
        List<S> instances = new ArrayList<>(clazzList.size()); 
        for (Class<? extends S> clazz : clazzList) { 
            // 通过反射创建SPI实例
            S instance = createInstance(clazz); 
            instances.add(instance); 
        }
        return instances; 
    } 
}

​ Sentinel扩展点有如下几个:

​ Sentinel扩展点 - InitFunc:

​ 如果想扩展 InitFunc,首先需要创建一个其实现类,然后重写 init 方法,然后在META-INF/services/com.alibaba.csp.sentinel.init.InitFunc文件中,添加自定义扩展点的全路径即可。

// 在META-INF/services/com.alibaba.csp.sentinel.init.InitFunc文件中,添加自定义扩展点的全路径
public class FlowRuleInitFunc implements InitFunc{ 
    @Override 
    public void init() throws Exception { 
        List<FlowRule> rules=new ArrayList<>(); 
        FlowRule rule=new FlowRule(); 
        rule.setResource("doTest");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS); 
        rule.setCount(5); rules.add(rule); 
        FlowRuleManager.loadRules(rules); 
    }
}

​ Sentinel扩展点 - SlotChainBuilder:

​ 扩展 SlotChainBuilder 和上面一样,首先定义其实现类,然后在META-INF/services/com.alibaba.csp.sentinel.slotchain.SlotChainBuilder文件中,添加自定义扩展点的全路径。

public class MySlotChainBuilder implements SlotChainBuilder { 
    @Override 
    public ProcessorSlotChain build() { 
        ProcessorSlotChain chain = DefaultSlotChainBuilder ; 
        chain.addLast(new NodeSelectorSlot()); 
        chain.addLast(new ClusterBuilderSlot()); 
        chain.addLast(new FlowSlot()); 
        chain.addLast(new DegradeSlot()); 
        return chain; 
    }
}

(二)Sentinel实现动态规则

​ 1、动态配置规则

​ 对于规则的管理,如果使用API代码创建,则缺乏动态灵活性;如果使用 Dashboard配置,则无法持久化,因此 Sentinel 提供了 DataSource 来适配不同数据源修改。

​ Sentinel 动态规则配置的基本原理如下图所示,首先将 Dashboard 配置内容同步到配置中心,然后将配置中心数据与 Sentinel DataSource 进行同步。

​ Sentinel DataSource的类结构如下所示:

​ 首先是 ReadableDataSource接口,提供了加载配置、读取配置、获取属性、关闭等方法

​ 然后是抽象类 AbstractDataSource,提供了数据转换和属性信息

​ 最后是自动刷新类 AutoRefreshDataSource,提供了定时任务调度来做数据刷新。

public interface ReadableDataSource<S, T> { 
    T loadConfig() throws Exception; 
    // 读取数据源
    S readSource() throws Exception; 
    SentinelProperty<T> getProperty(); 
    void close() throws Exception; 
}

public abstract class AbstractDataSource<S, T> implements ReadableDataSource<S, T> {
    // 转换数据类型
    protected final Converter<S, T> parser; 
    protected final SentinelProperty<T> property; 
}

public abstract class AutoRefreshDataSource<S, T> extends AbstractDataSource<S, T> { 
    // 定时调度任务
    private ScheduledExecutorService service; 
    protected long recommendRefreshMs = 3000L; 
}

​ AbstractDataSource 代码如下所示:

public abstract class AbstractDataSource<S, T> implements ReadableDataSource<S, T> { 
    protected final Converter<S, T> parser;
    protected final SentinelProperty<T> property; 
    public AbstractDataSource(Converter<S, T> parser) {
        if (parser == null) {
            throw new IllegalArgumentException("parser can't be null");
        } 
        else {
            this.parser = parser; 
            this.property = new DynamicSentinelProperty(); 
        } 
    }
    
    public T loadConfig() throws Exception { 
        return this.loadConfig(this.readSource()); 
    }
    
    public T loadConfig(S conf) throws Exception { 
        // 执行转换并返回值
        T value = this.parser.convert(conf); 
        return value; 
    }
    
    public SentinelProperty<T> getProperty() { 
        return this.property; 
    } 
}

​ DataSource 扩展方式与其他中间件一样,提供了拉模式和推模式。

​ 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件等。这种方式实现简单,缺点是无法及时获取变更。 代表是 File、Consul、Eureka

​ 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化。这种方式有更好的实时性和一致性保证。代表是 ZooKeeper、Redis、Nacos、Apollo、Etcd

​ 那么如果需要对数据源扩展,就可以选择推模式和拉模式的一种来做扩展:

​ 拉模式拓展:继承AutoRefreshDataSource抽象类,然后实现readSource方法,在该方法里从指定数据源读取字符串格式的配置数据,比如基于文件的数据源。

​ 推模式拓展:继承AbstractDataSource 抽象类,在其构造方法中添加监听器,并实现readSource 方法从指定数据源读取字符串格式的配置数据,比如基于Nacos的数据源。

​ DataSource扩展步骤:

​ InitFunc:基于InitFunc扩展点实现规则扩展

​ Converter:实现原始配置和规则之间的转换

​ ReadableDataSource:创建动态规则的数据源

​ RuleManager:将数据源注册到规则管理器

​ 如果是简单的集成,可以参考:https://www.cnblogs.com/liconglong/p/15429991.html#_label8

​ 2、基于Zookeeper实现推式动态规则数据源

(三)Sentinel实现定制化降级策略

​ 1、定制开关降级策略

​ 这里以开关降级为例进行说明和演示,开关降级是指活动期间、流量高峰期间、业务动态切换期间,关闭非核心功能,降低系统压力和提供系统灵活性。

​ 实现开关降级主要包含定义规则、实现判断处理机制、集成开关逻辑、实现SPI扩展点。

​ SwitchRule:定义和加载开关降级规则类

​ SwitchChecker:实现开关判断处理机制

​ SwitchSlot:集成开关逻辑

​ SlotChainBuilder:实现SPI扩展点

​ 首先是定义SwitchRule:主要是定义开关的状态,以及开关控制的资源

public class SwitchRule { 
    public static final String SWITCH_KEY_OPEN = "open"; 
    public static final String SWITCH_KEY_CLOSE = "close"; 
    // 开关状态 
    private String status = SWITCH_KEY_OPEN; 
    // 开关控制的资源 
    private Resources resources; 
    
    @Data 
    @ToString 
    public static class Resources { 
        // 包含的资源 
        private Set<String> include; 
        // 排除的资源 
        private Set<String> exclude; 
    }
}

​ 然后是开关判断处理机制实现类SwitchRuleChecker:

​ 根据开关控制的资源进行判断,如果资源不配置,则开关不作用到任何资源;如果配置include,则作用到include指定的所有资源;如果不配置include且配置了exclude,则除exclude指定的资源外,其它资源都受这个开关的控制。

Set<SwitchRule> switchRuleSet = initSwitchRule(); 
// 遍历规则 
for (SwitchRule rule : switchRuleSet) { 
    // 判断开关状态,开关未打开则跳过 
    if (!rule.getStatus().equalsIgnoreCase(SwitchRule.SWITCH_KEY_OPEN)) { 
        continue; 
    }
    if (rule.getResources() == null) { 
        continue; 
    }
    // 实现 include 语意 
    if (!CollectionUtils.isEmpty(rule.getResources().getInclude())) { 
        if (rule.getResources().getInclude().contains(resource.getName())) { 
            throw new SwitchException(resource.getName(), "switch"); 
        } 
    }
    // 实现 exclude 语意 
    if (!CollectionUtils.isEmpty(rule.getResources().getExclude())) { 
        if (!rule.getResources().getExclude().contains(resource.getName())) { 
            throw new SwitchException(resource.getName(), "switch");
        } 
    } 
}

​ 2、基于SlotChainBuilder扩展点实现自定义开关降级

​ 定义规则

@Data
@ToString
public class SwitchRule {
    public static final String SWITCH_KEY_OPEN = "open";
    public static final String SWITCH_KEY_CLOSE = "close";
    // 开关状态
    private String status = SWITCH_KEY_OPEN;
    // 开关控制的资源
    private Resources resources;

    @Data
    @ToString
    public static class Resources {
        // 包含的资源
        private Set<String> include;
        // 排除的资源
        private Set<String> exclude;
    }
}

​ 定义判断处理机制实现类,这里需要注意一点,就是如果被拦截,抛出的异常要是BlockException或其子类,这样才能够被Sentinel拦截处理。

public class SwitchRuleChecker {

    public static void checkSwitch(ResourceWrapper resource, Context context) throws SwitchException{

        Set<SwitchRule> switchRuleSet = initSwitchRule();
        
        // 遍历规则
        for (SwitchRule rule : switchRuleSet) {
            // 判断开关状态,开关未打开则跳过
            if (!rule.getStatus().equalsIgnoreCase(SwitchRule.SWITCH_KEY_OPEN)) {
                continue;
            }
            if (rule.getResources() == null) {
                continue;
            }
            // 实现 include 语意
            if (!CollectionUtils.isEmpty(rule.getResources().getInclude())) {
                if (rule.getResources().getInclude().contains(resource.getName())) {
                    throw new SwitchException(resource.getName(), "switch");
                }
            }
            // 实现 exclude 语意
            if (!CollectionUtils.isEmpty(rule.getResources().getExclude())) {
                if (!rule.getResources().getExclude().contains(resource.getName())) {
                    throw new SwitchException(resource.getName(), "switch");
                }
            }
        }
    }

    private static Set<SwitchRule> initSwitchRule() {
        Set<SwitchRule> switchRuleSet = new HashSet<>();


        Set<String> include = new HashSet<>();
        include.add("/chatRecords/switch");

        SwitchRule.Resources resources = new SwitchRule.Resources();
        resources.setInclude(include);

        SwitchRule switchRule = new SwitchRule();
        switchRule.setStatus(SwitchRule.SWITCH_KEY_OPEN);
        switchRule.setResources(resources);

        switchRuleSet.add(switchRule);
        return switchRuleSet;
    }
}

public class SwitchException extends BlockException {
    public SwitchException(String ruleLimitApp, String message) {
        super(ruleLimitApp, message);
    }
}

​ 定义 Slot,在调用资源时执行开关降级判断,被拦截则抛出异常

public class SwitchSlot extends AbstractLinkedProcessorSlot {
    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, Object o, int i, boolean b, Object... objects) throws Throwable {
        // 在调用资源时执行开关降级判断
        SwitchRuleChecker.checkSwitch(resourceWrapper, context);
        fireEntry(context, resourceWrapper, o, i, b, objects);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int i, Object... objects) {
        fireExit(context, resourceWrapper, i, objects);
    }
}

​ 定义SlotChainBuilder实现类,将自定义的Slot加入SlotChain中

public class SwitchSlotChainBuilder extends DefaultSlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = super.build();
        chain.addLast(new SwitchSlot());
        return chain;
    }
}

​ 使用SPI机制加载自定义SlotChainBuilder:

​ 在META-INF/services目录下创建一个com.alibaba.csp.sentinel.slotchain.SlotChainBuilder文件,文件内容是自定义的SlotChainBuilder实现类

com.lcl.galaxy.microservice.frontend.chat.degrade.SwitchSlotChainBuilder
posted @ 2023-11-10 17:29  李聪龙  阅读(107)  评论(0编辑  收藏  举报