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的执行顺序

Sentinel核心原理 - 图1

**SphU.entry**

entry这个方法内部会创建一个叫StringResourceWrapper的对象,该对象表示被保护的资源。这个对象有三个参数。

  1. //资源名,也就是entry()方法的第一个入参
  2. protected final String name;
  3. //表示是入口流量(IN)还是出口流量(OUT),
  4. //两个参数的区别在于是否被SystemSlot检查,IN会被检查,OUT不会,默认是OUT
  5. protected final EntryType entryType;
  6. //表示资源类型,sentinel提供了common、web、sql、api等类型,资源类型用于统计使用
  7. protected final int resourceType;

Entry

有了资源的对象,接着就是创建Entry对象,这个对象是SphU的这个返回值。通过Entry对象应用程序就可以知道sentinel里的内部情况了。

  1. //resourceWrapper:是StringResourceWrapper对象,表示资源
  2. //count:表示令牌数,默认是1,一般一个请求对应一个令牌,也可以指定一个请求对应多个令牌,如果令牌不够,则禁止访问
  3. //prioritized:在FlowSlot里面使用,没找到具体的使用含义,有看懂的小伙伴可以告知一下
  4. private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
  5. throws BlockException {
  6. //构建上下文对象,上下文对象存储在ThreadLocal中
  7. Context context = ContextUtil.getContext();
  8. if (context instanceof NullContext) {
  9. return new CtEntry(resourceWrapper, null, context);
  10. }
  11. //一般的线程第一次访问资源,context都是null,我也可以在应用程序中使用ContextUtil自己创建Context对象
  12. if (context == null) {
  13. //下面创建了一个名字为sentinel_default_context的Context对象
  14. context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
  15. }
  16. //全局开关,可以使用它来关闭sentinel
  17. if (!Constants.ON) {
  18. return new CtEntry(resourceWrapper, null, context);
  19. }
  20. //使用SPI构建slot链,每个slot对象都有一个next属性,可以使用该属性指定下一个slot对象
  21. ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
  22. if (chain == null) {
  23. return new CtEntry(resourceWrapper, null, context);
  24. }
  25. //创建Entry对象
  26. Entry e = new CtEntry(resourceWrapper, chain, context);
  27. try {
  28. //对该请求,遍历每个slot对象
  29. chain.entry(context, resourceWrapper, null, count, prioritized, args);
  30. } catch (BlockException e1) {
  31. e.exit(count, args);
  32. throw e1;
  33. } catch (Throwable e1) {
  34. RecordLog.info("Sentinel unexpected exception", e1);
  35. }
  36. return e;
  37. }

源码如上所示,可以看到先Context对象,这个对象会走遍整个请求的过程。一些共享的数据是可以放在这里,也可以 在应用的程序里创建Context对象。创建完Context对象之后,就会使用SPI来构建slot链,之后就是创建Entry对象,最后就是遍历slot链是不是可以请求访问资源。

**Entry.exit()**

当资源访问完后,就需要开始调用exit这个方法来告诉sentinel可以结束访问了,这时候sentinel就会做一些资源的清理跟数据统计的工作。

  1. protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException {
  2. if (context != null) {
  3. if (context instanceof NullContext) {
  4. return;
  5. }
  6. //如果Context对象记录的Entry对象不是当前对象,
  7. //意味着entry.exit()与SphU.entry()不是成对出现的,
  8. //sentinel要求两者必须成对出现,而且要一一对应,否则抛出异常
  9. //Context有父子关系,这个在文章后面介绍
  10. if (context.getCurEntry() != this) {
  11. String curEntryNameInContext = context.getCurEntry() == null ? null
  12. : context.getCurEntry().getResourceWrapper().getName();
  13. // Clean previous call stack.
  14. CtEntry e = (CtEntry) context.getCurEntry();
  15. while (e != null) {
  16. e.exit(count, args);
  17. e = (CtEntry) e.parent;
  18. }
  19. String errorMessage = String.format("The order of entry exit can't be paired with the order of entry"
  20. + ", current entry in context: <%s>, but expected: <%s>", curEntryNameInContext,
  21. resourceWrapper.getName());
  22. throw new ErrorEntryFreeException(errorMessage);
  23. } else {
  24. //在遍历每个slot的exit方法,每个slot清理和统计数据
  25. if (chain != null) {
  26. chain.exit(context, resourceWrapper, count, args);
  27. }
  28. //遍历exitHandlers,相当于回调,一般的DegradeSlot有回调,
  29. //DegradeSlot根据服务访问状态,决定是否将降级状态由HALF_OPEN变为OPEN
  30. callExitHandlersAndCleanUp(context);
  31. //设置为上一级Context对象
  32. context.setCurEntry(parent);
  33. if (parent != null) {
  34. ((CtEntry) parent).child = null;
  35. }
  36. if (parent == null) {
  37. // Default context (auto entered) will be exited automatically.
  38. if (ContextUtil.isDefaultContext(context)) {
  39. ContextUtil.exit();
  40. }
  41. }
  42. //设置当前对象的this.context = null
  43. clearEntryContext();
  44. }
  45. }
  46. }

Context

Context顾名思义上下文对象,这个对象是把一整个资源的访问过程都贯穿了,它保存在ThreadLocal里。创建Context是有多种方式的。

  1. //name表示Context的名称或者链路入口的名称,origin表示调用来源的名称,默认为空字符串
  2. public static Context enter(String name, String origin);
  3. public static Context enter(String name);

不管是哪种方式,到最后面都是要调用ContextUtil.trueEnter方法

  1. protected static Context trueEnter(String name, String origin) {
  2. //contextHolder是ThreadLocal<Context>类型
  3. Context context = contextHolder.get();
  4. if (context == null) {
  5. //contextNameNodeMap持有系统所有的入口节点
  6. Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
  7. DefaultNode node = localCacheNameMap.get(name);
  8. if (node == null) {
  9. //sentinel最大只能支撑2000个入口节点,如果超过2000个,sentinel无法提供对资源的保护
  10. if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
  11. setNullContext();
  12. return NULL_CONTEXT;
  13. } else {
  14. LOCK.lock();
  15. try {
  16. node = contextNameNodeMap.get(name);
  17. if (node == null) {
  18. if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
  19. setNullContext();
  20. return NULL_CONTEXT;
  21. } else {
  22. //创建入口节点
  23. node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
  24. //入口节点作为虚拟根节点的子节点
  25. Constants.ROOT.addChild(node);
  26. Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
  27. newMap.putAll(contextNameNodeMap);
  28. newMap.put(name, node);
  29. contextNameNodeMap = newMap;
  30. }
  31. }
  32. } finally {
  33. LOCK.unlock();
  34. }
  35. }
  36. }
  37. //创建Context对象,可以看到Context对象与入口节点一一对应
  38. context = new Context(node, name);
  39. //设置调用来源
  40. context.setOrigin(origin);
  41. contextHolder.set(context);
  42. }
  43. return context;
  44. }

在Context对象有一个名称与一个入口节点的对象,入口节点它就对于了线程访问的第一个资源,Context对象是对应了线程对资源的访问,一个线程就对应一个Context对象。每一个入口节点的对象它都是虚拟的根对象root的子节点。

  1. //ROOT_ID=machine-root
  2. public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
  3. new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));

每一次调用SphU.entry方法会在访问的链路上添加一个子节点,经过这个树就可以还原出来资源的访问路径了。每访问一个资源,Context对象都会使用curEntry这个属性记录下正在访问的资源所对应的Entry对象。