介绍

Spring Cloud Alibaba Sentinel 服务限流/熔断实战

实验环境准备

需要提前下载并启动sentinel的dashboard,下载路径及wiki,本次下载的是sentinel-dashboard-1.8.1

启动服务端

启动本地nacos(略)
启动sentinel的dashboard

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

tips: sentinel 控制台的默认账号和密码都是 “sentinel”

搭建微服务系统

这里在上一个dubbo的例子中修改,主要结构如下:

  • dubbo-sample-api: 服务接口定义,供 consumer/provider 引用
  • dubbo-provider-sample : Dubbo 服务端,对外提供一些服务
  • dubbo-consumer-sample: Spring Boot Web 应用,其中的一些 API 会作为 consumer 来调用 dubbo-provider 获取结果。里面一共定义了三个 API path:
    • /demo/hello: 接受一个 name 参数,会 RPC 调用后端的 FooService:sayHello(name) 方法。
    • /demo/time:调用后端的 FooService:getCurrentTime 方法获取当前时间;里面可以通过 slow 请求参数模拟慢调用。
    • /demo/bonjour/{name}: 直接调用本地 DemoService 服务。

配置sentinel

添加依赖

在 dubbo-provider-sample和dubbo-consumer-sample中的build.gradle文件中加入sentinel的依赖包,配置如下:

dependencies {
    implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel'
    implementation 'com.alibaba.csp:sentinel-apache-dubbo-adapter'
}

修改代码和配置

修改dubbo-provider-sample和dubbo-consumer-sample中application.properties文件,添加sentinel的配置,如下:

# Sentinel 控制台地址
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8849
# 取消Sentinel控制台懒加载
# 默认情况下 Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包
# 配置 sentinel.eager=true 时,取消Sentinel控制台懒加载功能
spring.cloud.sentinel.eager=true

在dubbo-consumer-sample中,增加sentinel的配置类,以此来处理限流异常

package com.example.dubboconsumersample.demos.sentinel;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.PrintWriter;

@Configuration
public class SentinelWebConfig {

	@Bean
	public BlockExceptionHandler sentinelBlockExceptionHandler() {
		return (request, response, e) -> {
			// 429 Too Many Requests
			response.setStatus(429);

			PrintWriter out = response.getWriter();
			out.print("Oops, blocked by Sentinel: " + e.getClass().getSimpleName());
			out.flush();
			out.close();
		};
	}
}

在dubbo-consumer-sample中,新建DemoService类,增加sentinel的限流配置,增加后代码如下

package com.example.dubboconsumersample.demos.sentinel;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;

@Service
public class DemoService {

	@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
	public String bonjour(String name) {
		return "Bonjour, " + name;
	}

	public String bonjourFallback(Throwable t) {
		if (BlockException.isBlockException(t)) {
			return "Blocked by Sentinel: " + t.getClass().getSimpleName();
		}
		return "Oops, failed: " + t.getClass().getCanonicalName();
	}
}

在dubbo-sample-api添加dubbo的接口类,如下

package com.example.dubbo.api;

/**
 * 该接口为 Dubbo 的服务端、消费端公用的接口定义。
 * 当前案例中,通过复制代码的方式实现接口发布,这不是最优雅的使用方法。更好的建议是通过maven坐标的方式独立维护api。
 */
public interface FooService {

	String sayHello(String name);

	long getCurrentTime(boolean slow);
}

在dubbo-provider-sample中新建FooServiceImpl类,来实现接口功能

package com.example.dubboprovidersample.service;

import com.example.dubbo.api.FooService;
import org.apache.dubbo.config.annotation.DubboService;

/**
 * demo dubbo sentinel
 */
@DubboService
public class FooServiceImpl implements FooService {
	@Override
	public String sayHello(String name) {
		return "Hello, " + name;
	}

	@Override
	public long getCurrentTime(boolean slow) {
		// Simulate slow invocations randomly.
		if (slow) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException ignored) {
			}
		}
		return System.currentTimeMillis();
	}
}

编译并启动dubbo-provider-sample和dubbo-consumer-sample两个应用

测试访问接口:

流控规则

流量控制配置

(略)根据文档操作

熔断降级规则

熔断降级通常用于自动切断不稳定的服务,防止调用方被拖垮导致级联故障。熔断降级规则通常在调用端,针对弱依赖调用进行配置,在熔断时返回预定义好的 fallback 值,这样可以保证核心链路不被不稳定的旁路影响。

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOWREQUESTRATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs,默认为 1s)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

下面我们来在 Web 应用中针对 Dubbo consumer 来配置慢调用熔断规则,并模拟慢调用来观察效果。我们在 web-api-demo 中针对 com.alibaba.csp.sentinel.demo.dubbo.FooService 服务调用配置熔断降级规则。

控制台配置的统计时长默认为 1s。在上面的这条规则中,我们设定慢调用临界值为 50ms,响应时间超出 50ms 即记为慢调用。当统计时长内的请求数 >=5 且慢调用的比例超出我们配置的阈值(80%)就会触发熔断,熔断时长为 5s,经过熔断时长后会允许一个请求探测通过,若请求正常则恢复,否则继续熔断。

我们的实例中 /demo/time API 可以通过 slow 请求参数模拟慢调用,当 slow=true 时该请求耗时会超过 100ms。我们可以用 ab 等压测工具或脚本,批量请求 localhost:8090/demo/time?slow=true,可以观察到熔断的返回

2021-02-19 10:26:46.415 ERROR 13588 --- [io-62000-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: SentinelBlockException: DegradeException] with root cause

java.lang.RuntimeException: SentinelBlockException: DegradeException
	at com.alibaba.csp.sentinel.slots.block.BlockException.block(BlockException:0) ~[sentinel-core-1.8.0.jar:1.8.0]
021

如果我们一直模拟慢调用,我们可以观察到熔断后每 5s 会允许通过一个请求,但该请求仍然是慢调用,会重新打回熔断,无法恢复。我们可以在触发熔断后,等待一段时间后手动发一个不带 slow=true 的正常请求,然后再进行请求,可以观察到熔断恢复。

需要注意的是,即使服务调用方引入了熔断降级机制,我们还是需要在 HTTP 或 RPC 客户端配置请求超时时间,来做一个兜底的防护。

注解方式自定义埋点

刚才我们看到的埋点都是 Sentinel 适配模块提供的自动埋点。有的时候自动埋点可能没法满足我们的需求,我们希望在某个业务逻辑的位置进行限流,能不能做到呢?当然可以!Sentinel 提供两种方式进行自定义埋点:SphU API 和 @SentinelResource 注解,前者最为通用但是代码比较繁杂,耦合度较高;注解方式侵入性较低,但有使用场景的限制。这里我们来动手在 Web 应用的 DemoService 上添加注解,来达到针对本地服务埋点的目标。

在 DemoService 中我们实现了一个简单的打招呼的服务:

@Service
public class DemoService {

	public String bonjour(String name) {
		return "Bonjour, " + name;
	}

}

下面我们给 bonjour 这个函数添加 @SentinelResource 注解,注解的 value 代表这个埋点的名称(resourceName),会显示在簇点链路/监控页面。


	@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
	public String bonjour(String name) {
		return "Bonjour, " + name;
	}

加上该注解后,再通过网关访问 /demo/bonjour/{name} 这个 API 的时候,我们就可以在簇点链路页面看到我们自定义的 DemoService#bonjour 埋点了。
添加注解埋点只是第一步。一般在生产环境中,我们希望在这些自定义埋点发生限流的时候,有一些 fallback 逻辑,而不是直接对外抛出异常。这里我们可以写一个 fallback 函数:

	public String bonjourFallback(Throwable t) {
		if (BlockException.isBlockException(t)) {
			return "Blocked by Sentinel: " + t.getClass().getSimpleName();
		}
		return "Oops, failed: " + t.getClass().getCanonicalName();
	}

完整代码如下:


import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;

@Service
public class DemoService {

	@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
	public String bonjour(String name) {
		return "Bonjour, " + name;
	}

	public String bonjourFallback(Throwable t) {
		if (BlockException.isBlockException(t)) {
			return "Blocked by Sentinel: " + t.getClass().getSimpleName();
		}
		return "Oops, failed: " + t.getClass().getCanonicalName();
	}
}

这样当我们自定义的 DemoService#bonjour 资源被限流或熔断的时候,请求会走到 fallback 的逻辑中,返回 fallback 结果,而不会直接抛出异常。我们可以配一个 QPS=1 的限流规则,然后快速请求后观察返回值:
通过页面刷新访问http://127.0.0.1:62000/demo/bonjour/theonefx 返回:

Blocked by Sentinel: FlowException

注意:使用 @SentinelResource 注解要求对应的类必须由 Spring 托管(即为 Spring bean),并且不能是内部调用(没法走到代理),不能是 private 方法。Sentinel 注解生效依赖 Spring AOP 动态代理机制。

配置自定义的流控处理逻辑

Sentinel 的各种适配方式均支持自定义的流控处理逻辑。以 Spring Web 适配为例,我们只需要提供自定义的 BlockExceptionHandler 实现并注册为 bean 即可为 Web 埋点提供自定义处理逻辑。其中 BlockExceptionHandler 的定义如下:


import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.PrintWriter;

@Configuration
public class SentinelWebConfig {

	@Bean
	public BlockExceptionHandler sentinelBlockExceptionHandler() {
		return (request, response, e) -> {
			// 429 Too Many Requests
			response.setStatus(429);

			PrintWriter out = response.getWriter();
			out.print("Oops, blocked by Sentinel WebConfig: " + e.getClass().getSimpleName());
			out.flush();
			out.close();
		};
	}
}

在sentinel的链路簇点资源/demo/bonjour/{name}处增加流控规则,测试访问http://127.0.0.1:62000/demo/bonjour/theonefx ,返回结果如下:

Oops, blocked by Sentinel WebConfig: FlowException

该 handler 会获取流控类型并打印返回信息,返回状态码为 429。我们可以根据实际的业务需求,配置跳转或自定义的返回信息。

对于注解方式,我们上一节已经提到,可以指定 fallback 函数来处理流控异常和业务异常,这里不再展开讲解;对于 Dubbo 适配,我们可以通过 DubboAdapterGlobalConfig 注册 provider/consumer fallback 来提供自定义的流控处理逻辑;对于 Spring Cloud Gateway 适配,我们可以注册自定义的 BlockRequestHandler 实现类来为网关流控注册自定义的处理逻辑。

对 Spring Cloud 其他组件的支持
Spring Cloud Alibaba Sentinel 还提供对 Spring Cloud 其它常用组件的支持,包括 RestTemplate、Feign 等。篇幅所限,我们不展开实践。大家可以参考 Spring Cloud Alibaba 文档来进行接入和配置。

总结

通过本教程,我们了解了流控降级作为高可用防护手段的重要性,了解了 Sentinel 的核心特性和原理,并通过动手实践学习了如何快速接入 SCA Sentinel 来为微服务进行流控降级。Sentinel 还有着非常多的高级特性等着大家去发掘,如热点防护、集群流控等,大家可以参考 Sentinel 官方文档来了解更多的特性和场景。

那么是不是服务的量级很小就不用进行限流防护了呢?是不是微服务的架构比较简单就不用引入熔断保护机制了呢?其实,这与请求的量级、架构的复杂程度无关。很多时候,可能正是一个非常边缘的服务出现故障而导致整体业务受影响,造成巨大损失。我们需要具有面向失败设计的意识,在平时就做好容量规划和强弱依赖的梳理,合理地配置流控降级规则,做好事前防护,而不是在线上出现问题以后再进行补救。

posted on 2021-02-19 13:49  学业未成  阅读(188)  评论(0编辑  收藏  举报