|
|
51CTO旗下网站
|
|
移动端

Sentinel 是怎样拦截异常流量的?

各位在家里用电的过程中,一定也经历过「跳闸」。这个「闸」就是在电量超过负荷的时候用来保护我们用电安全的,也被称为「断路器」,还有个响亮的英文名 -- CircuitBreaker。

作者:侯树成 来源:Tomcat那些事儿|2020-09-15 08:38

各位在家里用电的过程中,一定也经历过「跳闸」。这个「闸」就是在电量超过负荷的时候用来保护我们用电安全的,也被称为「断路器」,还有个响亮的英文名 -- CircuitBreaker。

和用电安全一样,对于「限流」、「降级」、「熔断」...,你我应该也都耳熟能详。我们开发的各类软件、系统、互联网应用等为了不被异常流量压垮,也需要一个断路器。

在 Spring 应用中,使用断路器很方便,我们可以使用 Spring Cloud CircuitBreaker。

Spring Cloud Circuit Breaker 是啥?如果你熟悉 Spring 是什么人的话,你能猜个八九不离十。和Spring Data JPA 这些类似,Spring 他又搞了个抽象的,标准的API 出来。这次他抽象的是关于降级熔断的「断路器」。有了这一层,具体实现是谁可以方便的更换,我们使用的代码里改动基本为0。

我们先来从官方Demo有个初步印象:

  1. @RestController 
  2. public class DemoController { 
  3.   private CircuitBreakerFactory circuitBreakerFactory; 
  4.   private HttpBinService httpBin; 
  5.   public DemoController(CircuitBreakerFactory circuitBreakerFactory, HttpBinService httpBinService) { 
  6.     this.circuitBreakerFactory = circuitBreakerFactory; 
  7.     this.httpBin = httpBinService; 
  8.   } 
  9.   @GetMapping("/delay/{seconds}"
  10.   public Map delay(@PathVariable int seconds) { 
  11.     return circuitBreakerFactory.create("delay").run(httpBin.delaySuppplier(seconds), t -> { 
  12.       Map<String, String> fallback = new HashMap<>(); 
  13.       fallback.put("hello""world"); 
  14.       return fallback; 
  15.     }); 
  16.   } 

千言万语,总结出来这样一句circuitBreakerFactory.create("delay").run()

因为是抽象,对应的实现就有好多种啦。

目前支持的实现有:

  • Hystrix
  • Resilience4j
  • Sentinel
  • Spring Retry

而抽象相当于定了个标准,像JDBC一样,无论我们把数据库换成了MySQL,Oracle 还是SQLite,接口等非特定类型的代码都不需要改变。断路器也一样。

这里的断路器工厂,创建方法都是标准的。具体这里执行业务逻辑的时候断路器实现要怎样进行拦截降级,就可以交给具体的实现来完成。

这次,我们以开源的 Sentinel 为例,来看看他们是怎样拦住异常流量的。

首先,因为是Spring Cloud,所以还会基于 Spring Boot 的 Autoconfiguration。以下是配置类,我们看到生成了一个工厂。

  1. public class SentinelCircuitBreakerAutoConfiguration { 
  2.   @Bean 
  3.   @ConditionalOnMissingBean(CircuitBreakerFactory.class) 
  4.   public CircuitBreakerFactory sentinelCircuitBreakerFactory() { 
  5.     return new SentinelCircuitBreakerFactory(); 
  6.   } 
  7.   } 

在我们实际代码执行逻辑的时候,create 出来的是什么呢?

是个断路器 CircuitBreaker,用来执行代码。

  1. public interface CircuitBreaker { 
  2.  
  3.   default <T> T run(Supplier<T> toRun) { 
  4.     return run(toRun, throwable -> { 
  5.       throw new NoFallbackAvailableException("No fallback available.", throwable); 
  6.     }); 
  7.   }; 
  8.   <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback); 

包含两个执行的方法,需要在的时候可以指定fallback逻辑。具体到 Sentinel 是这样的:

  1. public CircuitBreaker create(String id) { 
  2.     SentinelConfigBuilder.SentinelCircuitBreakerConfiguration conf = getConfigurations() 
  3.         .computeIfAbsent(id, defaultConfiguration); 
  4.     return new SentinelCircuitBreaker(id, conf.getEntryType(), conf.getRules()); 
  5.   } 

你会看到创建了一个SentinelCircuitBreaker。我们的业务逻辑,就会在这个断路器里执行,run方法就是各个具体实现的舞台。

  1. @Override 
  2.   public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) { 
  3.     Entry entry = null
  4.     try { 
  5.       entry = SphU.entry(resourceName, entryType); 
  6.       // If the SphU.entry() does not throw `BlockException`, it means that the 
  7.       // request can pass. 
  8.       return toRun.get(); 
  9.     } 
  10.     catch (BlockException ex) { 
  11.       // SphU.entry() may throw BlockException which indicates that 
  12.       // the request was rejected (flow control or circuit breaking triggered). 
  13.       // So it should not be counted as the business exception. 
  14.       return fallback.apply(ex); 
  15.     } 
  16.     catch (Exception ex) { 
  17.       // For other kinds of exceptions, we'll trace the exception count via 
  18.       // Tracer.trace(ex). 
  19.       Tracer.trace(ex); 
  20.       return fallback.apply(ex); 
  21.     } 
  22.     finally { 
  23.       // Guarantee the invocation has been completed. 
  24.       if (entry != null) { 
  25.         entry.exit(); 
  26.       } 
  27.     } 
  28.   } 

OK,到此为止, Spring Cloud CircuitBreaker 已经展现完了。其它的细节都放到了具体实现的「盒子」里。下面我们把这个盒子打开。

Sentinel 是个熔断降级框架,官方这样自我介绍:

面向分布式服务架构的高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。

官网的这张代码截图简洁的说明了他是怎样工作的

挡在业务代码的前面,有事儿先冲它来,能通过之后才走业务逻辑,和各类闯关还真类似。

在上面 CircuitBreaker 的 run 方法里,咱们一定都注意到了这句

  1. entry = SphU.entry(resourceName, entryType); 

这就是一切拦截的秘密。

无论我们是通过前面的CircuitBreaker的方式,还是 @SentinelResource 这种注解形式,还是通过 Interceptor 的方式,没什么本质区别。只是触发点不一样。最后都是通过SphU来搞定。

既然是拦截,那一定要拦下来做这样或那样的检查。

实际检查的时候,entry 里核心代码有这些:

  1. Entry entryWithPriority(ResourceWrapper resourceWrapper, ...) 
  2.         throws BlockException { 
  3.         ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); 
  4.         Entry e = new CtEntry(resourceWrapper, chain, context); 
  5.         try { 
  6.             chain.entry(context, resourceWrapper,...); 
  7.         } catch (BlockException e1) { 
  8.             e.exit(count, args); 
  9.             throw e1; 
  10.         }  
  11.         return e; 
  12.     } 

注意这里的ProcessorSlot chain = lookProcessChain(resourceWrapper);会在请求过来处理的时候,如果未初始化处理链,则进行初始化,将各种first,next设置好,后面的请求都会按这个来处理。所有需要拦截的Slot,都会加到这个 chain 里面,再逐个执行 chain 里的 slot。和Servlet Filter 类似。

chain里都加了些啥呢?

  1. public class HotParamSlotChainBuilder implements SlotChainBuilder { 
  2.     public ProcessorSlotChain build() { 
  3.         ProcessorSlotChain chain = new DefaultProcessorSlotChain(); 
  4.         chain.addLast(new NodeSelectorSlot()); 
  5.         chain.addLast(new ClusterBuilderSlot()); 
  6.         chain.addLast(new LogSlot()); 
  7.         chain.addLast(new StatisticSlot()); 
  8.         chain.addLast(new ParamFlowSlot()); 
  9.         chain.addLast(new SystemSlot()); 
  10.         chain.addLast(new AuthoritySlot()); 
  11.         chain.addLast(new FlowSlot()); 
  12.         chain.addLast(new DegradeSlot()); 
  13.         return chain; 
  14.     } 

初始的时候,first 指向一个匿名内部类,这些加进来的slot,会在每次addLast的时候,做为链的next,

  1. AbstractLinkedProcessorSlot<?> end = first
  2.  
  3.     @Override 
  4.     public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) { 
  5.         protocolProcessor.setNext(first.getNext()); 
  6.         first.setNext(protocolProcessor); 
  7.         if (end == first) { 
  8.             end = protocolProcessor; 
  9.         } 
  10.     } 
  11.     @Override 
  12.     public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) { 
  13.         end.setNext(protocolProcessor); 
  14.         end = protocolProcessor; 
  15.     } 

而每个 slot,有自己的特定用处,处理完自己的逻辑之后,会通过 fireEntry 来触发下一个 slot的执行。

给你一张长长的线程调用栈就会过分的明显了:

  1. java.lang.Thread.State: RUNNABLE 
  2.     at com.alibaba.csp.sentinel.slots.block.flow.FlowSlot.checkFlow(FlowSlot.java:168) 
  3.     at com.alibaba.csp.sentinel.slots.block.flow.FlowSlot.entry(FlowSlot.java:161) 
  4.     at com.alibaba.csp.sentinel.slots.block.flow.FlowSlot.entry(FlowSlot.java:139) 
  5.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  6.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.fireEntry(AbstractLinkedProcessorSlot.java:32) 
  7.     at com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot.entry(AuthoritySlot.java:39) 
  8.     at com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot.entry(AuthoritySlot.java:33) 
  9.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  10.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.fireEntry(AbstractLinkedProcessorSlot.java:32) 
  11.     at com.alibaba.csp.sentinel.slots.system.SystemSlot.entry(SystemSlot.java:36) 
  12.     at com.alibaba.csp.sentinel.slots.system.SystemSlot.entry(SystemSlot.java:30) 
  13.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  14.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.fireEntry(AbstractLinkedProcessorSlot.java:32) 
  15.     at com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot.entry(ParamFlowSlot.java:39) 
  16.     at com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot.entry(ParamFlowSlot.java:33) 
  17.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  18.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.fireEntry(AbstractLinkedProcessorSlot.java:32) 
  19.     at com.alibaba.csp.sentinel.slots.statistic.StatisticSlot.entry(StatisticSlot.java:57) 
  20.     at com.alibaba.csp.sentinel.slots.statistic.StatisticSlot.entry(StatisticSlot.java:50) 
  21.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  22.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.fireEntry(AbstractLinkedProcessorSlot.java:32) 
  23.     at com.alibaba.csp.sentinel.slots.logger.LogSlot.entry(LogSlot.java:35) 
  24.     at com.alibaba.csp.sentinel.slots.logger.LogSlot.entry(LogSlot.java:29) 
  25.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  26.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.fireEntry(AbstractLinkedProcessorSlot.java:32) 
  27.     at com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot.entry(ClusterBuilderSlot.java:101) 
  28.     at com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot.entry(ClusterBuilderSlot.java:47) 
  29.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  30.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.fireEntry(AbstractLinkedProcessorSlot.java:32) 
  31.     at com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot.entry(NodeSelectorSlot.java:171) 
  32.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  33.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.fireEntry(AbstractLinkedProcessorSlot.java:32) 
  34.     at com.alibaba.csp.sentinel.slotchain.DefaultProcessorSlotChain$1.entry(DefaultProcessorSlotChain.java:31) 
  35.     at com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot.transformEntry(AbstractLinkedProcessorSlot.java:40) 
  36.     at com.alibaba.csp.sentinel.slotchain.DefaultProcessorSlotChain.entry(DefaultProcessorSlotChain.java:75) 
  37.     at com.alibaba.csp.sentinel.CtSph.entryWithPriority(CtSph.java:148) 
  38.     at com.alibaba.csp.sentinel.CtSph.entryWithType(CtSph.java:347) 
  39.     at com.alibaba.csp.sentinel.CtSph.entryWithType(CtSph.java:340) 
  40.     at com.alibaba.csp.sentinel.SphU.entry(SphU.java:285) 

降级有三种类型

每种类型,都会根据对应的配置项数据比对,不符合就中断,中断之后也不能一直断着,啥时候再恢复呢?就根据配置的时间窗口,会启动一个恢复线程,到时间就会调度,把中断标识恢复。

  1. public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) { 
  2.         if (cut.get()) { 
  3.             return false
  4.         } 
  5.         ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource()); 
  6.         if (clusterNode == null) { 
  7.             return true
  8.         } 
  9.         if (grade == RuleConstant.DEGRADE_GRADE_RT) { 
  10.             double rt = clusterNode.avgRt(); 
  11.             if (rt < this.count) { 
  12.                 passCount.set(0); 
  13.                 return true
  14.             } 
  15.             // Sentinel will degrade the service only if count exceeds. 
  16.             if (passCount.incrementAndGet() < rtSlowRequestAmount) { 
  17.                 return true
  18.             } 
  19.         } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { 
  20.             double exception = clusterNode.exceptionQps(); 
  21.             double success = clusterNode.successQps(); 
  22.             double total = clusterNode.totalQps(); 
  23.             // If total amount is less than minRequestAmount, the request will pass. 
  24.             if (total < minRequestAmount) { 
  25.                 return true
  26.             } 
  27.             // In the same aligned statistic time window, 
  28.             // "success" (aka. completed count) = exception count + non-exception count (realSuccess) 
  29.             double realSuccess = success - exception; 
  30.             if (realSuccess <= 0 && exception < minRequestAmount) { 
  31.                 return true
  32.             } 
  33.             if (exception / success < count) { 
  34.                 return true
  35.             } 
  36.         } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) { 
  37.             double exception = clusterNode.totalException(); 
  38.             if (exception < count) { 
  39.                 return true
  40.             } 
  41.         } 
  42.         if (cut.compareAndSet(falsetrue)) { 
  43.             ResetTask resetTask = new ResetTask(this); 
  44.             pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS); 
  45.         } 
  46.         return false
  47.     } 

恢复做了两件事:一、把passCount设置成0,二、中断标识还原

上面介绍了对请求的拦截处理,这其中最核心的,也就是我们最主要配置的,一个是「流控」,一个是「降级」。这两个对应的Slot,会在处理请求的时候,根据配置好的 「规则」rule 来判断。比如我们上面看到的时间窗口、熔断时间等,以及流控的线程数,QPS数这些。

这些规则默认的配置在内存里,也可以通过不同的数据源加载进来。同时启用了Sentinel 控制台的话,在控制台 也可以配置规则。这些规则,会通过 HTTP 发送给对应使用了 sentinel 的应用实例节点。

本文转载自微信公众号「 Tomcat那些事儿」,可以通过以下二维码关注。转载本文请联系 Tomcat那些事儿公众号。

【编辑推荐】

  1. 手把手教你 springBoot 整合 rabbitMQ,利用 MQ 实现事务补偿
  2. Spring Security 中如何让上级拥有下级的所有权限?
  3. 贼厉害,手撸的 SpringBoot缓存系统,性能杠杠的!
  4. 这篇带你熟悉SpringBoot+RabbitMQ方式收发消息
  5. 一脸懵逼,面试官:过滤器和拦截器有啥区别?
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

5人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

170人订阅学习

搭建数据中心实验Lab

搭建数据中心实验Lab

实验平台Datacenter
共5章 | ITGO(老曾)

108人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微