阅读 1316

分布式系统流控、熔断: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控制台,如果可以看到机器列表就可以了。

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个请求。

至此,我们已经可以在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类型的数组对象,所以配置内容里面的格式和属性名称要对应起来,否则解析会失败。

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

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

总结

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

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

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