0
点赞
收藏
分享

微信扫一扫

Sentinel你了解多少

事件

12月20日上午九点半的时间,西安一部分核酸检测点,地铁站,出现西安一码通系统故障(临时)问题,前来做核酸的市民,不能扫描刷新西安一码通,随后,现场暂时停止了做核酸检测。

Sentinel你了解多少_限流

什么是Sentinel

Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。

Sentinel是分布式系统的防御系统。以流量为切入点,通过动态设置的流量控制、服务熔断等手段达到 保护系统的目的,通过服务降级增强服务被拒后用户的体验。

官网:https://github.com/alibaba/Sentinel/wiki

Sentinel主要特性:

Sentinel你了解多少_限流_02

Sentinel与Hystrix的区别

Hystrix常用的线程池隔离会造成线程上下切换的overhead比较大;Hystrix使用的信号量隔离对某个资源调用的并发数进行控制,效果不错,但是无法对慢调用进行自动降级;Sentinel通过并发线程数的流量控制提供信号量隔离的功能;

此外,Sentinel支持的熔断降级维度更多,可对多种指标进行流控、熔断,且提供了实时监控和控制面板,功能更为强大。

Sentinel架构图

若要读懂Sentinel源码,则必须要搞明白官方给出的Sentinel的架构图。

Sentinel你了解多少_责任链模式_03

Sentinel的核心骨架是ProcessorSlotChain。其将不同的 Slot 按照顺序串在一起(责任链模式),从而 将不同的功能组合在一起(限流、降级、系统保护)。系统会为每个资源创建一套SlotChain。

责任链模式

在现实生活中,一个事件需要经过多个对象处理是很常见的场景。例如,采购审批流程、请假流程等。公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据需要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这无疑增加了难度。

在计算机软硬件中也有相关例子,如总线网中数据报传送,每台计算机根据目标地址是否同自己的地址相同来决定是否接收;还有异常处理中,处理程序根据异常的类型决定自己是否处理该异常;还有 Struts2 的拦截器、JSP 和 Servlet 的 Filter 等,所有这些,都可以考虑使用责任链模式来实现。

模式的定义与特点

责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

注意:责任链模式也叫职责链模式。

在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。

责任链模式是一种对象行为型模式,其主要优点如下。

  1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
  3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

其主要缺点如下。

  1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

模式的应用场景

前边已经讲述了关于责任链模式的结构与特点,下面介绍其应用场景,责任链模式通常在以下几种情况使用。

  1. 多个对象可以处理一个请求,但具体由哪个对象处理该请求在运行时自动确定。
  2. 可动态指定一组对象处理请求,或添加新的处理者。
  3. 需要在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。

SPI机制 

Sentinel槽链中各Slot的执行顺序是固定好的。但并不是绝对不能改变的。Sentinel将ProcessorSlot 作 为 SPI 接口进行扩展,使得 SlotChain 具备了扩展能力。用户可以自定义Slot并编排Slot 间的顺序。

Sentinel你了解多少_限流_04

同时我们也能看到,我们可以自定义Slot的实现,来实现我们定义的业务逻辑。

Slot简介

NodeSelectorSlot 

负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降。

public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);


@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
//从上下文获取Context命名的Node节点
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
//创建resource对应的Node 类型为DeaultNode
node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
//保存Node 一个resource对应一个Node
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
}
// 添加到Context的Node节点,这里构造了一棵节点🌲
((DefaultNode)context.getLastNode()).addChild(node);
}
}
//设置当前Context的当前节点为node
context.setCurNode(node);
//调用下一个Slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}


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

可以看到,在NodeSelectorSlot节点中,首先根据ContextName获取默认节点,若默认节点不存在,则创建一个默认节点,并将改节点保存在缓存map中,然后设置当前节点为默认节点。

ClusterBuilderSlot 

用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count,Block count, Exception count 等等,这些信息将用作为多维度限流,降级的依据。简单来说,就是用于构建 ClusterNode。

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args)
throws Throwable {
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
// Create the cluster node.
clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
// 将clusterNode保存到全局的map中去
HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);


clusterNodeMap = newMap;
}
}
}
// 将clusterNode塞到DefaultNode中去
node.setClusterNode(clusterNode);


/*
* if context origin is set, we should get or create a new {@link Node} of
* the specific origin.
*/
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}


fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

首先参数里的node就是上层NodeSelectorSlot选择得到的node。

这里的主要逻辑就是全局一个map缓存, Resource --> ClusterNode 映射关系。根据资源从全局缓存中取一个,塞到当前node的ClusterNode字段中。以供后面StatisticSlot使用。

StatisticSlot 

用于记录、统计不同纬度的 runtime 指标监控信息。

ParamFlowSlot 

对应热点流控。

FlowSlot 

用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制。对应流控规则。

public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

private final FlowRuleChecker checker;

public FlowSlot() {
this(new FlowRuleChecker());
}

FlowSlot(FlowRuleChecker checker) {
AssertUtil.notNull(checker, "flow checker should not be null");
this.checker = checker;
}

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
//先检测,通过检测(不限流)再往下传递
checkFlow(resourceWrapper, context, node, count, prioritized);

fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
throws BlockException {
//核心的功能都交给FlowRuleChecker来处理
checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

//其实什么也没做,只是向后传递
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}

private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {
@Override
public Collection<FlowRule> apply(String resource) {
// Flow rule map should not be null.
Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
return flowRules.get(resource);
}
};
}

经典的限流算法有:

计数器算法  

漏桶算法(Leaky Bucket) 

令牌桶算法(Token Bucket)

AuthoritySlot 

根据配置的黑白名单和调用来源信息,来做黑白名单控制。对应授权规则。

AuthoritySlot 来源访问控制,也称为黑白名单控制。

根据配置中的limitApp,对不同的origin进行限制,如果是白名单的则不做限流,如果黑名单的禁止访问。

其实更像一种对访问来源系统的授权操作。

DegradeSlot 

通过统计信息以及预设的规则,来做熔断降级。对应降级规则。

SystemSlot 

通过系统的状态,例如 load1 等,来控制总的入口流量。对应系统规则。


Context简介 

Context是对资源操作的上下文,每个资源操作必须属于一个Context。如果代码中没有指定Context, 则会创建一个name为sentinel_default_context的默认Context。一个Context生命周期中可以包含多个 资源操作。Context生命周期中的最后一个资源在exit()时会清理该Conetxt,这也就意味着这个Context 生命周期结束了。

哨兵节点(Sentinel Node)

Sentinel你了解多少_责任链模式_05

哨兵节点相当于一个哑单元,或者一个傀儡,作用是防止首节点为空时(first == null),出现无法指向下个节点情况。哨兵节点指向的下个单元为首节点。

public class SLList {
public class IntNode {
public int item;
public IntNode next;
public IntNode(int i, IntNode n) {
item = i;
next = n;
}
}


private IntNode first;
private int size;


public SLList() {
first = null;
size = 0;
}


public SLList(int x) {
first = new IntNode(x, null);
size = 1;
}


/** Adds an item to the front of the list. */
public void addFirst(int x) {
first = new IntNode(x, first);
size += 1;
}


/** Retrieves the front item from the list. */
public int getFirst() {
return first.item;
}


/** Returns the number of items in the list. */
public int size() {
return size;
}


/** Adds an item to the end of the list. */
/* 这里已经通过讨论首节点为空的情况修正,但采用哨兵节点会更简洁*/
public void addLast(int x) {
size += 1;
if(first == null){
first = new IntNode(x , first);
return 0;
IntNode p = first;
while(p != null){
p = p.next;
}
p.next = IntNode(x , null);
}


/** Crashes when you call addLast on an empty SLList. Fix it. */
public static void main(String[] args) {
SLList x = new SLList();
x.addLast(5);
}
}

添加节点的方法:

public void addLast(int x) {
size += 1;
if(first == null){
first = new IntNode(x , first);
return 0;
IntNode p = first;
while(p != null){
p = p.next;
}
p.next = IntNode(x , null);
}

利用哨兵节点进行优化:

public void addLast(int x) {
size += 1;
IntNode p = sentinel;
while(p != null){
p = p.next;
}
p.next = IntNode(x , null);
}

Sentinel你了解多少_责任链模式_06

  • Node:用于完成数据统计的接口 
  • StatisticNode:统计节点,是Node接口的实现类,用于完成数据统计
  • EntranceNode:入口节点,一个Context会有一个入口节点,用于统计当前Context的总体流量数 据 
  • DefaultNode:默认节点,用于统计一个资源在当前Context中的流量数据
  • ClusterNode:集群节点,用于统计一个资源在所有Context中的总体流量数据

滑动时间窗算法

对于滑动时间窗算法的源码解析分为两部分:对数据的统计,与对统计数据的使用。不过,在分析源 码之前,需要先理解该算法原理。

算法原理

Sentinel你了解多少_责任链模式_07


该算法原理是,系统会自动选定一个时间窗口的起始零点,然后按照固定长度将时间轴划分为若干定长 的时间窗口。所以该算法也称为“固定时间窗算法”。 

当请求到达时,系统会查看该请求到达的时间点所在的时间窗口当前统计的数据是否超出了预先设定好 的阈值。未超出,则请求通过,否则被限流。

存在的问题

Sentinel你了解多少_责任链模式_08


该算法存在这样的问题:连续两个时间窗口中的统计数据都没有超出阈值,但在跨窗口的时间窗长度范 围内的统计数据却超出了阈值。

滑动时间窗限流算法

算法原理

Sentinel你了解多少_责任链模式_09


滑动时间窗限流算法解决了固定时间窗限流算法的问题。其没有划分固定的时间窗起点与终点,而是将 每一次请求的到来时间点作为统计时间窗的终点,起点则是终点向前推时间窗长度的时间点。这种时间 窗称为“滑动时间窗”。

算法改进

Sentinel你了解多少_限流_10


针对以上问题,系统采用了一种“折中”的改进措施:将整个时间轴拆分为若干“样本窗口”,样本窗口的 长度是小于滑动时间窗口长度的。当等于滑动时间窗口长度时,就变为了“固定时间窗口算法”。 一般 时间窗口长度会是样本窗口长度的整数倍。

那么是如何判断一个请求是否能够通过呢?当到达样本窗口终点时间时,每个样本窗口会统计一次本样 本窗口中的流量数据并记录下来。当一个请求到达时,会统计出当前请求时间点所在样本窗口中的流量 数据,然后再获取到当前请求时间点所在时间窗中其它样本窗口的统计数据,求和后,如果没有超出阈 值,则通过,否则被限流。


Sentinel你了解多少_责任链模式_11

举报

相关推荐

0 条评论