分布式系统流控、熔断:Sentinel的使用

前言

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。

  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。

  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。

  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

以上内容引自 Sentinel 官方介绍。在本文中,笔者将从实际应用的角度,来学习Sentinel的使用。

一、初识Sentinel

首先,我们需要引入Sentinel的依赖。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.7.2</version>
</dependency>

Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则 和 热点参数规则。

在这里,我们来展示一个流量控制和熔断降级的示例。

1、流量控制

流量控制,其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

我们以 QPS 为例,先来定义它的规则,相关属性含义见注释。

/**
 * 加载限流规则
 * @param resource
 */
public static void loadFlowRules(String resource){
    FlowRule rule = new FlowRule();
    //资源名称,可以是任意字符串
    rule.setResource(resource);
    //限流阈值
    rule.setCount(5);
    //限流阈值类型,设置为QPS。即每秒QPS大于5时,触发限流
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    //针对的调用来源
    rule.setLimitApp("default");
    //调用关系限流策略,默认按照资源本身
    rule.setStrategy(RuleConstant.STRATEGY_DIRECT);
    //限流效果,默认直接拒绝
    rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
    //是否集群限流
    rule.setClusterMode(false);
    FlowRuleManager.loadRules(Collections.singletonList(rule));
}

如上代码,当每秒的请求数达到 5 之后,就会直接拒绝当前时间窗口的后续请求。

接下来,我们把需要控制流量的代码用 Sentinel API SphU.entry("resource") 和 entry.exit() 包围起来即可。

public static void main(String[] args) throws InterruptedException {
    loadFlowRules("orderService");
    while (!stop){
        count.incrementAndGet();
        Entry entry = null;
        try {
            entry = SphU.entry(resource);
            logger.info("业务操作...{}",count.get());
        } catch (BlockException e) {
            logger.error("请求被限流...{}",count.get());
            Thread.sleep(1000);
        } finally {
            if (entry != null) {
                entry.exit();
            }
            if (count.get()>=20){
                stop = true;
            }
        }
    }
}

如上代码,我们先通过loadFlowRules()方法加载限流规则。然后将业务操作用Sentinel API包围起来。

我们定义的限流阈值是5,这里一共有20个请求。触发限流之后,我们的线程停顿1秒,以便度过当前的时间窗口,所以会有3个请求被限流。

运行代码,我们可以得到以下结果:

14:38:00.463  - 业务操作...1
14:38:00.465  - 业务操作...2
14:38:00.465  - 业务操作...3
14:38:00.465  - 业务操作...4
14:38:00.465  - 业务操作...5
14:38:00.494  - 请求被限流...6
14:38:01.494  - 业务操作...7
14:38:01.494  - 业务操作...8
14:38:01.495  - 业务操作...9
14:38:01.495  - 业务操作...10
14:38:01.495  - 业务操作...11
14:38:01.496  - 请求被限流...12
14:38:02.497  - 业务操作...13
14:38:02.497  - 业务操作...14
14:38:02.497  - 业务操作...15
14:38:02.497  - 业务操作...16
14:38:02.497  - 业务操作...17
14:38:02.497  - 请求被限流...18
14:38:03.498  - 业务操作...19
14:38:03.498  - 业务操作...20

2、熔断

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。

Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。

那怎么来衡量资源是否稳定呢?

Sentinel提供了三种方式,平均响应时间、异常比例和异常数。

我们拿平均响应时间为例,先来定义它的规则。

/**
 * 1秒内的5个请求,平均响应时间大于10ms,接下来的3秒内都会自动熔断。
 * @param resourceName
 */
public static void loadDegradeRule(String resourceName){
    List<DegradeRule> rules = new ArrayList<>();
    DegradeRule rule = new DegradeRule();
    //资源名称
    rule.setResource(resourceName);
    //阈值 - 10ms
    rule.setCount(10);
    //熔断策略 - RT模式
    rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
    //时间窗口 - 3s
    rule.setTimeWindow(3);
    //RT模式下,1秒内连续多少个请求的平均RT超出阈值,才可以触发熔断
    rule.setRtSlowRequestAmount(5);
    rules.add(rule);
    DegradeRuleManager.loadRules(rules);
}

如上代码,我们定义了熔断的规则,属性的含义见注释内容,然后来看测试用例。

public static void main(String[] args)throws InterruptedException {
    loadDegradeRule(resource);
    while (!stop){
        count.incrementAndGet();
        Entry entry = null;
        try {
            entry = SphU.entry(resource);
            logger.info("业务操作...{}",count.get());
            Thread.sleep(15);
        } catch (BlockException e) {
            if (e instanceof DegradeException){
                logger.error("触发熔断机制...{}",count.get());
                Thread.sleep(500);
            }
        } finally {
            if (entry != null) {
                entry.exit();
            }
            if (count.get()>=20){
                stop = true;
            }
        }
    }
    logger.info("----------------------------");
}

在上面的代码中,我们一共有20个请求。我们让线程停顿15ms使平均RT超过阈值,也就是超过10ms。

我们定义的规则里面是1秒内连续5个请求的平均RT超出阈值,就可以触发熔断,所以当第6个请求到达时,就会触发熔断。

熔断多久呢?就在3秒的时间窗口。

上面的测试代码中,在触发熔断之后,我们又手动让线程停顿了 1000ms ,所以每次熔断的请求会有3个。

是不是这样,我们运行代码,看下结果:

10:56:20.022 [main] INFO orderService - 业务操作...1
10:56:20.040 [main] INFO orderService - 业务操作...2
10:56:20.056 [main] INFO orderService - 业务操作...3
10:56:20.072 [main] INFO orderService - 业务操作...4
10:56:20.088 [main] INFO orderService - 业务操作...5
10:56:20.127 [main] ERROR orderService - 触发熔断机制...6
10:56:21.128 [main] ERROR orderService - 触发熔断机制...7
10:56:22.128 [main] ERROR orderService - 触发熔断机制...8
10:56:23.129 [main] INFO orderService - 业务操作...9
10:56:23.145 [main] INFO orderService - 业务操作...10
10:56:23.160 [main] INFO orderService - 业务操作...11
10:56:23.176 [main] INFO orderService - 业务操作...12
10:56:23.192 [main] INFO orderService - 业务操作...13
10:56:23.207 [main] ERROR orderService - 触发熔断机制...14
10:56:24.208 [main] ERROR orderService - 触发熔断机制...15
10:56:25.208 [main] ERROR orderService - 触发熔断机制...16
10:56:26.209 [main] INFO orderService - 业务操作...17
10:56:26.224 [main] INFO orderService - 业务操作...18
10:56:26.240 [main] INFO orderService - 业务操作...19
10:56:26.255 [main] INFO orderService - 业务操作...20
10:56:26.271 [main] INFO orderService - ----------------------------

至此,我们就可以说,Sentinel 能够正常工作了。

二、系统集成

上面只是一个很简单的Demo示例,如果我们希望在我们的SpringBoot项目中使用Sentinel,还需要一些工作。

1、Sentinel 控制台

Sentinel 提供一个轻量级的开源控制台,它是使用SpringBoot开发的。

它提供机器发现以及健康情况管理、监控(单机和集群),规则管理和推送的功能。

所以,我们先把这个控制台运行起来。

第一步,需要在https://github.com/alibaba/Sentinel/releases这个地址,下载最新版本的控制台 jar 包。

第二步,使用命令启动控制台程序,其中 -Dserver.port=9080 用于指定 Sentinel 控制台端口。

java -Dserver.port=9080 -Dcsp.sentinel.dashboard.server=localhost:9080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

第三步,我们的业务系统引入 Transport 模块来与 Sentinel 控制台进行通信。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>1.7.2</version>
</dependency>

第四步,在我们的业务系统中,设置JVM启动参数,用来指明Sentinel控制台的地址。

-Dcsp.sentinel.dashboard.server=127.0.0.1:9080

最后,启动我们的业务系统,然后打开Sentinel控制台,如果可以看到机器列表就可以了。


image

2、定义规则

在定义规则之前,我们需要规划好资源范围。

什么意思呢?比如我们拿一个订单业务来说,是不是所有的订单操作都算一个资源?还是拆分开来看,创建订单算一个资源,订单查询算另外一个资源。

所以,我们可以先把希望流控的资源名称定义出来。

public final class ResourceConstants {
    public static final String ORDER_SERVICE = OrderService.class.getName();
    public static final String ORDER_SERVICE_ORDERS = ORDER_SERVICE+".orders";
    public static final String ORDER_SERVICE_CREATE = ORDER_SERVICE+".create";
}

由于是一个SpringBoot项目,我们可以在系统启动的时候,来加载流控规则。

@Component
public class ApplicationStartup implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        initFlowRule(ResourceConstants.ORDER_SERVICE,5);
        initFlowRule(ResourceConstants.ORDER_SERVICE_ORDERS,5);
    }
    public void initFlowRule(String resourceName,int count) {
        FlowRule flowRule = new FlowRule(resourceName)
                .setCount(count)
                .setGrade(RuleConstant.FLOW_GRADE_QPS);
        List<FlowRule> list = new ArrayList<>();
        list.add(flowRule);
        FlowRuleManager.loadRules(list);
    }
}

然后,我们在Controller加入Sentinel的代码,来达到流控的效果。

@RequestMapping("/getOrders")
public ResponseEntity getOrders(){
    Entry entry = null;
    try {
        entry = SphU.entry(ResourceConstants.ORDER_SERVICE_ORDERS);
        return ResponseEntity.ok(orderService.orders());
    } catch (BlockException e) {
        logger.error("请求被限流...{}",e.getRule().getResource());
        return ResponseEntity.badRequest().body(e.getRule());
    } finally {
        if (entry != null) {
            entry.exit();
        }
    }
}

现在,我们拿JMeter来测试一下,启动10个线程来请求这个接口。只会通过5个请求,拒绝5个请求。

image

至此,我们已经可以在SpringBoot项目中简单使用Sentinel了,不过此时还有两个很明显的问题。

  • 在每个需要流控的地方,通过API硬编码,侵入性太强而且也不方便;
  • 流控规则只保留在内存中,系统重启就没了,没有持久化规则数据。

接下来,我们来解决上述的两个问题。

三、框架适配

得益于广泛的开源生态,Sentinel 提供开箱即用的与其它开源框架/库的整合模块。我们只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。

我们希望可以对 Web 请求进行流量控制,那么需要引入Sentinel 提供与 Servlet 的整合。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-web-servlet</artifactId>
    <version>1.7.2</version>
</dependency>

1、Filter配置

因为是SpringBoot应用,我们通过Configuration进行配置。

@Configuration
public class SentinelFilterConfig {
    @Bean
    public FilterRegistrationBean sentinelFilterRegistration() {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new CommonFilter());
        registration.addUrlPatterns("/*");
        registration.setName("sentinelFilter");
        registration.setOrder(1);
        return registration;
    }
}

在我们自己的业务代码中,就可以免去Sentinel API部分了。

@RequestMapping("/getOrders")
public ResponseEntity getOrders(){
    return ResponseEntity.ok(orderService.orders());
}

在流控规则不变的情况下,我们拿JMeter启动10个线程来请求这个接口。同样的只会通过5个请求,拒绝5个请求。

2、UrlBlockHandler

默认情况下,当请求被限流时会返回默认的提示页面。

我们可以在代码中调用WebServletConfig.setBlockPage(blockPage) 方法设定自定义的跳转 URL,当请求被限流时会自动跳转至设定好的 URL。

如果不打算让它跳转页面,我们也可以实现 UrlBlockHandler 接口并编写定制化的限流处理逻辑。

比如像下面这样,限流或熔断之后,会向客户端返回一个异常的HTTP状态码和提示信息。

public class SentinelUrlBlockHandler implements UrlBlockHandler {

    public static final String flowMsg = "触发流控机制~";
    public static final String degradeMsg = "触发熔断机制~";
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex){
        logger.error("熔断限流...{}",ex.getRule());
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        PrintWriter out = response.getWriter();
        if (ex instanceof FlowException){
            out.print(flowMsg);
        }else if (ex instanceof DegradeException){
            out.print(degradeMsg);
        }
        out.flush();
        out.close();
    }
}

然后将其注册至 WebCallbackManager 中。

WebCallbackManager.setUrlBlockHandler(new SentinelUrlBlockHandler());

3、UrlCleaner

Sentinel Web Filter 会将每个到来的不同的 URL 都作为不同的资源处理。

比如订单业务中的,创建订单、订单查询、订单删除等等,因为URL的不同,都会被当作不同的资源。

如果我们希望将这些操作都归到订单资源下/order/*,就需要实现 UrlCleaner 接口清洗一下资源。

比如像下面这样,将资源归类。比如/order/getOrders和/order/createOrder,都会变成/order/*

public class SentinelUrlClean implements UrlCleaner {
    @Override
    public String clean(String originUrl) {
        if (originUrl == null || originUrl.isEmpty()) {
            return originUrl;
        }
        int lastSlashIndex = originUrl.lastIndexOf("/");
        if (lastSlashIndex >= 0) {
            originUrl = originUrl.substring(0, lastSlashIndex) + "/*";
        }
        return originUrl;
    }
}

然后将其注册至 WebCallbackManager 中。

WebCallbackManager.setUrlCleaner(new SentinelUrlClean());

当时,更绝对一些,如果整个系统都采用一个资源,那么这里只返回一个固定的url也可以。

四、最佳实践

上面我们说到,现在的Sentinel规则数据都只保留在内存中,没办法做到集中管理和推送规则,不具备生产环境可用性。

规则管理及推送,一般有三种方式。

  • 原始模式

将规则推送至客户端并直接更新到内存中。重启即消失,不建议在生产环境中使用。

  • Pull 模式

客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等。不保证实时性,拉取过于频繁可能会导致性能问题。

  • Push 模式

规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心,有更好的实时性和一致性。生产环境下一般采用 push 模式的数据源。

生产环境下一般更常用的是 push 模式的数据源。对于 push 模式的数据源,如远程配置中心(ZooKeeper, Nacos, Apollo等等),推送的操作不应由 Sentinel 客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。因此推送规则正确做法应该是 ** 配置中心控制台/Sentinel 控制台 → 配置中心 → Sentinel 数据源 → Sentinel **,而不是经 Sentinel 数据源推送至配置中心。

接下来我们来实现由Nacos配置中心统一管理数据。

1、启动Nacos

关于Nacos本文不再多说,下载一个启动就好了。

2、引入依赖

NacosDataSource,官方已经提供了,我们引入相关依赖即可。

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-extension</artifactId>
    <version>1.7.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    <version>1.7.2</version>
</dependency>

3、从数据源中读取规则数据

在初始化NacosDataSource的时候,我们要指定Nacos的服务地址,groupId和dataId。

然后根据这些信息连接Nacos,去读取里面的数据。并且注册监听器,在Nacos配置中心的规则数据发生变化后,通知到客户端。

说起来可能比较复杂,但是作为客户端使用的话,其实比较简单。我们搞一个类,去连接它就可以了。

@Component
public class DataSourceRuleManager {

    private static final String remoteAddress = "localhost:8848";
    private static final String groupId = "sentinel.group";
    private static final String flowDataId = "flow.rule";

    @PostConstruct
    public void loadFlowRules() {
        FlowConverter converter = new FlowConverter();
        //连接Nacos,读取配置信息并通过converter将内容转换为对象
        ReadableDataSource<String, List<FlowRule>> flowRuleDataSource =
                new NacosDataSource<>(remoteAddress,groupId,flowDataId,converter);
        FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
    }
    //转换器 从Nacos配置中心读取到的数据转换为对象
    public class FlowConverter implements Converter {
        @Override
        public Object convert(Object source) {
            return JSON.parseArray(source.toString(),FlowRule.class);
        }
    }
}

配置完之后,我们就可以启动业务系统了。

4、从Nacos配置中心添加规则数据

现在就可以通过Nacos控制台,向配置中心添加规则数据了。

有一点需要注意的是,由于我们的转换器是通过JSON解析FlowRule类型的数组对象,所以配置内容里面的格式和属性名称要对应起来,否则解析会失败。


image

通过扩展读数据源的方式,当我们在Nacos配置中心发布新的内容后,相应的我们业务系统里面的规则也会更新,Sentinel控制台里面的规则也一样会同步更新,就实现了规则中心统一推送和持久化。

还有一种方式是直接通过 Sentinel 控制台 → 配置中心,这样的话需要修改dashboard的实现,过程虽然不难但比较复杂,由于篇幅有限,本文就不再赘述。感兴趣的朋友可以留言交流~

总结

本文简单介绍了分布式系统熔断、限流组件Sentinel的使用。为了达到生产环境的基本可用,包含了 Sentinel 与 Servlet 的整合和规则中心统一推送和持久化。

本文只是Sentinel生态中的一小部分,更多内容如多种策略的流控和熔断机制、黑白名单控制、框架适配、实现原理等内容,有时间后续分享~

原创不易,客官们点个赞再走嘛,这将是笔者持续写作的动力~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349