2流高手速成记(之八):基于Sentinel实现微服务体系下的限流与熔断

我们接上回

上一篇中,我们进行了简要的微服务实现,也体会到了SpringCloudAlibaba的强大和神奇之处

我们仅改动了两个注释,其他全篇代码不变,原来的独立服务就被我们分为了provider和consumer两个独立部分,二者各司其职,分工明确

篇尾我们留下了一个疑问,consumer对provider有依存关系,如果下游的provider出现异常,上游的consumer如何自处?

其实这种单一的上下游关系仅是微服务日常运转下各种复杂情境的一个缩影,真实的生产环境下各个微服务节点可能会形成更为复杂的依赖关系,那我们该如何解决这些问题?

本节中我们引入Sentinel框架:

什么是 Sentinel?

在基于 SpringCloud 构建的微服务体系中,服务间的调用链路会随着系统的演进变得越来越长,这无疑会增加了整个系统的不可靠因素。

在并发流量比较高的情况下,由于网络调用之间存在一定的超时时间,链路中的某个服务出现宕机都会大大增加整个调用链路的响应时间,而瞬间的流量洪峰则会导致这条链路上所有服务的可用线程资源被打满,从而造成整体服务的不可用,这也就是我们常说的 “雪崩效应”。

而在微服务系统设计的过程中,为了应对这样的糟糕情况,最常用的手段就是进行 ”流量控制“ 以及对网络服务的调用实现“熔断降级”。因此,Sentinel 就因运而生了。

Sentinel 是一款面向分布式服务架构的轻量级流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来保障服务的稳定性,核心思想是:根据对应资源配置的规则来为资源执行相应的流控/降级/系统保护策略,Sentinel 的主要特性如下图:

 

 

 以上内容来源于Sentinel官方文档,看到这一大堆不明所以的名词接释,新入坑的同学可能已经劝退了。。

接下来我们依然沿用前几篇的思想,先不要管这一堆的理论,先来看怎么用,实际运用过程中如何达到我们预期的效果,返回来再看这些名词,你自然会有更清晰的认知

Sentinel怎么用?

Sentinel的使用分为两部分 —— 代码和控制台

首先我们先下载Sentinel控制台://github.com/alibaba/Sentinel/tags,本节中我使用的是Sentinel1.8版本

下载编译好的jar包,而后执行命令行:

java -jar .\sentinel-dashboard.jar --server.port=9999

之后我们打开浏览器,输入地址://127.0.0.1:9999/

打开Sentinel控制台如图所示:

 

 

整个面板是空白的,什么都没有?!不用着急,我们继续往下看代码的部分

上一节我们提到了微服务环境下consumer工程存在的隐患问题,接下来我们就要消除这个隐患!

dubbo-nacos-consumer工程引入Sentinel依赖库

        <!-- 引入sentinel依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

这一步肯定是必须的,没什么好解释的

添加Sentinel相关配置

 

 

在SpringCloudAlibaba体系下,我们以Nacos为配置中心,所以我们编辑Nacos中的相关配置即可,非常方便

我把代码文本也贴出来,便于大家自行复制

# sentinel看板配置
spring.cloud.sentinel.transport.dashboard = 127.0.0.1:9999
# 开启对sentinel看板的饥饿式加载。sentinel默认是懒加载机制,只有访问过一次的资源才会被监控,通过关闭懒加载,在项目启动时就连接sentinel控制台
spring.cloud.sentinel.eager = true

这两句的主要意图在于将Consumer工程关联到Sentinel控制台,便于后续通过控制台统一管控

改造原有的PersonController

我们先创建一个ViewObject

 

 

代码如下:

package com.example.dubbonacosconsumer.vo;

import com.example.dubbonacosapi.model.Person;

import java.util.List;

public class SelectRetVo {
    private List<Person> persons;
    private String error;

    public List<Person> getPersons() {
        return persons;
    }

    public void setPersons(List<Person> persons) {
        this.persons = persons;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }
}

然后是PersonController的改造

package com.example.dubbonacosconsumer.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.dubbonacosapi.model.Person;
import com.example.dubbonacosapi.service.PersonService;
import com.example.dubbonacosconsumer.vo.SelectRetVo;
import org.apache.dubbo.config.annotation.DubboReference;
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;

import java.util.ArrayList;

@RestController
@RequestMapping("/person")
public class PersonController {
    @DubboReference
    PersonService service;

    @PostMapping("/insert")
    public Integer insert(Person person) {
        return service.insert(person);
    }

    @PostMapping("/update")
    public Integer update(Person person) {
        return service.update(person);
    }

    @PostMapping("/delete")
    public Integer delete(int id) {
        return service.delete(id);
    }

    @GetMapping("/select")
    @SentinelResource(value = "person/select", blockHandler = "selectBlock")
    public SelectRetVo select() {
        SelectRetVo vo = new SelectRetVo();
        vo.setPersons(service.select());
        vo.setError("ok");
        return vo;
    }

    public SelectRetVo selectBlock(BlockException e) {
        SelectRetVo vo = new SelectRetVo();
        vo.setPersons(new ArrayList<Person>());
        vo.setError(“当前访问人数过多,请稍后...”);
        return vo;
    }
}

我们将原有的select方法的返回值从原来的List<Person>升级为SelectPersonVo,后者在前者原有基础上扩展了一个error字段,用于返回异常信息

接着是本节的关键:select方法新增@SentinelResource注解,我们前边讲过:Sentinel根据对应资源配置的规则来为资源执行相应的流控/降级/系统保护策略

因此这个注解的作用是——用于标明这是一个Sentinel系统中的资源

value代表这个资源的名称是person/select,这个名字可以随自己的习惯自定义

blockHandler指定了一个方法,这个方法在Sentinel系统触发某种规则的时候会被执行,关于“某种规则”我们稍后会讲

这里指定的selectBlock方法,在定义时是有硬性要求的:

1. 保留select方法一样的参数,外加一个BlockException参数

2. 返回值必须和select方法相同 —— 所以明白为什么要额外定义一个SelectRetVo了吧?

控制台的使用

我们像上一节一样,分别启动provider和consumer,然后刷新Sentinel控制台页面

 

 

看到这个界面的时候,你是否有种豁然开朗的感觉?

因为consumer中定义了Sentinel资源,所以当dubbo-nacos-consumer工程执行之后,控制台会有相关显示

而功能菜单中有N多项的名称都是XX规则,这也就是前边我们定义的blockHandler对应的某种规则

我们在post中调用consumer中的select方法,此时实时监控页面显示如下内容:

 

 

这里的person/select自然就是我们定义的【资源】,控制台配套显示了其各项指标数据,很直观也很方便

下边还有一项/person/select(开头多个/),这个又是什么?这里先直接告诉大家答案——Sentinel默认会将所有Controller添加请求映射的方法视为资源

那我们额外添加一个@SentinelResource注解是否多此一举?答案是否,因为Controller生成的默认Sentinel资源是不带自定义规则触发方法的

因此@SentinelResource依然是有必要的,待本篇内容结束之后,大家可以自行验证这个说法

而从第二项【簇点链路】中,我们也能看到person/select和/person/select本身具备从属关系

 

 

流控规则

第三项【流控规则】对应了本节标题中提到的【限流】

 

 

这里涉及到三个概念:

阈值类型

QPS —— 服务器每秒接受的最大请求数

线程数 —— 服务器能容忍的最大线程占用数,一般用于保护服务器的业务线程池不被耗尽

流控模式

直接 —— 默认项,接口到达限流要求时,规则直接生效

关联 —— 当关联在资源到达阈值时,直接限流自己,一般应用于效率让步的诉求

链路 —— 记录链路流量,当入口资源到达阈值,则限流自己

流控效果

快速失败 —— 默认项,超出阈值后新请求直接拒绝

排队等待 —— 让请求匀速通过(漏桶算法),每个请求在一个允许的延迟时长范围

Warm up —— 冷启动模式,防止流量瞬间暴增直接将服务压垮,而是逐渐外放请求上限

我们先按图所示创建一个最简单的限流规则 —— QPS阈值为1,直接快速失败

而后我们启动两个post,同时向服务器端发送select请求,则结果对比如下:

 

 

 由于QPS指定的阈值为1,因此同时发起的第二个请求会因触发限流规则而执行blockHandler方法

而我们针对 阈值类型、流控模式、流控效果 这三个指标交叉组合,可以创造出适用于各种服务器场景之下的限流规则

再比如我们按如下设置限流规则

 

这种设置方式代表我们所能允许select方法同时请求的上限值为2000,但是这个数值是在5分钟(300秒)之间逐步放开的,比如我们用这种模式可以应对类似双11期间大力度优惠而带来的突发流量洪峰

通过【限流】可以很好的起到保护服务器的作用,在特殊时期针对流量进行削峰填谷,使得服务器处在一个长期稳定的环境

降级规则

第四项【降级规则】对应了本节标题中的【熔断】

 

 所谓“熔断”,其实最早这个概念来源于家庭电路中的保险丝,当家庭中的某件家用电器功率异常,或者某处电路出现短路,

为防止电流过大造成更大的损失,此刻保险丝即会【熔断】,造成我们日常所说的“掉闸”

熔断策略

慢调用比例 —— 慢调用其实就是响应超过预定时长,当这样的调用到达一定比例后,触发降级规则

异常比例 —— 方法调用出现异常数到达一定比例后,触发降级规则

异常数 —— 方法调用出现异常到达一定数目后,触发降级规则

我们按照图中所示创建一条降级规则,其含义为【任意1次并发请求(QPS)中有1次异常则触发熔断规则,时长为10秒】

这里的10秒时间是我们一个大概的预估值,一般理解为系统达到自愈效果需要大概10秒左右

之后我们直接关停下游的provider,则第一次请求会导致异常,而第二次请求则触发降级规则

 

若我们不设定降级规则,则大量的异常请求会堆积在consumer一侧,导致consumer最终崩溃

而熔断机制的设立,可以使得下游provider异常的情况下,上游的consumer依然做出正常的应答,

而10秒钟provider服务自愈之后,所有的异常影响将消失于无形 —— 这就是微服务方案下针对我们上一节所遗留的问题给出的答案

其他熔断策略同理,我们可以自行尝试

本节我们重点讲述了Sentinel系统中的限流熔断,而除此之外,Sentinel还有更多更丰富的规则设定,可以应对微服务体系中更加复杂多变的场景

这里我们推荐几篇文章,大家感兴趣可以自行阅读

sentinel 限流熔断神器详细介绍_张维鹏的博客-CSDN博客_sentinel熔断

Sentinel限流熔断最全教程_思月行云的博客-CSDN博客

本节内容到此为止,而微服务领域的探索却远不止于此

众多节点之间的复杂调用,不同微服务不同的认证方式,跨域问题等等一系列问题接踵而至,我们又当如何应对?

请期待下一节内容 —— 基于SpringCloudGateway的网关设计,谢谢~

 

 

e.getMessage()