前回の記事では Sentinel の使い方を紹介しましたが、今回は Sentinel の原理をソースコードを通してさらに詳しく解説します。ソース コードのダウンロード アドレス: Sentinel ソース コードのダウンロード アドレス
目次
1 データを保存するためのスライディング タイム ウィンドウ アルゴリズム
2 データを取得するためのスライディング タイム ウィンドウ アルゴリズム
核となるコンセプト
ソース コードを理解する前に、ソース コードを理解するのに役立ついくつかの中心的な概念を理解する必要があります。まず、sentinel の公式 Web サイトのフレーム図を見てみましょう。
スロット チェーン: 関数スロット、異なるスロットには異なる関数があります。7 つのシステム定義スロットがあります。もちろん、スロット、カスタム関数スロットをカスタマイズすることもできます。その実行順序は FlowSlot の前です。7 つのシステム定義スロットは次のとおりです (出典: Sentinel 公式 Web サイト):
NodeSelectorSlot : 主にリソースのパスを収集し、これらのリソースの呼び出しパスをスタンプ構造に保存するために使用されます。このツリー構造の各ノードを以下に紹介します。
ClusterBuilderSlot : リソース統計と呼び出し元情報 (リソース qps (1 秒あたりの訪問数)、rt (インターフェイス応答時間)、スレッド数など) を保存するために使用され、多次元の電流制限とダウングレードとして使用されます。基底はクラスター ポイント リンクに対応します。ClusterNodeノード情報を構築します。
StaticSlot : ランタイム インジケーターの監視情報をさまざまな次元で記録およびカウントするために使用され、リアルタイム監視であり、最下層はスライディング タイム ウィンドウ アルゴリズムを採用しています (この章の以降の内容で紹介します)。
以下の ParamFlowSlot、SystemSlot、AuthoritySlot、FlowSlot、および DegradeSlot は、電流制限ヒューズの各チェックに対応するスロットであり、対応する電流制限劣化タイプがルールを満たしているかどうかを判断するために使用されます。 -制限劣化が実行され、それ以外の場合は通常パスです。
ノード: ノード情報を格納します。リソースのさまざまな次元の情報を格納するために使用されます。ノードには次の分類があります。
StatisticNode : データ統計を完了するために使用される統計ノード。
エントランスノード: エントリノードに属し、コンテキストの全体的なトラフィックデータをカウントするために使用されます。その統計的次元はコンテキストです。
DefaultNode : 現在のコンテキスト内のリソースのトラフィック データをカウントするために使用され、その統計的次元はコンテキスト + リソースです。
ClusterNode : さまざまなコンテキストでリソースのトラフィック データを保存するために使用され、その統計的次元はリソースです。
次の図に示すように、理解を容易にするために、上記のアーキテクチャ図のノード ノードのアーキテクチャ情報を再編成してみましょう。
Context : リソース操作のコンテキスト。各リソース操作は Context に属している必要があります。プログラムで Context が指定されていない場合は、「sentinel_default_context」という名前の Context が作成されます。コンテキスト ライフ サイクルには複数のリソース操作が存在する場合があり、コンテキスト ライフ サイクルの最後のリソースが終了すると、コンテキストはクリーンアップされます。これは、コンテキスト ライフ サイクルの終了も示します。
Entry : リソース操作を示し、現在の呼び出し情報が内部に保存されます。Context ライフサイクル内の複数のリソース操作も複数の Entry に対応し、これらの Entry トリップの親/子構造が Entry インスタンスで報告されます。
2つのソースコード分析
1 ソースコード入力
次に、電流制限とダウングレードのソースコード解析に入ります。センチネルを使用することで、彼が実際にアスペクト プログラミングである aop を使用していることがわかります。彼は私たちが作成したビジネスコードに侵入したわけではありませんが、リクエストが行われるたびに、電流制限とダウングレードの検証がトリガーされます。さらに、カスタマイズを使用するときは、@SentinelResource アノテーションを使用します。これをベースとして使用して、ソース コードから対応する側面を見つけることができます: SentinelResourceAspect (カット ポイント、通知、その他の情報が定義されている場所)
// 切面
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
// 切点
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {
}
// 环绕通知
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
....
String resourceName = getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
int resourceType = annotation.resourceType();
Entry entry = null;
try {
// 这里就对应我们所说的资源对象entry
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
// 调用原方法,通过限流降级规则
return pjp.proceed();
} catch (BlockException ex) {
// 限流或者降级
return handleBlockException(pjp, annotation, ex);
} catch (Throwable ex) {
...
} finally {
if (entry != null) {
entry.exit(1, pjp.getArgs());
}
}
}
}
上記のことから、メインのメソッドは、Sentinel の動作原理のすべての処理を含む Entry オブジェクトの作成であることがわかります。次に、フォローアップを続け、一連のオーバーロードされたメソッドをスキップして、フォローアップします。次のコードに直接記述します。
@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
Object[] args) throws BlockException {
// 第一步,分装资源对象,是根据资源名称以及@SentinelResource注解中的相关信息
StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
// 第二步,进入sentinel具体的工作流程,prioritized这个字段默认是false,标识不按照优先级的方式执行接下来的流程
return entryWithPriority(resource, count, prioritized, args);
}
次に、entryWithPriority の処理に入りますが、ここでは主に 1. コンテキストの取得、2. spi インターフェース拡張を使用した責任の連鎖の構築、3. 責任の連鎖の実行の 3 つのことを行います。
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
// 第一步,获取context
// 通过跟进代码,发现这里是通过从ThreadLocal中获取
Context context = ContextUtil.getContext();
// 如果获取的是NullContext类型,则为当前context的数量超多阈值,然后只进行Entry的初始化
if (context instanceof NullContext) {
return new CtEntry(resourceWrapper, null, context);
}
// 如果context为空,则进行context的初始化操作
if (context == null) {
// 初始化时,默认的context的名称为sentinel_default_context,和上面介绍核心概念时的介绍匹配上了
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// 如果全局的限流规则为关闭,只进行Entry资源的初始化
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
/**第二步 构建责任链
*这里进行一下重点说明,这里采用了spi的接口扩展方式构建处理链,处理链的数据结构为单向链表
*之所以构建这个单向链表,目的为了与业务进行解耦,因为限流降级规则很多,如果写在一起,耦合会
*很严重,为了遵循oop的设计思想,因此进行解耦,各司其职
* /
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 第三步,责任链的执行,针对上下文和资源进行操作
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
2 コンテキストの取得
次に、まずコンテキストの取得について整理します。 1. 現在のスレッドのキャッシュからコンテキストを取得します。 2. 現在のスレッドがコンテキストを作成していない場合は、コンテキストを初期化します。次にコンテキストの初期化処理を主に見ていきますが、この処理ではスレッドセーフを確保するためにシングルトンモードのメソッドであるダブルチェックを使用し、ダブルチェックを経てentryNodeを作成します。コードを追跡すると、trueEnter メソッドが直接見つかります。早速コードにいきましょう
protected static Context trueEnter(String name, String origin) {
// 从当前线程中再次获取,进行线程安全保证
Context context = contextHolder.get();
if (context == null) {
// 如果当前线程中context为空,则从缓存中获取node信息
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// 如果node信息为空,判断当前context的容量是否超过限制,如果是,则直接返回,不进行流控校验
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
// 这里相信大家非常熟悉,采用了双重检查的方式,保证线程安全
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
// 进行节点创建,这里创建的是EntranceNode,在上面介绍核心概念的时候,我们知道,它的统计维度为context
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// 在上面介绍核心概念的时候,我们说过,node的存储结构是树状结构,这里就是树的构建
Constants.ROOT.addChild(node);
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
// 根据node以及contextName创建context
context = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
3 責任連鎖の構築と実行
コンテキストが構築されたら、次のステップは責任のチェーンを構築することです。ここでは、スロット チェーンの初期化について見ていきます。アーキテクチャ図内のスロットの呼び出し順序をまだ覚えていますか? それはここに反映されます次に、コードを直接見ていきます。
// CtSph.lookProcessChain获取责任链
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// 通过资源信息,从缓存中获取责任链信息
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
// 通过创冲检查的方式获取责任链信息,保证线程安全,也保证值加载一次
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// 责任链缓存的长度超过最大值,则返回null
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
// 进行责任链的初始化,初始化完成后,将责任链信息放入缓存中
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
// SlotChainProvider.newSlotChain 初始化责任链
public static ProcessorSlotChain newSlotChain() {
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
// 获取责任链的构建器,读取的是配置文件
// META-INF/services/com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();
if (slotChainBuilder == null) {
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}",
slotChainBuilder.getClass().getCanonicalName());
}
// 通过责任链构建器,初始化责任链
return slotChainBuilder.build();
}
// DefaultSlotChainBuilder.build真正进行责任链,也就是slot插槽的构建
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// 读取配置文件
// META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot
List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
// 对获取的slot进行校验,排除类型不是AbstractLinkedProcessorSlot的slot
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
}
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
責任チェーンを作成するとき、上記のソース コード分析を通じて、spi 拡張インターフェイスを介して構成ファイルを読み取ることがわかります。上記のコメント内の構成ファイルの場所は相対的な場所であることに注意してください。これら 2 つの構成ファイルはSentinel-core サブモジュールで、これら 2 つの構成ファイルの内容を見てみましょう。
スロットチェーンビルダー:
com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder
プロセッサースロット:
com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot
com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot
com.alibaba.csp.sentinel.slots.logger.LogSlot
com.alibaba.csp.sentinel.slots.statistic.StatisticSlot
com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
com.alibaba.csp.sentinel.slots.system.SystemSlot
com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot
上記の設定ファイルのクラス名に注目してください。見覚えはありますか? これは、アーキテクチャ図のスロット情報に対応しており、上から下への順序はスロットの呼び出し順序とまったく同じです。この順序は、次に覚えておく必要があることでもあり、責任の連鎖のビジネス機能を実行するときは、この順序で実行されます。つまり、責任の連鎖の一方向リンクリストが上から下に構築されます。この設定ファイルに。
最後に、リソース統計、電流制限、ダウングレードなど、リソースに対する一連の責任の操作を入力します。次に、分析用の特定のコードを入力します。上記の構成ファイルの構成内容によると、責任チェーンの最初のスロットが NodeSelectNode であることがわかります。
NodeSelectNode.entry は責任チェーンの開始点であり、ここから責任チェーンが呼び出され、DefaultNode がここに作成されます。
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
// 根据context的name获取DefaultNode信息
// 从核心概念中我们可以这道,DefaultNode统计信息的维度为context+resource
DefaultNode node = map.get(context.getName());
if (node == null) {
// 如果defaultNodez还没被撞见,则通过双重检查的方式进行创建
synchronized (this) {
node = map.get(context.getName());
node = new DefaultNode(resourceWrapper, null);
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
// 将创建的node放入缓存中
cacheMap.put(context.getName(), node);
map = cacheMap;
// 将新建的node放入到node树中
((DefaultNode) context.getLastNode()).addChild(node);
}
}
}
context.setCurNode(node);
// 触发下一个节点
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
// 这里使用了模板的设计模式,所有的slot都是AbstractLinkedProcessorSlot的子类,在父类中定义了触发下一个slot的方法
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
// 如果责任链中slot还没执行结束,则执行下一个slot,这里的next根据上一个的slot,根据配置文件中由上到下的顺序,
// 在后面slot的执行中,我们会经常的调用到这个方法,要根据上一个调用者来确定下一个slot
if (next != null) {
next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}
// 进入到下一个slot中的处理中
void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args)
throws Throwable {
T t = (T)o;
entry(context, resourceWrapper, t, count, prioritized, args);
}
構成ファイル内のスロットのシーケンスから、次のステップは ClusterBuilderSlot.entry メソッドを呼び出すことであることがわかります。このメソッドでは、このメソッドの機能は ClusterNode を初期化し、DefaultNode との関係を確立することです。リソース コール情報とリソース RT、QPS などのコール ユーザー情報を保存します。これらのデータは、電流制限とダウングレードの基礎として使用されます。
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args)
throws Throwable {
// 通过双重检查的方式创建ClusterNode,
// 在核心概念中我们知道,ClusterNode统计数据的维度是Resource
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
// Create the cluster node.
clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
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 (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
// 进入下一个slot的执行
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
次に、StaticSlot に入ります。このスロットは非常に重要なスロットに属します。このスロットは、Sentinel の主要なアルゴリズム、つまりスライディング タイム ウィンドウ アルゴリズムの入り口でもあります。データ統計はここで実行され、データ統計はこのアルゴリズムでは、ソース コード内のデータを見てみましょう。タイム ウィンドウ アルゴリズムについては後で詳しく説明するので、ここでは分解しません。
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
try {
// 会调用slotchain后续的所有slot,进行规则统计
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// 增加线程数,这里是使用的原子类LongAddr,感性的同学可以去看我以前的文章,有对它的讲解
node.increaseThreadNum();
// 增加通过请求的数量(滑动时间窗算法)
node.addPassRequest(count);
......
}
上記の 3 つのスロットは、電流制限とダウングレードの準備とその後の作業用です。次に、特定のフロー制御ルールのスロットに入ります。スペースの都合上、ソース コードの導入には、FlowSlot と DegradeSlot の 2 つのスロットのみを紹介します。残り スロットに興味のある学生は、自分で見てみることができます。
FlowSlot はフロー制御ルールのスロットと呼ばれるものです。事前に設定されたリソース統計情報に従って、フロー制御ルールが検証されます。ここで、前回の記事で説明したフロー制御ルールの永続性に関する設定フィールドの情報も追加します。対応するリソース情報と列挙情報はここで見つけることができます。
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
// 监测并且应用流量规则
checkFlow(resourceWrapper, context, node, count, prioritized);
// 触发下一个slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
// 我们根据代码的调用,定位到下面的这个方法
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
// 判断资源和规则不能为空
if (ruleProvider == null || resource == null) {
return;
}
// 根据资源名获取所有流控规则,我们跟进去以后会发现,他是通过FlowRuleManager来进行FlowRule的管理
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
// 每一个规则进行校验,如果校验失败,则抛出异常,抛出的异常是FlowException
// sentinel触发流控的规则时会抛出BlockException,我们查看FlowException会发现它是BlockException的子类
for (FlowRule rule : rules) {
if (!canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}
ここでは、対応するフロー制御ルールが永続化されるときに関連するフィールドが含まれる FlowRule のソース コードを見ていきます。ここの各フィールドは、前の記事で設定した電流制限ルールに対応しており、永続化する場合は、次のフィールドと対応する列挙に従って設定できます。
// 阈值类型,默认为1-qps,还有0-并发线程数
private int grade = RuleConstant.FLOW_GRADE_QPS;
// 单机阈值
private double count;
// 流控模式,0-直接,1-关联,2-链路
private int strategy = RuleConstant.STRATEGY_DIRECT;
// 流控模式为关联时,设置的关联资源
private String refResource;
// 流控效果,0-快速失败,1-预热(warm up),2-排队等待
private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
// 预热时长
private int warmUpPeriodSec = 10;
// 排队超时时长
private int maxQueueingTimeMs = 500;
// 集群模式
private boolean clusterMode;
フロー制御効果におけるプレヒートとキューイングには、トークン バケット アルゴリズムとファネル アルゴリズムという 2 つのアルゴリズムが関係します。これらのアルゴリズムについては、以降のアルゴリズムに関する記事で詳しく紹介しますので、ここで簡単に説明します。
トークンバケットアルゴリズム: システムは一定の速度でトークンをバケットに入れます。リクエストを処理する必要がある場合、最初にバケットからトークンを取得する必要があります。バケットにトークンがない場合、サービスは拒否されます。
ファネル アルゴリズム: リクエストは最初にリーキー バケットに入り、リーキー バケットは処理のために固定速度でリクエストを解放します。ただし、リーキー バケット内のリクエストの数がバケットの容量を超えると、リクエストは直接拒否されます。
次に、canPass メソッドを見てみましょう。このメソッドでは、クラスター フロー制御検証かスタンドアロン フロー制御検証かを選択します。コードは公開されません。スタンドアロン フロー制御検証を分析します。ここでは passLocalCheck メソッドです。このメソッドでは 2 つのことが行われます: 1. 現在の制限ルールに従って、コンテキストはノード情報、つまり統計情報を保存するノードを取得するために使用されます。2. フローに従ってルールで設定されたコントロール効果、特定の選択 コントローラーは canPass メソッドを実行します。
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
// 根据请求获取节点,我们去跟进代码,在这个方法中会根据context和rule的信息,来返回不同的node节点
// 我们以流控模式为直接时为例,它返回的就是ClusterNode
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
if (selectedNode == null) {
return true;
}
// 根据rule中配置的流控效果选择对应的类进行处理,我们会发现这里会有四个controller
// DefaultController 流控效果为快速失败
// WarmUpController 流控效果为预热(warm up)
// RateLimiterController 流控效果为排队等待
// WarmUpRateLimiterController 预热+排队等待,需要注意的是,这种方式在dashbord中是无法直接配置的
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
高速障害を例にして分析を続けていきます ここで少し余談をさせていただきますが、ソースコードを分析する際には、DefaultController.canPass メソッドという単純な分岐を使用して分析することができます。この方法では、主に 2 つのことが行われます: 1. 現在のデータを計算する; 2. 電流制限ルールを検証する。
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 获取当前node节点的线程数或者qps总数,在这里就涉及到了滑动窗口算法
int curCount = avgUsedTokens(node);
// 当前请求数+申请的请求数量 > 阈值
if (curCount + acquireCount > count) {
if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
long currentTime;
long waitInMs;
currentTime = TimeUtil.currentTimeMillis();
waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
sleep(waitInMs);
throw new PriorityWaitException(waitInMs);
}
}
return false;
}
return true;
}
最後に、DegradeSlot をもう一度分析しましょう。そのコードは他のルール スロットとは多少異なります。DegradeSlot はヒューズのスロットを表します。ヒューズのヒューズ ルールはわかっています: 平均応答時間、異常数、異常比率。これらのデータはこの中にある必要があります。インターフェイスが呼び出された後にのみ取得できるため、DegradeSlot はエントリ内の融合ルールのみを取得し、融合ルールの検証は終了時に実行されます。まずヒューズ ルールの取得を見てみましょう。ついでに、エンティティの構成に使用されるヒューズ ルールのエンティティ クラスを見てみましょう。
void performChecking(Context context, ResourceWrapper r) throws BlockException {
// 获取所有资源的熔断器
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
return;
}
for (CircuitBreaker cb : circuitBreakers) {
// 对当前熔断状态进行判断,我们在上一章中也说过有关熔断状态的判断
if (!cb.tryPass(context)) {
throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
}
}
}
public class DegradeRule extends AbstractRule {
// 熔断策略,0-慢调用比例,1-异常比例,2-异常数量
private int grade = RuleConstant.DEGRADE_GRADE_RT;
// 阈值
private double count;
// 熔断时长
private int timeWindow;
// 最小请求数量
private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT;
// 慢调用比例
private double slowRatioThreshold = 1.0d;
// 慢调用统计时长
private int statIntervalMs = 1000;
}
enum State {
// 开启状态,服务熔断
OPEN,
// 半开启状态,超出熔断时间后,如果下次请求正常,则服务恢复正常;否则,继续熔断
HALF_OPEN,
// 关闭状态,服务正常
CLOSED
}
次に、サーキット ブレーカー ルールの検証プロセスを見てみましょう。終了メソッドでは主に 2 つの処理が行われます: 1. 他のスロットが異常かどうかを判断し、異常であれば検証を続行せずにそのまま終了します。2.リソース名 サーキット ブレーカー ルールを取得し、サーキット ブレーカー ルールを確認します。
サーキット ブレーカー ルールを確認するには 2 つの方法があり、1 つは ExceptionCircuitBreaker (異常なサーキット ブレーカー ルール)、もう 1 つは ResponseTimeCircuitBreaker 応答時間のサーキット ブレーカー ルールであり、ここでは ExceptionCircuitBreaker の異常なサーキット ブレーカー ルールにのみ焦点を当てます。
public void onRequestComplete(Context context) {
Entry entry = context.getCurEntry();
if (entry == null) {
return;
}
Throwable error = entry.getError();
// 异常事件窗口计数器
SimpleErrorCounter counter = stat.currentWindow().value();
// 本次请求是否抛异常,如果是,则异常数加一
if (error != null) {
counter.getErrorCount().add(1);
}
// 请求总数加一
counter.getTotalCount().add(1);
// 熔断规则校验
handleStateChangeWhenThresholdExceeded(error);
}
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
// 如果熔断器为开启状态,则直接返回
if (currentState.get() == State.OPEN) {
return;
}
// 如果熔断状态为半开启
if (currentState.get() == State.HALF_OPEN) {
// 如果本次请求为正常请求,则将熔断状态置为关闭,通过cas的方式
if (error == null) {
fromHalfOpenToClose();
} else {
// 将熔断状态置为开启状态,在修改状态时需要计算下次半开启状态的起始时间
fromHalfOpenToOpen(1.0d);
}
return;
}
List<SimpleErrorCounter> counters = stat.values();
long errCount = 0;
long totalCount = 0;
// 统计总的异常请求数以及总请求数
for (SimpleErrorCounter counter : counters) {
errCount += counter.errorCount.sum();
totalCount += counter.totalCount.sum();
}
// 如果总请求数没有超过最小请求数量,则直接返回
if (totalCount < minRequestAmount) {
return;
}
double curCount = errCount;
// 如果熔断策略为异常比例,则计算异常比例
if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
curCount = errCount * 1.0d / totalCount;
}
// 如果异常数量/异常比例达到了熔断标砖,则将熔断器置为开启状态
if (curCount > threshold) {
transformToOpen(curCount);
}
}
トリプル スライディング タイム ウィンドウ アルゴリズム
スライディング タイム ウィンドウ アルゴリズムは、センチネル内のデータ統計のコア アルゴリズムです。上のアーキテクチャ図では、タイム ウィンドウ全体がリング データ グループであり、リング配列の各要素がタイム サンプル ウィンドウであることがわかります。ウィンドウ それぞれには独自の長さがあり、その長さは固定されているため、各時間ウィンドウの長さも固定されます。データ統計を実行するときは、現在時刻が位置するタイム サンプル ウィンドウを計算し、その後、その時間ウィンドウの統計を計算します。サンプル ウィンドウ データを取得し、この時間ウィンドウ内の他のサンプル ウィンドウの合計統計を計算し、2 つの統計を加算してこの時間ウィンドウの合計値を取得します。もちろん、この計算方法には多少の誤差が生じます (現在時刻が、その時刻が属する時間枠の終わりに達していない可能性があります)。この誤差はルール内で許容されているため、心配する必要はありません。
1 データを保存するためのスライディング タイム ウィンドウ アルゴリズム
次に、さらに理解するために、スライディング タイム ウィンドウ アルゴリズムのソース コードを見てみましょう。前のパートのソース コードの説明では、タイム ウィンドウのデータ統計は StatisticSlot の node.addPassRequest であり、これをソース コード分析に入るエントリ ポイントとして使用します。コード分析を通じて、StatisticSlot の addPassRequest メソッドに入ります。ここで、スライディング カウンターを使用してこのデータが追加されます。引き続きコードを追跡し、ArrayMetric.addPass メソッドに入ります。
public void addPass(int count) {
// 获取当前时间点所在的样本窗口
WindowWrap<MetricBucket> wrap = data.currentWindow();
// 在当前样本窗口中加入本次请求
wrap.value().addPass(count);
}
まずコードの最初の行を見て、LeapArray.currentWindow() メソッドに続いて、最初に LeapArray クラスを見てみましょう。このクラスはアーキテクチャ図のリング配列です。内部の要素を見てみましょう。
ublic abstract class LeapArray<T> {
// 样本窗口长度
protected int windowLengthInMs;
// 一个时间窗口中包含的样本窗口量
protected int sampleCount;
// 时间窗的长度
protected int intervalInMs;
private double intervalInSecond;
// 元素为样本窗口类型,这里的泛型实际为MetricBucket
protected final AtomicReferenceArray<WindowWrap<T>> array;
......
}
そこには WindowWrp という別のタイプがあります。このクラスを見てみましょう。このクラスには、チーム サンプルのウィンドウの定義がいくつか含まれています。
public class WindowWrap<T> {
// 样本窗口长度
private final long windowLengthInMs;
// 样本窗口的起始时间戳
private long windowStart;
// 存储具体的统计数据,类型为MetricBucket,统计的多维数据存储在MetricEvent中
private T value;
......
}
これら 2 つのカテゴリを読んだ後、元のメソッド呼び出しに進み、コードをたどって、LeapArray.currentWindow メソッドに入ります。
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
// 计算当前时间所在的样本窗口所在的索引,计算方式为当前时间戳/样本的窗口长度,然后用计算出的值对样本数量取余
int idx = calculateTimeIdx(timeMillis);
// 计算当前样本窗口的开始时间点,计算方式为,当前时间-(当前时间%样本窗口长度)
long windowStart = calculateWindowStart(timeMillis);
while (true) {
// 根据计算得到的索引,获取当前时间窗中的样本窗口
WindowWrap<T> old = array.get(idx);
// 如果当前样本窗口不存在,则进行样本窗口的新建
if (old == null) {
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
// 如果当前时间所在样本窗口的开始时间等于计算得到的样本窗口的开始时间,证明这两个窗口是同一个,直接返回
return old;
} else if (windowStart > old.windowStart()) {
// 如果当前时间所在样本窗口的开始时间大于计算得到的样本窗口的开始时间,则证明原有样本窗口已经过期,需要进行替换
if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
// 这种情况再正常情况下是不会出现的,除非调整服务器时间,我们不做过多分析
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
現在のサンプル ウィンドウを取得したら、次のステップはリクエスト情報をサンプル ウィンドウに追加することです。
// 需要注意这里存储多维度数据使用的是LongAddr[],这里是在数组下标代表的是PASS的位置进行数据相加
public void addPass(int n) {
add(MetricEvent.PASS, n);
}
// 我们看一下MetricEvent
public enum MetricEvent {
PASS,
BLOCK,
EXCEPTION,
SUCCESS,
RT,
OCCUPIED_PASS
}
2 データを取得するためのスライディング タイム ウィンドウ アルゴリズム
ここまでで、スライディング タイム ウィンドウ アルゴリズムを通じてデータを追加するプロセスは終了しました。次に、スライディング タイム ウィンドウ アルゴリズムを通じてデータを取得するソース コード部分を見ていきます。上で FlowSlot のソース コードを紹介するときに、コード内の DefaultController.pass メソッドにあるスライディング タイム ウィンドウ アルゴリズムについて言及しましたが、このメソッドが何をするのか覚えていますか?これは、電流制限効果の直接的な障害を処理します。 。この方法では、
// int curCount = avgUsedTokens(node);获取当前node节点的线程数或者qps
private int avgUsedTokens(Node node) {
if (node == null) {
return DEFAULT_AVG_USED_TOKENS;
}
// 根据不同的数据类型湖区不同的值,这里我们以qps分支分析
return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}
// 进入到StatisticNode.passQps方法
public double passQps() {
// 获取qps的值,即当前时间窗中的通过的请求数/当前时间窗长度
return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
}
次に注意する必要があるのはパスです。スライディング タイム ウィンドウから統計を取得します。
public long pass() {
// 这个方法相信大家很熟悉,就是上面通过滑动时间窗算法增加数据时进行样本窗口数据更新的方法
data.currentWindow();
long pass = 0;
// 获取当前时间窗口中的所有样本窗口
List<MetricBucket> list = data.values();
// 将样本窗口中统计的多维数据中,状态为PASS的数据的总数量
for (MetricBucket window : list) {
pass += window.pass();
}
return pass;
}
// 我们通过跟进方法,跳过重载方法,来到以下方法,获取所有的有效样本窗口
public List<T> values(long timeMillis) {
if (timeMillis < 0) {
return new ArrayList<T>();
}
int size = array.length();
List<T> result = new ArrayList<T>(size);
// 遍历每一个样本窗口
for (int i = 0; i < size; i++) {
WindowWrap<T> windowWrap = array.get(i);
// 若当前数据为空,或者已经过时,则本条数据不处理
// 超时代表的是:当前时间节点-样本窗口的起始节点时间>时间窗口长度,代表不是同一个时间窗口
if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
continue;
}
result.add(windowWrap.value());
}
return result;
}