目录
雪崩问题
超时处理
线程隔离
熔断降级
流量控制
服务对比
安装Sentinel控制台
案例
簇点链路
限流规则
流控模式
直接模式
关联模式
链路模式
流控效果
Warm up
排队等待
热点参数限流
隔离与降级
Feign整合Sentinel
线程隔离
规则设置
熔断降级
慢调用
规则设置
异常比例、异常数
规则设置
授权规则
案例实现
规则设置
自定义异常结果
规则持久化
原始模式
Pull模式
Push模式
实现持久化
雪崩问题
在微服务中,当服务D宕机,服务A去调用服务D时,由于服务A等待服务D的返回结果而阻塞,在阻塞期间不会释放tomcat的连接资源,当持续的调用服务D请求发送,tomcat的资源总会被耗尽。这时调用服务B与C的请求也会阻塞在服务A中,导致服务A也宕机。
解决雪崩问题有如下四种解决方案
超时处理
设置超时时间,请求超过一定时间没有响应就返回错误信息,不会阻塞。
但这只是缓解了雪峰问题,当每秒请求量大于每秒释放量时,还是会占用很多tomcat资源。
线程隔离
限定每个业务能使用的线程个数,避免耗尽整个tomcat的资源。
虽然解决了雪崩问题,但还是存在资源浪费,如果服务C真的宕机,那么请求C的资源就被浪费了。
熔断降级
由断路器统计业务执行的异常比例如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
是一种比较好的解决雪崩问题的一种方式
流量控制
限制业务访问的QPS(每秒钟处理的请求数量),避免业务因流量的突增而故障
属于预防雪崩问题的发生,通过Sentinel控制向服务发起请求的频率
服务对比
Sentinel | Hystrix | |
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于慢调用比例或异常比例 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 |
流量整形 | 支持慢启动、匀速排队模式 | 不支持 |
系统自适应保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
- 线程池隔离:对每个服务创建一个独立线程池,会存在线程数量大于CPU核数的情况,而线程切换又占用CPU资源,性能存在损失
- 信号量隔离:只有一个线程池,对业务已经使用的线程进行统计,当到达一定数量后,不会对该业务分配更多的线程。
- 慢调用比例:某个业务的请求耗时量久的数量与该业务总请求数量的比例
安装Sentinel控制台
下载地址:https://github.com/alibaba/Sentinel/releases
下载好后,放在无中文目录下启动
Java -jar sentinel-dashboard-你的版本.jar
访问8080端口,默认账号密码均为sentinel。
如果需要修改默认端口或是账号密码,需要添加启动参数
配置项 | 默认值 | 说明 |
server.port | 8080 | 服务端口 |
sentinel.dashboard.auth.username | sentinel | 默认用户名 |
sentinel.dashboard.auth.password | sentinel | 默认密码 |
java -Dserver.port=8088 -jar sentinel-dashboard-你的版本.jar
更多配置请参考Sentinel文档
案例
demo下载地址:day01微服务保护
下载完demo之后,启动nacos服务。
引入sentinel依赖
<!--引入sentinel依赖--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
添加配置内容
spring:cloud:sentinel:transport:dashboard: localhost:你的sentinel启动端口
启动服务并访问地址:localhost:8080/order/101
观察sentinal的控制台出现如下界面
簇点链路
就是项目中的调用链路,链路中被监控的每个接口就是一个资源。默认情况下sentinel会监控SpringMVC的每一个端点(可以理解为Controller的每个方法),因此SpringMVC的每一个端点就是调用链路中的一个资源。
限流规则
流控模式
流控的三种模式
- 默认是直接模式。也就是说,当前资源到达阈值时,对当前资源限流。
- 关联模式:统计与当前资源相关联的另一个资源,当触发阈值时,对关联的资源进行限流
- 链路模式:统计从指定链路访问到本地资源的请求,触发阈值时,对指定链路限流。
直接模式
超出阈值的请求会被拦截并报错。
这里我们将阈值设置为5。同时使用jemter模拟并发问题
启动查看运行结果
成功5次。失败5次。查看Sentinel的控制台
关联模式
使用场景:当用户支付时需要修改订单状态,同时也要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是支付和更新订单信息。支付的优先级要高于更新订单,因此当修改订单业务触发阈值时,需要对查询订单业务限流。
当update方法到达阈值后,会对query方法限流。
update与query方法的代码如下
@GetMapping("/query")public String queryOrder(){return "查询订单成功";}@GetMapping("/update")public String updateOrder(){return "修改订单成功";}
接下来,对update进行压测,同时访问query方法。
可以看到,updata请求全都可以进行访问。但query方法被限流
使用条件:
- 一个优先级高,一个优先级低
- 两个有竞争关系的资源
链路模式
使用场景:查询订单和创建订单业务,两者都需要查询商品,当查询订单并发较大时,也会影响创建订单的业务。因此可以针对从查询订单进入到查询商品的请求统计,并设置限流。
Controller代码如下
@GetMapping("/query")public String queryOrder(){orderService.queryGoods();return "查询订单成功";}@GetMapping("/save")public String saveOrder(){orderService.queryGoods();return "创建订单";}
Service代码如下
@SentinelResource("goods")public String queryGoods() {return "查询商品";}
访问两个接口查看Sentinel控制台
发现goods只在save链路上,而query中不存在goods。这是因为Sentinel对Controller进行了context整合,Sentinel将/query与/save整合到sentinel_spting_web_context同一链路下,goods无法在同一链路下出现两次。因此导致链路模式的限流失效。
解决方案,添加配置内容
spring:cloud:sentinel:web-context-unify: false
重启项目,再次访问两个接口。
添加流控规则
使用jmeert进行测试
前者为save链路的请求结果,后者为query的请求结果。
说明:
- Sentinel默认只会监控Controller中的资源。如果需要对Service中的资源进行监控需要添加注解@SentinelResource
- 在Sentinel的1.6.3版本开始,Sentinel Web filter默认收敛所有的URL入口context,导致链路限流不生效。但在1.7.0本版开始,在CommonFilter引入了WEB_CONTEXT_UNIFY参数,用于控制是否收敛context。
流控效果
流控效果是指,当请求达到流量阈值时的采取措施。包括三种:
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowExeception异常。是默认的处理方式。
- warm up:预热模式。对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大值。
- 排队等待:让所有请求按照先后次序排队执行,两个请求的间隔不能小于指定时长。
Warm up
是应对服务冷启动的一种方案,请求阈值初始值是【最大阈值/冷启动因子】,持续指定时长后,主键提高到最大阈值,默认冷启动因子为3。
例如最大阈值为10,预热时间为5s。那么初始阈值为10/3。然后再5内后逐渐增长到10
案例实现:
添加流控效果
启动jmeter测试
观察Sentinal控制台
可以看到随着时间增加不断增加通过QPS数量。
排队等待
让所有请求进入一个队列,按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
例如QPS等于5,那么每200ms处理队列中的一个请求,timeout=2000,意味着预期等待超过2秒的请求会被拒绝并抛出异常
案例实现:
编辑流控规则如下
启动jmeter进行测试
观察Sentinel控制台
可以看到。随着时间的增加,响应时间也逐渐增加,同时也出现超过最大等待时间的请求被拒绝。
热点参数限流
统计参数值相同的请求,判断其QPS是否超过了阈值
上图的配置含义为:对hot资源的第一个参数做统计,每秒相同参数值的请求数量不能超过5。
需要注意的是,热点参数对默认的SpringMVC资源无效。(Sentinel的热点参数功能是针对特定的接口和方法的,它通过统计接口的调用次数、流量控制、熔断降级等功能来保护服务的稳定性和可靠性。然而,默认的 SpringMVC 资源(例如 @RequestMapping 注解)并不是针对特定接口和方法的,而是针对整个 URL 的)
需要添加@SentimentResource注解
@SentinelResource("hot")
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId, @RequestHeader(value = "gateway", required = false) String str) {// 根据id查询订单并返回return orderService.queryOrderById(orderId);
}
热点规则的高级设置需要自己新建。
启动jmeter测试
观察Sentinel控制台
隔离与降级
Feign整合Sentinel
之所以使用Feign来整合Sentinel是因为无论是隔离还是降级都是保护调用方的服务能够正常运行。因此可以在调用方添加保护。
案例实现
引入依赖
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId>
</dependency>
配置文件
feign:sentinel:enabled: true
编写FeignClient编写失败后的降级逻辑
- FallbackClass:无法对远程调用的异常做处理
- FallbackFactory:可以对远程调用的异常做处理。通常使用这种。
编写FallbackFactory代码
@Slf4j
public class UserClientFallback implements FallbackFactory<UserClient> {@Overridepublic UserClient create(Throwable throwable) {return new UserClient() {@Overridepublic User queryByUserId(Long id) {//编写降级逻辑log.info("查询用户失败:",throwable);//返回默认值或空值return new User();}};}
}
对Client指定对应的降级类
@FeignClient(value = "user-server",fallbackFactory = UserClientFallback.class)
public interface UserClient{@GetMapping("/user/{id}")User queryByUserId(@PathVariable("id")Long id);
}
将UserClientFallback加载为
启动并访问地址:localhost:8080/order/101。
观察Sentinel控制台。
线程隔离
- 线程池隔离
- 信号量隔离(Sentinel的默认实现)
两种隔离方式对比
- 线程池隔离支持主动超时(通过线程的超时时间控制),支持异步调用。信号量隔离不支持主动超时,不支持异步调用
- 线程池隔离线程的额外开销较大。而信号量隔离则比较轻量,无额外开销。
- 线程池隔离适用于低扇出(该服务能够调用的其他服务较少)。信号量隔离适用于高频调用,高扇出
规则设置
使用jmeter进行测试
虽然测试全部通过,但是有些响应是通过降级逻辑返回。
熔断降级
熔断降级的思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。当服务恢复正常则会放行该服务的请求。
断路器的三种状态:
- Closed:关闭状态,统计异常比例
- Open:当达到失败阈值后由closed状态转为open状态。
- Half-Open:当熔断时间结束,进入半开状态,放行一次请求,如果失败则回到Open状态,成功则进入Closed状态。
慢调用
响应时长大于指定时长的请求被认为慢调用。在指定时间内,如果请求数量超过设定的最小数量,且慢调用比例到达阈值就会触发断路器。
规则设置
修改代码,让线程休眠60ms
@GetMapping("/{id}")public User queryById(@PathVariable("id") Long id) throws InterruptedException {if (id == 1) {Thread.sleep(60);}return userService.queryById(id);}
对id为1的请求快速访问5次,接着访问id为2的请求。
可以看到,id为2的用户也查询不到数据了。等待五秒后再次访问id为2的请求
异常比例、异常数
统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
规则设置
修改代码如下
@GetMapping("/{id}")public User queryById(@PathVariable("id") Long id) throws InterruptedException {if (id == 1) {Thread.sleep(60);}if (id ==2){throw new RuntimeException("抛出错误");}return userService.queryById(id);}
快速访问id为2的请求5次,然后访问id为3的请求发现,也无法查询到数据
等待5秒后再次访问可以正常访问。
授权规则
对请求者身份进行判断。在白名单的调用者可以访问,在黑名单的调用者不允许访问
与Gateway的区别:Gateway是对网关端口访问的身份校验。不对服务器访问者的身份进行校验。Sentinel是对当外部通过直接访问服务器地址的身份校验。通常来讲,我们只会在白名单中设置gateway的地址。
案例实现
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源。由于Gateway与浏览器发起的请求来源默认都是default,因此,Sentinel无法识别哪些请求来自网关,哪些请求来自浏览器。对此,我们要实现该接口,对网关请求与浏览器请求进行区分。
在Gateway中,可以通过添加过滤器,对每个经过网关的请求添加请求头
spring:cloud:gateway:routes: # 路由地址- id: order-service # 路由唯一标识uri: lb://order-service #路由目标地址predicates: # 路由断言。判断请求是否符合规则- Path=/order/**filters:- AddRequestHeader=origin,gateway
添加请求来源处理器
@Component
public class HandlerOriginParser implements RequestOriginParser {@Overridepublic String parseOrigin(HttpServletRequest httpServletRequest) {String origin = httpServletRequest.getHeader("origin");if (StringUtils.isEmpty(origin)){origin = "blank";}return origin;}
}
规则设置
访问8080服务器端口,发现无法正常访问。
访问10010网关端口。可以正常访问
自定义异常结果
无论是流控还是熔断又或是热点与授权管理,得到的都是限流异常(flow limiting)。这对用户来说不很友好,我们需要针对不同异常返回不同的异常结果。具体实现思路是实现BlockExceptionHandler接口。
常见的BlockException有如下五类
异常 | 说明 |
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException· | 系统规则异常 |
实现代码如下
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {String msg = "未知异常";int status = 429;if (e instanceof FlowException){msg = "请求被限流";}else if (e instanceof ParamFlowException){msg ="热点参数请求被限流";}else if (e instanceof DegradeException){msg = "请求被降级";}else if (e instanceof AuthorityException){msg = "没有权限访问";status = 401;}httpServletResponse.setContentType("application/json;charset=utf-8");httpServletResponse.setStatus(status);httpServletResponse.getWriter().println("{\"msg\":"+msg+",\"status\":"+status+"}");}
}
规则持久化
Sentinel默认将规则保存在内存当中。因此每次重启服务后都需要重新设置规则。Sentinel控制台规则管理有如下三种模式
原始模式
Sentinel默认的模式,将规则保存在内存当中,重启后服务丢失
Pull模式
控制台将配置的规则推送到Sentinel的客户端,客户端会将规则保存在本地文件或数据库中。微服务会定时去本地文件或数据库中查询规则是否改变。来更新本地规则。
缺点:时效性比较差。在集群中,存在一个服务器更改规则而其他服务器还没有进行更改的情况。
Push模式
控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地更新。这是推荐的一种实现方案
实现持久化
引入依赖
<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
配置nacos
spring:cloud:sentinel:datasource:flow:nacos:server-addr: localhost:8848 # nacos地址dataId: orderservice-flow-rulesgroupId: SENTINEL_GROUPrule-type: flow # 还可以是:degrade、authority、param-flow
接下来修改Sentinel的源码。下载Sentinel的源代码文件
下载地址:https://github.com/alibaba/Sentinel/tags
下载好后使用IDEA打开(时间较久),修改模块sentinel-dashboard中的pom文件
<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId><!--<spoce>test</spoce> 注释掉-->
</dependency>
添加nacos的支持。sentinel-dashboard模块下的test文件夹下已经编写好对nacos的支持,我们需要拷贝到main下
修改nacos地址
修改nacos数据源
修改前端页面
注释打开并修改其中内容
打包
运行启动
访问Sentinel端口并进行清除缓存刷新。
点击F12后,右键刷新键
测试
在新出现的选项中添加流控规则。
点击新增,后观察nacos的控制台。出现如下配置文件
现在即使重启服务器。Sentinel的信息也不会丢失。