Sentinel源码—2.Context和处理链的初始化二
大纲
1.Sentinel底层的核心概念
2.Sentinel中Context的设计思想与源码实现
3.Java SPI机制的引入
4.Java SPI机制在Sentinel处理链中的应用
5.Sentinel默认处理链ProcessorSlot的构建
4.Java SPI机制在Sentinel处理链中的应用
(1)初始化Entry会初始化处理链
(2)初始化处理链的设计分析
(3)Sentinel初始化处理链的完整流程
(4)Sentinel初始化处理任链之加载SPI文件
(5)Sentinel初始化处理链之实例化Class
(1)初始化Entry会初始化处理链
初始化Entry时会调用两个与处理链相关的核心方法:一是调用CtSph的lookProcessChain()方法初始化处理链,二是调用ProcessorSlot的entry()方法执行处理链节点的逻辑。
public class CtSph implements Sph {...//Record statistics and perform rule checking for the given resource.//@param name the unique name for the protected resource//@param type the traffic type (inbound, outbound or internal).//This is used to mark whether it can be blocked when the system is unstable, only inbound traffic could be blocked by SystemRule//@param count the amount of calls within the invocation (e.g. batchCount=2 means request for 2 tokens)//@param args args for parameter flow control or customized slots@Overridepublic Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {//StringResourceWrapper是ResourceWrapper的子类,且StringResourceWrapper的构造方法默认了资源类型为COMMONStringResourceWrapper resource = new StringResourceWrapper(name, type);return entry(resource, count, args);}//Do all {@link Rule}s checking about the resource.public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {//调用CtSph.entryWithPriority()方法,执行如下处理://初始化Context -> 将Context与线程绑定 -> 初始化Entry -> 将Context和ResourceWrapper放入Entry中return entryWithPriority(resourceWrapper, count, false, args);}private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException {//从当前线程中获取ContextContext context = ContextUtil.getContext();if (context instanceof NullContext) {return new CtEntry(resourceWrapper, null, context);}//如果没获取到Contextif (context == null) {//Using default context.//创建一个名为sentinel_default_context的Context,并且与当前线程绑定context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);}//Global switch is close, no rule checking will do.if (!Constants.ON) {return new CtEntry(resourceWrapper, null, context);}//调用CtSph.lookProcessChain()方法初始化处理链(处理器插槽链条)ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);if (chain == null) {return new CtEntry(resourceWrapper, null, context);}//创建出一个Entry对象,将处理链(处理器插槽链条)、Context与Entry绑定//其中会将Entry的三个基础属性(封装在resourceWrapper里)以及当前Entry所属的Context作为参数传入CtEntry的构造方法Entry e = new CtEntry(resourceWrapper, chain, context);try {//处理链(处理器插槽链条)入口,负责采集数据,规则验证//调用DefaultProcessorSlotChain.entry()方法执行处理链每个节点的逻辑(数据采集+规则验证)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;}...private final static class InternalContextUtil extends ContextUtil {static Context internalEnter(String name) {//调用ContextUtil.trueEnter()方法创建一个Context对象return trueEnter(name, "");}static Context internalEnter(String name, String origin) {return trueEnter(name, origin);}}
}
(2)初始化处理链的设计分析
一.初始化处理链需要加锁确保线程安全
由于每个线程在执行CtSph的entry()方法创建一个Entry对象时,都需要首先调用CtSph的lookProcessChain()方法获取一个处理链,然后再根据这个处理链去初始化一个Entry对象,所以多个同样的Entry对象会共用一个处理链对象。
需要注意:即便是多个线程访问同样的资源(ResourceWrapper对象的属性一样),多个线程也会对应多个Entry对象(Entry对象之间的基本属性一样),多个线程也会对应多个Context对象(使用ThreadLocal存放Context对象),从而多个Entry对象会对应各自的Context对象。多个Entry对象会共用一个处理链对象(使用HashMap来缓存处理链对象),多个Context对象会共用一个Node对象(使用HashMap来缓存Node对象),多个Entry对象会共用一个Node对象(使用HashMap来缓存Node对象)。
因此当出现多线程并发创建多个Entry对象时,CtSph的lookProcessChain()方法自然就会被多线程并发调用。所以在初始化处理链时,需要考虑线程安全和性能问题。
为了确保线程安全,可以采用加锁的方式来初始化处理链,然后将处理链缓存到HashMap中来提高性能,如下伪代码所示:
//缓存处理链,key为资源,value为处理链
//多个线程创建多个Entry对象时,也会创建多个ResourceWrapper对象
//但这些ResourceWrapper对象,在同一个资源下,其属性是一样的
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap = new HashMap<ResourceWrapper, ProcessorSlotChain>();//加锁synchronized
synchronized ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {ProcessorSlotChain chain = chainMap.get(resourceWrapper);if (chain == null) {//构建(初始化)处理链(处理器插槽链条)chain = SlotChainProvider.newSlotChain();//将处理链(处理器插槽链条)放到缓存中chainMap.put(resourceWrapper, chain);}return chain;
}
二.初始化处理链通过SPI机制动态管理节点
初始化处理链时可使用List硬编码添加每个节点。
public class DefaultSlotChainBuilder implements SlotChainBuilder {@Overridepublic ProcessorSlotChain build() {//硬编码ProcessorSlotChain chain = new DefaultProcessorSlotChain();chain.addLast(new NodeSelectorSlot());chain.addLast(new ClusterBuilderSlot());chain.addLast(new StatisticSlot());chain.addLast(new AuthoritySlot());chain.addLast(new SystemSlot());chain.addLast(new FlowSlot());chain.addLast(new DegradeSlot());return chain;}
}
但硬编码的缺点是无法动态增减ProcessorSlot。比如不需要授权AuthoritySlot,硬编码时就不好删除了。比如内置的几个ProcessorSlot不符合业务需求,硬编码难以实现自定义。因此,需要使用SPI机制来解决硬编码带来的问题。也就是在配置文件中注册多个实现类,然后通过迭代器的方式逐个加载。
具体就是在META-INF/services目录下创建一个接口文件,然后将需要注册的实现类的全类名每行一个写入该文件中。框架启动时依次读取该文件中的实现类,并按照文件中注册的顺序加载。如果业务系统也创建了META-INF/services目录以及一个接口文件,那么业务系统的实现类并不会覆盖框架内置的实现类,而是叠加起来使用。
利用SPI机制,可以实现责任链模式的可插拔式扩展,处理链(处理器插槽链条)的每个节点都可以动态添加、删除、替换。
三.使用SPI机制的两大步骤
步骤一:创建META-INF/services目录,然后在该目录下创建一个文件名为如下的文件,文件内容为定义了处理链由哪些节点组成的类的类名全路径。
com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
步骤二:通过ServiceLoader初始化处理链节点。
public static void main(String[] args){ServiceLoader<SlotChainBuilder> serviceLoader = ServiceLoader.load(SlotChainBuilder.class);...
}
四.初始化处理链的设计要点总结
要点一:初始化处理链时加锁
要点二:使用HashMap缓存处理链
要点三:使用SPI机制初始化处理链
(3)Sentinel初始化处理链的完整流程
一.将处理链存储在全局缓存 + 使用锁初始化处理链
如果对CtSph的lookProcessChain()方法加锁,则锁的粒度过大。所以可以在操作缓存处理链的HashMap时才加synchronized锁,而且在操作缓存处理链的HashMap时,使用了Double Check + 写时复制。
二.利用SPI机制完成处理链的初始化
为了增加扩展性,Sentinel初始化处理链时使用了SPI机制两次。第一次使用SPI机制,是为了可以自定义处理链的节点编排。第二次使用SPI机制,是为了可以自定义处理链各节点的具体逻辑。
public class CtSph implements Sph {//Same resource will share the same ProcessorSlotChain}, no matter in which Context.//Same resource is that ResourceWrapper#equals(Object).private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap = new HashMap<ResourceWrapper, ProcessorSlotChain>();private static final Object LOCK = new Object();...//Get ProcessorSlotChain of the resource.//new ProcessorSlotChain will be created if the resource doesn't relate one.//Same resource will share the same ProcessorSlotChain globally, no matter in which Context.//Same resource is that ResourceWrapper#equals(Object).//Note that total ProcessorSlot count must not exceed Constants.MAX_SLOT_CHAIN_SIZE, otherwise null will return.ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {ProcessorSlotChain chain = chainMap.get(resourceWrapper);if (chain == null) {//操作chainMap时才加锁synchronized (LOCK) {chain = chainMap.get(resourceWrapper);if (chain == null) {//Double Check//Entry size limit.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;}...
}//A provider for creating slot chains via resolved slot chain builder SPI.
public final class SlotChainProvider {private static volatile SlotChainBuilder slotChainBuilder = null;//The load and pick process is not thread-safe,//but it's okay since the method should be only invoked via CtSph.lookProcessChain() under lock.public static ProcessorSlotChain newSlotChain() {//如果存在,则直接返回if (slotChainBuilder != null) {return slotChainBuilder.build();}//Resolve the slot chain builder SPI.//第一次使用SPI: 通过SPI机制初始化SlotChainBuilderslotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();if (slotChainBuilder == null) {//Should not go through here.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();}private SlotChainProvider() {}
}public final class SpiLoader<S> {//Cache the SpiLoader instances, key: classname of Service, value: SpiLoader instanceprivate static final ConcurrentHashMap<String, SpiLoader> SPI_LOADER_MAP = new ConcurrentHashMap<>();//Cache the classes of Providerprivate final List<Class<? extends S>> classList = Collections.synchronizedList(new ArrayList<Class<? extends S>>());//Cache the sorted classes of Providerprivate final List<Class<? extends S>> sortedClassList = Collections.synchronizedList(new ArrayList<Class<? extends S>>());...//Create SpiLoader instance via Service class Cached by className, and load from cache firstpublic static <T> SpiLoader<T> of(Class<T> service) {AssertUtil.notNull(service, "SPI class cannot be null");AssertUtil.isTrue(service.isInterface() || Modifier.isAbstract(service.getModifiers()), "SPI class[" + service.getName() + "] must be interface or abstract class");String className = service.getName();SpiLoader<T> spiLoader = SPI_LOADER_MAP.get(className);if (spiLoader == null) {synchronized (SpiLoader.class) {spiLoader = SPI_LOADER_MAP.get(className);if (spiLoader == null) {//Double CheckSPI_LOADER_MAP.putIfAbsent(className, new SpiLoader<>(service));spiLoader = SPI_LOADER_MAP.get(className);}}}return spiLoader;}//Load the first-found Provider instance,if not found, return default Provider instancepublic S loadFirstInstanceOrDefault() {//SPI机制加载Class,然后将加载的Class放到classList数组里load();//循环遍历,根据classList里的Class来初始化对应的实例for (Class<? extends S> clazz : classList) {if (defaultClass == null || clazz != defaultClass) {return createInstance(clazz);}}//初始化默认的DefaultSlotChainBuilderreturn loadDefaultInstance();}//Load all Provider instances of the specified Service, sorted by order value in class's {@link Spi} annotationpublic List<S> loadInstanceListSorted() {//比如读取com.alibaba.csp.sentinel.slotchain.ProcessorSlot文件里的Class名字//然后根据这些Class名字加载Class//接着将Class放到sortedClassList集合中load();//实例化sortedClassList集合里的每个Classreturn createInstanceList(sortedClassList);}...
}//Builder for a default {@link ProcessorSlotChain}.
@Spi(isDefault = true)
public class DefaultSlotChainBuilder implements SlotChainBuilder {@Overridepublic ProcessorSlotChain build() {ProcessorSlotChain chain = new DefaultProcessorSlotChain();//通过SPI机制加载责任链的节点ProcessorSlot实现类//然后按照@Spi注解的order属性进行排序并进行实例化//最后将ProcessorSlot实例放到sortedSlotList中List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();//遍历已排好序的ProcessorSlot集合for (ProcessorSlot slot : sortedSlotList) {//安全检查,防止业务系统也写了一个SPI文件,但没按规定继承AbstractLinkedProcessorSlotif (!(slot instanceof AbstractLinkedProcessorSlot)) {RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");continue;}//调用DefaultProcessorSlotChain.addLast()方法构建单向链表//将责任链的节点ProcessorSlot实例放入DefaultProcessorSlotChain中chain.addLast((AbstractLinkedProcessorSlot<?>) slot);}//返回单向链表return chain;}
}
第一次使用SPI机制是初始化SlotChainBuilder,在com.alibaba.csp.sentinel.slotchain.SlotChainBuilder文件中,框架默认的是com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder。
在SpiLoader的loadFirstInstanceOrDefault()方法中,根据"if (defaultClass == null || clazz != defaultClass)"可知,系统接入Sentinel时可以自定义SPI接口文件来替换DefaultSlotChainBuilder。
比如想删除AuthoritySlot,那么可以在META-INF/services目录下,创建文件名如下,文件值为自定义接口的实现类全路径,如下:
文件名:com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
文件值:com.example.MyCustomSlotChainBuilder
然后在MyCustomSlotChainBuilder中就可以自定义一套处理链的规则。
public class MyCustomSlotChainBuilder implements SlotChainBuilder {@Overridepublic ProcessorSlotChain build() {ProcessorSlotChain chain = new DefaultProcessorSlotChain();chain.addLast(new NodeSelectorSlot());chain.addLast(new ClusterBuilderSlot());chain.addLast(new StatisticSlot());chain.addLast(new FlowSlot());return chain;}
}
第二次使用SPI机制是执行DefaultSlotChainBuilder的build()方法初始化处理链,其中的核心代码是SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted()。所调用的方法会读取com.alibaba.csp.sentinel.slotchain.ProcessorSlot文件内容,然后根据文件内容加载Class,接着将Class放到sortedClassList集合中,最后实例化sortedClassList集合中的Class并返回ProcessorSlot实例列表。
注意:ProcessorSlot实现类是有序的,即处理链的节点是有序的。比如ClusterBuilderSlot的前一个节点必须是NodeSelectorSlot,StatisticSlot的前一个节点必须是ClusterBuilderSlot等。
(4)Sentinel初始化处理链之加载SPI文件
Sentinel初始化处理链时会先后调用如下两个方法,这两个方法都会调用SpiLoader的load()方法来加载SPI文件。
SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault()
SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted()
SpiLoader的load()方法的执行过程就是加载SPI文件的过程。但是由于SPI文件可能会有很多个,比如负责管控整个处理链的Builder对应的SPI文件:
com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
以及处理链节点的ProcessorSlot对应的SPI文件:
com.alibaba.csp.sentinel.slotchain.ProcessorSlot
虽然它们都隶属于META-INF/services下,但其文件名是不一样的。所以按理应在load()方法添加一个参数如:load(String fileName)。但是Sentinel却没这么做,因为Sentinel使用配置去替代参数。
具体做法就是:先在SpiLoader内定义常量SPI_FILE_PREFIX = "META-INF/services/"。有了SPI文件夹后,还需接口全路径名为名称的文件,就可读取文件了。
于是SpiLoader提供了一个静态的of()方法,来指定要加载那一类接口。也就是SpiLoader的of()方法会返回一个指定加载某种接口的SpiLoader实例。
然后在执行指定加载某一类接口的SpiLoader实例的load()方法时,就能将指定要加载的接口的名称拼接上SPI_FILE_PREFIX文件夹前缀,得到一份完整的文件路径,接着就可通过IO流读取文件里的内容。
从文件中获取到指定加载的某一类接口的实现类类名之后,就可以通过Class.forName() + 线程上下文类加载器去加载对应的实现类,加载到的实现类会放入到classList和sortedClassList两个列表中。
注意:Sentinel的SPI机制没有使用JDK内置的ServiceLoader,而是自己实现。因为Sentinel的SPI有一些定制化逻辑,比如@Spi注解的order属性可以指定实例化类时的顺序。但本质上和JDK内置的ServiceLoader一致,只是多了个性化的逻辑。
public final class SpiLoader<S> {//Default path for the folder of Provider configuration file//SPI文件夹路径private static final String SPI_FILE_PREFIX = "META-INF/services/";//Cache the SpiLoader instances, key: classname of Service, value: SpiLoader instance//每个接口Class对应的SpiLoader,只需new一次即可,new完就可以将其缓存起来private static final ConcurrentHashMap<String, SpiLoader> SPI_LOADER_MAP = new ConcurrentHashMap<>();//Cache the classes of Providerprivate final List<Class<? extends S>> classList = Collections.synchronizedList(new ArrayList<Class<? extends S>>());//Cache the sorted classes of Providerprivate final List<Class<? extends S>> sortedClassList = Collections.synchronizedList(new ArrayList<Class<? extends S>>());//The Service class, must be interface or abstract class//指定要使用SPI进行加载的实现类的接口的Class,比如SlotChainBuilder.class、ProcessorSlot.classprivate Class<S> service;...private SpiLoader(Class<S> service) {this.service = service;}//Create SpiLoader instance via Service class//Cached by className, and load from cache first//@param service Service class 指定要使用SPI加载的实现类的接口的Classpublic static <T> SpiLoader<T> of(Class<T> service) {//判断是不是nullAssertUtil.notNull(service, "SPI class cannot be null");//判断是不是interface类型AssertUtil.isTrue(service.isInterface() || Modifier.isAbstract(service.getModifiers()), "SPI class[" + service.getName() + "] must be interface or abstract class");//获取接口的全路径名,判断缓存里是否已经存在String className = service.getName();SpiLoader<T> spiLoader = SPI_LOADER_MAP.get(className);//缓存里没有,则使用Double Check + 锁机制去初始化SpiLoader,然后将初始化好的SpiLoader实例放到缓存if (spiLoader == null) {synchronized (SpiLoader.class) {spiLoader = SPI_LOADER_MAP.get(className);if (spiLoader == null) {//new SpiLoader<>(service)初始化SpiLoader实例SPI_LOADER_MAP.putIfAbsent(className, new SpiLoader<>(service));spiLoader = SPI_LOADER_MAP.get(className);}}}//返回SpiLoader实例return spiLoader;}//Load the first-found Provider instance,if not found, return default Provider instancepublic S loadFirstInstanceOrDefault() {//SPI机制加载Class,然后将加载的Class放到classList数组里load();//循环遍历,根据classList里的Class来初始化对应的实例for (Class<? extends S> clazz : classList) {if (defaultClass == null || clazz != defaultClass) {return createInstance(clazz);}}//初始化默认的DefaultSlotChainBuilderreturn loadDefaultInstance();}//Load all Provider instances of the specified Service, sorted by order value in class's {@link Spi} annotationpublic List<S> loadInstanceListSorted() {//比如读取com.alibaba.csp.sentinel.slotchain.ProcessorSlot文件里的Class名字//然后根据这些Class名字加载Class//接着将Class放到sortedClassList集合中load();//实例化sortedClassList集合里的每个Classreturn createInstanceList(sortedClassList);}//Load the Provider class from Provider configuration filepublic void load() {...//IO流读取文件内容while (urls.hasMoreElements()) {...clazz = (Class<S>) Class.forName(line, false, classLoader);//加入到集合中classList.add(clazz);...}...//生成新的按照order排序的集合sortedClassList.addAll(classList);//进行排序Collections.sort(sortedClassList, new Comparator<Class<? extends S>>() {@Overridepublic int compare(Class<? extends S> o1, Class<? extends S> o2) {//获取Spi注解Spi spi1 = o1.getAnnotation(Spi.class);//获取Spi注解的order属性int order1 = spi1 == null ? 0 : spi1.order();Spi spi2 = o2.getAnnotation(Spi.class);int order2 = spi2 == null ? 0 : spi2.order();return Integer.compare(order1, order2);}});}...
}
(5)Sentinel初始化处理链之实例化Class
执行完SpiLoader的load()方法加载好指定的实现类的Class后,就会调用SpiLoader的createInstance()方法或createInstanceList()方法实例化Class。
SpiLoader放入createInstance(clazz)方法内部会判断是否是单例,如果是单例则放到缓存当中,如果不是单例则每次都通过clazz.newInstance()方法创建一个新的对象。
public final class SpiLoader<S> {//Cache the singleton instance of Provider, key: classname of Provider, value: Provider instanceprivate final ConcurrentHashMap<String, S> singletonMap = new ConcurrentHashMap<>();...//Create Provider instance listprivate List<S> createInstanceList(List<Class<? extends S>> clazzList) {if (clazzList == null || clazzList.size() == 0) {return Collections.emptyList();}List<S> instances = new ArrayList<>(clazzList.size());for (Class<? extends S> clazz : clazzList) {S instance = createInstance(clazz);instances.add(instance);}return instances;}//Create Provider instanceprivate S createInstance(Class<? extends S> clazz) {Spi spi = clazz.getAnnotation(Spi.class);boolean singleton = true;if (spi != null) {singleton = spi.isSingleton();}return createInstance(clazz, singleton);}//Create Provider instanceprivate S createInstance(Class<? extends S> clazz, boolean singleton) {S instance = null;try {if (singleton) {instance = singletonMap.get(clazz.getName());if (instance == null) {synchronized (this) {instance = singletonMap.get(clazz.getName());if (instance == null) {instance = service.cast(clazz.newInstance());singletonMap.put(clazz.getName(), instance);}}}} else {instance = service.cast(clazz.newInstance());}} catch (Throwable e) {fail(clazz.getName() + " could not be instantiated");}return instance;}...
}
(6)总结
一.初始化处理链时需要加锁和使用缓存
初始化一个Entry对象时需要根据一个处理链对象进行初始化,并发请求同一接口时的多个同样的Entry对象会共用一个处理链对象,所以初始化处理链的过程中需要加锁来保证线程安全性,初始化完处理链后需要将其放到全局缓存里来提高性能。
二.初始化处理链时分两步使用SPI
首先初始化Builder,负责管控处理链整体即编排处理链节点。Sentinel要求Builder只能存在一个,而且是外部系统优先原则。可以在com.alibaba.csp.sentinel.slotchain.SlotChainBuilder文件中,指定替代默认的DefaultSlotChainBuilder的自定义接口实现类。
然后通过Builder初始化完整的处理链,这里也是通过SPI机制进行初始化。因此可以进行扩展,比如外部系统自定义ProcessorSlot添加到处理链。注意ProcessorSlot是有顺序的,如果顺序没指定正确,则可能造成异常。ProcessorSlot的顺序可以通过@Spi注解的order属性设置。为了防止出现bug,建议直接将自定义的ProcessorSlot放到最后。
三.Sentinel通过配置的方式来替代传入参数
SpiLoader的of()方法会返回一个指定加载某种实现类的SpiLoader实例,这样就可以使用类似SpiLoader.of().load()的方式通过SPI进行加载。
四.Sentinel没有采用JDK内置的ServiceLoader来实现SPI机制
Sentinel单独写一套SPI的实现逻辑,核心原因是需要支持个性化的配置。比如@Spi注解支持order排序属性以及isSingleton是否单例属性。如果是单例,则每个类全局只能实例化一次(通过Map缓存实现)。如果不是单例,则每次都new一个新的对象。
五.Sentinel的这套SPI机制可以当作工具类拷贝到业务系统中
因为没有其他外部依赖,它被单独放到一个包里。
5.Sentinel默认处理链ProcessorSlot的构建
(1)Sentinel默认处理链的节点
(2)Sentinel默认处理链的构建
(1)Sentinel默认处理链的节点
Sentinel中的ProcessorSlot是流量控制处理过程中的处理器,每个ProcessorSlot处理器子类负责处理特定的任务。Sentinel默认的处理链会基于SPI配置在sentinel-core模块的如下文件中:
# Sentinel-1.8.6/sentinel-core/src/main/resources/META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot
# Sentinel default ProcessorSlots
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
一共8个Slot,每个Slot都是一个过滤器,各自承担不同的职责。这些Slot整体可以划分为两类:指标数据采集的Slot和规则验证的Slot。其中规则验证的Slot包括:授权验证、流控验证、熔断降级验证。
每个Slot都有自己在整个处理链(处理器插槽链条)中的顺序,具体的顺序(也就是实例化顺序)并不是根据SPI文件的编写顺序来确定的,而是基于修饰每个Slot的@Spi注解来确定的。
@Spi注解有一个order属性用于设置顺序。SpiLoader的load()方法在读取SPI文件时,会按order属性对Slot进行排序,并将排好序的ProcessorSlot实现类放入sortedClassList中。这样后续就可以遍历sortedClassList,按照顺序实例化这些Slot的实现类。
一.NodeSelectorSlot
负责创建和维护资源调用树(资源调用关系),同时为资源访问对象Entry关联对应的统计节点DefaultNode。这样不仅可实现对资源调用链路的监控,还能统计每个资源的调用信息。
二.ClusterBuilderSlot
负责根据资源的统计信息,计算集群维度的统计数据。集群维度统计数据是从资源维度的统计数据中整合得到的,这些统计数据用于实现资源在集群中的流量控制、熔断降级等功能。
三.LogSlot
负责记录请求异常时的日志,可用于故障排查。
四.StatisticsSlot
负责统计资源的调用数据,如成功调用次数、异常次数、响应时间等。这些数据可用于分析资源的性能,或驱动其他Slot(如限流降级Slot)运行。
五.AuthoritySlot
负责进行授权控制,根据资源的授权规则来判断是否允许请求进行访问。如果请求不被允许访问,AuthoritySlot将抛出AuthorityException异常。
六.SystemSlot
负责进行系统保护,根据系统保护规则(如CPU使用率、负载等)判断请求是否需要被限制。如果需要被限制,SystemSlot将抛出SystemException异常。
七.FlowSlot
负责进行流量控制,根据资源的流量控制规则(如QPS限制等)来判断请求是否需要被限流。如果需要被限流,FlowSlot将抛出一个FlowException异常。
八.DegradeSlot
负责进行熔断降级,根据资源的熔断降级规则(如异常比例等)来判断请求是否需要被降级。如果需要被降级,DegradeSlot将抛出一个DegradeException异常。
(2)Sentinel默认处理链的构建
处理链(处理器插槽链条)ProcessorSlotChain其实就是一条责任链,由于责任链是典型的单向链表结构,所以每个Slot必须要有一个next属性,用于指向下一个节点。
为了实现单向链表结构:可以利用已排好序的Slot列表以及每个Slot都有的next引用的特性。只需遍历已排序的Slot列表,让每个Slot的next引用指向下一个Slot即可。这是一个非常简单的单向链表操作,可将这个过程封装到DefaultProcessorSlotChain类中的addLast()方法中。
//Builder for a default {@link ProcessorSlotChain}.
@Spi(isDefault = true)
public class DefaultSlotChainBuilder implements SlotChainBuilder {@Overridepublic ProcessorSlotChain build() {//创建一个DefaultProcessorSlotChain对象实例ProcessorSlotChain chain = new DefaultProcessorSlotChain();//通过SPI机制加载责任链的节点ProcessorSlot实现类//然后按照@Spi注解的order属性进行排序并进行实例化//最后将ProcessorSlot实例放到sortedSlotList中List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();//遍历已排好序的ProcessorSlot列表for (ProcessorSlot slot : sortedSlotList) {//安全检查,防止业务系统也写了一个SPI文件,但没按规定继承AbstractLinkedProcessorSlotif (!(slot instanceof AbstractLinkedProcessorSlot)) {RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");continue;}//调用DefaultProcessorSlotChain.addLast()方法构建单向链表//将责任链的节点ProcessorSlot实例放入DefaultProcessorSlotChain中chain.addLast((AbstractLinkedProcessorSlot<?>) slot);}//返回单向链表return chain;}
}public class DefaultProcessorSlotChain extends ProcessorSlotChain {AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable {super.fireEntry(context, resourceWrapper, t, count, prioritized, args);}@Overridepublic void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {super.fireExit(context, resourceWrapper, count, args);}};AbstractLinkedProcessorSlot<?> end = first;@Overridepublic void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {protocolProcessor.setNext(first.getNext());first.setNext(protocolProcessor);if (end == first) {end = protocolProcessor;}}@Overridepublic void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {end.setNext(protocolProcessor);end = protocolProcessor;}@Overridepublic void setNext(AbstractLinkedProcessorSlot<?> next) {addLast(next);}@Overridepublic AbstractLinkedProcessorSlot<?> getNext() {return first.getNext();}@Overridepublic void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable {first.transformEntry(context, resourceWrapper, t, count, prioritized, args);}@Overridepublic void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {first.exit(context, resourceWrapper, count, args);}
}public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {private AbstractLinkedProcessorSlot<?> next = null;@Overridepublic void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable {if (next != null) {next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);}}@SuppressWarnings("unchecked")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);}@Overridepublic void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {if (next != null) {next.exit(context, resourceWrapper, count, args);}}public AbstractLinkedProcessorSlot<?> getNext() {return next;}public void setNext(AbstractLinkedProcessorSlot<?> next) {this.next = next;}
}
相关文章:
Sentinel源码—2.Context和处理链的初始化二
大纲 1.Sentinel底层的核心概念 2.Sentinel中Context的设计思想与源码实现 3.Java SPI机制的引入 4.Java SPI机制在Sentinel处理链中的应用 5.Sentinel默认处理链ProcessorSlot的构建 4.Java SPI机制在Sentinel处理链中的应用 (1)初始化Entry会初始化处理链 (2)初始化处…...
Java基础第20天-JDBC
JDBC为访问不同的数据库提供了统一的接口,为使用者屏蔽了细节问题,程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统,从而完成对数据库的各种操作 ResultSet 表示数据库结果集的数据表,通常通过执行查询数据库的语句生…...
VMware下Ubuntu空间扩容
目的: Ubuntu空间剩余不足,需要对Ubuntu进行扩容。 使用工具: 使用Ubuntu系统中的gparted工具进行系统扩容。 前提: 1、电脑有多余的未分配磁盘空间,比如我的Ubuntu磁盘G盘是200G,现在快满了,…...
第十一章 网络编程
在TCP/IP协议中,“IP地址TCP或UDP端口号”唯一标识网络通讯中的一个进程。 因此可以用Socket来描述网络连接的一对一关系。 常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)…...
Bad Request 400
之前一直以为400就是前端代码有问题 这下遇到了,发现是因为前后端不一致 后端代码注意:现在我写的int 前端请求 原因 :前后端不一致 💡 问题核心:后端 amount 类型是 int,但前端传了小数 237.31...
行业深度:金融数据治理中的 SQL2API 应用创新
金融行业作为数据密集型领域,面临着监管合规要求严苛、数据交互频次高、安全风险防控难度大等多重挑战。SQL2API 技术通过 “数据服务化 合规化” 的双重赋能,成为金融机构破解数据治理难题的核心工具,在多个关键场景实现突破性创新。 &…...
记录学习的第二十六天
还是每日一题。 今天这道题有点难度,我看着题解抄的。 之后做了两道双指针问题。 这道题本来是想用纯暴力做的,结果出错了。😓...
MySQLQ_数据库约束
目录 什么是数据库约束约束类型NOT NULL 非空约束UNIQUE 唯一约束PRIMARY KEY主键约束FOREIGN KEY外键约束CHECK约束DEFAULT 默认值(缺省)约束 什么是数据库约束 数据库约束就是对数据库添加一些规则,使数据更准确,关联性更强 比如加了唯一值约束&#…...
数据库ocp证书是什么水平
专业知识与技能:OCP 证书是对持证人在 Oracle 数据库管理、安装、配置、性能调优、备份恢复等方面专业知识和技能的权威认证。它要求考生通过一系列严格的考试,包括理论知识和实际操作能力的考核,以证明其具备扎实的 Oracle 数据库专业知识和…...
1022 Digital Library
1022 Digital Library 分数 30 全屏浏览 切换布局 作者 CHEN, Yue 单位 浙江大学 A Digital Library contains millions of books, stored according to their titles, authors, key words of their abstracts, publishers, and published years. Each book is assigned an u…...
基于Python的PC控制Robot 小程序开发历程
1、Background:用万能语言Python进行Robot 的控制一直以来是我想做的事,刚好有机会付诸实践。Just Do It~ 2、Python 代码编写: import socket import time HOST "192.168.0.1" #IP PORT 2008 #Por…...
Coze平台技术解析:零代码AI开发与智能体应用实践
【资源软件】 伏脂撺掇蒌葶苘洞座 /835a36NvQn😕 链接:https://pan.quark.cn/s/5180c62aacf7 「微信被删好友检测工具」筷莱坌教狴犴狾夺郝 链接:https://pan.quark.cn/s/fe4976448ca1 HitPaw Watermark Remover 链接:https://pan…...
在 K8s 上构建和部署容器化应用程序(Building and Deploying Containerized Applications on k8s)
在 Kubernetes 上构建和部署容器化应用程序 Kubernetes 是一个用于管理容器化工作负载和服务的开源平台。它提供了一个强大的框架来自动化部署、扩展和管理容器化应用程序。本博客将指导您完成在 Kubernetes 上构建和部署容器化应用程序的过程,重点介绍技术方面并使…...
【教程】如何使用Labelimg查看已经标注好的YOLO数据集标注情况
《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...
**Windows 系统**的常用快捷键大全
以下是 Windows 系统的常用快捷键大全,涵盖日常操作、文件管理、窗口控制、系统功能等,助你大幅提升效率: 一、基础系统操作 Win:打开/关闭「开始菜单」Win E:打开「文件资源管理器」Win D:一键显示桌面…...
L1-025 正整数A+B
L1-025 正整数AB L1-025 正整数AB - 团体程序设计天梯赛-练习集 (pintia.cn) 题解 第一次做这道题时,没有注意到num1 和 num2 是在区间 [1, 1000] 内,num1和num2的长度应该是4位数并且num1和num2不能等于0,num1和num2不能大于1000。这两个…...
Go 语言的 map 在解决哈希冲突时,主要使用了链地址法同时参考了开放地址法的思想即每个桶的 8个 key val对是连续的
总结一下 Go map 的哈希冲突解决机制。 1. 哈希表结构: Go 语言的 map 底层有两个主要结构:hmap 和 bmap,它们分别负责管理整个 map 的元数据和存储键值对的桶。 hmap:包含 map 的元数据,如桶的数量、已插入的键值对…...
未支付订单如何释放库存
在电商或交易系统中,处理未支付订单的库存释放是典型的高并发场景问题。以下是结合 Java 技术栈的完整解决方案,涵盖 设计思路、技术实现、容错机制,并基于实际项目经验(如标易行平台的标书资源预约场景)进行分析: 一、核心设计原则 最终一致性:确保库存释放与订单状态的…...
HDFS Full Block Report超限导致性能下降的原因分析
文章目录 前言发现问题失败的为什么是FBR块汇报频率的变化为什么FBR会反复失败HDFS性能下降导致Yarn负载变高的形式化分析理解线程理解IO Wait理解HDFS性能下降导致Yarn负载和使用率增高 引用 前言 我们的Yarn Cluster主要用来运行一批由Airflow定时调度的Spark Job࿰…...
[Java实战经验]链式编程与Builder模式
目录 链式编程Builder模式 链式编程 链式编程(Fluent AP)是一种编程风格,它通过在同一个对象上连续调用多个方法来执行一系列操作(让方法返回对象本身(return this))。这种风格的编程使代码更加…...
TypeScript 快速上手--禹神
TypeScript 快速上手 🪩 禹神:三小时快速上手TypeScript,TS速通教程_哔哩哔哩_bilibili ⼀、TypeScript 简介 TypeScript 由微软开发,是基于 JavaScript 的⼀个扩展语⾔。 TypeScript 包含了 JavaScript 的所有内容,即: TypeScript 是 Jav…...
YOLOv2 快速入门与核心概念:更快、更准的目标检测利器
今天,我们就来聊聊 YOLO 系列的第二代—— YOLOv2,看看它是如何在速度的基础上,进一步提升检测精度的。 目标检测的重要性:让机器“看懂”世界 想象一下,自动驾驶汽车需要实时识别道路上的车辆、行人、交通标志&…...
Differentiable Micro-Mesh Construction 论文阅读
信息 2024 CVPR 论文地址 摘要 本文提出了一个可微分框架,用于将标准网格转换为Micro-mesh( μ \mu μ-mesh)这种非常高效的格式,与以前基于阶段的方法相比,提供了一个整体方案。 本文的框架为高质量的 μ \mu μ 网格生产提供了许多优势&…...
groovy运行poi包处理xlsx文件报NoClassDefFoundError
背景:简单的在java上运行poi包处理xlsx文件,正常解析。使用groovy执行相关xlsx文件解析的程序时,报错。报错日志: java.lang.NoClassDefFoundError: org/openxmlformats/schemas/spreadsheetml/x2006/main/CTExtensionList poi版…...
基于Espressif-IDE的esp32开发
日后填坑 新建工程 基本操作 创建一个工程 编译工程 下载程序 运行成功...
emotn ui桌面软件tv版下载安装教程-emotn ui桌面好用吗
在智能电视和电视盒子的使用场景中,一款出色的桌面软件能显著提升用户体验。Emotn UI桌面软件TV版就是这样一款备受关注的产品,与此同时,乐看家桌面也以其独特功能在市场中占据一席之地。接下来,我们将会详细介绍Emotn UI桌面软件…...
抖音ai无人直播间助手场控软件
获取API权限 若使用DeepSeek官方AI服务,登录其开发者平台申请API Key或Token。 若为第三方AI(如ChatGPT),需通过接口文档获取访问权限。 配置场控软件 打开DeepSeek场控软件,进入设置界面找到“AI助手”或“自动化”…...
机器学习中的距离度量与优化方法:从曼哈顿距离到梯度下降
目录 前言一、曼哈顿距离(Manhattan Distance):二、切比雪夫距离 (Chebyshev Distance):三、 闵可夫斯基距离(Minkowski Distance):小结四、余弦距离(Cosine Distance)五、杰卡德距离(Jaccard Distance)六、交叉验证方法6.1 HoldOut Cross-v…...
在GitHub action中使用添加项目中配置文件的值为环境变量
比如我项目的根目录有一个package.json文件,但是我想在工作流中使用某个值,例如使用version的值,就需要从package.json里面取出来,然后存储到环境变量中,供后续步骤使用这个值。 读值存储 读取项目根目录中的某个jso…...
MCP认证难题破解指南
一、MCP 认证体系与核心挑战 1.1 认证体系解析 MCP(Microsoft Certified Professional)作为微软认证体系的基础,覆盖操作系统、云服务、开发工具等核心领域。2025 年最新认证体系包含以下关键方向: Azure 云服务: 覆盖 Azure 虚拟机、容器化部署、云原生应用开发等核心技…...
STM32F407实现内部FLASH的读写功能
文章目录 前言一、FLASH 简介二、读数据三、写数据四、最终效果五、完整工程 前言 我们通过仿真器下到芯片的程序一般会保存到eflash里面,在我们的STM32F407里面这里的空间挺大的,我所使用的芯片型号是STM32F407ZGT6,FLASH 容量为 1024K 字节…...
沃尔玛墨西哥30分钟极速配送:即时零售战争中的「超导革命」
当全球电商还在争夺「次日达」话语权时,沃尔玛在墨西哥城投下「时空压缩弹」——30分钟极速配送服务上线首周,订单转化率较常规渠道提升270%,生鲜品类客单价突破87美元(墨西哥财政部2024Q2数据)。这场物流革命正在改写…...
html页面打开后中文乱码
在HTML页面中遇到中文乱码通常是因为字符编码设置不正确或者不一致。要解决这个问题,你可以按照以下步骤进行: 确认HTML文件的编码 确保你的HTML文件保存时使用的是UTF-8编码。大多数现代的文本编辑器和IDE(如Visual Studio Code, Sublime Te…...
MySQL中的公用表表达式CTE实战案例应用
很多同学不会使用MySQL中的公用表表达式,今天这篇文章详细为大家介绍一下。 基础概念: 公用表表达式:MySQL中的公用表表达式(Common Table Expressions,简称CTE)是一种临时的结果集,它可以在一…...
开源TTS项目GPT-SoVITS,支持跨语言合成、支持多语言~
简介 GPT-SoVITS 是一个开源的文本转语音(TTS)项目,旨在通过少量语音数据实现高质量的语音合成。其核心理念是将基于变换器的模型(如 GPT)与语音合成技术(如 SoVITS,可能指“唱歌语音合成”&am…...
从零开始:Python运行环境之VSCode与Anaconda安装配置全攻略 (1)
从零开始:Python 运行环境之 VSCode 与 Anaconda 安装配置全攻略 在当今数字化时代,Python 作为一种功能强大且易于学习的编程语言,被广泛应用于数据科学、人工智能、Web 开发等众多领域。为了顺利开启 Python 编程之旅,搭建一个稳…...
3月报|DolphinScheduler项目进展一览
各位热爱 Apache DolphinScheduler 的小伙伴们,社区3月报来啦!来查看上个月项目的进展吧! 月度Merge Star 感谢以下小伙伴上个月为 Apache DolphinScheduler 所做的精彩贡献(排名不分先后): “ruanwenj…...
Flutter 应用在真机上调试的流程
在真机上调试 Flutter 应用的方法 调试 Flutter 应用有多种方式,可以使用 USB 数据线连接设备到电脑进行调试,也可以通过无线方式进行真机调试。对于 iOS 开发者,使用 appuploader 这样的工具可以更高效地管理开发流程。 1. 有线调试 设备…...
QML TableView:基础用法和自定义样式实现
目录 引言相关阅读工程结构示例一:基础TableView实现代码解析运行效果 示例二:自定义样式TableView代码解析运行效果 主界面运行效果 总结工程下载 引言 TableView作为Qt Quick中的一个核心控件,具有高性能、灵活性强的特点,能够…...
实战指南:封装Whisper为FastAPI接口并实现高并发处理-附整合包
实战指南:封装Whisper为FastAPI接口并实现高并发处理 下面给出一个详细的示例,说明如何使用 FastAPI 封装 OpenAI 的 Whisper 模型,提供一个对外的 REST API 接口,并支持一定的并发请求。 下面是主要步骤和示例代码。 1. 环境准备…...
算法——通俗讲解升幂定理
一、生活比喻:台阶与放大镜 想象你有一盏灯,光线穿过一层玻璃(基础台阶),每层玻璃会过滤掉一定颜色的光(质数 ( p ))。升幂定理就像在灯前叠加放大镜(指数 ( n ))&#…...
DeepSpeed ZeRO++:降低4倍网络通信,显著提高大模型及类ChatGPT模型训练效率
图1: DeepSpeed ZeRO 简介 大型 AI 模型正在改变数字世界。基于大型语言模型 (LLM)的 Turing-NLG、ChatGPT 和 GPT-4 等生成语言模型用途广泛,能够执行摘要、代码生成和翻译等任务。 同样,DALLE、Microsoft Designer 和 Bing Image Creator 等大型多模…...
【Audio开发四】音频audio中underrun和overrun原因详解和解决方案
一,underrun & overrun定义 我们知道,在Audio模块中数据采用的是生产者-消费者模式,生产者负责生产数据,消费者用于消费数据,针对AudioTrack和AudioRecord,其对应的角色不同; AudioTrack …...
[CMake] vcpkg的使用方法
C第三方库管理工具vcpkg使用教程。 如果要在vscode当中使用 1. 使用 CMakePresets.txt 来配置configure时的参数 2. 设置如下 即可正常编译...
光纤模块全解:深入了解XFP、SFP、QSFP28等类型
随着信息技术的快速发展,数据中心和网络的带宽需求不断提高,光纤模块的选择与应用显得尤为重要。光纤模块是实现高速网络连接的重要组件,选择合适的模块能够显著提升传输性能、降低延迟。本文将深入解析几种常见的光纤模块类型,包…...
【数据结构】之散列
一、定义与基本术语 (一)、定义 散列(Hash)是一种将键(key)通过散列函数映射到一个固定大小的数组中的技术,因为键值对的映射关系,散列表可以实现快速的插入、删除和查找操作。在这…...
利用pnpm patch命令实现依赖包热更新:精准打补丁指南
需求场景 在Element Plus的el-table组件二次开发中,需新增列显示/隐藏控件功能。直接修改node_modules源码存在两大痛点: 团队协作时修改无法同步 依赖更新导致自定义代码丢失 解决方案选型 通过patch-package工具实现: 📦 非…...
pve常用命令
pve常用命令 虚拟机管理容器管理集群管理存储与磁盘管理网络相关备份/还原手动备份计划任务备份(Web 界面常用)还原备份 快照创建快照查看快照恢复快照删除快照 其他实用命令 虚拟机管理 # 查看所有虚拟机列表 qm list# 查看虚拟机运行状态 qm status 1…...
数据结构(三)---单向循环链表
单向循环链表(Circular Linked List) 一、基本概念 循环链表是一种特殊的链表,其末尾节点的后继指针指向头结点,形成一个闭环。 循环链表的操作与普通链表基本一致,但需注意循环特性的处理。 二、代码实现 clList…...
HCIP-H12-821 核心知识梳理 (3)
从EBGP邻居接受的路由发送给IBGP邻居的时候,下一跳不会自动修改。一个 Route - Policy 最多可配置 65535个节点 BFD 单跳使用UDP3784端口多跳使用UDP 4784端口。 防火墙 Local:代表防火墙自身,处理防火墙本地发起或接收的流量 。 优先级 100I…...