Sentinel 微服务保护
#Sentinel#
本文章为个人摘抄笔记,原文章来源于www.cnblogs.com/xiegongz...
Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:https://sentinelguard.io/zh-cn/index.html
雪崩问题与解决方式
所谓的雪崩指的是:微服务之间相互调用,调用链中某个微服务出现问题了,导致整个服务链的所有服务也跟着出问题,从而造成所有服务都不可用
解决方式:
-
超时处理:是一种临时方针,即设置定时器,请求超过规定的时间就返回错误信息,不会无休止等待
缺点:在超时时间内,还未返回错误信息内,服务未处理完,请求激增,一样会导致后面的请求阻塞
-
线程隔离:也叫舱壁模式,即限定每个业务能使用的线程数,避免耗尽整个tomcat的资源
缺点:会造成一定资源的浪费。明明服务已经不可用了,还占用固定数量的线程
-
熔断降级:
- 熔断: 由“断路器”统计业务执行的异常比例,如果超出“阈值”则会熔断/暂停该业务,拦截访问该业务的一切请求,后续搞好了再开启。从而做到在流量过大时(或下游服务出现问题时),可以自动断开与下游服务的交互,并可以通过自我诊断下游系统的错误是否已经修正,或上游流量是否减少至正常水平来恢复自我恢复。熔断更像是自动化补救手段,可能发生在服务无法支撑大量请求或服务发生其他故障时,对请求进行限制处理,同时还可尝试性的进行恢复
- 降级: 丢车保帅。针对非核心业务功能,核心业务超出预估峰值需要进行限流;所谓降级指的就是在预计流量峰值前提下,整体资源快不够了,忍痛将某些非核心服务先关掉,待渡过难关,再开启回来
-
限流: 也叫流量控制。指的是限制业务访问的QPS,避免服务因流量的突增而故障。是防御保护手段,从流量源头开始控制流量规避问题
限流是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施
超时处理、线程隔离、降级熔断是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施
服务保护技术对比
在SpringCloud当中支持多种服务保护技术:
早期比较流行的是Hystrix框架(后面这叼毛不维护、不更新了),所以目前国内实用最广泛的是阿里巴巴的Sentinel框架
Sentinel | Hystrix | |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于慢调用比例或异常比例 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 |
流量整形 | 支持慢启动、匀速排队模式 | 不支持 |
系统自适应保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
安装sentinel
-
下载:https://github.com/alibaba/Sentinel/releases 是一个jar包,这是sentinel的ui控制台,下载了放到“非中文”目录中
-
运行
java -jar sentinel-dashboard-1.8.1.jar
如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:
配置项 | 默认值 | 说明 |
---|---|---|
server.port | 8080 | 服务端口 |
sentinel.dashboard.auth.username | sentinel | 默认用户名 |
sentinel.dashboard.auth.password | sentinel | 默认密码 |
例如,修改端口:
java -Dserver.port=8090 -jar sentinel-dashboard-1.8.1.jar
- 访问。如http://localhost:8080,用户名和密码都是sentinel
入手sentinel
-
依赖
<!--sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
-
YAML配置
server: port: 8088 spring: cloud: sentinel: transport: # sentinel的ui控制台地址 dashboard: localhost:8080
-
然后将服务提供者、服务消费者、网关、Feign……启动,发送请求即可在前面sentinel的ui控制台看到信息了
限流 / 流量控制
雪崩问题虽然有四种方案,但是限流是避免服务因突发的流量而发生故障,是对微服务雪崩问题的预防,因此先来了解这种模式,但在了解这个之前先了解一下限流算法
限流算法
固定窗口计数器算法
- 将时间划分为多个窗口,窗口时间跨度称为Interval
- 每个窗口维护一个计数器,每有一次请求就将计数器 +1,限流就是设置计数器阈值
- 如果计数器超过了限流阈值,则超出阈值的请求都被丢弃
但是有个缺点:时间是不固定的。如0 - 1000ms是QPS(1秒内的请求数),这样来看没有超过阈值,可是:4500 - 5500ms也是1s啊,这是不是也是QPS啊,像下面这样就超出阈值了,服务不得干爬了
滑动窗口计数器算法
在固定窗口计数器算法的基础上,滑动窗口计数器算法会将一个窗口划分为n个更小的区间,如:
- 窗口时间跨度Interval为1秒;区间数量 n = 2 ,则每个小区间时间跨度为500ms
- 限流阈值依然为3,时间窗口(1秒)内请求超过阈值时,超出的请求被限流
- 窗口会根据当前请求所在时间(currentTime)移动,窗口范围是从(currentTime-Interval)之后的第一个时区开始,到currentTime所在时区结束
令牌桶算法
- 以固定的速率生成令牌,存入令牌桶中,如果令牌桶满了以后,多余令牌丢弃
- 请求进入后,必须先尝试从桶中获取令牌,获取到令牌后才可以被处理
- 如果令牌桶中没有令牌,则请求等待或丢弃
也有个缺点:
- 假如限流阈值是1000个请求
- 假设捅中只能放1000个令牌,前一秒内没有请求,但是令牌已经生成了,放入了捅中
- 之后下一秒来了2000个请求,可捅中前一秒生成了1000令牌,所以可以有1000个请求拿到令牌,从而放行,捅中没令牌了
- 然后当前这一秒就要生成令牌,这样另外1000个请求也可以拿到令牌
- 最后2000个请求都放行了,服务又干爬了
漏桶算法
是对令牌桶算法做了改进:可以理解成请求在桶内排队等待
- 将每个请求视作"水滴"放入"漏桶"进行存储
- "漏桶"以固定速率向外"漏"出请求来执行,如果"漏桶"空了则停止"漏水”
- 如果"漏桶"满了则多余的"水滴"会被直接丢弃
限流算法对比
因为计数器算法一般都会采用滑动窗口计数器,所以这里只对比三种算法
对比项 | 滑动时间窗口 | 令牌桶 | 漏桶 |
---|---|---|---|
能否保证流量曲线平滑 | 不能,但窗口内区间越小,流量控制越平滑 | 基本能,在请求量持续高于令牌生成速度时,流量平滑。但请求量在令牌生成速率上下波动时,无法保证曲线平滑 | 能,所有请求进入桶内,以恒定速率放行,绝对平滑 |
能否应对突增流量 | 不能,徒增流量,只要高出限流阈值都会被拒绝。 | 能,桶内积累的令牌可以应对突增流量 | 能,请求可以暂存在桶内 |
流量控制精确度 | 低,窗口区间越小,精度越高 | 高 | 高 |
簇点链路
簇点链路: 就是项目内的调用链路,链路中被监控的每个接口就是一个“资源”
当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源
默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点就是调用链路中的一个资源
例如下图中的端点:/order/{orderId}
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
- 流控:流量控制
- 降级:降级熔断
- 热点:热点参数限流
- 授权:请求的权限控制
入门流控
- 点击下图按钮
-
设置基本流控信息
上图的含义:限制 /order/{orderId} 这个资源的单机QPS为1,即:每秒只允许1次请求,超出的请求会被拦截并报错
流控模式的分类
在添加限流规则时,点击高级选项,可以选择三种流控模式:
- 直接模式:一句话来说就是“对当前资源限流”。统计当前资源的请求,当其触发阈值时,对当前资源直接限流。上面这张图就是此种模式。这也是默认的模式。采用的算法就是滑动窗口算法
- 关联模式:一句话来说就是“高优先级触发阈值,对低优先级限流”。统计与当前资源A “相关” 的另一个资源B,A资源触发阈值时,对B资源限流
如:在一个Controller中,一个高流量的方法和一个低流量的方法都调用了这个Controller中的另一个方法,为了预防雪崩问题,就对低流量的方法进行限流设置
适用场景:两个有竞争关系的资源,一个优先级高,一个优先级低,优先级高的触发阈值时,就对优先级低的进行限流 - 链路模式:一句话来说就是“对请求来源做限流”。统计从“指定链路”访问到本资源的请求,触发阈值时,对指定链路限流
如:两个不同链路的请求,如需要读库和写库,这两个请求都调用了同一个服务/资源/接口,所以为了需求考虑,可以设置读库达到了阈值就进行限流
示例:
-
关联模式: 对谁进行限流,就点击谁的流控按钮进行设置
上图含义:当 /order/update 请求单机达到 每秒1000 请求量的阈值时,就会对 /order/query 进行限流,从而避免影响 /order/update 资源
-
链路模式: 请求链路访问的是哪个资源,就点击哪个资源的流控按钮进行配置
上图含义:只有来自 /user/queryGoods 链路的请求来访问 /order/queryGoods 资源时,每秒请求量达到1000,就会对 /user/queryGoods 进行限流链路模式的注意事项:
-
默认情况下,Service中的方法是不被Sentinel监控的,想要Service中的方法也被Sentinel监控的话,则需要我们自己通过 @SentinelResource("起个名字 或 像controllerz中请求路径写法") 注解来标记要监控的方法
-
链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源,进行了context整合,所以会导致链路模式失效。因此需要关闭一个context整合设置:
spring: cloud: sentinel: web-context-unify: false # 关闭context整合
同一个root资源指的是:
-
流控效果及其分类
流控效果:指请求达到流控阈值时应该采取的措施
分类
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出 FlowException异常。是默认的处理方式
- warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值
- 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
warn up 预热模式
warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值
阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机
warm up也叫预热模式,是应对服务冷启动的一种方案
请求阈值初始值 = maxThreshold / coldFactor
- maxThreshold 就是设置的QPS数量。持续指定时长后,逐渐提高到maxThreshold值。
- coldFactor 预热因子,默认值是3
排队等待
排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
采用的算法:基于漏桶算法
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常
而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝
QPS = 5,那么 1/5(个/ms) = 200(个/ms),意味着每200ms处理1个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常
那什么叫做预期等待时长呢?
如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑
平滑的QPS曲线,对于服务器来说是更友好的
热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值
热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值
采用的算法: 令牌桶算法
注意事项:热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源,例如:
但是配置时不要通过上面按钮点击配置,会有BUG,而是通过下图中的方式:
所谓的参数值指的是:
id参数值会有变化,热点参数限流会根据参数值分别统计QPS
当id=1的请求触发阈值被限流时,id值不为1的请求不受影响
全局参数限流
就是基础设置,没有加入高级设置的情况
上图含义:对于来访问hot资源的请求,每1秒相同参数值的请求数不能超过10000
热点参数限流
刚才的配置中,对查询商品这个接口的所有商品一视同仁,QPS都限定为10000
而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。那就需要配置热点参数限流的高级选项了
上图含义:对于来访问hot资源的请求,id=110时的QPS阈值为30000,id=4132443时的QPS阈值为50000,id为其他的则QPS阈值为10000
Seatinel限流与Gateway限流的差异
Gateway则采用了基于Redis实现的令牌桶算法。而Sentinel内部所有算法都有:
- 默认限流模式是基于滑动时间窗口算法
- 排队等待的限流模式则基于漏桶算法
- 而热点参数限流则是基于令牌桶算法
Sentinel整合Feign
Sentinel是做服务保护的,而在微服务中调来调去是常有的事,要远程调用就离不开Feign
- 修改配置,开启sentinel功能: 在服务“消费方”的feign配置添加如下配置内容
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
- feign-client中编写失败降级逻辑: 后面的流程就是前面玩Fengn时失败降级的流程
package com.zixieqing.feign.fallback;
import com.zixieqing.feign.clients.UserClient;
import com.zixieqing.feign.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
/**
* userClient失败时的降级处理
*
* <p>@author : ZiXieqing</p>
*/
@Slf4j
public class UserClientFallBackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
/**
* 重写userClient中的方法,编写失败时的降级逻辑
*/
@Override
public User findById(Long id) {
log.info("userClient的findById()在进行 id = {} 时失败", id);
return new User();
}
};
}
}
- 将失败降级逻辑的类丢给Spring容器
@Bean
public UserClientFallBackFactory userClientFallBackFactory() {
return new UserClientFallBackFactory();
}
- 在相关feign-client定义处使用fallbackFactory回调函数即可
package com.zixieqing.feign.clients;
import com.zixieqing.feign.fallback.UserClientFallBackFactory;
import com.zixieqing.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice",fallbackFactory = UserClientFallBackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
- 调用,失败时就会进入自定义的失败逻辑中
package com.zixieqing.order.service;
import com.zixieqing.feign.clients.UserClient;
import com.zixieqing.feign.pojo.User;
import com.zixieqing.order.mapper.OrderMapper;
import com.zixieqing.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用
User user = userClient.findById(order.getId());
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}
}
离与降级
线程隔离
线程隔离有两种方式实现:
-
线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
优点:- 支持主动超时:也就是调用进行逻辑处理时超过了规定时间,直接噶了,不再让其继续处理
- 支持异步调用:线程池隔离了嘛,彼此不干扰,因此可以异步了
缺点:造成资源浪费。明明被调用的服务都出问题了,还占用固定的线程池数量
适用场景:低扇出。MQ中扇出交换机的那个扇出,也就是较少的请求量,扇出/广播到很多服务上 -
信号量隔离(Sentinel默认采用):不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求
优点:轻量级、无额外开销
缺点:不支持主动超时、不支持异步调用
适用场景:高频调用、高扇出
配置Sentinel的线程隔离-信号量隔离
在添加限流规则时,可以选择两种阈值类型:
熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求
断路器控制熔断和放行是通过状态机来完成的:
断路器熔断策略有三种:慢调用、异常比例、异常数
状态机包括三个状态:
-
Closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
-
Open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态默认5秒后会进入half-open状态
-
Half-Open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到closed状态
- 请求失败:则切换到open状态
断路器熔断策略:慢调用
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求
在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断
上图含义:
-
响应时间为500ms的即为慢调用
-
如果1000ms内有100次请求,且慢调用比例不低于0.05(即:100*0.05=5个慢调用),则触发熔断(暂停该服务)
-
熔断时间达到1s进入half-open状态,然后放行一次请求测试
- 成功则进入Closed状态关闭断路器
- 失败则进入Open状态打开断路器,继续像前面一样开始统计RT=500ms,1s内有100次请求……………..
断路器熔断策略:异常比例 与 异常数
- 异常比例:
上图含义:在1s内,若是请求数量不低于100个,且异常比例不低于0.08(即:100*0.08=8个有异常),则触发熔断,熔断时长达到1s就进入half-open状态
- 异常数: 直接敲定有多少个异常数量就触发熔断
授权规则
授权规则可以对请求方来源做判断和控制
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式:
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问
-
资源名:就是受保护的资源,例如 /order/
-
流控应用:是来源者的名单
- 如果是勾选白名单,则名单中的来源被许可访问
- 如果是勾选黑名单,则名单中的来源被禁止访问
我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)
但是上图中怎么区分请求是从网关来的还是浏览器来的?在微服务中的想法是所有请求只能走网关,然后由网关路由到具体的服务,直接访问服务应该阻止才对,像下面直接跳过网关去访问服务,应该不行才对
要做到就需要使用授权规则了:
- 网关授权拦截:针对于别人不知道内部服务接口的情况可以拦截成功
- 服务授权控制/流控应用控制:针对“内鬼“ 或者 别人知道了内部服务接口,我们限定只能从哪里来的请求才能访问该服务,否则直接拒绝
流控应用怎么控制的?
下图中的名字怎么定义?
需要实现 RequestOriginParser 这个接口的 parseOrigin() 来获取请求的来源从而做到
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
示例:
- 在需要进行保护的服务中编写请求来源解析逻辑
package com.zixieqing.order.intercepter;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 拦截请求,允许从什么地方来的请求才能访问此微服务
*
* <p>@author : ZiXieqing</p>
*/
@Component
public class RequestInterceptor implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 获取请求中的请求头 可自定义
String origin = request.getHeader("origin");
if (StringUtils.isEmpty(origin))
origin = "black";
return origin;
}
}
- 在网关中根据2中 parseOrigin() 的逻辑添加相应的东西
- 添加流控规则:不要在簇点链路中选择相应服务来配置授权,会有BUG
经过上面的操作之后,要进入服务就只能通过网关路由过来了,不是从网关过来的就无法访问服务
自定义异常
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截
而如果要自定义异常时的返回结果,需要实现 BlockExceptionHandler 接口:
public interface BlockExceptionHandler {
/**
* 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
*
* @param e 被sentinel拦截时抛出的异常
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}
这里的BlockException包含多个不同的子类:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
示例:
- 在需要的服务中实现 BlockExceptionHandler 接口
package com.zixieqing.order.exception;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义sentinel的各种异常处理
*
* <p>@author : ZiXieqing</p>
*/
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, 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;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
- 重启服务,不同异常就会出现不同结果了
规则持久化
在默认情况下,sentinel的所有规则都是内存存储,重启后所有规则都会丢失。在生产环境下,我们必须确保这些规则的持久化,避免丢失
规则是否能持久化,取决于规则管理模式,sentinel支持三种规则管理模式:
- 原始模式:Sentinel的默认模式,将规则保存在内存,重启服务会丢失
- pull模式
- push模式
pull模式
pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则
缺点:服务之间的规则更新不及时。因为是定时去读取,在时间还未到时,可能规则发生了变化
push模式
push模式:控制台将配置规则推送到远程配置中心(如Nacos)。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新
使用push模式实现规则持久化
在想要进行规则持久化的服务中引入如下依赖:
<!--sentinel规则持久化到Nacos的依赖-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
配置此服务的YAML文件,内容如下:
spring:
cloud:
sentinel:
datasource:
flow: # 流控规则持久化
nacos:
server-addr: localhost:8848 # nacos地址
dataId: orderservice-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow # 还可以是:degrade 降级、authority 授权、param-flow 热点参数限流
# degrade: # 降级规则持久化
# nacos:
# server-addr: localhost:8848 # nacos地址
# dataId: orderservice-degrade-rules
# groupId: SENTINEL_GROUP
# rule-type: degrade
# authority: # 授权规则持久化
# nacos:
# server-addr: localhost:8848 # nacos地址
# dataId: orderservice-authority-rules
# groupId: SENTINEL_GROUP
# rule-type: authority
# param-flow: # 热电参数限流持久化
# nacos:
# server-addr: localhost:8848 # nacos地址
# dataId: orderservice-param-flow-rules
# groupId: SENTINEL_GROUP
# rule-type: param-flow
修改sentinel的源代码
因为阿里的sentinel默认采用的是将规则内容存到内存中的,因此需要改源码
- 使用git克隆sentinel的源码,之后IDEA等工具打开
git clone https://github.com/alibaba/Sentinel.git
- 修改nacos依赖。在sentinel-dashboard模块的pom文件中,nacos的依赖默认的scope是test,那它只能在测试时使用,所以要去除 scope 标签
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- 添加nacos支持。在sentinel-dashboard的test包下,已经编写了对nacos的支持,我们需要将其拷贝到src/main/java/com/alibaba/csp/sentinel/dashboard/rule 下
- 修改nacos地址,让其读取application.properties中的配置
- 在sentinel-dashboard的application.properties中添加nacos地址配置
nacos.addr=127.0.0.1:8848 # ip和port改为自己想要的即可
- 配置nacos数据源
- 修改前端
- 重现编译打包Sentinel-Dashboard模块
- 重现启动sentinel即可
java -jar -Dnacos.addr=127.0.0.1:8848 sentinel-dashboard.jar
补充:Sentinel基础知识
Sentinel实现限流、隔离、降级、熔断等功能,本质要做的就是两件事情:
- 统计数据:统计某个资源的访问数据(QPS、RT等信息)
- 规则判断:判断限流规则、隔离规则、降级规则、熔断规则是否满足
这里的资源就是希望被Sentinel保护的业务,例如项目中定义的controller方法就是默认被Sentinel保护的资源
ProcessorSlotChain
实现上述功能的核心骨架是一个叫做ProcessorSlotChain的类。这个类基于责任链模式来设计,将不同的功能(限流、降级、系统保护)封装为一个个的Slot,请求进入后逐个执行即可
责任链中的Slot也分为两大类:
-
统计数据构建部分(statistic)
- NodeSelectorSlot:负责构建簇点链路中的节点(DefaultNode),将这些节点形成链路树
- ClusterBuilderSlot:负责构建某个资源的ClusterNode,ClusterNode可以保存资源的运行信息(响应时间、QPS、block 数目、线程数、异常数等)以及来源信息(origin名称)
- StatisticSlot:负责统计实时调用数据,包括运行信息、来源信息等
-
规则判断部分(rule checking)
- AuthoritySlot:负责授权规则(来源控制)
- SystemSlot:负责系统保护规则
- ParamFlowSlot:负责热点参数限流规则
- FlowSlot:负责限流规则
- DegradeSlot:负责降级规则
Node
Sentinel中的簇点链路是由一个个的Node组成的,Node是一个接口,包括下面的实现:
所有的节点都可以记录对资源的访问统计数据,所以都是StatisticNode的子类
按照作用分为两类Node:
- DefaultNode:代表链路树中的每一个资源,一个资源出现在不同链路中时,会创建不同的DefaultNode节点。而树的入口节点叫EntranceNode,是一种特殊的DefaultNode
- ClusterNode:代表资源,一个资源不管出现在多少链路中,只会有一个ClusterNode。记录的是当前资源被访问的所有统计数据之和。
DefaultNode记录的是资源在当前链路中的访问数据,用来实现基于链路模式的限流规则。ClusterNode记录的是资源在所有链路中的访问数据,实现默认模式、关联模式的限流规则。
例如:我们在一个SpringMVC项目中,有两个业务:
- 业务1:controller中的资源
/order/query
访问了service中的资源/goods
- 业务2:controller中的资源
/order/save
访问了service中的资源/goods
创建的链路图如下:
Entry
默认情况下,Sentinel会将controller中的方法作为被保护资源,那么问题来了,我们该如何将自己的一段代码标记为一个Sentinel的资源呢?前面是用了 @SentinelResoutce 注解来实现的,那么这个注解的原理是什么?要搞清这玩意儿,那就得先来了解Entry这个吊毛玩意儿了
Sentinel中的资源用Entry来表示。声明Entry的API示例:
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
// 被保护的业务逻辑
// do something here...
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 在此处进行相应的处理操作
}
打开sentinel控制台,查看簇点链路:
@SentinelResoutce 注解标记资源
通过给方法添加@SentinelResource注解的形式来标记资源:
这是怎么实现的?
Sentinel依赖中有自动装配相关的东西,spring.factories声明需要就是自动装配的配置类,内容如下:
我们来看下SentinelAutoConfiguration
这个类:
可以看到,在这里声明了一个Bean,SentinelResourceAspect
:
/**
* Aspect for methods with {@link SentinelResource} annotation.
*
* @author Eric Zhao
*/
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
// 切点是添加了 @SentinelResource 注解的类
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {
}
// 环绕增强
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
// 获取受保护的方法
Method originMethod = resolveMethod(pjp);
// 获取 @SentinelResource 注解
SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
if (annotation == null) {
// Should not go through here.
throw new IllegalStateException("Wrong state for SentinelResource annotation");
}
// 获取注解上的资源名称
String resourceName = getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
int resourceType = annotation.resourceType();
Entry entry = null;
try {
// 创建资源 Entry
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
// 执行受保护的方法
Object result = pjp.proceed();
return result;
} catch (BlockException ex) {
return handleBlockException(pjp, annotation, ex);
} catch (Throwable ex) {
Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
// The ignore list will be checked first.
if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
throw ex;
}
if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
traceException(ex);
return handleFallback(pjp, annotation, ex);
}
// No fallback function can handle the exception, so throw it out.
throw ex;
} finally {
if (entry != null) {
entry.exit(1, pjp.getArgs());
}
}
}
}
简单来说,@SentinelResource注解就是一个标记,而Sentinel基于AOP思想,对被标记的方法做环绕增强,完成资源(Entry
)的创建。
Context
上一节,我们发现簇点链路中除了controller方法、service方法两个资源外,还多了一个默认的入口节点:
sentinel_spring_web_context,是一个EntranceNode类型的节点
这个节点是在初始化Context的时候由Sentinel帮我们创建的
什么是Context?
- Context 代表调用链路上下文,贯穿一次调用链路中的所有资源(
Entry
),基于ThreadLocal - Context 维持着入口节点(
entranceNode
)、本次调用链路的 curNode(当前资源节点)、调用来源(origin
)等信息 - 后续的Slot都可以通过Context拿到DefaultNode或者ClusterNode,从而获取统计数据,完成规则判断
- Context初始化的过程中,会创建EntranceNode,contextName就是EntranceNode的名称
对应的API如下:
// 创建context,包含两个参数:context名称、 来源名称
ContextUtil.enter("contextName", "originName");
Context的初始化
Context又是在何时完成初始化的?
进入SentinelWebAutoConfiguration这个类:可以直接搜,也可以去Sentinel依赖的Spring.factories中找
WebMvcConfigurer是SpringMVC自定义配置用到的类,可以配置HandlerInterceptor
SentinelWebInterceptor
的声明如下:
发现继承了AbstractSentinelInterceptor
这个类。
AbstractSentinelInterceptor
HandlerInterceptor
拦截器会拦截一切进入controller的方法,执行preHandle
前置拦截方法,而Context的初始化就是在这里完成的。
我们来看看这个类的preHandle
实现:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
try {
// 获取资源名称,一般是controller方法的 @RequestMapping 路径,例如/order/{orderId}
String resourceName = getResourceName(request);
if (StringUtil.isEmpty(resourceName)) {
return true;
}
// 从request中获取请求来源,将来做 授权规则 判断时会用
String origin = parseOrigin(request);
// 获取 contextName,默认是sentinel_spring_web_context
String contextName = getContextName(request);
// 创建 Context
ContextUtil.enter(contextName, origin);
// 创建资源,名称就是当前请求的controller方法的映射路径
Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
return true;
} catch (BlockException e) {
try {
handleBlockException(request, response, e);
} finally {
ContextUtil.exit();
}
return false;
}
}
ContextUtil
创建Context的方法就是ContextUtil.enter(contextName, origin);
进入该方法:
public static Context enter(String name, String origin) {
if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
throw new ContextNameDefineException(
"The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
}
return trueEnter(name, origin);
}
进入trueEnter
方法:
protected static Context trueEnter(String name, String origin) {
// 尝试获取context
Context context = contextHolder.get();
// 判空
if (context == null) {
// 如果为空,开始初始化
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
// 尝试获取入口节点
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
// 入口节点为空,初始化入口节点 EntranceNode
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// 添加入口节点到 ROOT
Constants.ROOT.addChild(node);
// 将入口节点放入缓存
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
} finally {
LOCK.unlock();
}
}
// 创建Context,参数为:入口节点 和 contextName
context = new Context(node, name);
// 设置请求来源 origin
context.setOrigin(origin);
// 放入ThreadLocal
contextHolder.set(context);
}
// 返回
return context;
}
综合流程
标签:服务,请求,阈值,保护,限流,链路,Sentinel,sentinel From: https://www.cnblogs.com/anhaoyang/p/sentinel-microservice-protection-1kmwvn.html