在微服务架构中,我们的业务是拆分成一个一个的单体服务,一个服务调用另一个服务时,请求不断发送,如果出现网络波动或者故障,延迟处理,请求就会不断堆积,最终可能造成系统的瘫痪,经典案例就是服务雪崩。
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证100%可用,单个实例故障时,处理请求缓慢或者没有响应,导致上层调用它的服务实例也变慢,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这种由单个服务引发的级联故障称为服务雪崩。
在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
服务响应慢或⼤量超时熔断该服务,后续调⽤直接返回,直到⽬标服务出现好转。
这种模式主要是参考电路熔断,如果⼀条线路电压过⾼,保险丝会熔断,防⽌⽕灾。放到我们的系统中,如果某个⽬标服务调⽤慢或者有⼤量超时,此时,熔断该服务的调⽤,对于后续调⽤请求,不在继续调⽤⽬标服务,直接返回,快速释放资源。如果⽬标服务情况好转则恢复调⽤。
不同业务使⽤不同线程池,不同业务间的请求互不影响,某⼀类业务耗尽线程池资源,后续请求直接返回。
这种模式就像对系统请求按类型划分成⼀个个⼩岛的⼀样,当某个⼩岛被⽕少光了,不会影响到其他的⼩岛。例如可以对不同类型的请求使⽤线程池来资源隔离,每种类型的请求互不影响,如果⼀种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调⽤后续资源。这种模式使⽤场景⾮常多,例如将⼀个服务拆开,对于重要的服务使⽤单独服务器来部署。
对应⽤QPS有⼀个合理的预期,⾼于预期阀值时,后续请求会直接返回。
上述的熔断模式和隔离模式都属于出错后的容错处理机制,⽽限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最⾼的QPS阈值,若⾼于设置的阈值则对该请求直接返回,不再调⽤后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。
服务降级是当服务器压⼒剧增的情况下,根据实际业务情况及流量,对⼀些服务和⻚⾯有策略的不处理或换种简单的⽅式处理,从⽽释放服务器资源以保证核⼼交易的能正常运⾏。
服务降级主要⽤于当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运⾏,将⼀些 不重要 或 不紧急 的服务或任务进⾏服务的 延迟使⽤ 或 暂停使⽤。
例如,B站部分服务器某⼀时刻突发⽹络故障,⼤量请求⼀下⼦转移到剩余正常的集群节点上,导致服务器超负荷运转,各个组件均⽆法正常访问,这时候就需要使⽤降级模式,在故障恢复之前,优先保障视频服务、⽀付等核⼼组件⼯作正常,⽽直播、搜索、动态等次要组件降级,请求延迟或快速返回。
这里主要介绍 Sentinel
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
轻量级的流量控制,熔断降级Java库。
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
响应时间是指系统对请求作出响应的时间。
指系统在单位时间内处理请求的数量。
是指系统可以同时承载的正常使用系统功能的用户的数量。
Queries Per Second 每秒请求数量。
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。
Sentinel 作为一个调配器,流量控制有以下几个角度:
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。
Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。
Sentinel 对这个问题采取了两种手段:
Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
Sentinel 的主要工作机制如下:
从 release 页面 下载最新版本的控制台 jar 包 sentinel-dashboard.jar
启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
其中 -Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080。
注意:只有1.6.0及以上版本,才有这个简单的登录页面。默认用户名和密码都是sentinel。对于用户登录的相关配置可以在启动命令中增加下面的参数来进行配置:
本地启动,访问:localhost:8080,输入账户密码登录后,可以看到如下页面:
登录成功后
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
# actuator
management:
endpoints:
web:
exposure:
include: '*'
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # dashboard 控制台地址
# 如果有多套网络,又无法正确获取本机IP,则需要使用下面的参数设置当前机器可被外部访问的IP地址,供admin控制台使用
# client-ip:
eager: true # 默认情况下 sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包,取消sentinel控制台懒加载功能
@RestController
public class TestController{
@GetMapping("/list")
public String test(){
return "sentinel test";
}
}
启动项目,访问 /list 可以看到
可以看到服务已经注册到sentinel 上了。
点击“簇点链路”菜单,然后在需要添加流控规则的资源点击“+流控”按钮弹出流控规则设置弹窗。
阈值类型中QPS表示每秒访问的次数,线程数表示同时可以访问的线程的数量,单机阈值则位具体的数据,如图所示:表示每秒只允许访问1次,超过1次会报异常。
点击之前保存的流控规则的编辑按钮,并打开高级选项
默认的流控模式,直接限制资源名的QPS或者线程数
编辑关联资源 /list1 ,并点击保存按钮。
此时表示,如果 /list1 的访问 QPS 超过1,则 /list 会被sentinel限流(即 /list1 超量,/list 限流)。
测试代码
@RestController
public class TestController {
@Autowired
private ConsumerHelloServiceImpl consumerHelloService;
@GetMapping("/list")
public String list(){
consumerHelloService.test();
return "sentinel list";
}
@GetMapping("/list1")
public String list1(){
consumerHelloService.test();
return "sentinel list1";
}
@SneakyThrows
public static void main(String[] args) {
RestTemplate template = new RestTemplate();
while (true){
String forObject = template.getForObject("http://localhost:8090/list1", String.class);
Thread.sleep(500);
}
}
}
之前我们都是基于 controller 层的限流,sentinel同样支持 颗粒度更细的从 controller 到 service 层的限流。
配置文件 yaml 增加配置:让Sentinel 源码中 CommonFilter 中的 WEB_CONTEXT_UNIFY 参数为 false,将其配置为 false 即可根据不同的URL 进行链路限流,如果不配置将不会生效。
spring:
cloud:
sentinel:
# 配置为false
web-context-unify: false
新增 service 方法
@Service
public class ConsumerHelloServiceImpl{
@SentinelResource("test")
public void test(){
System.out.println("test");
}
}
Sentinel默认只标记Controller中的方法为资源,如果要标记其它方法,需要利用 @SentinelResource 注解
Controller 代码
@RestController
public class TestController {
@Autowired
private ConsumerHelloServiceImpl consumerHelloService;
@GetMapping("/list")
public String list(){
consumerHelloService.test();
return "sentinel list";
}
@GetMapping("/list1")
public String list1(){
consumerHelloService.test();
return "sentinel list1";
}
}
重启项目后,在浏览器访问几次 /list 和 /list1 ,可以在sentinel控制台上看到
在任意的一条 test 记录所在行点击流控按钮,选择链路流控模式,入口资源编辑成 / list1
此时相当于限制了 TestController.list1 -> ConsumerHelloServiceImpl.test 这条链路,而 TestController.list -> ConsumerHelloServiceImpl.test 这条链路没有被限制,可分别频繁访问 /list 和 /list1 测试,发现 /list1 这条路径已经被限流,而 /list可以随意访问无限制。
当QPS或线程数超过规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出 FlowExcepion。这种方式使用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动“,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。例如在系统刚刚启动的时候,系统可能有很多初始化工作要处理,加载大量的缓存等,这时候大量的请求过来,容易把系统压垮,需要一个冷处理过程,慢慢提高系统的处理请求能力。
严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的时漏桶算法。这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
慢调用比例 ( SLOW_REQUEST_RATIO ): 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于则统计为慢调用。当单位统计时长( statIntervalMs 默认1秒)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
异常比例 ( ERROR_RATIO ): 当单位统计时长( statIntervalMs 默认1秒 )内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有异常,则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0] ,代表 0% - 100%。
异常数 ( ERROR_COUNT ):当单位统计时长内(默认1秒)的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALFOPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
当系统load1(1分钟平均负载)超过阈值,且并发线程数超过系统容量时触发,建议设置为系统CPU核心数 * 2.5;仅对Linux/Unix系统有效。
其中的load1,可以在Linux系统上通过命令 uptime 查看:
这个命令返回3个值,分别为load1、load5、load15,表示系统1分钟的平均负载、5分钟的平均负载、15分钟的平均负载。
其中的系统容量,含义如下所示:
所有入口流量的平均 RT达到 阈值触发
所有入口流量的并发线程数达到阈值触发
所有入口流量的平均QPS达到阈值触发
相关源码所在位置:
com.alibaba.csp.sentinel.slots.system.SystemRuleManager#checkSystem
例如,设定CPU使用率为20%(即0.2),当接入应用所在机器的CPU使用率超过0.2之后,默认情况下会收到response code=429,content=Blocked by Sentinel (flow limiting)的返回。
授权规则的作用是限制指定来源(origin)的请求对指定资源的访问。有白名单和黑名单两种方式。
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
在调用者服务中添加代码:
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 1.获取请求头
String origin = request.getHeader("origin");
// 2.非空判断
if (StringUtils.isEmpty(origin)) {
origin = "blank";
}
return origin;
}
}
既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头。
这个需要利用GatewayFilter来实现,AddRequestHeaderGatewayFilter。
修改gateway服务中的application.yml,添加一个defaultFilter:
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=origin,gateway
routes:
# ...略
这样,从gateway路由的所有请求都会带上origin头,值为gateway。而从其它地方到达微服务的请求则没有这个头。
对需要防护的资源增加相应的授权规则,即可达到拒绝访问的效果。
在 Sentinel 中所有流控降级相关的异常都是异常类 BlockException 的子类:
Sentinel 提供了 @SentinelResource 注解用于定义资源,并提供了 AspectJ 的扩展用于自动定义资源、处理 BlockException 等。
注意:注解方式埋点不支持 private 方法。
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:
value:资源名称,必需项(不能为空)
entryType:entry 类型,可选项(默认为 EntryType.OUT)
blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallback / fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandler、fallback 和 defaultFallback,则被限流降级时会将 BlockException 直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)。
public class TestService {
// 原函数
@SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
public String hello(long s) {
return String.format("Hello at %d", s);
}
// Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
public String helloFallback(long s) {
return String.format("Halooooo %d", s);
}
// Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
public String exceptionHandler(long s, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return "Oops, error occurred at " + s;
}
// 这里单独演示 blockHandlerClass 的配置.
// 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,并且必须为 public static 函数.
@SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
public void test() {
System.out.println("Test");
}
}
Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
dependency>
spring:
application:
name: gateway3-service
cloud:
sentinel:
transport:
dashboard: localhost:8080
注:通过 Spring Cloud Alibaba Sentinel 自动接入的 API Gateway 整合则无需此参数
-Dcsp.sentinel.app.type=1
可以在sentinel控制台上看到
Sentinel 从 1.6.0 版本开始,提供了 Spring Cloud Gateway 的适配模块,可提供两种资源维度的限流:
Sentinel 网关流控默认的粒度是 route 维度以及自定义 API 分组维度,默认不支持 URL 粒度。若通过 Spring Cloud Alibaba 接入,请将 spring.cloud.sentinel.filter.enabled 配置项置为 false(若在网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)。
route维度比较好理解,就是根据网关中配置的路由配置作为资源名
API分组就是对接口进行分组,然后对不同组的接口,实施不同的限流策略。
对API进行限流,需要先对API进行分组。如下图所示:
配置模式:精确、前缀和正则三种模式。