【微服务专题】深入理解与实践微服务架构(十六)之Sentinel实现服务限流、降级和熔断功能

1,235 阅读38分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

服务限流功能

1. 服务限流的概念

服务限流包含有客户端限流服务端限流两种方式。客户端限流是针对具体某一个应用的流量进行限制,可以使用Google Guava的RateLimiter组件搭配原子类Semaphore信号量实现,但是一般客户端限流不能感知到全局的流量压力;而服务端限流就是在流量的入口进行限流,一般用于保护整个集群的服务可用。两者并不冲突,在生产环境中一般相互搭配使用。

生产环境中进行全局限流,如果直接使用网关上的Sentinel进行滑动窗口限流,那么窗口切换的一瞬间流量的瓶颈就变成了网关。一般采用集群限流的模式,采用令牌桶算法通过Token Server发放令牌给下游的服务进行全局限流。

在这里,Sentinel属于服务端限流的方式。

我们之前在 SentinelController 类中创建了两个接口,下面我们基于这两个接口和Sentinel控制面板进行快速限流配置:

我们下面进行流控规则配置,限制通过网关的请求速率为1次/秒。

2. 添加路由ID限流规则

首先,先新增网关流控规则(默认为路由ID的限流方式):

image-20220704110319565

点击新增网关流控规则,然后填写网关代理服务的限流参数(包括路由ID、QPS阈值等)进行限流策略配置:

image-20220704110653262

并发线程数、QPS和平均耗时等概念

并发线程数: 指的是施压机施加的同时请求的线程数量。比如,我启动并发线程数100,即我会在施压机器上面启动100个线程,不断地向服务器发请求。

QPS: 每秒请求数,即在不断向服务器发送请求的情况下,服务器每秒能够处理的请求数量。

平均耗时: 平均每个请求的耗时。即所有线程所有请求的总耗时➗总请求数。平均耗时反映的是接口处理请求的时间,往往跟被测服务器的繁忙程度和资源有关。

95分位耗时: 相对于平均耗时,95分位耗时更多地被用于反映接口性能的方面。因为95分位耗时能够去除一些最大值毛刺对整体数据的影响。更加能够反馈出接口真实的体验。

这里需要填写路由ID,因此我们需要将配置文件的静态路由开启,注册中心路由发现功能关闭:

server:
  # 服务运行端口
  port: 9080
spring:
  application:
    # 服务名称
    name: service-sentinel
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: localhost:8848
        # Nacos认证信息
        username: nacos
        password: nacos
        # 注册到 nacos 的指定 namespace,默认为 public
        namespace: public
    gateway:
      enabled: true
      discovery:
        locator:
          # 动态路由开关
          enabled: false
          lower-case-service-id: true
      # 路由数组
      routes:
        # 我们⾃定义的路由 ID,保持唯⼀
        - id: service-provider-nacos
          # ⽬标服务地址(部署多实例)
#          uri: http://localhost:9010 # 不引入loadbalancer依旧可以调用
          uri: lb://service-provider-nacos
          # 断⾔:路由条件
          predicates:
            - Path=/service-provider-nacos/provider-nacos/**
          # 过滤器
          filters:
            - StripPrefix=1
        - id: service-sentinel
          uri: lb://service-sentinel
          predicates:
            - Path=/service-sentinel/sentinel/**
          filters:
            - StripPrefix=1
    sentinel:
      # sentinel开关
      enabled: true
      # 是否饥开启饿加载(默认为false)
      eager: false # 默认情况下Sentinel会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包
      transport:
        dashboard: localhost:8849
        # sentinel客户端-数据端口(默认为8719端口,但因为控制台默认是8719端口,为了防止冲突会使用8720端口)
        # 这个端口配置会在应用对应的机器上启动一个Http Server,该Server会与Sentinel控制台做通信
        port: 8721
        # 指定心跳周期,默认null
#        heartbeat-interval-ms: 10000
        # sentinel客户端-外部端口
        # 如果有多套网络,又无法正确获取本机IP,则需要使用下面的参数设置当前机器可被外部访问的IP地址,供admin控制台使用
  #        client-ip: 0.0.0.0:8849

注意:因为这里需要填写路由ID,开启注册中心路由发现(动态路由)后路由ID为UUID的值,我们不方便获取;因此,我们选择以静态路由的方式使用Gateway网关代理。并且以Gateway默认配置项方式开启的动态路由,每次重启服务都需要重新构建路由(没有持久化),在此期间服务调用都不可用。在实际开发中,手动实现动态路由功能并且持久化路由的开销远大于直接使用静态路由(路由组)的方式,以稳定性、可用性以及实现代价这几个方面综合考虑后选择使用静态路由的方式。

当然,上面还可以使用sentinel提供的API分组进行分类。

完成流控规则配置后,可以看到网关路由对应的限流规则了:

image-20220704110916103

3. 新增路由ID限流接口

创建完限流规则后,然后在SentinelController中添加限流测试接口:

package com.deepinsea.controller;
​
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * Created by deepinsea on 2022/7/4.
 */
@RestController
@RequestMapping("/sentinel")
public class SentinelController {
​
    @GetMapping("/hello")
    public String hello() {
        return "hi, this is service-sentinel!";
    }
​
    @PostMapping("/test")
    public String test() {
        return "hi, this is service-sentinel[POST]!";
    }
​
    //测试流控规则
    @GetMapping("/limit")
    public String limit() { //路由id限流
        return "hi, this is service-sentinel-limit test!";
    }
}

4. 测试路由ID限流功能

我们下面使用curl命令请求这条唯一路由ID对应的网关代理服务 service-sentinel 的hello接口,来测试1次/s的限流规则效果:

C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
hi, this is service-sentinel-limit test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"code":429,"message":"Blocked by Sentinel: ParamFlowException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"code":429,"message":"Blocked by Sentinel: ParamFlowException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"code":429,"message":"Blocked by Sentinel: ParamFlowException"}

可以看到,上面的请求速率基本上是限制在一秒一次(后面加快了一下请求速度)。

curl一秒内调用接口超过一次时,返回Blocked by Sentinel: ParamFlowException 错误。Sentinel全局异常处理类BlockException,包含不用的子类,对应的处理场景如下表所示:

异常说明
FlowException限流异常
ParamFlowException热点参数限流的异常
DegradeException降级异常
AuthorityException授权规则异常
SystemBlockException系统规则异常

的确是触发了Sentinel的热点参数限流规则,限流成功!

注意如果把QPS改为线程数时,再请求接口,无论请求速率多么快,都不会进入限流回执

类型区别描述关键点
QPS假设100个请求同时过来,QPS设置一秒一个请求线程通过,1秒内其他请求直接拦截每秒请求数
线程数假设100个请求同时过来,线程数允许所有的请求进入,但每次只允许一个请求执行操作而其他请求等待最大请求数

另外通过网关监控到的请求QPS速率图,可以看到相隔一秒的时间线内拒绝QPS维持在1.0,也就是限制请求速率在1次/s。

image-20220704112538592

路由ID限流功能测试成功,下面使用Sentinel进行服务降级的使用和测试:

服务降级功能

1. 服务降级的概念

什么是服务降级?

服务降级就是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务页面有策略的不处理或换种简单的方式处理(比如关闭订单接口重定向到诸如"抱歉,目前人数较多,请稍后下单"等静态页面) ,以此释放服务器资源以保证核心任务的正常运行。

服务降级有很多种方式,最好的方式就是利用Docker来实现。当需要对某个服务进行降级时,直接将这个服务所有的容器停掉,需要恢复的时候重新启动就可以了。还有就是在 API 网关层进行处理,当某个服务被降级了,前端过来的请求就直接拒绝掉,不往内部服务转发,将流量挡回去。

流量控制本质上是减小访问量,而服务处理能力不变;而服务降级本质上是降低了部分服务的处理能力,增强另一部分服务处理能力,而访问量不变。因此,这种设计属于"牺牲边缘服务可用性,保证核心服务可用性"的服务有损的高可用策略。

服务降级的方式

降级方式分类

  • 降级按照是否自动化可分为:自动开关降级和人工开关降级。
  • 降级按照功能可分为:读服务降级、写服务降级。
  • 降级按照处于的系统层次可分为:多级降级。

服务降级的具体方式

降级的功能点主要从服务端链路考虑,即根据用户访问的服务调用链路来梳理哪里需要降级:

  • 页面降级: 在大促或者某些特殊情况下,某些页面占用了一些稀缺服务资源,在紧急情况下可以对其整个降级,以达到丢卒保帅;
  • 页面片段降级:比如商品详情页中的商家部分因为数据错误了,此时需要对其进行降级;
  • 页面异步请求降级: 比如商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,可以进行降级;
  • 服务功能降级: 比如渲染商品详情页时需要调用一些不太重要的服务:相关分类、热销榜等,而这些服务在异常情况下直接不获取,即降级即可;
  • 读降级: 比如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景;
  • 写降级: 比如秒杀抢购,我们可以只进行Cache的更新,然后异步同步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
  • 爬虫降级: 在大促活动时,可以将爬虫流量导向静态页或者返回空数据,从而保护后端稀缺资源。
  • 自动开关降级:自动降级是根据系统负载、资源使用情况、SLA等指标进行降级。
  • 超时降级:当访问的数据库/http服务/远程调用响应慢或者长时间响应慢,且该服务不是核心服务的话可以在超时后自动降级;比如商品详情页上有推荐内容/评价,但是推荐内容/评价暂时不展示对用户购物流程不会产生很大的影响;对于这种服务是可以超时降级的。如果是调用别人的远程服务,和对方定义一个服务响应最大时间,如果超时了则自动降级。

参考:聊聊高并发系统之降级特技

服务降级的处理方案

服务降级是前后端联动,相互配合来做到的,意味着,在代码设计阶段,前后端必须要共同考虑服务降级的方案。根据事态的严重性,会制定不同级别的降级方案:

  • 1. 按比例执行API: 预先设定一定的比例,将这部分流量带来的API请求,不做处理,直接返回默认值,其余请求能继续正常返回。
  • 2. 关闭非核心服务API: 前端页面能继续访问,但是将与核心功能无关的API关闭掉,保证主流程能继续执行,前端隐藏对应的信息展示。
  • 3. 延迟返回,结果转异步返回: 页面能正常访问,但是涉及到记录变更,会提示稍晚更新结果,将数据记录更新的返回转到异步MQ。
  • 4. 将前端页面切到静态页* *:**通过Nginx设置,将页面跳转到一个静态页面。例如“目前系统正在维护,blabla”这样的页面。

降级预案

在进行降级之前要对系统进行梳理,根据服务使用优先级,梳理出哪些不可降级哪些可降级。

比如可以参考日志级别设置预案:

一般: 比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;

警告: 有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;

错误: 比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;

严重错误: 比如因为特殊原因数据错误了,此时需要紧急人工降级。

有哪些自动降级的方式?

自动降级的方式:

  • 超时降级 —— 主要配置好超时时间和超时重试次数和机制,并使用异步机制探测恢复情况;
  • 失败次数降级 —— 主要是一些不稳定的API,当失败调用次数达到一定阀值自动降级,同样要使用异步机制探测回复情况;
  • 故障降级 —— 如要调用的远程服务挂掉了(网络故障、DNS故障、HTTP服务返回错误的状态码和RPC服务抛出异常),则可以直接降级;
  • 限流降级 —— 当触发了限流超额时,可以使用暂时屏蔽的方式来进行短暂的屏蔽。

当我们去秒杀或者抢购一些限购商品时,此时可能会因为访问量太大而导致系统崩溃,此时开发者会使用限流来进行限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:排队页面(将用户导流到排队页面等一会重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)。

了解完上面关于服务降级的概念后,下面进行服务降级的实践:

2. 添加降级熔断规则

首先,在 请求链路 上新增服务降级规则:

实际上在请求链路上添加的降级规则,是基于路由ID的

image-20220706122204788

因为慢调用比例异常比例异常数这三种降级熔断处理策略中,只有慢调用比例是基于响应时间的,无需手动编写fallback类即可测试。因此,我们使用基于调用响应时间来降级处理的策略:

image-20220713172000859

这里设置最大的响应时间(RT)为1ms,因此除了第一次请求,后面的请求都会触发降级规则,并且在两次请求后进一步触发熔断规则。

预期效果:第一次请求正常,然后第二次返回降级异常,然后十秒内的所有请求都会返回降级异常提醒,直到十秒后的第一次请求又再次正常。

配置说明:

  • 最大 RT:慢调用临界 RT,超出该值计为慢调用。单位毫秒
  • 比例阈值:RT模式下慢速请求比率的阈值。默认1.0d
  • 熔断时长:断路器打开时的恢复超时(以秒为单位)。超时后,断路器将转换为半开状态以尝试一些请求。。单位秒,图中表示触发熔断后,接下来的10秒内请求会自动被熔断,经过10S后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 最小请求数:可以触发熔断中断的最小请求数(在有效的统计时间范围内),默认值为5。
  • StatIntervalMs: 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入),默认1000,在控制台没有选项,需代码实现。

新增成功后,请求链路中有了新增的降级熔断反应规则:

image-20220707021641560

为什么添加降级配置后,变成了熔断规则

这是因为在sentinel 1.8.3版本中,将服务降级和熔断配置放在了一起(相当于将服务降级机制划分为了服务熔断机制的前置概念,也就是说只有服务降级和服务熔断机制了),其实本身服务熔断也是在服务降级后做出的反应机制

3. 新增降级接口

然后我们需要在SentinelController中添加降级测试接口:

package com.deepinsea.controller;
​
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * Created by deepinsea on 2022/7/4.
 */
@RestController
@RequestMapping("/sentinel")
public class SentinelController {
​
    @GetMapping("/hello")
    public String hello() {
        return "hi, this is service-sentinel!";
    }
​
    @PostMapping("/test")
    public String test() {
        return "hi, this is service-sentinel[POST]!";
    }
​
    //测试流控规则
    @GetMapping("/limit")
    public String limit() { //路由id限流
        return "hi, this is service-sentinel-limit test!";
    }
​
    //测试降级规则
    @GetMapping("/degrade")
    public String degrade(){
        return "hi, this is service-sentinel-degrade test!";
    }
}

4. 测试降级功能

然后重启项目,使用curl命令对接口进行请求测试降级功能:

也可以使用压测工具访问测试接口,查看限流效果图,具体限流过程大概为: (1)请求进入后台,sentinel会根据设置的统计时长(默认1S)统计时间段内请求的总数 (2)首先判断统计的请求总数是否小于用户的设置的最小请求数(默认5),小于则不熔断,反之则进入下一步 (3)然后根据用户设置的最大 RT,判断统计中的请求是否为慢调用,大于设置值为是慢调用请求 (4)再次计算慢调用请求/总统计请求比例,是否超过设置的比例阈值。 (5)当统计时间内的请求数及慢调用比例阈值都超过设置的阈值后,接下来的熔断时长内请求会自动被熔断 (6)熔断时长结束后,熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
hi, this is service-sentinel-degrade test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/degrade
hi, this is service-sentinel-degrade test!

可以看到,上面测试请求一次成功后,然后请求返回降级异常(DegradeException),间隔十秒后请求又再次正常。达到预期效果,降级熔断功能测试成功!

注意:因为有基于service-sentinel路由ID生效的全局限流规则,为了防止限流规则生效干扰熔断规则的效果,我们可以先删除基于Route ID的流控规则(重启项目重新添加即可恢复)。

另外,我们还可以通过控制台的流量实时监控来验证降级机制的效果:

image-20220713181807005

可以看到,仪表盘请求监控中在通过一次QPS后,后面10s的都为拒绝QPS,验证成功。

5. Sentinel使用总结

6大设计模式

1.建造者模式

2.工厂模式

3.策略模式

4.漏桶模式

5.令牌桶模式

6.单例模式

Sentinel常见使用场景

1.sentinel降级(熔断), 出现不稳定的系统服务时,暂时对此服务的访问

2.sentinel实现热点参数限流, 热点视频,文章

3.sentinel系统规则配置, 例如cpu使用率,QPS

4.sentinel授权设置 , 黑名单和白名单 黑白名单设计由业务决定

常见问题

1.如何理解熔断? 保险丝

2.如何自定义熔断异常处理规则? 实现BlockExceptionHandler接口

3.如何理解热点参数? 频繁的访问数据,系统底层如何判断哪些数据是频繁访问-lru算法

4.系统规则是全局规则吗? 是的

5.授权规则需要自己写请求解析类吗 ? 需要,自定义实现RequestOriginParser接口

常见Bug所在

  • 注意注解和请求路径别写错了;
  • 设置熔断时别用错名字。

下面进行服务熔断的配置,因为Sentinel 1.8.3将服务降级熔断合并为一体了,因此下面主要以服务限流、降级和熔断的相互配合以及自定义熔断策略为主:

服务熔断功能

1. 服务熔断的概念

简介

服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障从而采用的一种保护措施。很多地方把熔断亦称为过载保护,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务开始)。

简单来说,熔断就是:用来避免微服务架构中雪崩现象,达到某个阈值条件之后自动触发拒绝服务的一种链路保护机制。

服务降级和服务熔断的区别

  • 服务降级:不管在什么情况下,服务降级的流程都是前面的请求调用正常的方法,超出限流策略的请求再调用fallback方法。
  • 服务熔断:假设服务宕机或者在单位时间内请求调用服务失败次数过多,即服务降级的次数太多那么则服务熔断。 并且熔断以后会跳过正常的方法,会直接调用fallback方法,即所谓“服务熔断后不可用”。当达到最大服务访问后,会直接拒绝访问;然后调用服务降级的fallback方法,返回友好提示。

本质上服务熔断是基于服务降级的阈值进行的逻辑,当服务降级达到一定次数后,会触发服务熔断。

为什么有了服务降级,还需要服务熔断?

正如上面提到的两者的区别,服务降级并不代表不进行处理,只是在单位时间处理完限流次数的请求后调用备用的处理逻辑。而服务熔断是服务降级达到次数后,服务直接触发拒绝逻辑,将不再处理请求直接全部进行fallback处理。因此,降级还是有限流次数的请求处理压力,而熔断则是没有正常请求处理压力的压力,因此熔断对于服务器的压力缓解效果相比降级更佳

服务限流、降级和熔断这三者的总体区别

  • 限流:限制并发的请求访问量,超过阈值则拒绝,核心是请求数
  • 降级:从整体负荷考虑,将服务划分优先级,牺牲非核心服务以保证核心服务稳定可用,核心是限流后的请求采用备用逻辑
  • 熔断:依赖的下游服务故障触发熔断,避免引发本系统崩溃,系统自动执行熔断并可延时恢复,核心是降级(失败)次数阈值时钟

限流、降级和熔断这三种都是服务可用性保证方式,每一个都是基于上一者的功能基础上进行处理的。在一个服务调用中,请求达到阈值会触发限流,后续的请求会触发降级的备用逻辑,多次降级次数达到阈值会触发熔断并于一段时间后恢复。因此,这三者的相互配合是一层接一环,密不可分。

2. 熔断规则的种类

当监控到调用链路中某一个服务,符合预先配置条件(RT/异常比例/异常数)后自动触发熔断,在触发熔断之后对于该微服务调用不可用。该预先配置的条件,分别为:

  • RT(响应时间):根据请求响应时间熔断;
  • 异常比例:根据请求调用过程中出现异常百分比进行熔断;
  • 异常数:根据请求调用过程中异常数进行熔断

平均响应时间

当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。

image

异常比例

当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100% image

异常数

当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。

image

参考:SpringCloudAlibaba(十)——sentinel组件的熔断降级和热点规则

3. 服务熔断设计优化

我们可以学习游戏设计模式中的状态机设计模式,采用状态机设计模式进行服务熔断的设计优化,达到异步抽象事件处理的目的。

(有限)状态机简单用一句话概括就是:对象的内部状态随外部执行条件的变化而变化。因此,我们可以通过服务可用性状态的流转来决定请求处理的方式。另外,状态机还可以运用在订单系统中。在有限状态机架构中,通过系统自动处理不同状态的事件做到非阻塞和故障自动处理和解决,以降低系统阻塞和故障的效果,是一种高级的设计模式。

熔断器实现的三个状态机:

  • Closed: 熔断器关闭状态,调用失败次数积累,到了阈值(或一定比例)则启动熔断机制;
  • Open: 熔断器打开状态,此时对下游的调用都内部直接返回错误,不走网络,但设计了一个时钟选项,默认的时钟达到了一定时间(这个时间一般设置成平均故障处理时间,也就是MTTR),到了这个时间,进入半熔断状态;
  • Half-Open: 半熔断状态,允许定量的服务请求,如果调用都成功(或一定比例)则认为恢复了,关闭熔断器,否则认为还没好,又回到熔断器打开状态;

通过切换这三种状态,状态机分别给予不同的系统处理模式,相当于在系统内部状态层面进行了高度抽象。因此,系统能够根据状态机划分的这三种状态设计出更佳的处理外部输入处理方案。

4. 添加熔断规则

首先我们需要在控制台添加熔断规则:

image-20220713171451458

点击新增熔断规则后,我们进行熔断规则参数配置:

image-20220713171810583

可以看到,这里除了新增规则配置名字((降级/熔断))不一样,配置和效果都是一样的(因为熔断规则默认并入到降级规则中了)。

熔断降级规则(DegradeRule)包含下面几个重要的属性(后面持久化降级熔断规则会使用到):

Field说明默认值
resource资源名,即规则的作用对象
grade熔断策略,支持慢调用比例/异常比例/异常数策略慢调用比例
count慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow熔断时长,单位为 s
minRequestAmount熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入)5
statIntervalMs统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入)1000 ms
slowRatioThreshold慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

新增规则后,控制台出现了熔断规则:

image-20220713175410675

5. 新增熔断接口

下面我们创建一个专属于熔断测试的接口:

package com.deepinsea.controller;
​
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * Created by deepinsea on 2022/7/4.
 */
@RestController
@RequestMapping("/sentinel")
public class SentinelController {
​
    @GetMapping("/hello")
    public String hello() {
        return "hi, this is service-sentinel!";
    }
​
    @PostMapping("/test")
    public String test() {
        return "hi, this is service-sentinel[POST]!";
    }
​
    //测试流控规则
    @GetMapping("/limit")
    public String limit() { //路由id限流
        return "hi, this is service-sentinel-limit test!";
    }
​
    //测试降级规则
    @GetMapping("/degrade")
    public String degrade(){
        return "hi, this is service-sentinel-degrade test!";
    }
​
    //熔断测试
    @GetMapping("/fusing")
    public String fusing(){
        return "hi, this is service-sentinel-fusing test!";
    }
}

6. 测试熔断规则

降级熔断功能在sentinel 1.8.3中合并为一个降级熔断功能了,因此降级和熔断功能测试效果是相同的。另外,因为有基于service-sentinel路由ID生效的全局限流规则,为了防止限流规则生效干扰熔断规则的效果,我们可以先删除限流规则(重启项目重新添加即可恢复)。

重启项目,然后使用curl命令测试熔断功能(同样的我们删除基于路由ID的限流规则后测试):

C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/fusing
hi, this is service-sentinel-fusing test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/fusing
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/fusing
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/fusing
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/fusing
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/fusing
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/fusing
{"code":429,"message":"Blocked by Sentinel: DegradeException"}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/fusing
hi, this is service-sentinel-fusing test!

查看实时流量监控,可以发现成功触发基于慢比例调用的降级熔断规则(DegradeException) 后,后面10s内的QPS都为拒绝,直到10秒以后的请求才恢复正常:

image-20220713185238290

另外,需要注意的是:服务降级熔断后,属于熔断下的所有接口在一定时间内都会拒绝服务,直接抛出异常提示

熔断规则测试成功,下面使用 @ResourceSentinel注解,来处理限流和降级熔断的自定义异常和异常处理方法逻辑:

@SentinelResource定义资源

@SentinelResource注解一般用于处理接口方法级别的自定义异常处理逻辑,全局异常处理是全局服务级别的,可搭配使用

什么是资源

我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。使用 Sentinel 来进行资源保护,主要分为几个步骤:

  1. 定义资源
  2. 定义规则
  3. 检验规则是否生效

先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。

一般来说,资源就是API名称,包括路由IDAPI分组的API名称

定义资源的方式

sentinel 支持都多种方式来定义资源,常见的有:

  • 方式一:整合到常见的主流框架,比如 Web Servlet、Dubbo、Spring Cloud
  • 方式二:抛出异常的方式,使用 SphU 这个类的 try-catch 方式
  • 方式三:返回布尔值方式定义资源,使用 SphO 提供 if-else 风格的 API
  • 方式四:注解方式定义资源,使用 @SentinelResource 注解 。
  • 方式五:异步调用支持,使用 SphU.asyncEntry 异步方法。

示例有: 抛出异常的方式 来定义资源 使用 SphU 这个类的 try-catch 风格的 API。当“资源”发生了限流之后会抛出 BlockException,然后捕捉异常进行限流之后的逻辑处理。

示例代码如下:

try (Entry entry = SphU.entry("resourceName")) {
  // 被保护的业务逻辑
  // do something here...
} catch (BlockException ex) {
  // 资源访问阻止,被限流或被降级
  // 在此处进行相应的处理操作
}

使用资源

Sentinel中定义资源有多种方式,可以在Sentinel控制台定义资源(已经演示过了,这里换一个维度定义),也可以通过代码定义资源。在代码层面常用的是:使用@SentinelResource注解定义资源

@SentinelResource注解参数

属性名是否必填说明
value资源名称 。(必填项,需要通过 value 值找到对应的规则进行配置)
entryTypeentry类型,标记流量的方向,取值IN/OUT,默认是OUT
blockHandler处理BlockException的函数名称(可以理解为对Sentinel的配置进行方法兜底) 。函数要求: 1.必须是 public 修饰 2.返回类型与原方法一致 3. 参数类型需要和原方法相匹配,并在最后加 BlockException 类型的参数。 4. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 blockHandlerClass ,并指定blockHandlerClass里面的方法。
blockHandlerClass存放blockHandler的类。 对应的处理函数必须 public static 修饰,否则无法解析,其他要求:同blockHandler。
fallback用于在抛出异常的时候提供fallback处理逻辑(可以理解为对Java异常情况方法兜底) 。 fallback函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求: 1.返回类型与原方法一致 2.参数类型需要和原方法相匹配,Sentinel 1.6开始,也可在方法最后加 Throwable 类型的参数。 3.默认需和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定fallbackClass里面的方法。
fallbackClass【1.6】存放fallback的类。 对应的处理函数必须static修饰,否则无法解析,其他要求:同fallback。
defaultFallback【1.6】用于通用的 fallback 逻辑。 默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求: 1.返回类型与原方法一致 2.方法参数列表为空,或者有一个 Throwable 类型的参数。 3.默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置 fallbackClass ,并指定 fallbackClass 里面的方法。
exceptionsToIgnore【1.6】指定排除掉哪些异常。 排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
exceptionsToTrace需要trace的异常

Sentinel的 @SentinelResource 注解的实现为sentinel-annotation-aspectj 依赖,底层依赖于Spring AOP实现(虽然官方号称是AspectJ机制实现的Spring AOP,但并不是完全的AspectJ),同时也有基于AspectJ的实现接口。

下面是Sentinel官方文档的参数释义:Sentinel 注解支持

注意:注解方式埋点不支持私人方法。

1.8.0版本开始,defaultFallback支持在类进行等级配置。

注:1.6.0 之前的版本回退处理 只针对降级异常(DegradeException)进行处理,不能针对业务进行处理

特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 BlockException 直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)。

了解了注解的参数解释,下面我们使用@SentinelResource注解来定义一个资源:

首先我们需要创建一个SentinelResourceController资源定义注解测试Controller类

package com.deepinsea.controller;
​
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
​
/**
 * Created by deepinsea on 2022/7/13.
 */
@RestController
@RequestMapping("/sentinel")
public class SentinelResourceController {
​
    /**
     * spring初始化方法
     * @PostConstruct 是Spring带的注解,当前这个bean在被spring容器创建的时候,就会自动的调用
     * 被@PostConstruct这个注解修饰的方法进行初始化
     */
    @PostConstruct // init-method
    private static void initFlowRules() {
        List<FlowRule> rules = new ArrayList<>(); //流控规则列表
        FlowRule rule = new FlowRule(); //流控规则
        rule.setResource("sentinel-resource"); //为哪个资源进行流控
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS); //设置流控规则类型QPS
        rule.setCount(1); //设置受保护的资源阙值
        rule.setLimitApp("default");
        rules.add(rule); //将配置的规则放入规则列表中
        FlowRuleManager.loadRules(rules); //加载配置好的规则
    }
​
    // 原函数
    @GetMapping("/sentinel-resource") //注意接口不要与Sentinel定义的规则接口重复了
    @SentinelResource(value = "sentinel-resource", blockHandler = "exceptionHandler")
    public String test() {
        return "hi, this is SentinelResource test! ";
    }
​
    // Block异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
    public String exceptionHandler(BlockException ex) {
//        ex.printStackTrace(); //1.4以前的版本需要手动记录业务异常
        return "被限流了!";
    }
​
    // Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
    public String fallbackHandler() {
        return "被降级熔断了!";
    }
}

注意

  • @SentinelResource注解中value的值对应API名称(路由ID/API分组);
  • @SentinelResource注解中的blockHandler和fallback参数的值对应相同方法名的public方法;
  • @SentinelResource注解中自定义异常处理逻辑优先级低于Sentinel默认的异常处理机制;
  • @SentinelResource注解规则加载是通过sentinel-core包的SPI(slot)机制,因此只支持flow规则而不是gw-flow;
  • @SentinelResource注解对应的限流和降级熔断规则需要通过 @PostConstruct初始化规则或通过提供的SPI机制重写InitFunc接口的init()方法来加载旧版本的限流和降级熔断规则。

上面其实已经通过@PostConstruct注解初始化限流规则了,我们其实已经完成了资源保护步骤的前两步了,下面我们来进行资源保护的第三步——资源保护(限流规则)测试:

首先一定要先删除控制台上干扰@SentinelResource自定义异常返回的全局限流和熔断规则,然后就可以使用curl命令测试了

C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
hi, this is SentinelResource test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
被限流了!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
被限流了!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
被限流了!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
被限流了!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
hi, this is SentinelResource test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
被限流了!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
被限流了!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
hi, this is SentinelResource test!

测试成功,通过@SentinelResource注解成功实现自定义异常处理。

注意:如果测试没有返回自定义异常提示,那么可以从下面三个维度进行检查:

  • 没有删除全局的Sentinel控制台的限流和降级熔断规则;
  • 没有通过 @PostConstruct初始化规则或通过提供的SPI机制重写InitFunc接口的init()方法来加载旧版本的限流和降级熔断规则;
  • @SentinelResource注解中value参数没有对应资源名或其他异常处理参数没有与方法名相同。

什么是规则

根据Sentinel的资源保护步骤,我们定义完成资源后,下面需要定义规则来进行限流、降级熔断等操作,正如官方介绍的:

Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功后可以动态增加各种流控降级规则。

Sentinel 提供两种方式修改规则:

  • 通过 API 直接修改 (loadRules)
  • 通过 DataSource 适配不同数据源修改

通过 API 修改比较直观,可以通过以下几个 API 修改不同的规则:

FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则

手动修改规则(硬编码方式)一般仅用于测试和演示,生产上一般通过动态规则源的方式来动态管理规则。

定义规则的方式

Sentinel 支持以下几种规则:

  • 流量控制规则
  • 熔断降级规则
  • 系统保护规则
  • 来源访问控制规则
  • 热点参数规则

流量控制规则(FlowRule) 支持 QPS 模式(1)或并发线程数模式(0)。

熔断降级规则(DegradeRule) 熔断策略,支持慢调用比例/异常比例/异常数策略

系统保护规则 (SystemRule) 结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡

来源访问控制规则 (AuthorityRule) 即黑名单,白名单规则。

规则的持久化 建议和 nacos 一起使用,方法见本文后面章节。

使用规则

上面在每一个接口创建一个List用于存储一个限流或降级熔断规则,然后放到限流/降级熔断管理器中,有点浪费资源。并且,这种方式比较繁复。

我们有没有办法在全局定义@SentinelResource支持的规则(旧版本规则)?

答案是有的:虽然我们无法使用新版控制台的限流和降级熔断规则,但是sentinel-core核心包中提供了SPI机制构建源限流和降级熔断规则的方式。

在持久化Sentinel配置规则时,我们也可以使用Sentinel的SPI机制进行持久化数据源规则(注意限流规则为新版本的gw-flow)。

借助 Sentinel 的 InitFunc SPI 扩展接口,只需要实现自己的 InitFunc 接口,在 init 方法中编写注册数据源的逻辑。

下面我们采用Sentinel的SPI机制来实现全局规则定义:

首先我们需要创建一个FlowRuleInitFunc类来实现InitFunc接口

package com.deepinsea.common.config;
​
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
​
import java.util.ArrayList;
import java.util.List;
​
/**
 * Created by deepinsea on 2022/7/14.
 * 初始化规则
 */
public class FlowRuleInitFunc implements InitFunc {
    @Override
    public void init() throws Exception {
        //限流规则
        List<FlowRule> rules = new ArrayList<>(); //可以使用HashSet => 并发Set去重后放入进行优化
        FlowRule rule = new FlowRule();
        rule.setResource("sentinel-resource"); //sentinel-core核心包中没有resourceMode参数,无需定义
        rule.setLimitApp("default");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS); //阈值类型(QPS/线程数)
        rule.setCount(1); //QPS阈值/线程数
        rule.setStrategy(0); //流控方式
        // 因为是FlowRule是sentinel-core核心包中的限流规则,
        // 因此对于新版Sentinel的流控规则(gw-flow)的间隔时间(interval)和突发流量大小(burst)参数不支持
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
        //降级熔断规则
        List<DegradeRule> degradeRules = new ArrayList<>();
        DegradeRule degradeRule = new DegradeRule();
        degradeRule.setResource("sentinel-resource");
        degradeRule.setLimitApp("default");
        degradeRule.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType()); //熔断策略
        degradeRule.setCount(1); //最大RT
        degradeRule.setSlowRatioThreshold(0.5); //比例阈值
        degradeRule.setTimeWindow(10); //熔断时长(单位s)
        degradeRule.setMinRequestAmount(5); //最小请求数
        degradeRule.setStatIntervalMs(1000); //统计时长(单位ms)
        degradeRules.add(degradeRule);
        DegradeRuleManager.loadRules(degradeRules);
    }
}

PS:需要注意的是,我们需要在系统启动的时候调用该数据源注册的方法,否则不会生效的。具体的方式有很多,可以借助 Spring 来初始化该方法,也可以自定义一个类来实现 Sentinel 中的 InitFunc 接口来完成初始化。

Sentinel 会在系统启动的时候通过 spi 来扫描 InitFunc 的实现类,并执行 InitFunc 的 init 方法,所以这也是一种可行的方法,如果我们的系统没有使用 Spring 的话,可以尝试这种方式。

然后再resource目录下新增service/com.alibaba.csp.sentinel.init.InitFunc文件

com.deepinsea.common.config.FlowRuleInitFunc

文件的内容就是上面创建的FlowRuleInitFunc类的路径。

image-20220715025035289

我们还需要将上面定义的@PostConstruct注解初始化规则的方法注释,然后在@SentinelResource注解中添加fallback参数

package com.deepinsea.controller;
​
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * Created by deepinsea on 2022/7/13.
 */
@RestController
@RequestMapping("/sentinel")
public class SentinelResourceController {
​
    /**
     * spring初始化方法
     * @PostConstruct 是Spring带的注解,当前这个bean在被spring容器创建的时候,就会自动的调用
     * 被@PostConstruct这个注解修饰的方法进行初始化
     */
//    @PostConstruct // init-method
//    private static void initFlowRules() {
//        List<FlowRule> rules = new ArrayList<>(); //流控规则列表
//        FlowRule rule = new FlowRule(); //流控规则
//        rule.setResource("sentinel-resource"); //为哪个资源进行流控
//        rule.setGrade(RuleConstant.FLOW_GRADE_QPS); //设置流控规则类型QPS
//        rule.setCount(1); //设置受保护的资源阙值
//        rule.setLimitApp("default");
//        rules.add(rule); //将配置的规则放入规则列表中
//        FlowRuleManager.loadRules(rules); //加载配置好的规则
//    }
​
    // 原函数
    @GetMapping("/sentinel-resource") //注意接口不要与Sentinel定义的规则接口重复了
//    @SentinelResource(value = "sentinel-resource", blockHandler = "exceptionHandler")
    @SentinelResource(value = "sentinel-resource", blockHandler = "exceptionHandler", fallback = "fallbackHandler")
    public String test() {
        return "hi, this is SentinelResource test! ";
    }
​
    // Block异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
    public String exceptionHandler(BlockException ex) {
//        ex.printStackTrace(); //1.4以前的版本需要手动记录业务异常
        return "被限流了!";
    }
​
    // Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
    public String fallbackHandler() {
        return "被降级熔断了!";
    }
}

下面我们重启项目,然后删除控制台的全局限流和降级熔断规则,然后使用curl命令进行测试:

C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
hi, this is SentinelResource test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
被限流了!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
被限流了!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/sentinel-resource
hi, this is SentinelResource test!

测试成功,限流规则成功生效。

注意SPI注入的规则以及 @PostConstruct注解初始化的规则,优先级依旧低于Sentinel控制台定义的限流和降级熔断规则,记得删除。

另外,莫名的原因,原先通过SPI注入的规则可以生效;在使用@PostConstruct初始化规则后,调转测试突然不能生效了。并且,降级熔断规则这两种都测试失败,无法生效。总之,对新版Sentinel的规则适配的坑不少,这里对于旧版升级到新版Sentinel需要谨慎考虑

欢迎点赞,谢谢大佬了ヾ(◍°∇°◍)ノ゙