See the 中文文档 for document in Chinese.
经过之前的学习,我们了解了微服务存在的雪崩问题,也就是说一个微服务出现问题,有可能导致整个链路直接不可用,这种时候我们就需要进行及时的熔断和降级,这些策略,我们之前通过使用 Hystrix 来实现。
SpringCloud Alibaba 也有自己的微服务容错组件,但是它相比 Hystrix 更加的强大。
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
Sentinel 具有以下特征:
它和 Nacos一样,它是独立安装和部署的,下载地址:https://github.com/alibaba/Sentinel/releases
注意下载下来之后是一个 jar 文件(其实就是个SpringBoot项目),我们需要在 IDEA 中部署:
启动之后,就可以访问到 Sentinel 的监控页面了,用户名和密码都是 sentinel
,地址:http://localhost:8858
这样就成功开启监控页面了,接着我们需要让我们的服务连接到 Sentinel 控制台,老规矩,导入依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
然后在配置文件中添加 Sentinel 相关信息(实际上 Sentinel 是本地在进行管理,但是我们可以连接到监控页面,这样就可以图形化操作了):
spring:
application:
name: userservice
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
# 添加监控页面地址即可
dashboard: localhost:8858
现在启动我们的服务,然后访问一次服务,这样 Sentinel 中就会存在信息了(懒加载机制,不会一上来就加载):
现在我们就可以在 Sentinel 控制台中对我们的服务运行情况进行实时监控了,可以看到监控的内容非常的多,包括时间点
、QPS(每秒查询率)
、响应时间
等数据。
按照上面的方式,我们将所有的服务全部连接到 Sentinel 管理面板中。
回到目录…
前面我们完成了对 Sentinel 的搭建与连接,接着我们来看看Sentinel的第一个功能,流量控制。
我们的机器不可能无限制的接受和处理客户端的请求,如果不加以限制,当发生高并发情况时,系统资源将很快被耗尽。为了避免这种情况,我们就可以添加流量控制(也可以说是限流)。当一段时间内的流量到达一定的阈值的时候,新的请求将不再进行处理,这样不仅可以合理地应对高并发请求,同时也能在一定程度上保护服务器不受到外界的恶意攻击。
那么要实现限流,正常情况下,我们该采取什么样的策略呢?
拒绝信息
,告诉用户访问频率过高。缓冲保护
。进队列先等一下
,如果规定时间内能够执行,那么就执行,要是超时就算了。漏桶算法
顾名思义,就像一个桶开了一个小孔,水流进桶中的速度肯定是远大于水流出桶的速度的,这也是最简单的一种限流思路:
我们知道,桶是有容量的,所以当桶的容量已满时,就装不下水了,这时就只有丢弃请求了。
利用这种思想,我们就可以写出一个简单的限流算法。
令牌桶算法
只能说有点像信号量机制。现在有一个令牌桶,这个桶是专门存放令牌的,每隔一段时间就向桶中丢入一个令牌(速度由我们指定)当新的请求到达时,将从桶中删除令牌,接着请求就可以通过并给到服务,但是如果桶中的令牌数量不足,那么不会删除令牌,而是让此数据包等待。
可以试想一下,当流量下降时,令牌桶中的令牌会逐渐积累,这样如果突然出现高并发,那么就能在短时间内拿到大量的令牌。
固定时间窗口算法
我们可以对某一个时间段内的请求进行统计和计数,比如在14:15~14:16
这一分钟内,请求量不能超过100,也就是一分钟之内不能超过 100次请求,那么就可以像下面这样进行划分:
虽然这种模式看似比较合理,但是试想一下这种情况:14:15:59的时候来了100个请求、14:16:01的时候又来了100个请求
出现上面这种情况,符合固定时间窗口算法的规则,所以这 200个请求都能正常接受,但是,你应该发现了,我们其实希望的是 60秒内只有100个请求,但是这种情况却是在 3秒内出现了200个请求,很明显已经违背了我们的初衷。
因此,当遇到临界点时,固定时间窗口算法存在安全隐患。
滑动时间窗口算法
相对于固定窗口算法,滑动时间窗口算法更加灵活,它会动态移动窗口,重新进行计算:
虽然这样能够避免固定时间窗口的临界问题,但是这样显然是比固定窗口更加耗时的。
回到目录…
了解完了我们的限流策略和判定方法之后,我们在 Sentinel 中进行实际测试一下,打开管理页面的簇点链路模块 -> 流控:
这里演示对我们的图书接口进行限流,点击流控,会看到让我们添加流控规则
:
回到目录…
上面我们学过的流控规则都是对某个接口进行的限流,这里我们讲解一下@SentinelResource
的使用,对某个方法进行限流。
①我们先在控制层新增一个接口,但实际上和之前的接口内容一样,调用同样的方法。
②@SentinelResource
监控此方法,无论被谁执行都在监控范围内,这里给的 value 是自定义名称,这个注解可以加在任何方法上,包括 Controller 中的请求映射方法,跟 @HystrixCommand 贼像
③我们这里创建两个请求映射,都来调用 Service 的被监控方法,需要配置它们单独控制:
spring:
application:
name: bookservice
cloud:
sentinel:
transport:
dashboard: localhost:8858
# 关闭 Context 收敛,这样被监控方法可以进行不同链路的单独控制
web-context-unify: false
④然后我们在 Sentinel 控制台中对该方法添加流控规则:
最后我们在浏览器中对 /book/{bid}
和 /book2/{bid}
这两个接口都进行测试,会发现无论请求哪个接口,只要调用了 Service 中的getBookById(int bid)
这个方法,都会被限流。
回到目录…
①QPS 阈值为1,流控模式: 直接,流控效果: 快速失败。
当我们快速地进行请求时 (每秒超过1次),会直接返回失败信息:
②QPS 阈值为3,流控模式: 直接,流控效果: Warm up 缓存预热。
③QPS 阈值为1,流控模式: 关联 /error,流控效果: 快速失败。
当我们快速地请求自带的/error接口时 (每秒超过1次),会关联的影响到我们的/book接口。
我们使用 PostMan 的 Runner 连续对关联资源发起请求:
开启Postman,然后我们会发现图书服务已经凉凉。当我们关闭掉 Postman 的任务后,恢复正常。
④QPS 阈值为1,流控模式: 链路,流控效果: 快速失败。
最后我们来讲解一下链路模式,它能够更加精准的进行流量控制。链路流控模式指的是当从指定接口过来的资源请求达到限流条件时,开启限流。
我们在上面学习了@SentinelResource 对方法流控
,这种方案会对调用该方法的 /book/{uid}
和 /book2/{uid}
都进行限流。
而这个链路选项实际上就是决定只限流从哪个方向来的调用,比如我们只对/book2
这个接口对getBook(int uid)
方法的调用进行限流,那么我们就可以为其指定链路:
此时我们会发现,限流效果只对我们配置的链路接口有效,而其他链路是不会被限流的。
回到目录…
除了直接对接口进行限流规则控制之外,我们也可以根据当前系统的资源使用情况,决定是否进行限流:
系统规则支持以下的模式:
maxQps * minRt
估算得出。设定参考值一般是 CPU cores * 2.5
。回到目录…
我们还可以对某一热点数据进行精准限流,比如在某一时刻,不同参数被携带访问的频率是不一样的:
由于携带参数a
的请求比较多,我们就可以只对携带参数a
的请求进行限流。
①这里我们创建一个新的测试请求映射:
@RestController
public class ParamController {
@GetMapping("/param")
@SentinelResource("getParam") // 必须添加该注解,才能设置参数精准限流
public String getParamABC(@RequestParam(value = "a", required = false) String a,
@RequestParam(value = "b", required = false) String b,
@RequestParam(value = "c", required = false) String c) {
return "{a = " + a + ", b = " + b + ", c = " + c + "}";
}
}
②启动之后,我们在 Sentinel 的热点规则模块进行热点配置:
③然后开始访问我们的测试接口,分别携带3个参数进行超阈值请求:
④除了直接对某个参数精准限流外,我们还可以对参数携带的指定值单独设定阈值。
比如我们可以对参数b
限流阈值放宽到 20,但当参数b=8
时,QPS 达到1就限流:
回到目录…
还记得我们前所说的服务降级吗,也就是说我们需要在整个微服务调用链路出现问题的时候,及时对服务进行降级,以防止问题进一步恶化。
那么,各位是否有思考过,如果在某一时刻,服务B 出现故障(可能就卡在那里了),而这时服务A 依然有大量的请求,在调用服务B,那么,由于服务A 没办法再短时间内完成处理,新来的请求就会导致线程数不断地增加,这样,CPU 的资源很快就会被耗尽。
那么要防止这种情况,就只能进行隔离了,这里我们提两种隔离方案:
Semaphore
类实现的(如果不了解,可以观看本系列 并发编程篇 视频教程),思想基本上与上面是相同的,也是限定指定的线程数量能够同时进行服务调用,但是它相对于线程池隔离,开销会更小一些,使用效果同样优秀,也支持超时等。好了,说回我们的熔断和降级,当下游服务因为某种原因变得不可用或响应过慢时,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务而是快速返回或是执行自己的替代方案,这便是服务降级。
整个过程分为三个状态:
- 关闭:熔断器不工作,所有请求全部该干嘛干嘛。
- 打开:熔断器工作,所有请求一律降级处理。
- 半开:尝试进行一下下正常流程,要是还不行继续保持打开状态,否则关闭。
那么我们来看看 Sentinel 中如何进行熔断和降级操作,打开管理页面,我们可以自由新增熔断规则,其中熔断策略有三种模式:
回到目录…
慢调用比例:如果出现那种半天都处理不完的调用,有可能就是服务出现故障,导致卡顿,这个选项是按照最大响应时间(RT
)进行判定。如果一次请求的处理时间超过了指定的 RT,那么就被判定为慢调用。在一个统计时长内,如果请求数目大于最小请求数目,并且被判定为慢调用的请求比例已经超过阈值,将触发熔断。经过熔断时长之后,将会进入到半开状态进行试探(这里和Hystrix一致)
然后创建一个接口,来模拟一下慢调用:
@GetMapping("/slow")
public String slow() throws InterruptedException {
Thread.sleep(500);
return "这是一个慢比例调用!";
}
可以看到,熔断规则内是可以正常访问的:
但是突破熔断规则时,就会服务熔断:
回到目录…
异常比例:这个与慢调用比例类似,不过这里判断的是出现异常的次数,与上面一样,我们也来进行一些小测试:
@GetMapping("/ecpPCT")
public String EcpPCT() {
int n = (int) (Math.random() * 10);
if(n % 2 == 0) {
throw new RuntimeException("运行时异常: " + n);
}
return "异常比例测试";
}
启动服务器,接着添加我们的熔断规则:
现在我们进行访问,会发现后台疯狂报错,然后就熔断了:
回到目录…
异常数:这个和上面的唯一区别就是,只要达到指定的异常数量,就熔断,这里我们修改一下熔断规则:
现在我们再次不断访问此接口,可以发现,效果跟之前其实是差不多的,只是判断的策略稍微不同罢了:
回到目录…
同样,Sentinel 也支持我们进行方法级别细粒度的熔断降级。
我们在需要控制的方法上添加 @SentinelResource
注解,可以配置 blockHandler
参数返回熔断后的代替方案。
@GetMapping("/test2")
@SentinelResource(value = "testMethod", blockHandler = "block")
public String testMethod() {
throw new RuntimeException();
}
public String block(BlockException e) {
return "发生服务降级/熔断了";
}
接着我们对进行熔断配置,注意是对我们添加的@SentinelResource
中指定名称的testMethod
进行配置:
回到目录…
我们上面学的都是在一个微服务中,对接口或方法访问时,进行了降级和熔断。那此时,我们还需要知道在微服务之间的远程调用过程中,某个微服务出现问题导致无法访问时,我们该如何做熔断和降级呢?
我们以 borrowservice 的 /borrow/{uid}
接口中的远程调用为例进行讲解吧!
方案一:OpenFeign 实现降级熔断:
Spring Cloud CircuitBreaker
断路器,实现了熔断降级的功能。@EnableHystrix
注解来修饰。OpenFeign 实现降级
模块①首先我们需要在配置文件中开启 sentinel
对 feign
的支持:
feign:
sentinel:
enabled: true
②我们给 borrowservice
中的 BookClient
接口创建替代实现类:
@Component
public class BookClientFallback implements BookClient {
@Override
public Book findBookById(int bid) {
return new Book(-1, "代替方案", "");
}
}
③为 BookClient
接口指定替代方案,使用 @FeignClient
注解的 fallback
属性
@FeignClient(value = "bookservice", fallback = BookClientFallback.class)
public interface BookClient {
@GetMapping("/book/{bid}")
Book findBookById(@PathVariable("bid") int bid);
}
④然后直接启动就可以了,中途的时候我们把图书服务全部下掉,可以看到正常使用替代方案:
这样 Feign 的配置就OK了,
方案二:@SentinelRestTemplate
实现降级熔断:
原本传统的 RestTemplate 需要借助 @SentinelRestTemplate
注解实现:
@Bean
@LoadBalanced
@SentinelRestTemplate(blockHandler = "handleException",
blockHandlerClass = ExceptionUtil.class,
fallback = "fallback",
fallbackClass = ExceptionUtil.class) // 这里同样可以设定 fallback 等参数
public RestTemplate restTemplate() {
return new RestTemplate();
}
这里就不多做赘述了。
回到目录…
现在我们已经了解了如何进行限流、服务熔断降级操作,那么这些状态下的返回结果该怎么修改呢,我们看到返回的是 Sentinel 默认的数据,现在我们希望自定义改如何操作?不仅仅是限流、服务降级熔断,只要是 Sentinel 包括的异常都可以处理。
这里我们先创建好被限流状态下需要返回的内容,定义一个请求映射:
@Controller
public class BlockedController {
@RequestMapping("/blocked")
public JSONObject blocked(){
JSONObject object = new JSONObject();
object.put("code", 403);
object.put("success", false);
object.put("massage", "您的请求频率过快,请稍后再试!");
return object;
}
}
接着我们在配置文件中将此页面设定为限流页面:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8858
# 将刚刚编写的请求映射设定为限流页面
block-page: /blocked
默认的返回结果:
修改后,当被限流时,就会被重定向到指定页面:
回到目录…
那么,对于方法级别的限流呢?经过前面的学习我们知道,当某个方法被限流时,会直接在后台抛出异常,那么这种情况我们该怎么处理呢?
比如我们之前在 Hystrix 中可以直接添加一个替代方案,@HystrixCommand(fallbackMethod = "onError")
,这样当出现异常时会直接执行我们的替代方法并返回,Sentinel 也可以。
比如我们还是在 getBookById(int bid)
方法上进行配置:注意blockHandler
只能处理限流情况下抛出的异常
@Override
@SentinelResource(value = "getBook", blockHandler = "blocked") //监控此方法,无论被谁执行都在监控范围内,这里给的value是自定义名称,这个注解可以加在任何方法上,包括Controller中的请求映射方法,跟HystrixCommand贼像
public Book getBookById(int bid) {
return bookMapper.getBookById(bid);
}
// 替代方案,注意参数和返回值需要保持一致,并且参数最后还需要额外添加一个 BlockException
public Book blocked(int bid, BlockException e) {
return new Book(-1, "", "");
}
回到目录…
如果是方法本身抛出的其他类型异常,不在blockHandler
管控范围内,但是可以通过@SentinelResource
的其它参数进行处理:
@RestController
public class FallbackController {
@GetMapping("/test")
@SentinelResource(value = "testFallback",
fallback = "except", // 指定异常时的替代方案
blockHandler = "block", // 出现限流时的替代方案
exceptionsToIgnore = IOException.class) // 可以忽视的异常,出现该异常时不使用替代方案
public String testFallback() {
throw new RuntimeException("出现了运行时异常");
}
// 替代方法必须和原方法返回值和参数一致,最后可以添加一个Throwable作为参数接受异常
public String except(Throwable t) {
return t.getMessage();
}
}
这样,其它的异常也可以有替代方案了:
同样的,当超阈值访问时,Sentinel 的限流异常也可以处理:
特别注意:
blockHandler
的情况下,fallback
会将 Sentinel 机制内(限流、熔断)的异常也一并处理了。blockHandler
,那么在出现限流/熔断时,依然只会执行 blockHandler
指定的替代方案(因为限流是在方法执行之前进行的)。回到目录…
总结:
提示:这里对文章进行总结:
本文是对Sentinel 流量防卫兵的学习,学习了 Sentinel 的安装与部署,还使用了它提供的功能:流量控制、服务降级熔断,并且还可以自定义sentinel异常的处理方案。之后的学习内容将持续更新!!!