3.2 责任链模式在Sentinel中的应用
在第2章,我们将Sentinel提供的所有ProcessorSlot分为两类:一类是负责完成资源指标数据统计的ProcessorSlot;一类是实现限流、熔断等流量控制功能的ProcessorSlot。
Sentinel使用责任链模式将注册的所有ProcessorSlot按照一定的顺序串成一个单向链表。实现资源指标数据统计的ProcessorSlot必须在实现流量控制功能的ProcessorSlot的前面,原因很简单,限流、熔断降级等都需要依赖资源的实时指标数据做判断。当然,如果ProcessorSlot不依赖资源的指标数据,那么这个ProcessorSlot的位置就不需要约束。
除按分类排序外,同一个分类下的每个ProcessorSlot可能也需要有严格的排序。例如,负责完成资源指标数据统计的ProcessorSlot的排序为NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot,如果排序乱了,就会抛出异常。
ProcessorSlotChain用于将ProcessorSlot串成一个单向链表,并且ProcessorSlotChain由SlotChainBuilder构造。DefaultSlotChainBuilder是默认使用的SlotChainBuilder,其构造的ProcessorSlotChain所注册的ProcessorSlot及顺序如下。
ProcessorSlot接口的定义如下。
方法说明如下。
• entry:入口方法。
• fireEntry:调用下一个ProcessorSlot的entry方法。
• exit:出口方法。
• fireExit:调用下一个ProcessorSlot的exit方法。
参数说明如下。
• context:当前调用链上下文,即Context实例。
• resourceWrapper:资源ID。
• param:泛型参数,一般用于传递资源的DefaultNode实例。
• count:表示申请占用共享资源的数量,只有申请到足够的共享资源时才能继续执行。
• prioritized:表示是否对请求进行优先级排序。
• args:调用方法传递的参数,用于实现热点参数限流。
Sentinel将需要被保护的资源包装起来,这与锁的实现是一样的,即需要先获取锁才能继续执行。count与并发编程AQS中tryAcquire方法的参数作用一样,例如,线程池有200个线程,当前方法执行需要申请3个线程才能执行,那么count就是3。
count的值一般为1,当限流规则配置的限流阈值类型为Threads时,表示需要申请一个线程,当限流规则配置的限流阈值类型为QPS时,表示需要申请令牌(假设使用令牌桶算法)。
之所以能够将所有的ProcessorSlot构造成一个ProcessorSlotChain,是因为这些ProcessorSlot继承了AbstractLinkedProcessorSlot类。AbstractLinkedProcessorSlot类有一个指向下一个ProcessorSlot的字段,正是这个字段实现了将所有注册的ProcessorSlot串成一条单向链表。AbstractLinkedProcessorSlot类的部分源码如下。
• next:表示链表中当前节点的下一个节点。
实现责任链调用的方法:由前一个AbstractLinkedProcessorSlot实例调用fireEntry方法或fireExit方法,在fireEntry方法与fireExit方法中调用下一个AbstractLinkedProcessorSlot实例(next)的entry方法或exit方法。AbstractLinkedProcessorSlot实例的fireEntry方法与fireExit方法的实现源码如下。
• fireEntry方法:如果next不为空,则调用下一个ProcessorSlot的entry方法。
• fireExit方法:如果next不为空,则调用下一个ProcessorSlot的exit方法。
ProcessorSlotChain抽象类也继承了AbstractLinkedProcessorSlot类,只不过添加了两个方法:将一个ProcessorSlot添加到链表头节点的addFirst方法,以及将一个ProcessorSlot添加到链表末尾的addLast方法。
ProcessorSlotChain抽象类的默认实现子类是DefaultProcessorSlotChain,DefaultProcessorSlotChain类定义了一个指向链表头节点的first字段和一个指向链表尾节点的end字段。其中,first字段指向的是一个空实现的AbstractLinkedProcessorSlot实例。DefaultProcessorSlotChain类的源码如下。
• first字段:指向链表的头节点。
• end字段:指向链表的尾节点。
• addFirst方法:在链表头部添加一个节点。
• addLast方法:在链表尾部添加一个节点。
• entry方法:调用链表头节点的entry方法,由头节点调用下一节点的entry方法。
• exit方法:调用链表头节点的exit方法,由头节点调用下一节点的exit方法。
责任链模式是非常常用的一种设计模式。在Shiro框架中,使用责任链模式实现资源访问权限过滤的骨架(过滤器链);在Netty框架中,使用责任链模式将处理请求的ChannelHandler包装为链表,实现局部串行处理请求。
在责任链的实现上,Sentinel与Netty有相似的地方。在Sentinel中,ProcessorSlot实例的entry方法是按ProcessorSlot实例在链表中的顺序被调用的,这一点与Netty中ChannelHandler链表的实现相同,不同的是,ProcessorSlot实例的exit方法被调用的顺序是从后往前的。Netty可以实现局部串行以解决线程安全问题,即ChannelHandler链表只在收到数据包时被创建,在响应数据包后被销毁,而由于Sentinel是以资源为维度的,所以必然实现不了这种局部串行。
Sentinel会为每个资源创建且仅创建一个ProcessorSlotChain实例。ProcessorSlotChain实例被缓存在CtSph类的chainMap静态字段中,key为资源ID,每个资源的ProcessorSlotChain实例在CtSph#entryWithPriority方法中被创建,代码如下。
• chainMap字段:缓存ProcessorSlotChain实例,key为资源ID,value为ProcessorSlotChain实例。
• entryWithPriority方法:为资源创建ProcessorSlotChain实例(如果未创建)、为资源创建Entry实例,以及调用ProcessorSlotChain实例的entry方法。
CtSph类的chainMap静态字段最终会缓存整个应用中所有资源的ProcessorSlotChain实例。虽然chainMap的key类型为ResourceWrapper,但ResourceWrapper的equals方法只比较资源的名称,因此一个资源不会存在多个ProcessorSlotChain实例。