Sentinel之Slots插槽源码分析(二)

一、概述

上篇文章介绍过了NodeSelectorSlot和ClusterBuilderSlot两种插槽,接下来我们沿着默认的插槽链继续分析LogSlot和StatisticSlot

二、LogSlot

我们看代码:

     @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        try {
            fireEntry(context, resourceWrapper, obj, count, prioritized, args);
        } catch (BlockException e) {
            EagleEyeLogUtil.log(resourceWrapper.getName(), e.getClass().getSimpleName(), e.getRuleLimitApp(),
                context.getOrigin(), count);
            throw e;
        } catch (Throwable e) {
            RecordLog.info("Entry exception", e);
        }

    }

可以发现,如果在接下来的插槽链中发生BlockException异常的话,LogSlot会记录日志信息。
继续看EagleEyeLogUtil类:

public class EagleEyeLogUtil {

    public static final String FILE_NAME = "sentinel-block.log";

    private static StatLogger statLogger;

    static {
        String path = LogBase.getLogBaseDir() + FILE_NAME;

        statLogger = EagleEye.statLoggerBuilder("sentinel-block-log")
            .intervalSeconds(1)
            .entryDelimiter('|')
            .keyDelimiter(',')
            .valueDelimiter(',')
            .maxEntryCount(6000)
            .configLogFilePath(path)
            .maxFileSizeMB(300)
            .maxBackupIndex(3)
            .buildSingleton();
    }

    public static void log(String resource, String exceptionName, String ruleLimitApp, String origin, int count) {
        statLogger.stat(resource, exceptionName, ruleLimitApp, origin).count(count);
    }
}

可以发现block日志是记录在sentienl-block.log中,这里block日志的格式化及本地文件的写入功能在eagleeye包中,如图:

log

个人觉得这个异常日志记录相对于sentienl的record日志开发有点过于复杂,鉴于不是sentienl的核心功能,我也只大概看了里面的代码。
大致就是在StatEntry中调用count的方法,然后在StatRollingData中调用StatLogController中scheduleWriteTask方法进而创建一个定时任务线程池,任务是StatLogWriteTask,在这个任务中写入日志到本地文件中。

三、StatisticSlot

StatisticSlot是sentienl的指标数据统计插槽,也是sentienl种非常重要的一个模块,sentienl后续的限流,降级,熔断都是根据这一阶段的统计数据进行。

前面文章在介绍sentinel的滑动时间窗口时,已经知道sentienl的指标统计是基于滑动时间窗口的,可以看文章 Sentinel之滑动时间窗口设计

下面主要看在StatisticSlot插槽中,sentinel做了哪些数据的统计的。

public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        try {
            // Do some checking.
            fireEntry(context, resourceWrapper, node, count, prioritized, args);

            // Request passed, add thread count and pass count.
            node.increaseThreadNum();
            node.addPassRequest();

            if (context.getCurEntry().getOriginNode() != null) {
                // Add count for origin node.
                context.getCurEntry().getOriginNode().increaseThreadNum();
                context.getCurEntry().getOriginNode().addPassRequest();
            }

            if (resourceWrapper.getType() == EntryType.IN) {
                // Add count for global inbound entry node for global statistics.
                Constants.ENTRY_NODE.increaseThreadNum();
                Constants.ENTRY_NODE.addPassRequest();
            }

            // Handle pass event with registered entry callback handlers.
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        } catch (BlockException e) {
            // Blocked, set block exception to current entry.
            context.getCurEntry().setError(e);

            // Add block count.
            node.increaseBlockQps();
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseBlockQps();
            }

            if (resourceWrapper.getType() == EntryType.IN) {
                // Add count for global inbound entry node for global statistics.
                Constants.ENTRY_NODE.increaseBlockQps();
            }

            // Handle block event with registered entry callback handlers.
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onBlocked(e, context, resourceWrapper, node, count, args);
            }

            throw e;
        } catch (Throwable e) {
            // Unexpected error, set error to current entry.
            context.getCurEntry().setError(e);

            // This should not happen.
            node.increaseExceptionQps();
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseExceptionQps();
            }

            if (resourceWrapper.getType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseExceptionQps();
            }
            throw e;
        }
    }

在方法entry中,这里是先调用fireEntry方法继续资源保护检查。

请求通过

增加node的线程数increaseThreadNum。
在DefaultNode类中increaseThreadNum方法中:

@Override
    public void increaseThreadNum() {
        super.increaseThreadNum();
        this.clusterNode.increaseThreadNum();
    }

然后调用StatisticNode中increaseThreadNum方法,在increaseThreadNum方法中执行curThreadNum.incrementAndGet()。
这里线程数增加是通过定义一个原子变量的,curThreadNum是一个AtomicInteger原子变量,初始值是0 ,执行incrementAndGet方法加1,并返回当前值。

增加node的通过请求数addPassRequest。
在DefaultNode类中addPassRequest方法中:

@Override
    public void addPassRequest() {
        super.addPassRequest();
        this.clusterNode.addPassRequest();
    }

在StatisticNode中的addPassRequest方法:

     @Override
    public void addPassRequest() {
        rollingCounterInSecond.addPass();
        rollingCounterInMinute.addPass();
    }

根据滑动时间窗口统计了两个时间窗口的数据。

  • rollingCounterInSecond:时间窗口是1s
  • rollingCounterInMinute:时间窗口是1分钟

源节点originNode
如果originNode存在,则也需要增加originNode的线程数和请求通过数。

ENTRY_NODE
如果资源包装类型是IN的话,则需要ENTRY_NODE的线程数和请求通过数。
ENTRY_NODE是sentinel全局的统计节点,用于后续系统规则检查。

ProcessorSlotEntryCallback
然后再循环处理注册了ProcessorSlotEntryCallback的StatisticSlot。

StatisticSlotCallbackRegistry是一个StatisticSlot回调注册器,目前只有两种回调支持,ProcessorSlotEntryCallback和ProcessorSlotExitCallback。

ProcessorSlotEntryCallback是在资源正常调用时处理,ProcessorSlotExitCallback是在发送BlockException异常或者资源退出时调用。

目前只有热点限流的统计把ParamFlowStatisticSlotCallbackInit注册到ProcessorSlotEntryCallback中,看下面代码。

public class ParamFlowStatisticSlotCallbackInit implements InitFunc {

    @Override
    public void init() {
        StatisticSlotCallbackRegistry.addEntryCallback(ParamFlowStatisticEntryCallback.class.getName(),
            new ParamFlowStatisticEntryCallback());
    }
}

这里系统若是引用了热点参数限流模块,则客户端系统会在启动时把ProcessorSlotEntryCallback注册到StatisticSlotCallbackRegistry中。

ProcessorSlotEntryCallback

public class ParamFlowStatisticEntryCallback implements ProcessorSlotEntryCallback<DefaultNode> {

    @Override
    public void onPass(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args)
        throws Exception {
        // The "hot spot" parameter metric is present only if parameter flow rules for the resource exist.
        ParameterMetric parameterMetric = ParamFlowSlot.getParamMetric(resourceWrapper);

        if (parameterMetric != null) {
            parameterMetric.addPass(count, args);
        }
    }

    @Override
    public void onBlocked(BlockException ex, Context context, ResourceWrapper resourceWrapper, DefaultNode param,
                          int count, Object... args) {
        // Here we don't add block count here because checking the type of block exception can affect performance.
        // We add the block count when throwing the ParamFlowException instead.
    }
}

onPass方法就是用来热点参数的指标的,热点参数限流可以专门一个模块来学习,这里后面会再专门讲解。

抛出BlockException

SetError
这里会设置Context的CurEntry的Error属性,error属性可以在资源退出的时候判断使用,若不存在error,会统计响应时间rt等。

增加block线程数
在DefaultNode类中increaseBlockQps方法中:

 @Override
    public void increaseBlockQps() {
        super.increaseBlockQps();
        this.clusterNode.increaseBlockQps();
    }

在StatisticNode中的increaseBlockQps方法:

 @Override
    public void increaseBlockQps() {
        rollingCounterInSecond.addBlock();
        rollingCounterInMinute.addBlock();
    }

根据滑动时间窗口统计了两个时间窗口的数据。

rollingCounterInSecond:时间窗口是1s
rollingCounterInMinute:时间窗口是1分钟

源节点originNode
如果originNode存在,则也需要增加originNode请求Block数。

ENTRY_NODE
如果资源包装类型是IN的话,则需要ENTRY_NODE的Block数。

然后再进行ProcessorSlotEntryCallback的onBlocke方法,这个也是热点参数限流才会有。

抛出Throwable

先设置curEntry的error

增加node的异常数
node.increaseExceptionQps();

DefaultNode中:

@Override
    public void increaseExceptionQps() {
        super.increaseExceptionQps();
        this.clusterNode.increaseExceptionQps();
    }

然后判断originNode和包装类型,增加对应的exception数。

在Exit中


    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        DefaultNode node = (DefaultNode)context.getCurNode();

        if (context.getCurEntry().getError() == null) {
            // Calculate response time (max RT is TIME_DROP_VALVE).
            long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
            if (rt > Constants.TIME_DROP_VALVE) {
                rt = Constants.TIME_DROP_VALVE;
            }

            // Record response time and success count.
            node.rt(rt);
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().rt(rt);
            }

            node.decreaseThreadNum();

            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().decreaseThreadNum();
            }

            if (resourceWrapper.getType() == EntryType.IN) {
                Constants.ENTRY_NODE.rt(rt);
                Constants.ENTRY_NODE.decreaseThreadNum();
            }
        } else {
            // Error may happen.
        }

        // Handle exit event with registered exit callback handlers.
        Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();
        for (ProcessorSlotExitCallback handler : exitCallbacks) {
            handler.onExit(context, resourceWrapper, count, args);
        }

        fireExit(context, resourceWrapper, count);
    }

这里主要统计这次请求的响应时间rt,

    public final static int TIME_DROP_VALVE = 4900;

如果rt大于TIME_DROP_VALVE,则设置rt为TIME_DROP_VALVE。

  • 设置node的rt:node.rt(rt);
  • 设置originNode的rt:context.getCurEntry().getOriginNode().rt(rt);
  • 减掉node的线程数 node.decreaseThreadNum();
  • 减掉originNode线程数 context.getCurEntry().getOriginNode().decreaseThreadNum();
  • Constants.ENTRY_NODE的设置
  • 最后处理ProcessorSlotExitCallback的的onExit方法。

四、我的总结

1、介绍LogSlot和StatisticSlot插槽的代码设计,其中LogSlot没有详细深入。
2、LogSlot代码设计较为复杂,个人感觉功能可以重新设计。
3、StatisticSlot的统计是根据滑动时间窗口,但是线程数的统计是设置一个AtomicInteger原子变量。
3、如果StatisticSlotCallbackRegistry中注册ProcessorSlotEntryCallback和ProcessorSlotExitCallback回调器,则在回调器中也会统计数据,目前只有热点参数限流会使用。
4、Sentinel后续的插槽就是根据StatisticSlot统计的数据进行资源的保护的。


以上内容若有不当之处,请指正,谢谢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 一、概述 前面介绍过Sentinel核心框架就是通过插槽链一层层的调用,每个插槽的功能如下: NodeSelect...
    橘子_好多灰阅读 1,459评论 0 4
  • 北京刮了一夜的大风,龙卷风亦不过如此.凌晨1.35分醒了,睡不着,开始胡思乱想.我变成了电影和电视剧里各式...
    丑丑丑丫阅读 146评论 1 0
  • 今天觉察到一个点,当我们看着家人在那不停的抱怨的时候,我们就要让他的抱怨从我们的心上流走,如果我们觉察不到这个点,...
    三十七度光阅读 281评论 0 0
  • 感动的心❤,不停地在跳动,此刻正在陪伴我写下面的感动的话。 首先我要感谢自己,感谢自己的勤奋,让我在读书学习的过程...
    雅军Irene阅读 221评论 0 0