一、前言
思考:
1、当服务访问量达到一定程度,流量扛不住的时候,该如何处理?
2、服务之间相互依赖,当服务A出现响应时间过长,影响到服务B的响应,进而产生连锁反应,直至影响整个依赖链上的所有服务,该如何处理?
这是分布式、微服务开发不可避免的问题。
1.1 分布式系统遇到的问题
在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务. 如图所示: 调用三个依赖服务会共享商品详情服务的线程池。如果其中的商品评论服务不可用,就会出现线程池里所有线程都因等待响应而被阻塞,,从而造成服务雪崩。 如图所示:
服务雪崩效应:因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程,就叫服务雪崩效应
导致服务不可用的原因: 程序Bug,大流量请求,硬件故障,缓存击穿…
- 【大流量请求】:在秒杀和大促开始前,如果准备不充分(缓存预热),瞬间大量请求会造成服务提供者的不可用;
- 【硬件故障】:可能为硬件损坏造成的服务器主机宕机,,网络硬件故障造成的服务提供者的不可访问。
- 【缓存击穿】:一般发生在缓存应用重启,缓存失效时高并发,所有缓存被清空时,以及短时间内大量缓存失效时。大量的缓存不命中,使请求直击后端,造成服务提供者超负荷运行,引起服务不可用。
- 【重试机制】:在服务提供者不可用的时候,会出现大量重试的情况:用户重试、代码逻辑重试,这些重试最终导致:进一步加大请求流量。
所以归根结底导致雪崩效应的最根本原因是:大量请求线程同步等待造成的资源耗尽。当服务调用者使用同步调用时, 会产生大量的等待线程占用系统资源。一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了。
1.2 解决方案
超时机制
在不做任何处理的情况下,服务提供者不可用会导致消费者请求线程强制等待,而造成系统资源耗尽。加入超时机制,一旦超时,就释放资源。由于释放资源速度较快,一定程度上可以抑制资源耗尽的问题。
服务限流(资源隔离)
限制请求核心服务提供者的流量,使大流量拦截在核心服务之外,这样可以更好的保证核心服务提供者不出问题。
对于一些出问题的服务可以限制流量访问,只分配固定线程资源访问,这样能使整体的资源不至于被出问题的服务耗尽,进而整个系统雪崩。
那么服务之间怎么限流,怎么资源隔离?例如可以通过线程池+队列的方式,通过信号量的方式。
如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用。
服务熔断
远程服务不稳定或网络抖动时暂时关闭,就叫服务熔断。
现实世界的断路器大家肯定都很了解,断路器实时监控电路的情况,如果发现电路电流异常,就会跳闸,从而防止电路被烧毁。
软件世界的断路器可以这样理解:实时监测应用,如果发现在一定时间内失败次数/失败率达到一定阈值,就“跳闸”,断路器打开——此时,请求直接返回,而不去调用原本调用的逻辑。跳闸一段时间后(例如10秒),断路器会进入半开状态,这是一个瞬间态,此时允许一次请求调用该调的逻辑,如果成功,则断路器关闭,应用正常调用;如果调用依然不成功,断路器继续回到打开状态,过段时间再进入半开状态尝试——通过”跳闸“,应用可以保护自己,而且避免浪费资源;而通过半开的设计,可实现应用的自我修复。
所以,同样的道理,当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源。比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个依赖了,这个时候就应该使用断路器避免资源浪费。
服务降级
有服务熔断,必然要有服务降级。
所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据) 。这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景。
二、Sentinel: 分布式系统的流量防卫兵
2.1 Sentinel 是什么
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
源码地址:https://github.com/alibaba/Sentinel 官方文档:https://github.com/alibaba/Sentinel/wiki
Sentinel具有以下特征:
- 丰富的应用场景: Sentinel 承接了阿里巴巴近10年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。
- 完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
- 完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。
阿里云提供了企业级的Sentinel服务,应用高可用服务AHAS。
Sentinel和Hystrix对比
https://github.com/alibaba/Sentinel/wiki/Sentinel-%E4%B8%8E-Hystrix-%E7%9A%84%E5%AF%B9%E6%AF%94
2.2 Sentinel 工作原理
2.2.1 基本概念
1、资源: 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
2、只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
3、规则: 围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
2.2.2 Sentinel工作主流程
https://github.com/alibaba/Sentinel/wiki/Sentinel%E5%B7%A5%E4%BD%9C%E4%B8%BB%E6%B5%81%E7%A8%8B
在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:
NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;
2.3 Sentinel快速开始
在官方文档中,定义的Sentinel进行资源保护的几个步骤:
1、定义资源 2、定义规则 3、检验规则是否生效
Entry entry = null;
try {
entry = SphU.entry("自定义资源名");
} catch (BlockException ex) {
} catch (Exception ex) {
Tracer.traceEntry(ex, entry);
} finally {
if (entry != null) {
entry.exit();
}
}
Sentinel资源保护的方式
1、API实现
1、引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.0</version>
</dependency>
2、编写测试逻辑
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
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 lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@RestController
public class SentinelController {
private static final String RESOURCE_NAME = "hello";
@PostConstruct
private static void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource(RESOURCE_NAME);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
@RequestMapping(value = "/hello")
public String hello() {
Entry entry = null;
try {
entry = SphU.entry(RESOURCE_NAME);
String str = "hello world";
log.info("=====" + str);
return str;
} catch (BlockException e1) {
log.info("【限流降级触发】block!");
} catch (Exception ex) {
Tracer.traceEntry(ex, entry);
} finally {
if (entry != null) {
entry.exit();
}
}
return null;
}
}
我们代码中定义的规则是qps超过1就会触发限流或降级,我们多次调用该API测试效果: 发现此时限流和降级会被触发。
该方式缺点:
- 业务侵入性很强,需要在controller中写入非业务代码.
- 配置不灵活,若需要添加新的受保护资源,需要手动添加init方法来添加流控规则
2、基于@SentinelResource注解实现
注意:注解方式埋点不支持 private 方法。
@SentinelResource 注解用来标识资源是否被限流、降级。blockHandler 定义当资源内部发生了BlockException应该进入的方法(捕获的是Sentinel定义的异常)fallback 定义的是资源内部发生了Throwable应该进入的方法exceptionsToIgnore 配置fallback可以忽略的异常
源码入口:com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect
1、引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.8.0</version>
</dependency>
2、配置切面支持(如果是SprinBoot项目,不用配置。Spring需要配置)
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
3、UserController中编写测试逻辑,添加@SentinelResource ,并配置blockHandler 和fallback
@Slf4j
@RestController
public class UserController {
@RequestMapping(value = "/findOrderByUserId/{id}")
@SentinelResource(
value = "findOrderByUserId",
fallback = "fallback",
fallbackClass = ExceptionUtil.class,
blockHandler = "handleException",
blockHandlerClass = ExceptionUtil.class)
public Result findOrderByUserId(@PathVariable("id") Integer id) {
log.info("findOrderByUserId method execute successfully...");
throw new RuntimeException();
}
}
@Setter
@Getter
@NoArgsConstructor
@Builder
public class Result {
private int code;
private String message;
public Result(int code, String message) {
this.code = code;
this.message = message;
}
public static Result error(int code, String message) {
return new Result(code, message);
}
public static Result succ(int code, String message) {
return new Result(code, message);
}
}
4、编写ExceptionUtil,注意如果指定了class,方法必须是static 方法
public class ExceptionUtil {
public static Result fallback(Integer id, Throwable e) {
return Result.error(-2, "===被异常降级啦===");
}
public static Result handleException(Integer id, BlockException e) {
return Result.error(-2, "===被限流啦===");
}
}
Sentinel控制台构建
官网文档:https://github.com/alibaba/Sentinel/wiki/%E6%96%B0%E6%89%8B%E6%8C%87%E5%8D%97
流控规则设置可以通过Sentinel dashboard 配置
注意:资源创建完成后,需要定义规则(比如qps),那么此时我们不能在代码层面指定了,因为层面修改规则后需要重启。所以此时我们需要借助Sentinel控制台来指定规则。控制台制定的规则会推送到应用中。
1、引入maven依赖(注意版本)
客户端需要引入Transport 模块来与Sentinel控制台进行通信。
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.0</version>
</dependency>
- 启动时加入 JVM 参数
-Dcsp.sentinel.dashboard.server=consoleIp:port 指定控制台地址和端口。更多的参数参见启动参数文档。 - 确保应用端有访问量(不然没有监控数据)
2、获取Sentinel控制台
下载控制台sentinel-dashboard-1.8.0.jar 包并在本地启动:可以参考官网:https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0
获取Sentinel控制台: 可以从 release 页面下载最新版本的控制台 jar 包。
也可以从最新版本的源码自行构建 Sentinel 控制台:
- a. 下载 控制台工程
- b. 使用以下命令将代码打包成一个 fat jar:
mvn clean package
3、启动控制台
java -jar sentinel-dashboard-1.8.0.jar
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.0.jar
用户可以通过如下参数进行配置:
-Dsentinel.dashboard.auth.username=sentinel 用于指定控制台的登录用户名为 sentinel; -Dsentinel.dashboard.auth.password=123456 用于指定控制台的登录密码为 123456;如果省略这两个参数,默认用户和密码均为 sentinel; -Dserver.servlet.session.timeout=7200 用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟;
注意:注:若您的应用为 Spring Boot 或 Spring Cloud 应用,您可以通过 Spring 配置文件来指定配置,详情请参考 Spring Cloud Alibaba Sentinel 文档。
成功启动后访问http://localhost:8080/#/login ,默认用户名密码: sentinel/sentinel
Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包,所以要确保客户端有访问量。
4、服务启动类添加JVM参数
我们此时需要在服务启动类中添加JVM参数,来让服务与控制台进行通信。
-Dproject.name=sentinel-demo -Dcsp.sentinel.dashboard.server=127.0.0.1:8080 -Dcsp.sentinel.api.port=8719
-Dproject.name 控制台要显示的服务名称-Dcsp.sentinel.dashboard.server 控制台服务地址和端口(默认8080)Dcsp.sentinel.api.port 服务与Sentinel控制台交互的端口,默认为8719
然后我们重新启动服务。然后确保
5、登录控制台
确保应用端有访问量,这样才会有数据。 我们打开流控规则: 可以看到,我们之前测试的hello的规则也被推送到控制台了。
再点击簇点链路,可以看到被保护的资源: 这样我们就可以对资源进行流控或者降级等操作了。 设置完之后再来测试,发现频繁访问之后会触发我们的限流方法。
2.4 Spring Cloud Alibaba整合Sentinel
官网文档配置地址:https://gitee.com/rmlb/Sentinel/wikis/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81
1、引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2、代码创建
Controller类
@Slf4j
@RestController
public class UserController {
@Autowired
OrderFeignService orderFeignService;
@RequestMapping(value = "/findOrderByUserId/{id}")
@SentinelResource(
value = "findOrderByUserId",
fallback = "findOrderByUserIdFallback",
fallbackClass = CommonFallback.class,
blockHandler = "findOrderByUserIdHandleException",
blockHandlerClass = CommonBlockHandler.class)
public Result findOrderByUserId(@PathVariable("id") Integer id) {
Result r = orderFeignService.findOrderByUserId(id);
return r;
}
}
Result返回结果类:
public class Result extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public Result() {
put("code", 0);
put("msg", "success");
}
public static Result error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static Result error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static Result error(int code, String msg) {
Result result = new Result();
result.put("code", code);
result.put("msg", msg);
return result;
}
public static Result ok(String msg) {
Result result = new Result();
result.put("msg", msg);
return result;
}
public static Result ok(Map<String, Object> map) {
Result result = new Result();
result.putAll(map);
return result;
}
public static Result ok() {
return new Result();
}
public Result put(String key, Object value) {
super.put(key, value);
return this;
}
}
@SentinelResource 注解
注意:注解方式埋点不支持 private 方法。
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:
-
value :资源名称,必需项(不能为空) -
entryType :entry 类型,可选项(默认为 EntryType.OUT) -
blockHandler / blockHandlerClass : blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数签名和位置要求: ===> a. 函数访问范围需要是 public; ===> b. 注意对应的函数必需为 static 函数,否则无法解析; ===> c. 参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException; ===> d. 返回类型需要与原方法相匹配; ===> e. blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象. -
fallback / fallbackClass :fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求: ===> a. 返回值类型必须与原函数返回值类型一致; ===> b. 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。 ===> c. fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
CommonBlockHandler和CommonFallback 类:
public class CommonBlockHandler {
public static Result findOrderByUserIdHandleException(Integer id, BlockException exception) {
return Result.error(-1, "===被限流啦===" + exception);
}
}
public class CommonFallback {
public static Result findOrderByUserIdFallback(Integer id, Throwable e) {
return Result.error(-2, "===被异常降级啦===" + e.getMessage());
}
}
配置: Spring Cloud Alibaba 若您是通过 Spring Cloud Alibaba 接入的 Sentinel,则无需额外进行配置即可使用 @SentinelResource 注解。
Spring AOP 若您的应用使用了 Spring AOP(无论是 Spring Boot 还是传统 Spring 应用),您需要通过配置的方式将 SentinelResourceAspect 注册为一个 Spring Bean:
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
3、添加yml配置,为微服务设置sentinel控制台地址
注意:若您的应用为 Spring Boot 或 Spring Cloud 应用,您可以通过 Spring 配置文件来指定配置,详情请参考 Spring Cloud Alibaba Sentinel 文档。
server:
port: 9010
spring:
application:
name: mall-sentinel
cloud:
nacos:
discovery:
namespace: mall
server-addr: 192.168.131.172:8848
sentinel:
transport:
dashboard: 127.0.0.1:8080
management:
endpoints:
web:
exposure:
include: '*'
这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。
添加Sentinel后,需要暴露/actuator/sentinel端点。而Springboot默认是没有暴露该端点的,所以需要配置(上面的management部分配置)。配置后重新启动服务,测试http://localhost:9010/actuator/sentinel 发现http://custonhost:port/actuator/sentinel可以访问成功。
然后我们再登录sentinel:
3、在sentinel控制台中设置流控规则
我们点击簇点链路,选择列表模式,就可以看到我们要保护的资源: 可以看到,我们代码中设置的资源名为:findOrderByUserId ,但是列表中多了一个/findOrderByUserId/{id} 。这是因为sentinel会将Controller中的API自动注册为保护资源。
我们使用注解形式创建的资源更灵活,可以是任意一个Service方法,或者是restTemplate调用的方法等。
现在,我们来对findOrderByUserId 设置流控规则:
- 资源名: 接口的API
- 针对来源: 默认是default,当多个微服务都调用这个资源时,可以配置微服务名来对指定的微服务设置阈值
- 阈值类型: 分为QPS和线程数 假设阈值为10
- QPS类型: 只得是每秒访问接口的次数>10就进行限流
- 线程数: 为接受请求该资源分配的线程数>10就进行限流
测试: 因为QPS是1,所以1秒内多次访问会出现如下情形,因为此时触发了blockHandler = "findOrderByUserIdHandleException" 配置,执行并返回了findOrderByUserIdHandleException方法的结果。这也意味着我们的流控规则已经配置生效了。 我们接着来测试一个接口默认生成的资源:/findOrderByUserId/{id} ,这个资源是根据API自动生成的,我们也没有配置blockHandler,那我们来对这个资源设置一个流控规则,然后删除我们之前的资源findOrderByUserId 的流控规则,然后看看会发生什么: 可以测试到失败后的效果如下: 这里显示的是默认的流控错误。
4、查看所有流控规则
我们可以点击流控规则来查看设置的所有规则。 注意:此时的流控规则都是基于内存的,重启服务之后就会消息。具体的持久化和更多的控制台我们后面会学到。
4、查看flowRules
访问http://localhost:9010/actuator/sentinel, 可以查看flowRules:
2.5 微服务和Sentinel Dashboard通信原理
Sentinel控制台与微服务端之间,实现了一套服务发现机制,集成了Sentinel的微服务都会将元数据传递给Sentinel控制台,架构图如下所示: 可以通过http://localhost:8719/api 查看推送规则: 如果无法访问,将端口+1再访问,如:http://localhost:8720/api
|