Java SpringCloudAlibaba Sentinel
Sentinel简介
Sentinel它是面向了分布式服务构的对流量控制的一个组件,主要就是从限流、流量整型、熔断降级等等多个维度来对开发者保证微服务架构的稳定性。
Sentinel被设计得很精巧,只需要sentinel-core就可以了,可谓好用又强大!
Sentinel的基本原理
Sentinel它是在内部创建一个责任链,责任链的话它是由一系列的ProcessorSlot对象来组成的,每个ProcessorSlot这个对象负责了不同的功能,然后外部请求是否可以 访问资源,这个时候需要通过责任链校验,只有校验通过了,才可以进行资源访问。如果校验失败就抛出异常。
Sentinel的8个实现类
(1)DegradeSlot:用于服务降级,发现服务超时或者是报错的次数超过了限制的话,DegradeSlot就将会禁止再次访问该服务,需要等待一段时间后,DegradeSlot会试探性的把一个请求放过去,然后再根据这个请求的情况来判断是否要降级。
(2)AuthoritySlot:黑白名单校验,它是需要按照字符串来进行匹配,如果在黑名单里就是禁止访问的。
(3)ClusterBuilderSlot:构建一个ClusterNode对象,这个对象主要是用在统计访问的QPS、线程数等等。每一个资源对于一个ClusterNode。
(4)SystemSlot:用来校验QPS、线程数、CPU使用率是否已经超过了限制。
(5)StatisticSlot:这个用在多个维度来统计响应的时间、并发的线程数,处理成功/失败的数量、
(6)FlowSlot:用在流量监控,根据QPS或者并发的线程数来进行控制,当发现QPS高过设定值的时候,就会抛出异常,FlowSlot依赖StatisticSlot。
(7)NodeSelectorSlot:收集资源的路径、把这些资源调用的路径再以树结构存储 起来,根据调用的路径来进行限流。
(8)LogSlot:打印日志
Slot的执行顺序
**SphU.entry**
entry这个方法内部会创建一个叫StringResourceWrapper的对象,该对象表示被保护的资源。这个对象有三个参数。
//资源名,也就是entry()方法的第一个入参protected final String name;//表示是入口流量(IN)还是出口流量(OUT),//两个参数的区别在于是否被SystemSlot检查,IN会被检查,OUT不会,默认是OUTprotected final EntryType entryType;//表示资源类型,sentinel提供了common、web、sql、api等类型,资源类型用于统计使用protected final int resourceType;
Entry
有了资源的对象,接着就是创建Entry对象,这个对象是SphU的这个返回值。通过Entry对象应用程序就可以知道sentinel里的内部情况了。
//resourceWrapper:是StringResourceWrapper对象,表示资源//count:表示令牌数,默认是1,一般一个请求对应一个令牌,也可以指定一个请求对应多个令牌,如果令牌不够,则禁止访问//prioritized:在FlowSlot里面使用,没找到具体的使用含义,有看懂的小伙伴可以告知一下private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)throws BlockException {//构建上下文对象,上下文对象存储在ThreadLocal中Context context = ContextUtil.getContext();if (context instanceof NullContext) {return new CtEntry(resourceWrapper, null, context);}//一般的线程第一次访问资源,context都是null,我也可以在应用程序中使用ContextUtil自己创建Context对象if (context == null) {//下面创建了一个名字为sentinel_default_context的Context对象context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);}//全局开关,可以使用它来关闭sentinelif (!Constants.ON) {return new CtEntry(resourceWrapper, null, context);}//使用SPI构建slot链,每个slot对象都有一个next属性,可以使用该属性指定下一个slot对象ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);if (chain == null) {return new CtEntry(resourceWrapper, null, context);}//创建Entry对象Entry e = new CtEntry(resourceWrapper, chain, context);try {//对该请求,遍历每个slot对象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;}
源码如上所示,可以看到先Context对象,这个对象会走遍整个请求的过程。一些共享的数据是可以放在这里,也可以 在应用的程序里创建Context对象。创建完Context对象之后,就会使用SPI来构建slot链,之后就是创建Entry对象,最后就是遍历slot链是不是可以请求访问资源。
**Entry.exit()**
当资源访问完后,就需要开始调用exit这个方法来告诉sentinel可以结束访问了,这时候sentinel就会做一些资源的清理跟数据统计的工作。
protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException {if (context != null) {if (context instanceof NullContext) {return;}//如果Context对象记录的Entry对象不是当前对象,//意味着entry.exit()与SphU.entry()不是成对出现的,//sentinel要求两者必须成对出现,而且要一一对应,否则抛出异常//Context有父子关系,这个在文章后面介绍if (context.getCurEntry() != this) {String curEntryNameInContext = context.getCurEntry() == null ? null: context.getCurEntry().getResourceWrapper().getName();// Clean previous call stack.CtEntry e = (CtEntry) context.getCurEntry();while (e != null) {e.exit(count, args);e = (CtEntry) e.parent;}String errorMessage = String.format("The order of entry exit can't be paired with the order of entry"+ ", current entry in context: <%s>, but expected: <%s>", curEntryNameInContext,resourceWrapper.getName());throw new ErrorEntryFreeException(errorMessage);} else {//在遍历每个slot的exit方法,每个slot清理和统计数据if (chain != null) {chain.exit(context, resourceWrapper, count, args);}//遍历exitHandlers,相当于回调,一般的DegradeSlot有回调,//DegradeSlot根据服务访问状态,决定是否将降级状态由HALF_OPEN变为OPENcallExitHandlersAndCleanUp(context);//设置为上一级Context对象context.setCurEntry(parent);if (parent != null) {((CtEntry) parent).child = null;}if (parent == null) {// Default context (auto entered) will be exited automatically.if (ContextUtil.isDefaultContext(context)) {ContextUtil.exit();}}//设置当前对象的this.context = nullclearEntryContext();}}}
Context
Context顾名思义上下文对象,这个对象是把一整个资源的访问过程都贯穿了,它保存在ThreadLocal里。创建Context是有多种方式的。
//name表示Context的名称或者链路入口的名称,origin表示调用来源的名称,默认为空字符串public static Context enter(String name, String origin);public static Context enter(String name);
不管是哪种方式,到最后面都是要调用ContextUtil.trueEnter方法
protected static Context trueEnter(String name, String origin) {//contextHolder是ThreadLocal<Context>类型Context context = contextHolder.get();if (context == null) {//contextNameNodeMap持有系统所有的入口节点Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;DefaultNode node = localCacheNameMap.get(name);if (node == null) {//sentinel最大只能支撑2000个入口节点,如果超过2000个,sentinel无法提供对资源的保护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 {//创建入口节点node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);//入口节点作为虚拟根节点的子节点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();}}}//创建Context对象,可以看到Context对象与入口节点一一对应context = new Context(node, name);//设置调用来源context.setOrigin(origin);contextHolder.set(context);}return context;}
在Context对象有一个名称与一个入口节点的对象,入口节点它就对于了线程访问的第一个资源,Context对象是对应了线程对资源的访问,一个线程就对应一个Context对象。每一个入口节点的对象它都是虚拟的根对象root的子节点。
//ROOT_ID=machine-rootpublic final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));
每一次调用SphU.entry方法会在访问的链路上添加一个子节点,经过这个树就可以还原出来资源的访问路径了。每访问一个资源,Context对象都会使用curEntry这个属性记录下正在访问的资源所对应的Entry对象。
