微服务sentinel入门(微服务限流容错降级Sentinel实战)
微服务sentinel入门(微服务限流容错降级Sentinel实战)举例子:M类调用B服务,N类调用C服务,如果M类和N类使用相同的线程池,那么如果B服务挂了,N类调用B服务的接口并发又很高,你又没有任何保护措施,你的服务就很可能被M类拖死。而如果M类有自己的线程池,N类也有自己的线程池,如果B服务挂了,M类顶多是将自己的线程池占满,不会影响N类的线程池——于是N类依然能正常工作。M类使用线程池1,N类使用线程池2,彼此的线程池不同,并且为每个类分配的线程池大小,例如 coreSIze=10。代码演示:(针对调用方处理)//第一步:设置RestTemplate的超时时间 @Configuration publicclassWebConfig{ @Bean publicRestTemplaterestTemplate(){ //设置restTemplate的超时时间 SimpleClientHttpRequestFactoryrequestFactory=
作者公众号:一角钱技术(org_yijiaoqian)
一、什么是雪崩效应?业务场景,高并发调用
- 正常情况下,微服务A B C D 都是正常的。
- 随着时间推移,在某一个时间点 微服务A突然挂了,此时的微服务B 还在疯狂的调用微服务A,由于A已经挂了,所以B调用A必须等待服务调用超时。而我们知道每次B -> A 的适合B都会去创建线程(而线程由计算机的资源,比如cpu、内存等)。由于是高并发场景,B 就会阻塞大量的线程。那边B所在的机器就会去创建线程,但是计算机资源是有限的,最后B的服务器就会宕机。(说白了微服务B 活生生的被猪队友微服务A给拖死了)
- 由于微服务A这个猪队友活生生的把微服务B给拖死了,导致微服务B也宕机了,然后也会导致微服务 C D 出现类似的情况,最终我们的猪队友A成功的把微服务 B C D 都拖死了。这种情况也叫做服务雪崩。也有一个专业术语(cascading failures)级联故障。
简单来说就是超时机制,配置以下超时时间,假如1秒——每次请求在1秒内必须返回,否则到点就把线程掐死,释放资源!
思路:一旦超时,就释放资源。由于释放资源速度较快,应用就不会那么容易被拖死。
代码演示:(针对调用方处理)
//第一步:设置RestTemplate的超时时间
@Configuration
publicclassWebConfig{
@Bean
publicRestTemplaterestTemplate(){
//设置restTemplate的超时时间
SimpleClientHttpRequestFactoryrequestFactory=newSimpleClientHttpRequestFactory();
requestFactory.setReadTimeout(1000);
requestFactory.setConnectTimeout(1000);
RestTemplaterestTemplate=newRestTemplate(requestFactory);
returnrestTemplate;
}
}
//第二步:进行超时异常处理
try{
ResponseEntity<ProductInfo>responseEntity=restTemplate.getForEntity(uri orderInfo.getProductNo() ProductInfo.class);
productInfo=responseEntity.getBody();
}catch(Exceptione){
log.info("调用超时");
thrownewRuntimeException("调用超时");
}
//设置全局异常处理
@ControllerAdvice
publicclassNiuhExceptionHandler{
@ExceptionHandler(value={RuntimeException.class})
@ResponseBody
publicObjectdealBizException(){
OrderVoorderVo=newOrderVo();
orderVo.setOrderNo("-1");
orderVo.setUserName("容错用户");
returnorderVo;
}
}
2.2 舱壁隔离模式
有兴趣可以先了解一下船舱构造——一般来说,现代的轮船都会分很多舱室,舱室直接用钢板焊死,彼此隔离。这样即使有某个/某些船舱进水,也不会营销其它舱室,浮力够,船不会沉。
代码中的舱壁隔离(线程池隔离模式)
M类使用线程池1,N类使用线程池2,彼此的线程池不同,并且为每个类分配的线程池大小,例如 coreSIze=10。
举例子:M类调用B服务,N类调用C服务,如果M类和N类使用相同的线程池,那么如果B服务挂了,N类调用B服务的接口并发又很高,你又没有任何保护措施,你的服务就很可能被M类拖死。而如果M类有自己的线程池,N类也有自己的线程池,如果B服务挂了,M类顶多是将自己的线程池占满,不会影响N类的线程池——于是N类依然能正常工作。
思路:不把鸡蛋放在一个篮子里,你有你的线程池,我有我的线程池,你的线程池满类和我也没关系,你挂了也和我也没关系。
2.3 断路器模式现实世界的断路器大家肯定都很了解,每个人家里都会有断路器。断路器实时监控电路的情况,如果发现电路电流异常,就会跳闸,从而防止电路被烧毁。
软件世界的断路器可以这样理解:实时监测应用,如果发现在一定时间内失败次数/失败率达到一定阀值,就“跳闸”,断路器打开——次数,请求直接返回,而不去调用原本调用的逻辑。
跳闸一段时间后(例如15秒),断路器会进入半开状态,这是一个瞬间态,此时允许一个请求调用该调的逻辑,如果成功,则断路器关闭,应用正常调用;如果调用依然不成功,断路器继续回到打开状态,过段时间再进入半开状态尝试——通过“跳闸”,应用可以保护自己,而且避免资源浪费;而通过半开的设计,可以实现应用的“自我修复”
三、Sentinel 流量控制、容错、降级3.1 什么是Sentinel?A lightweight powerful flow control component enabling reliability and monitoring for microservices.(轻量级的流量控制、熔断降级 Java 库) github官网地址:https://github.com/alibaba/Sentinelwiki:https://github.com/alibaba/Sentinel/wiki/
Hystrix 在 Sentinel 面前就是弟弟
Sentinel的初体验niuh04-ms-alibaba-sentinel-helloworld
V1版本:
- 第一步:添加依赖包
<!--导入Sentinel的相关jar包-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.7.1</version>
</dependency>
- 第二步:controller
@RestController
@Slf4j
publicclassHelloWorldSentinelController{
@Autowired
privateBusiServiceImplbusiService;
/**
*初始化流控规则
*/
@PostConstruct
publicvoidinit(){
List<FlowRule>flowRules=newArrayList<>();
/**
*定义helloSentinelV1受保护的资源的规则
*/
//创建流控规则对象
FlowRuleflowRule=newFlowRule();
//设置流控规则QPS
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源
flowRule.setResource("helloSentinelV1");
//设置受保护的资源的阈值
flowRule.setCount(1);
flowRules.add(flowRule);
//加载配置好的规则
FlowRuleManager.loadRules(flowRules);
}
/**
*频繁请求接口http://localhost:8080/helloSentinelV1
*这种做法的缺点:
*1)业务侵入性很大 需要在你的controoler中写入非业务代码..
*2)配置不灵活若需要添加新的受保护资源需要手动添加init方法来添加流控规则
*@return
*/
@RequestMapping("/helloSentinelV1")
publicStringtestHelloSentinelV1(){
Entryentity=null;
//关联受保护的资源
try{
entity=SphU.entry("helloSentinelV1");
//开始执行自己的业务方法
busiService.doBusi();
//结束执行自己的业务方法
}catch(BlockExceptione){
log.info("testHelloSentinelV1方法被流控了");
return"testHelloSentinelV1方法被流控了";
}finally{
if(entity!=null){
entity.exit();
}
}
return"OK";
}
}
测试效果:http://localhost:8080/helloSentinelV1
V1版本的缺陷如下:
- 业务侵入性很大 需要在你的controoler中写入 非业务代码.
- 配置不灵活 若需要添加新的受保护资源 需要手动添加 init方法来添加流控规则
V2版本:基于V1版本,再添加一个依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.7.1</version>
</dependency>
- 编写controller
//配置一个切面
@Configuration
publicclassSentinelConfig{
@Bean
publicSentinelResourceAspectsentinelResourceAspect(){
returnnewSentinelResourceAspect();
}
}
/**
*初始化流控规则
*/
@PostConstruct
publicvoidinit(){
List<FlowRule>flowRules=newArrayList<>();
/**
*定义helloSentinelV2受保护的资源的规则
*/
//创建流控规则对象
FlowRuleflowRule2=newFlowRule();
//设置流控规则QPS
flowRule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源
flowRule2.setResource("helloSentinelV2");
//设置受保护的资源的阈值
flowRule2.setCount(1);
flowRules.add(flowRule2);
}
/**
*频繁请求接口http://localhost:8080/helloSentinelV2
*优点:需要配置aspectj的切面SentinelResourceAspect 添加注解@SentinelResource
*解决了v1版本中sentinel的业务侵入代码问题 通过blockHandler指定被流控后调用的方法.
*缺点:若我们的controller中的方法逐步变多 那么受保护的方法也越来越多,会导致一个问题
*blockHandler的方法也会越来越多引起方法急剧膨胀怎么解决
*
*注意点:
*blockHandler对应处理BlockException的函数名称,
*可选项。blockHandler函数访问范围需要是public,返回类型需要与原方法相匹配,
*参数类型需要和原方法相匹配并且最后加一个额外的参数,
*类型为BlockException。blockHandler函数默认需要和原方法在同一个类中
*@return
*/
@RequestMapping("/helloSentinelV2")
@SentinelResource(value="helloSentinelV2" blockHandler="testHelloSentinelV2BlockMethod")
publicStringtestHelloSentinelV2(){
busiService.doBusi();
return"OK";
}
publicStringtestHelloSentinelV2BlockMethod(BlockExceptione){
log.info("testRt流控");
return"testRt降级流控...." e;
}
测试效果:http://localhost:8080/helloSentinelV2
V3版本 基于V2缺点改进
/**
*初始化流控规则
*/
@PostConstruct
publicvoidinit(){
List<FlowRule>flowRules=newArrayList<>();
/**
*定义helloSentinelV3受保护的资源的规则
*/
//创建流控规则对象
FlowRuleflowRule3=newFlowRule();
//设置流控规则QPS
flowRule3.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源
flowRule3.setResource("helloSentinelV3");
//设置受保护的资源的阈值
flowRule3.setCount(1);
flowRules.add(flowRule3);
}
/**
*我们看到了v2中的缺点 我们通过blockHandlerClass来指定处理被流控的类
*通过testHelloSentinelV3BlockMethod来指定blockHandlerClass中的方法名称
****这种方式处理异常流控的方法必须要是static的
*频繁请求接口http://localhost:8080/helloSentinelV3
*@return
*/
@RequestMapping("/helloSentinelV3")
@SentinelResource(value="helloSentinelV3" blockHandler="testHelloSentinelV3BlockMethod" blockHandlerClass=BlockUtils.class)
publicStringtestHelloSentinelV3(){
busiService.doBusi();
return"OK";
}
//异常处理类
@Slf4j
publicclassBlockUtils{
publicstaticStringtestHelloSentinelV3BlockMethod(BlockExceptione){
log.info("testHelloSentinelV3方法被流控了");
return"testHelloSentinelV3方法被流控了";
}
}
测试效果:http://localhost:8080/helloSentinelV3
缺点:不能动态的添加规则。如何解决问题?
3.2 如何在工程中快速整合Sentinel<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
添加Sentinel后,会暴露/actuator/sentinel 端点http://localhost:8080/actuator/sentinel
而Springboot默认是没有暴露该端点的,所以我们需要自己配置
server:
port:8080
management:
endpoints:
web:
exposure:
include:'*'
下载地址:https://github.com/alibaba/Sentinel/releases (我这里版本是:1.6.3)
- 第一步:执行 java -jar sentinel-dashboard-1.6.3.jar 启动(就是一个SpringBoot工程)
- 第二步:访问我们的sentinel控制台(1.6版本加入登陆页面)http://localhost:8080/ ,默认账户密码:sentinel/sentinel
- 第三步:我们的微服务 niuh04-ms-alibaba-sentinel-order 整合 sentinel,我们也搭建好了Sentinel控制台,为微服务添加sentinel的控制台地址
spring:
cloud:
sentinel:
transport:
dashboard:localhost:9999
四、Sentinel监控性能指标详解4.1 实时监控面板
在这个面板中我们监控我们接口的 通过的QPS 和 拒绝的QPS,在没有设置流控规则,我们是看不到拒绝的QPS。
4.2 簇点链路用来线上微服务的所监控的API
4.3 流控设置簇点链路 选择具体的访问的API,然后点击“流控按钮”
含义:
- 资源名:为我们接口的API /selectOrderInfoById/1
- 针对来源:这里是默认的 default(标识不针对来源),还有一种情况就是假设微服务A需要调用这个资源,微服务B也需要调用这个资源,那么我们就可以单独的为微服务A和微服务B进行设置阀值。
- 阀值类型:分为QPS和线程数,假设阀值为2
- QPS类型:指的是每秒钟访问接口的次数 > 2 就进行限流
- 线程数:为接受请求该资源,分配的线程数 > 2 就进行限流
- 直接:这种很好理解,就是达到设置的阀值后直接被流控抛出异常
疯狂的请求这个路径
- 关联
业务场景:我们现在有两个API,第一个是保存订单,一个是查询订单,假设我们希望有限操作“保存订单”
测试:写两个读写测试接口
/**
*方法实现说明:模仿流控模式【关联】读接口
*@author:hejianhui
*@paramorderNo
*@return:
*@exception:
*@date:2019/11/2422:06
*/
@RequestMapping("/findById/{orderNo}")
publicObjectfindById(@PathVariable("orderNo")StringorderNo){
log.info("orderNo:{}" "执行查询操作" System.currentTimeMillis());
returnorderInfoMapper.selectOrderInfoById(orderNo);
}
/**
*方法实现说明:模仿流控模式【关联】写接口(优先)
*@author:hejianhui
*@return:
*@exception:
*@date:2019/11/2422:07
*/
@RequestMapping("/saveOrder")
publicStringsaveOrder()throwsInterruptedException{
//Thread.sleep(500);
log.info("执行保存操作 模仿返回订单ID");
returnUUID.randomUUID().toString();
}
测试代码:写一个for循环一直调用我们的写接口,让写接口QPS达到阀值
publicclassTestSentinelRule{
publicstaticvoidmain(String[]args)throwsInterruptedException{
RestTemplaterestTemplate=newRestTemplate();
for(inti=0;i<1000;i ){
restTemplate.postForObject("http://localhost:8080/saveOrder" null String.class);
Thread.sleep(10);
}
}
}
此时访问我们的读接口:此时被限流了。
- 链路
用法说明,本地实验没成功,用alibaba 未毕业版本0.9.0可以测试出效果,API级别的限制流量
代码:
@RequestMapping("/findAll")
publicStringfindAll()throwsInterruptedException{
orderServiceImpl.common();
return"findAll";
}
@RequestMapping("/findAllByCondtion")
publicStringfindAllByCondtion(){
orderServiceImpl.common();
return"findAllByCondition";
}
@Service
publicclassOrderServiceImpl{
@SentinelResource("common")
publicStringcommon(){
return"common";
}
}
根据流控规则来说: 只会限制/findAll的请求,不会限制/findAllByCondtion规则
流控效果- 快速失败(直接抛出异常)每秒的QPS 操作过1 就直接抛出异常
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
- 预热(warmUp)源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController>
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步增加,经过预期的时间后,到达系统处理请求个数的最大值。Warm Up (冷启动,预热)模式就是为了实现这个目的。
冷加载因子:codeFacotr 默认是3
- 默认 coldFactor 为3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阀值。
上图设置:就是QPS从100/3=33开始算, 经过10秒钟,达到一百的QPS 才进行限制流量。
详情文档:https://github.com/alibaba/Sentinel/wiki/限流---冷启动
- 排队等待源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
这种方式适合用于请求以突刺状来到,这个时候我们不希望一下子把所有的请求都通过,这样可能会把系统压垮;同时我们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,而不是拒绝所有请求。
选择排队等待的阀值类型必须是****QPS
上图设置:单机阀值为10,表示每秒通过的请求个数是10,也就是每个请求平均间隔恒定为 1000 / 10 = 100 ms,每一个请求的最长等待时间(maxQueueingTimeMs)为 20 * 1000ms = 20s。,超过20s就丢弃请求。
详情文档:https://github.com/alibaba/Sentinel/wiki/流量控制-匀速排队模式
4.4 降级规则rt(平均响应时间)平均响应时间(DEGRADE_GRADE_RT):当 1s 内持续进入5个请求,对应时刻的平均响应时间(秒级)均超过阀值(count,以 ms 为单位),那么在接下来的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。
注意:Sentinel 默认同级的 RT 上限是4900ms,超出此阀值都会算做4900ms,若需要变更此上限可以通过启动配置项:-Dcsp.sentinel.statistic.max.rt=xxx 来配置
异常比例(DEGRADE_GRADE_EXCEPTION_RATIO)当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阀值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比例的阀值范围是 [0.0 1.0],代表 0% ~ 100% 。
异常数(DEGRADE_GRADE_EXCEPTION_COUNT)当资源近千分之的异常数目超过阀值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
4.5 热点参数业务场景:秒杀业务,比如商场做促销秒杀,针对苹果11(商品id=1)进行9.9秒杀活动,那么这个时候,我们去请求订单接口(商品id=1)的请求流量十分大,我们就可以通过热点参数规则来控制 商品id=1 的请求的并发量。而其他正常商品的请求不会受到限制。那么这种热点参数规则使用。
五、Sentinel-dashboard 控制台 和 我们的微服务通信原理5.1 控制台如何获取到微服务的监控信息?5.2 在控制台配置规则,如何把规则推送给微服务的?我们通过观察到sentinel-dashboard的机器列表上观察注册服务微服务信息。我们的 控制台就可以通过这些微服务的注册信息跟我们的具体的微服务进行通信.
5.3 微服务整合sentinel时候的提供的一些接口API地址: http://localhost:8720/api 5.4 我们可以通过代码设置规则(我们这里用流控规则为例)@RestController
publicclassAddFlowLimitController{
@RequestMapping("/addFlowLimit")
publicStringaddFlowLimit(){
List<FlowRule>flowRuleList=newArrayList<>();
FlowRuleflowRule=newFlowRule("/testAddFlowLimitRule");
//设置QPS阈值
flowRule.setCount(1);
//设置流控模型为QPS模型
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRuleList.add(flowRule);
FlowRuleManager.loadRules(flowRuleList);
return"success";
}
@RequestMapping("/testAddFlowLimitRule")
publicStringtestAddFlowLimitRule(){
return"testAddFlowLimitRule";
}
}
添加效果截图: 执行:http://localhost:8080/addFlowLimit
Sentinel具体配置项:https://github.com/alibaba/Sentinel/wiki/启动配置项
spring:
cloud:
nacos:
discovery:
server-addr:localhost:8848
sentinel:
transport:
dashboard:localhost:9999
filter:
enabled:true#关闭Springmvc的端点保护
那么我们的这种类型的接口 不会被sentinel保护
只有加了 @SentinelResource 的注解的资源才会被保护
六、Ribbon整合Sentinel6.1 第一步:加配置<!--加入ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
6.2 第二步:加注解
在我们的RestTemplate组件上添加@SentinelRestTemplate注解。并且我们可以通过在@SentinelRestTemplate 同样的可以指定我们的 blockHandlerClass、fallbackClass、blockHandler、fallback 这四个属性
@Configuration
publicclassWebConfig{
@Bean
@LoadBalanced
@SentinelRestTemplate(
blockHandler="handleException" blockHandlerClass=GlobalExceptionHandler.class
fallback="fallback" fallbackClass=GlobalExceptionHandler.class
)
publicRestTemplaterestTemplate(){
returnnewRestTemplate();
}
}
*****************全局异常处理类*****************
@Slf4j
publicclassGlobalExceptionHandler{
/**
*限流后处理方法
*@paramrequest
*@parambody
*@paramexecution
*@paramex
*@return
*/
publicstaticSentinelClientHttpResponsehandleException(HttpRequestrequest
byte[]body ClientHttpRequestExecutionexecution BlockExceptionex){
ProductInfoproductInfo=newProductInfo();
productInfo.setProductName("被限制流量拉");
productInfo.setProductNo("-1");
ObjectMapperobjectMapper=newObjectMapper();
try{
returnnewSentinelClientHttpResponse(objectMapper.writeValueAsString(productInfo));
}catch(JsonProcessingExceptione){
e.printStackTrace();
returnnull;
}
}
/**
*熔断后处理的方法
*@paramrequest
*@parambody
*@paramexecution
*@paramex
*@return
*/
publicstaticSentinelClientHttpResponsefallback(HttpRequestrequest
byte[]body ClientHttpRequestExecutionexecution BlockExceptionex){
ProductInfoproductInfo=newProductInfo();
productInfo.setProductName("被降级拉");
productInfo.setProductNo("-1");
ObjectMapperobjectMapper=newObjectMapper();
try{
returnnewSentinelClientHttpResponse(objectMapper.writeValueAsString(productInfo));
}catch(JsonProcessingExceptione){
e.printStackTrace();
returnnull;
}
}
}
6.3 第三步:添加配置
什么时候关闭:一般在我们的自己测试业务功能是否正常的情况,关闭该配置
#是否开启@SentinelRestTemplate注解
resttemplate:
sentinel:
enabled:true
七、OpenFeign整合我们的Sentinel7.1 第一步:加配置
在niuh05-ms-alibaba-feignwithsentinel-order上 pom.xml中添加配置
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.niuh</groupId>
<artifactId>niuh03-ms-alibaba-feign-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
7.2 第二步:在Feign的声明式接口上添加fallback属性或者 fallbackFactory属性
- 为我们添加fallback属性的api
@FeignClient(name="product-center" fallback=ProductCenterFeignApiWithSentinelFallback.class)
publicinterfaceProductCenterFeignApiWithSentinel{
/**
*声明式接口 远程调用http://product-center/selectProductInfoById/{productNo}
*@paramproductNo
*@return
*/
@RequestMapping("/selectProductInfoById/{productNo}")
ProductInfoselectProductInfoById(@PathVariable("productNo")StringproductNo)throwsInterruptedException;
}
我们feign的限流降级接口(通过fallback没有办法获取到异常的)
@Component
publicclassProductCenterFeignApiWithSentinelFallbackimplementsProductCenterFeignApiWithSentinel{
@Override
publicProductInfoselectProductInfoById(StringproductNo){
ProductInfoproductInfo=newProductInfo();
productInfo.setProductName("默认商品");
returnproductInfo;
}
}
- 为我们添加fallbackFactory属性的api
packagecom.niuh.feignapi.sentinel;
importcom.niuh.entity.ProductInfo;
importcom.niuh.handler.ProductCenterFeignApiWithSentielFallbackFactoryasdasf;
importcom.niuh.handler.ProductCenterFeignApiWithSentinelFallback;
importorg.springframework.cloud.openfeign.FeignClient;
importorg.springframework.web.bind.annotation.PathVariable;
importorg.springframework.web.bind.annotation.RequestMapping;
/**
*Createdbyhejianhuion2019/11/22.
*/
@FeignClient(name="product-center" fallbackFactory=ProductCenterFeignApiWithSentielFallbackFactoryasdasf.class)
publicinterfaceProductCenterFeignApiWithSentinel{
/**
*声明式接口 远程调用http://product-center/selectProductInfoById/{productNo}
*@paramproductNo
*@return
*/
@RequestMapping("/selectProductInfoById/{productNo}")
ProductInfoselectProductInfoById(@PathVariable("productNo")StringproductNo)throwsInterruptedException;
}
通过FallbackFactory属性可以处理我们的异常
@Component
@Slf4j
publicclassProductCenterFeignApiWithSentielFallbackFactoryasdasfimplementsFallbackFactory<ProductCenterFeignApiWithSentinel>{
@Override
publicProductCenterFeignApiWithSentinelcreate(Throwablethrowable){
returnnewProductCenterFeignApiWithSentinel(){
@Override
publicProductInfoselectProductInfoById(StringproductNo){
ProductInfoproductInfo=newProductInfo();
if(throwableinstanceofFlowException){
log.error("流控了....{}" throwable.getMessage());
productInfo.setProductName("我是被流控的默认商品");
}else{
log.error("降级了....{}" throwable.getMessage());
productInfo.setProductName("我是被降级的默认商品");
}
returnproductInfo;
}
};
}
}
八、Sentinel 规则持久化
Sentinel-dashboard 配置的规则,在我们的微服务以及控制台重启的时候就清空了,因为它是基于内存的。
8.1 原生模式Dashboard 的推送规则方式是通过 API 将规则推送至客户端并直接更新到内存。
优缺点:这种做法的好处是简单,无依赖;坏处是应用重启规则就会消失,仅用于简单测试,不能用于生产环境。
8.2 Pull拉模式首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。使用 pull 模式的数据源时一般不需要对 Sentinel 控制台进行改造。
这种实现方法好处是简单,不引入新的依赖,坏处是无法保证监控数据的一致性
客户端Sentinel的改造(拉模式)通过SPI扩展机制进行扩展,我们写一个拉模式的实现类 com.niuh.persistence.PullModeByFileDataSource ,然后在工厂目录下创建 META-INF/services/com.alibaba.csp.sentinel.init.InitFun文件。
文件的内容就是写我们的拉模式的实现类:
代码在niuh05-ms-alibaba-sentinelrulepersistencepull-order 工程的persistence包下。
8.3 Push推模式(以Nacos为例,生产推荐使用) 原理简述- 控制台推送规则:
- 将规则推送至Nacos获取其他远程配置中心
- Sentinel客户端连接Nacos,获取规则配置;并监听Nacos配置变化,如果发送变化,就更新本地缓存(从而让本地缓存总是和Nacos一致)
- 控制台监听Nacos配置变化,如果发送变化就更新本地缓存(从而让控制台本地缓存和Nacos一致)
微服务改造方案
- 第一步:在niuh05-ms-alibaba-sentinelrulepersistencepush-order工程加入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- 第二步:加入yml的配置
spring:
cloud:
nacos:
discovery:
server-addr:localhost:8848
sentinel:
transport:
dashboard:localhost:9999
#namespace:bc7613d2-2e22-4292-a748-48b78170f14c#指定namespace的id
datasource:
#名称随意
flow:
nacos:
server-addr:47.111.191.111:8848
dataId:${spring.application.name}-flow-rules
groupId:SENTINEL_GROUP
rule-type:flow
degrade:
nacos:
server-addr:localhost:8848
dataId:${spring.application.name}-degrade-rules
groupId:SENTINEL_GROUP
rule-type:degrade
system:
nacos:
server-addr:localhost:8848
dataId:${spring.application.name}-system-rules
groupId:SENTINEL_GROUP
rule-type:system
authority:
nacos:
server-addr:localhost:8848
dataId:${spring.application.name}-authority-rules
groupId:SENTINEL_GROUP
rule-type:authority
param-flow:
nacos:
server-addr:localhost:8848
dataId:${spring.application.name}-param-flow-rules
groupId:SENTINEL_GROUP
rule-type:param-flow
Sentinel-dashboard改造方案
<!--forNacosrulepublishersample-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!--<scope>test</scope>-->//需要把test注释掉
</dependency>
控制台改造主要是为规则实现:
- DynamicRuleProvider :从Nacos上读取配置
- DynamicRulePublisher :将规则推送到Nacis上
在sentinel-dashboard工程目录com.alibaba.csp.sentinel.dashboard.rule 下创建一 个Nacos的包,然后把我们的各个场景的配置规则类写到该包下.
我们以ParamFlowRuleController(热点参数流控类作为修改作为演示)
/**
*@authorEricZhao
*@since0.2.1
*/
@RestController
@RequestMapping(value="/paramFlow")
publicclassParamFlowRuleController{
privatefinalLoggerlogger=LoggerFactory.getLogger(ParamFlowRuleController.class);
@Autowired
privateSentinelApiClientsentinelApiClient;
@Autowired
privateAppManagementappManagement;
@Autowired
privateRuleRepository<ParamFlowRuleEntity Long>repository;
@Autowired
@Qualifier("niuhHotParamFlowRuleNacosPublisher")
privateDynamicRulePublisher<List<ParamFlowRuleEntity>>rulePublisher;
@Autowired
@Qualifier("niuhHotParamFlowRuleNacosProvider")
privateDynamicRuleProvider<List<ParamFlowRuleEntity>>ruleProvider;
@Autowired
privateAuthService<HttpServletRequest>authService;
privatebooleancheckIfSupported(Stringapp Stringip intport){
try{
returnOptional.ofNullable(appManagement.getDetailApp(app))
.flatMap(e->e.getMachine(ip port))
.flatMap(m->VersionUtils.parseVersion(m.getVersion())
.map(v->v.greaterOrEqual(version020)))
.orElse(true);
//Iferroroccurredorcannotretrievemachineinfo returntrue.
}catch(Exceptionex){
returntrue;
}
}
@GetMapping("/rules")
publicResult<List<ParamFlowRuleEntity>>apiQueryAllRulesForMachine(HttpServletRequestrequest
@RequestParamStringapp
@RequestParamStringip
@RequestParamIntegerport){
AuthUserauthUser=authService.getAuthUser(request);
authUser.authTarget(app PrivilegeType.READ_RULE);
if(StringUtil.isEmpty(app)){
returnResult.ofFail(-1 "appcannotbenullorempty");
}
if(StringUtil.isEmpty(ip)){
returnResult.ofFail(-1 "ipcannotbenullorempty");
}
if(port==null||port<=0){
returnResult.ofFail(-1 "Invalidparameter:port");
}
if(!checkIfSupported(app ip port)){
returnunsupportedVersion();
}
try{
/*returnsentinelApiClient.fetchParamFlowRulesOfMachine(app ip port)
.thenApply(repository::saveAll)
.thenApply(Result::ofSuccess)
.get();*/
List<ParamFlowRuleEntity>rules=ruleProvider.getRules(app);
rules=repository.saveAll(rules);
returnResult.ofSuccess(rules);
}catch(ExecutionExceptionex){
logger.error("Errorwhenqueryingparameterflowrules" ex.getCause());
if(isNotSupported(ex.getCause())){
returnunsupportedVersion();
}else{
returnResult.ofThrowable(-1 ex.getCause());
}
}catch(Throwablethrowable){
logger.error("Errorwhenqueryingparameterflowrules" throwable);
returnResult.ofFail(-1 throwable.getMessage());
}
}
privatebooleanisNotSupported(Throwableex){
returnexinstanceofCommandNotFoundException;
}
@PostMapping("/rule")
publicResult<ParamFlowRuleEntity>apiAddParamFlowRule(HttpServletRequestrequest
@RequestBodyParamFlowRuleEntityentity){
AuthUserauthUser=authService.getAuthUser(request);
authUser.authTarget(entity.getApp() PrivilegeType.WRITE_RULE);
Result<ParamFlowRuleEntity>checkResult=checkEntityInternal(entity);
if(checkResult!=null){
returncheckResult;
}
if(!checkIfSupported(entity.getApp() entity.getIp() entity.getPort())){
returnunsupportedVersion();
}
entity.setId(null);
entity.getRule().setResource(entity.getResource().trim());
Datedate=newDate();
entity.setGmtCreate(date);
entity.setGmtModified(date);
try{
entity=repository.save(entity);
//publishRules(entity.getApp() entity.getIp() entity.getPort()).get();
publishRules(entity.getApp());
returnResult.ofSuccess(entity);
}catch(ExecutionExceptionex){
logger.error("Errorwhenaddingnewparameterflowrules" ex.getCause());
if(isNotSupported(ex.getCause())){
returnunsupportedVersion();
}else{
returnResult.ofThrowable(-1 ex.getCause());
}
}catch(Throwablethrowable){
logger.error("Errorwhenaddingnewparameterflowrules" throwable);
returnResult.ofFail(-1 throwable.getMessage());
}
}
private<R>Result<R>checkEntityInternal(ParamFlowRuleEntityentity){
if(entity==null){
returnResult.ofFail(-1 "badrulebody");
}
if(StringUtil.isBlank(entity.getApp())){
returnResult.ofFail(-1 "appcan'tbenullorempty");
}
if(StringUtil.isBlank(entity.getIp())){
returnResult.ofFail(-1 "ipcan'tbenullorempty");
}
if(entity.getPort()==null||entity.getPort()<=0){
returnResult.ofFail(-1 "portcan'tbenull");
}
if(entity.getRule()==null){
returnResult.ofFail(-1 "rulecan'tbenull");
}
if(StringUtil.isBlank(entity.getResource())){
returnResult.ofFail(-1 "resourcenamecannotbenullorempty");
}
if(entity.getCount()<0){
returnResult.ofFail(-1 "countshouldbevalid");
}
if(entity.getGrade()!=RuleConstant.FLOW_GRADE_QPS){
returnResult.ofFail(-1 "Unknownmode(blockGrade)forparameterflowcontrol");
}
if(entity.getParamIdx()==null||entity.getParamIdx()<0){
returnResult.ofFail(-1 "paramIdxshouldbevalid");
}
if(entity.getDurationInSec()<=0){
returnResult.ofFail(-1 "durationInSecshouldbevalid");
}
if(entity.getControlBehavior()<0){
returnResult.ofFail(-1 "controlBehaviorshouldbevalid");
}
returnnull;
}
@PutMapping("/rule/{id}")
publicResult<ParamFlowRuleEntity>apiUpdateParamFlowRule(HttpServletRequestrequest
@PathVariable("id")Longid
@RequestBodyParamFlowRuleEntityentity){
AuthUserauthUser=authService.getAuthUser(request);
if(id==null||id<=0){
returnResult.ofFail(-1 "Invalidid");
}
ParamFlowRuleEntityoldEntity=repository.findById(id);
if(oldEntity==null){
returnResult.ofFail(-1 "id" id "doesnotexist");
}
authUser.authTarget(oldEntity.getApp() PrivilegeType.WRITE_RULE);
Result<ParamFlowRuleEntity>checkResult=checkEntityInternal(entity);
if(checkResult!=null){
returncheckResult;
}
if(!checkIfSupported(entity.getApp() entity.getIp() entity.getPort())){
returnunsupportedVersion();
}
entity.setId(id);
Datedate=newDate();
entity.setGmtCreate(oldEntity.getGmtCreate());
entity.setGmtModified(date);
try{
entity=repository.save(entity);
//publishRules(entity.getApp() entity.getIp() entity.getPort()).get();
publishRules(entity.getApp());
returnResult.ofSuccess(entity);
}catch(ExecutionExceptionex){
logger.error("Errorwhenupdatingparameterflowrules id=" id ex.getCause());
if(isNotSupported(ex.getCause())){
returnunsupportedVersion();
}else{
returnResult.ofThrowable(-1 ex.getCause());
}
}catch(Throwablethrowable){
logger.error("Errorwhenupdatingparameterflowrules id=" id throwable);
returnResult.ofFail(-1 throwable.getMessage());
}
}
@DeleteMapping("/rule/{id}")
publicResult<Long>apiDeleteRule(HttpServletRequestrequest @PathVariable("id")Longid){
AuthUserauthUser=authService.getAuthUser(request);
if(id==null){
returnResult.ofFail(-1 "idcannotbenull");
}
ParamFlowRuleEntityoldEntity=repository.findById(id);
if(oldEntity==null){
returnResult.ofSuccess(null);
}
authUser.authTarget(oldEntity.getApp() PrivilegeType.DELETE_RULE);
try{
repository.delete(id);
/*publishRules(oldEntity.getApp() oldEntity.getIp() oldEntity.getPort()).get();*/
publishRules(oldEntity.getApp());
returnResult.ofSuccess(id);
}catch(ExecutionExceptionex){
logger.error("Errorwhendeletingparameterflowrules" ex.getCause());
if(isNotSupported(ex.getCause())){
returnunsupportedVersion();
}else{
returnResult.ofThrowable(-1 ex.getCause());
}
}catch(Throwablethrowable){
logger.error("Errorwhendeletingparameterflowrules" throwable);
returnResult.ofFail(-1 throwable.getMessage());
}
}
privateCompletableFuture<Void>publishRules(Stringapp Stringip Integerport){
List<ParamFlowRuleEntity>rules=repository.findAllByMachine(MachineInfo.of(app ip port));
returnsentinelApiClient.setParamFlowRuleOfMachine(app ip port rules);
}
privatevoidpublishRules(Stringapp)throwsException{
List<ParamFlowRuleEntity>rules=repository.findAllByApp(app);
rulePublisher.publish(app rules);
}
private<R>Result<R>unsupportedVersion(){
returnResult.ofFail(4041
"Sentinelclientnotsupportedforparameterflowcontrol(unsupportedversionordependencyabsent)");
}
privatefinalSentinelVersionversion020=newSentinelVersion().setMinorVersion(2);
}
8.4 阿里云的 AHAS
- 开通地址:https://ahas.console.aliyun.com/
- 开通规则说明:https://help.aliyun.com/document_detail/90323.html
第一步:访问 https://help.aliyun.com/document_detail/90323.html
第二步:免费开通
第三步:开通
第四步:接入应用
第五步:点击接入SDK
第六步:加入我们的应用
以niuh05-ms-alibaba-sentinelrulepersistence-ahas-order工程为例
- 加入ahas的依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>spring‐boot‐starter‐ahas‐sentinel‐client</artifactId>4<version>1.5.0</version>
</dependency>
- 加入配置:yml的配置
ahas.namespace:default
project.name:order-center
ahas.license:b833de8ab5f34e4686457ecb2b60fa46
- 测试接口
@SentinelResource("hot-param-flow-rule")
@RequestMapping("/testHotParamFlowRule")
publicOrderInfotestHotParamFlowRule(@RequestParam("orderNo")StringorderNo){
returnorderInfoMapper.selectOrderInfoById(orderNo);
}
第一次访问接口:
AHas控制台出现我们的微服务
添加我们直接的流控规则
疯狂刷新我们的测试接口:
九、Sentinel 线上环境的优化9.1 优化错误页面- 流控错误页面
- 降级错误页面
发现这两种错误都是医院,显然这里我们需要优化 UrlBlockHandler 提供了一个接口,我们需要实现这个接口
/**
*@vlog:高于生活,源于生活
*@desc:类的描述:处理流控 降级规则
*@author:hejianhui
*@createDate:2019/12/316:40
*@version:1.0
*/
@Component
publicclassNiuhUrlBlockHandlerimplementsUrlBlockHandler{
publicstaticfinalLoggerlog=LoggerFactory.getLogger(NiuhUrlBlockHandler.class);
@Override
publicvoidblocked(HttpServletRequestrequest HttpServletResponseresponse BlockExceptionex)throwsIOException{
if(exinstanceofFlowException){
log.warn("触发了流控");
warrperResponse(response ErrorEnum.FLOW_RULE_ERR);
}elseif(exinstanceofParamFlowException){
log.warn("触发了参数流控");
warrperResponse(response ErrorEnum.HOT_PARAM_FLOW_RULE_ERR);
}elseif(exinstanceofAuthorityException){
log.warn("触发了授权规则");
warrperResponse(response ErrorEnum.AUTH_RULE_ERR);
}elseif(exinstanceofSystemBlockException){
log.warn("触发了系统规则");
warrperResponse(response ErrorEnum.SYS_RULE_ERR);
}else{
log.warn("触发了降级规则");
warrperResponse(response ErrorEnum.DEGRADE_RULE_ERR);
}
}
privatevoidwarrperResponse(HttpServletResponsehttpServletResponse ErrorEnumerrorEnum)throwsIOException{
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setHeader("Content-Type" "application/json;charset=utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
ObjectMapperobjectMapper=newObjectMapper();
StringerrMsg=objectMapper.writeValueAsString(newErrorResult(errorEnum));
httpServletResponse.getWriter().write(errMsg);
}
}
优化后:
- 流控规则提示:
- 降级规则提示:
Sentinel 提供了一个 RequestOriginParser 接口,我们可以在这里实现编码从请求头中区分来源
/**
*@vlog:高于生活,源于生活
*@desc:类的描述:区分来源接口
*@author:hejianhui
*@createDate:2019/12/413:13
*@version:1.0
*/
/*@Component*/
@Slf4j
publicclassNiuhRequestOriginParseimplementsRequestOriginParser{
@Override
publicStringparseOrigin(HttpServletRequestrequest){
Stringorigin=request.getHeader("origin");
if(StringUtils.isEmpty(origin)){
log.warn("originmustnotnull");
thrownewIllegalArgumentException("requestoriginmustnotnull");
}
returnorigin;
}
}
配置设置区分来源为:yijiaoqian
9.3 解决RestFul风格的请求例如:/selectOrderInfoById/2 、 /selectOrderInfoById/1 需要转为/selectOrderInfoById/{number}
/**
*@vlog:高于生活,源于生活
*@desc:类的描述:解决RestFule风格的请求
*eg:/selectOrderInfoById/2/selectOrderInfoById/1需要转为/selectOrderInfoById/{number}
*@author:hejianhui
*@createDate:2019/12/413:28
*@version:1.0
*/
@Component
@Slf4j
publicclassNiuhUrlcleanimplementsUrlCleaner{
@Override
publicStringclean(StringoriginUrl){
log.info("originUrl:{}" originUrl);
if(StringUtils.isEmpty(originUrl)){
log.error("originUrlnotbenull");
thrownewIllegalArgumentException("originUrlnotbenull");
}
returnreplaceRestfulUrl(originUrl);
}
/**
*方法实现说明:把/selectOrderInfoById/2替换成/selectOrderInfoById/{number}
*@author:hejianhui
*@paramsourceUrl目标url
*@return:替换后的url
*@exception:
*@date:2019/12/413:46
*/
privateStringreplaceRestfulUrl(StringsourceUrl){
List<String>origins=Arrays.asList(sourceUrl.split("/"));
StringBuffertargetUrl=newStringBuffer("/");
for(Stringstr:origins){
if(NumberUtils.isNumber(str)){
targetUrl.append("/{number}");
}else{
targetUrl.append(str);
}
}
returntargetUrl.toString();
}
}
PS:以上代码提交在 Github :
https://github.com/Niuh-Study/niuh-cloud-alibaba.git
文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。