持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情
API分组管理
上面改造的双向数据同步的sentinel控制台我们可以在生产环境使用,但我们为了防止改造的sentinel控制台出现什么隐藏的bug,我们还是使用官方下载的sentinel原版jar包。我们需要将新增的 bootstrap.yml 配置文件重命名为 bootstrap.yml.nacos.bak ,然后恢复原来的配置文件;并且删除nacos配置列表中所有新创建的SENTINEL_GROUP组ID的规则配置,以还原sentinel控制台配置环境。
限流的资源可以为默认Gateway的路由ID,还可以是基于Sentinel自定义的API名,并且后者是URL级别的粒度。
如果同时配置了
路由维度和API维度的限流规则,那么会优先触发API维度的限流规则。
前面测试降级功能时,因为是在请求链路上进行的配置,因此资源名(路由ID/API名称)默认就是路由ID。并且降级功能可以搭配限流功能使用,也可以不搭配直接使用服务降级规则进行使用。在Sentinel 1.8.3版本因为将服务降级后熔断的逻辑整合到服务降级功能后了,因此,设置降级时还需要设置熔断时长等配置,相当于将服务熔断功能一并整合进去了。
上面因为资源名为Gateway提供的路由ID,粒度较大(相当于整个路由上的请求都会进行限流),因此我们可以使用Sentinel的自定义API分组来设置URL级别的限流、降级以及熔断规则。
因为设置的Route ID生效的范围较广(将sentinel所有路径的请求都包括进去了),我们需要避免多种限流规则和熔断规则重叠,因此我们需要将Gateway中的路由ID范围稍微缩小以方便测试自定义的API分组:
1. 添加API名称
首先我们需要在sentinel控制台面板添加新API分组:
抓包数据为:
[
{
"id":27,
"app":"service-sentinel",
"ip":"192.168.174.1",
"port":8721,
"gmtCreate":null,
"gmtModified":null,
"apiName":"/service-sentinel/sentinel/limit",
"predicateItems":[
{
"pattern":"/service-sentinel/sentinel/limit",
"matchStrategy":0
}
]
}
]
API 分组有三种配置模式:精确、前缀和正则三种模式:
- 精确模式:指对URL的路径完全匹配时,进行限流。例如,匹配串配置为 /order/1
- 前缀模式:指对URL的路径前缀匹配时,进行限流。例如,匹配串配置为 /order/*
- 正则模式:指对URL的路径符合正则表示式规则时,进行限流。例如,匹配串配置为 /order/\d*
然后可以在左侧 API管理 选项中看到新增的自定义API分组(基于Sentinel实现,URL级别的资源):
注意:这里因为没有在nacos配置文件中新增配置文件,因此重启项目后配置会消失。切记,API分组的路径是包括网关动态或静态路由的所有前缀,也就是说服务名不可省略,否则API分组的限流会失效。
Github Issues参考:spring cloud gateway Integration sentinel 限流不生效 #2278
2. 添加API限流规则
在控制台界面的 流控规则 选项中添加基于API为资源的限流规则:
保存配置,可以看到成功创建了基于API分组的限流规则:
3. 测试基于API分组的限流规则
基于API的限流规则是优先于基于路由ID的,如果担心干扰,可以删除上面基于Route ID的限流规则再进行测试
下面我们使用curl命令,测试基于API分组的限流规则是否生效:
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
hi, this is service-sentinel-limit test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
hi, this is service-sentinel-limit test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"msg":"热点参数限流","code":102}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"msg":"热点参数限流","code":102}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"msg":"热点参数限流","code":102}
可以看到成功触发了限流规则,并且返回了热点参数限流的异常提示,测试成功!
4. 持久化API和基于API分组的限流规则
但是新增的API以及基于API分组的限流规则,都是基于内存的,重启项目后会自动删除。因此,我们可以尝试将API和基于API分组的限流规则持久化到Nacos中。
首先我们需要从本地nacos动态数据源中,添加API和基于API分组的限流规则配置:
datasource:
# 限流(基于路由id和api分组)规则配置(名称可自定义)
flow:
nacos:
server-addr: localhost:8848 #nacos的访问地址,,根据上面准备工作中启动的实例配置
dataId: service-sentinel-flow-rules #nacos中存储规则的dataId
groupId: DEFAULT_GROUP #nacos中存储规则的groupId
# namespace: 5a5f3595-c5b3-4165-8fc5-a5dc44aaf80f #Nacos命名空间的ID(public下不用添加)
data-type: json #配置文件类型
# 流控规则类型来自RuleType,取值见: com.alibaba.cloud.sentinel.datasource.RuleType
# (flow,degrade,authority,system, param-flow, gw-flow, gw-api-group)
# 注意:网关流控规则数据源类型是gw-flow,若将网关流控规则数据源指定为flow则不生效(当然配置文件手动读取也可以强制生效)。
# rule-type: flow #网关限流规则(1.8.3以前的版本有效)
rule-type: gw-flow #网关限流规则(1.8.3及以后版本的flow参数)
# rule-type: gw-api-group #网关API分组
# 降级熔断规则配置
degrade:
nacos:
server-addr: 127.0.0.1:8848
dataId: service-sentinel-degrade-rules
groupId: DEFAULT_GROUP
data-type: json
rule-type: degrade
# api分组
api-group:
nacos:
server-addr: localhost:8848
dataId: service-sentinel-api-groups
groupId: DEFAULT_GROUP
data-type: json
rule-type: gw-api-group
接下来,就是在Nacos的配置列表中添加对应Data ID名称的配置了。
但是我没有新增分组的API的参考配置,怎么办? 答案是:通过抓包获取配置参数
对上面抓包获得的JSON进行处理(去除特征参数、json格式化)后,获得新增API同步配置参数,然后新增并添加到Nacos的配置文件service-sentinel-api-groups中:
[ { "app":"default", "apiName":"/service-sentinel/sentinel/limit", "predicateItems":[ { "pattern":"/service-sentinel/sentinel/limit", "matchStrategy":0 } ]
}
]
新增api组完成,可以看到Nacos控制台界面出现了添加配置文件(由于配置和应用开始变多了,我们为了管理更清晰,进行了备注):
参考基于Route ID的限流规则 service-sentinel-flow-rules的 配置 ;在 service-sentinel-flow-rules 配置中添加resourceMode 参数并设置为1(表示是基于API分组的限流规则),然后将 resource 参数设置为新增API名称(/service-sentinel/sentinel/limit):
注意sentinel-nacos数据源只允许一个限流配置文件(如果同时定义两个rule-type相同的配置,后者会覆盖前者),因此我们只需将路由id限流规则和api分组限流规则的配置合并到一个nacos配置文件中即可
[
{
"resource": "service-sentinel",
"resourceMode": "0",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
},
{
"resource": "/service-sentinel/sentinel/limit",
"resourceMode": "1",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
重启项目,我们在sentinel控制台界面可以看到已经同步了配置:
基于Route ID和API分组的限流规则
API分组
下面我们尝试测试基于API分组的限流规则:
5. 测试API分组限流功能
这里没有注释基于Route ID的限流规则(前缀为service-sentinel应用,范围更大),这是因为sentinel中同时配置了基于
路由ID和API分组的限流规则时,会优先触发基于API分组的限流规则。如果担心基于路由ID的限流规则干扰,可以在同步配置后在sentinel控制台删除路由ID限流规则。
使用curl命令,测试limit接口的API分组限流效果(这里我们先删除路由ID限流规则再进行测试):
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
hi, this is service-sentinel-limit test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
hi, this is service-sentinel-limit test!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"msg":"热点参数限流","code":102}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"msg":"热点参数限流","code":102}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"msg":"热点参数限流","code":102}
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/limit
{"msg":"热点参数限流","code":102}
可以看到,基于API分组的限流规则成功生效!
总结
虽然基于路由ID的限流也能实现基于API分组的限流中的精准匹配、前缀和正则表达式功能(filter实现),但是需要修改本地yml配置后重启项目生效(当然动态替换jar包修改也可以),基于API分组的限流规则已经实现了这个功能并且可以在Sentinel控制台面板上实时修改配置。
因此,推荐使用API分组作为限流规则的资源名。
OpenFeign调用实现熔断降级
前面我们在集成OpenFeign的时候,有看到FeignClient配置中的fallback降级熔断参数,我们这里来搭配Sentinel实现OpenFeign的调用降级熔断的备用逻辑。
这里我们分两步来进行:首先是Gateway集成OpenFeign,然后是OpenFeign使用Sentinel实现降级熔断。
Gateway集成OpenFeign
1. 引入依赖
首先在集成OpenFeign实现降级熔断调用之前,我们需要先引入OpenFeign依赖:
<!-- openfeign 远程调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<!-- 排除与nacos冲突的commons-io -->
<exclusion>
<artifactId>commons-io</artifactId>
<groupId>commons-io</groupId>
</exclusion>
</exclusions>
</dependency>
2. Sentinel开启Feign支持
在yml配置文件中,开启sentinel支持feign调用开关:
feign:
sentinel:
# feign调用支持开关
enabled: true
其实不用开启,也能使用feign调用......
3. 启动类启用OpenFeign客户端
package com.deepinsea;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* Created by deepinsea on 2022/7/3.
* 服务熔断主启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceSentinelApplication {
public static void main(String[] args) {
System.setProperty("csp.sentinel.app.type", "1"); //设置为支持API分组的新控制台界面(1.8.3及以上版本)
SpringApplication.run(ServiceSentinelApplication.class, args);
}
}
4. 添加OpenFeign客户端接口
package com.deepinsea.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Created by deepinsea on 2022/7/24.
* OpenFeign客户端接口
* 参数解释:
* value、name:value和name的作用一样,如果没有配置url那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。
* contextId:我们不想将所有的调用接口都定义在一个类中,有一种解决方案就是为每个Client手动指定不同的contextId,这样就不会冲突了。
* url:用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。像调试等场景可以使用。
* decode404:当调用请求发生404错误时,那么会执行decoder解码,否则抛出异常。
* fallback:定义容错的处理类,也就是回退逻辑,fallback的类必须实现Feign Client的接口,无法知道熔断的异常信息。
* fallbackFactory:也是容错的处理,可以知道熔断的异常堆栈信息。
* path:定义当前FeignClient访问接口时的统一前缀。
*/
@FeignClient(name = "service-provider-nacos", decode404 = true)
public interface OpenFeignClient {
@GetMapping("/provider-nacos/hello")
String hello();
}
后面我们需要实现fallback降级逻辑的话,可以直接实现OpenFeignClient客户端接口即可。
这里的decode404参数可以不用添加,不会影响正常调用;但是添加这个参数是为了执行请求404错误时的默认解码逻辑,而不是直接抛出异常。
5. 添加服务调用控制器
package com.deepinsea.controller;
import com.deepinsea.common.transport.FeignAsyncTransporter;
import com.deepinsea.service.OpenFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by deepinsea on 2022/7/24.
*/
@RestController
@RequestMapping("/sentinel")
public class OpenFeignController {
@Autowired
private OpenFeignClient openFeignClient; //注入openfeign客户端
@GetMapping("/feign")
public String feign() {
return openFeignClient.hello();
}
}
6. 测试OpenFeign服务调用
分别启动 service-sentinel、service-provider-nacos 和 service-provider-api 服务,然后使用curl命令进行调用:
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/feign
{"timestamp":"2022-07-25T07:38:59.797+00:00","path":"/sentinel/feign","status":500,"error":"Internal Server Error","requestId":"c1151cec-2"}
C:\Users\deepinsea>curl http://localhost:9080/service-provider-nacos/provider-nacos/hello
hi, this is service-provider-nacos!
C:\Users\deepinsea>curl http://localhost:9080/service-provider-nacos/provider-nacos/hello
hi, this is service-provider-api!
可以看到,直接调用经过网关代理的接口是OK的,但是通过OpenFeign调用经过网关代理的接口报500服务异常错误。因此,可以说明:正常后端调用和Gateway代理调用都是OK的,但是通过OpenFeign调用的Gateway代理的接口是有问题的。
OpenFeign服务调用失败,控制台输出日志如下所示:
错误一
2022-07-25 15:06:15.763 ERROR 36108 --- [ctor-http-nio-5] a.w.r.e.AbstractErrorWebExceptionHandler : [525ba601-2] 500 Server Error for HTTP GET "/sentinel/feign"
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-5
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.4.14.jar:3.4.14]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP GET "/sentinel/feign" [ExceptionHandlingWebHandler]
注意:Gateway网关使用Feign调用微服务接口会出现异常,Spring Boot 2.7.0中的WebFlux必须使用异步调用,而直接使用OpenFeign进行同步调用会报错。
因为Gateway 2.0采用Reactor-Netty异步调用的方式重构了,因此网关的webflux异步调用和下游服务的web阻塞调用会冲突。
解决方案:将OpenFeign的同步调用改造为异步调用,通过添加一个传输层将OpenFeign的同步调用请求转为Future异步调用请求。
有几大方案,分别是:
- 将OpenFeign转换为异步调用;
- 使用WebClient调用;
- 使用Reactive-Feign调用;
- 使用OkHttp实现OpenFeign。
参考:
- Spring Cloud 2020 网关调用 Feign 和 RestTemplate 失败#2126 -- 中间提到了使用基于webflux的WebClient客户端调用
- SpringCloud 2021.0.1 SpringCloudGateway 3.1.1新版中GlobalFilter使用OpenFeign失败(503)的问题 -- 中间提到了使用Feign的异步改造组件Reactive-Feign进行调用
- feign-reactive -- Reactive-Feign组件
- OpenFeign官方说明
下面我们采用第一种方式,从原理出发,来解决Gateway不支持OpenFeign的问题:
7. 解决OpenFeign不支持异步调用的问题
有以下几个需要注意的地方:
- 注入OpenFeignClient 必须使用 @Lazy
- FeignClient 必须要异步调用,使用Future方式,不能同步调用(即在异步转换层里加逻辑),不然会报不能在xxx线程执行的错。
- 使用 @EnableFeignClients ,开启 OpenFeign 功能
- 使用 @EnableAsync 开启异步执行功能,不然加了 @Async 注解也没用哦。
1、首先需要添加一个transport传输层到common包中,然后添加Feign异步传输器:
package com.deepinsea.common.transport;
import com.deepinsea.service.OpenFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import java.util.concurrent.Future;
/**
* Created by deepinsea on 2022/7/25.
* openfeign同步转gateway异步的中间处理层
*/
@Component
public class FeignAsyncTransporter {
@Lazy //懒加载(相当于后置处理器的作用,一般是处理bean的后置逻辑)
@Autowired
private OpenFeignClient openFeignClient;
@Async // 重点:这里必须在异步线程中执行,执行结果返回Future
public Future<String> hello() {
System.out.println("请求进到异步处理器中了...");
String result = openFeignClient.hello();
return new AsyncResult<>(result);
}
}
因为这里将OpenFeign的同步请求处理为了异步请求,因此后面的接口调用只需要注入Feign异步传输类即可。
这里的Future接口是为了接收异步线程调用的返回值,而Future接口的实现类FutureTask的实现底层是Callable。
注意:上面启用了Spring的@Async注解,来通过ThreadPoolTaskExecutor线程池实现异步调用,但是需要@EnableAsync注解开启才能生效。
2、开启异步线程池并设置线程池参数
package com.deepinsea.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* Created by deepinsea on 2022/7/25.
* 异步线程池配置
*/
@Configuration
@EnableAsync(proxyTargetClass = true)
public class AsyncTaskConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 线程数和任务数按照实际项目进行配置,此处为参考配置
// 可以使用 Runtime.getRuntime().availableProcessors() 获取系统核心数进行配置
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("feign-async-core-");
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
管中窥豹,可以知道异步调用的实现思路其实是:主线程用于接收请求并打上一个事件标识,然后另开一个线程去处理耗时的线程,最后按照调用顺序依次返回请求处理的结果给主线程。这样,主线程就无需阻塞处理耗时的请求调用,从而做到了同步(对应请求来说是同步的,线程之间是异步的)非阻塞的请求调用操作。异步线程池中不同线程处理的事件之间的流转处理,就称为事件循环,如下图所示:
根据鼠标事件的检测原理,可知:事件处理架构就是将需要处理的请求事件提前注册为事件函数,然后使用状态机等状态管理工具切换事件的状态。当事件的某个状态发生变化时,系统将会调用该事件函数的已注册的事件处理程序进行事件处理。
3、改变Controller层的OpenFeign服务调用方式
package com.deepinsea.controller;
import com.deepinsea.common.transport.FeignAsyncTransporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* Created by deepinsea on 2022/7/24.
*/
@RestController
@RequestMapping("/sentinel")
public class OpenFeignController {
// @Autowired
// private OpenFeignClient openFeignClient; //注入openfeign客户端
@Autowired
private FeignAsyncTransporter feignAsyncTransporter; //注入feign同步转异步请求传输器
// @GetMapping("/feign")
// public String feign() { //openfeign同步调用
// return openFeignClient.hello();
// }
@GetMapping("/feign")
public String feign() throws ExecutionException, InterruptedException {
Future<String> future = feignAsyncTransporter.hello(); //openfeign转换后的异步调用
return future.get();
}
}
重启项目,使用curl命令,再次进行测试:
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/feign
{"timestamp":"2022-07-25T18:22:40.942+00:00","path":"/sentinel/feign","status":500,"error":"Internal Server Error","requestId":"dba92ba4-2"}
请求调用失败,同样是OpenFeign第一次调用的错误返回,因此可以说明OpenFeign调用还是有问题。但是控制台打印的错误日志不同了,说明虽然还是OpenFeign调用错误,但不是同一个错误了。上面第一个OpenFeign异步调用的错误应该是解决了,但是下面出现了第二个OpenFeign解码的错误:
错误二
请求进到异步处理器中了...
2022-07-26 02:22:40.947 ERROR 23672 --- [ctor-http-nio-5] a.w.r.e.AbstractErrorWebExceptionHandler : [dba92ba4-2] 500 Server Error for HTTP GET "/sentinel/feign"
java.util.concurrent.ExecutionException: feign.codec.DecodeException: No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at java.util.concurrent.FutureTask.report(FutureTask.java:122) ~[na:1.8.0_311]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP GET "/sentinel/feign" [ExceptionHandlingWebHandler]
错误原因:Spring Cloud OpenFeign对Decoder无法解码的Response类型,自动进入失败逻辑(如果添加Fallback则自动进入失败降级逻辑)。你看不到相关的错误日志,也没有警告。
即便是大厂腾讯,相信他们的接口(例如微信支付等等),也是不区分Json具体格式的。当返回数据是Json,而ContentType 为 text/html;charset=UTF-8,这不影响他读取文本内容。
但是,在OpenFeign中依赖Spring的Jackson进行序列化和反序列化;当Json的格式是 text/html 文本格式,而不是 application/json 格式时,会出现序列化错误。因此,我们需要自定义HttpMessageConverters类配置以支持文本格式的Json数据传输:
我们需要添加一个transport层在common包下,然后添加Feign解码配置类FeignDecoderConfig:
package com.deepinsea.common.config;
import feign.codec.Decoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.util.ArrayList;
import java.util.List;
/**
* Created by deepinsea on 2022/7/25.
* Feign配置类(添加text/html类型的json格式支持)
* 配置Feign的Decoder, 解决在Gateway中使用Feign时找不到HttpMessageConverters的问题
*/
@Configuration
public class FeignDecoderConfig {
@Bean
public Decoder feignDecoder() {
return new ResponseEntityDecoder(new SpringDecoder(feignHttpMessageConverter()));
}
public ObjectFactory<HttpMessageConverters> feignHttpMessageConverter() {
final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(new GateWayMappingJackson2HttpMessageConverter());
return () -> httpMessageConverters;
}
public static class GateWayMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
GateWayMappingJackson2HttpMessageConverter() {
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.valueOf(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8"));
setSupportedMediaTypes(mediaTypes);
}
}
}
参考:SpringCloudGateway使用OpenFeign卡死,启动不了
8. 再次测试OpenFeign服务调用
这次是经过异步化传输中间层改造后的OpenFeign调用了,是异步调用的方式,因此Gateway可以支持异步化的OpenFeign调用请求了。
重启项目,删除sentinel控制台的限流和降级规则,然后使用curl命令调用接口:
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/feign
hi, this is service-provider-nacos!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/feign
hi, this is service-provider-api!
可以看到,openfeign请求调用成功,控制台输出日志为:
请求进到异步处理器中了...
请求进到异步处理器中了...
说明请求也经过了异步化处理逻辑,OpenFeign在Gateway中调用接口成功!
下面我们在上面逻辑的基础上,实现Sentinel降级熔断逻辑:
9. OpenFeign调用实现降级熔断
低版本的Gateway集成OpenFeign实现熔断降级处理逻辑
1、添加OpenFeign客户端接口实现类,作为降级的备用逻辑类
package com.deepinsea.service.impl;
import com.deepinsea.service.OpenFeignClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Created by deepinsea on 2022/7/24.
* 降级后的备用逻辑(但OpenFeign异步化改造后失效了)
*/
@Component
public class OpenFeignFallbackClient implements OpenFeignClient {
private static final Logger log = LoggerFactory.getLogger(OpenFeignFallbackClient.class);
@Override
public String hello() {
log.error("openfeign远程调用service-provider-nacos服务异常后的降级方法");
return "触发调用降级逻辑";
}
}
2、OpenFeign客户端接口,配置fallback降级参数
package com.deepinsea.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Created by deepinsea on 2022/7/24.
* OpenFeign客户端接口
* 参数解释:
* value、name:value和name的作用一样,如果没有配置url那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。
* contextId:我们不想将所有的调用接口都定义在一个类中,有一种解决方案就是为每个Client手动指定不同的contextId,这样就不会冲突了。
* url:用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。像调试等场景可以使用。
* decode404:当调用请求发生404错误时,那么会执行decoder解码,否则抛出异常。
* fallback:定义容错的处理类,也就是回退逻辑,fallback的类必须实现Feign Client的接口,无法知道熔断的异常信息。
* fallbackFactory:也是容错的处理,可以知道熔断的异常堆栈信息。
* path:定义当前FeignClient访问接口时的统一前缀。
*/
//@FeignClient(name = "service-provider-nacos", decode404 = true)
@FeignClient(name = "service-provider-nacos", decode404 = true, fallback = OpenFeignFallbackClient.class)
public interface OpenFeignClient {
@GetMapping("/provider-nacos/hello")
String hello();
}
3、配置文件中开启断路器支持
feign:
sentinel:
# feign调用支持开关(其实不启用无影响)
enabled: true
# 断路器支持(异步改造后开启了无用)
circuitbreaker:
enabled: true
但是在Gateway新版本,因为上面经过异步化改造后,OpenFeign的请求都会经过异步传输中间层改造,因此OpenFeign的降级熔断处理参数fallback和fallbackFactory都将失效。
那么我们怎么实现异步请求下,OpenFeign能进入到我们的自定义降级熔断处理逻辑中呢?
主要有三种方案:
- 使用Sentinel的全局降级熔断处理配置;
- 使用Feign-Reactive来实现Feign的异步请求降级熔断逻辑;
- 使用spring-cloud-circuitbreaker-sentinel来实现Feign的异步请求熔断逻辑。
我们采用第一种方案,最简单、快速(第二种也可以,不过需要一定的学习成本;第三种不熟悉,参考代码和文档太少)。
sentinel:
# 限流返回的响应
scg:
order: -2147483648 # 过滤器顺序,默认为 -2147483648 最高优先级
# fallback模式,目前有三种:response、redirect、空(可以实现对 fallback 的自定义处理逻辑)
fallback:
# 第一种:response返回文字提示信息
mode: response
response-status: 503 #响应状态码(默认为429)
response-body: 访问过于频繁,请稍后重试! #This request is blocked by service-sentinel.
content-type: application/json #内容类型(默认为 application/json)
这样就配置了全局熔断降级的处理逻辑了,同时我们把OpenFeign的实现类注释掉或者删除,fallback参数去掉。
下面我们来使用curl命令,对自定义openfeign服务调用熔断降级处理逻辑进行测试:
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/feign
hi, this is service-provider-api!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/feign
hi, this is service-provider-nacos!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/feign
访问过于频繁,请稍后重试!
C:\Users\deepinsea>curl http://localhost:9080/service-sentinel/sentinel/feign
访问过于频繁,请稍后重试!
可以看到,我们通过默认从nacos同步过来的全局流控规则和降级熔断规则生效后,就触发了我们自定义的异常处理逻辑。
当然,在配置文件中通过sentinel的fallback配置异常处理逻辑,也有一个缺点:就是所有的sentinel异常都会触发备用异常处理逻辑,对于异常处理的粒度太大了。
如果要服务的异常更加细粒度定义的话,参考上面配置Sentinel全局自定义异常来进行配置即可。
其它
常见的坑和解决方法
Alibaba Sentinel对接Spring Cloud Gateway关于不显示API管理及请求链路的坑。
解决方法:添加如下代码到启动类中,并重启sentinel客户端和服务端。
package com.deepinsea;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* Created by deepinsea on 2022/7/3.
* 服务熔断主启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceSentinelApplication {
public static void main(String[] args) {
System.setProperty("csp.sentinel.app.type", "1"); //设置为支持API分组的新控制台界面(1.8.3及以上版本)
SpringApplication.run(ServiceSentinelApplication.class, args);
}
}
社区扩展和方案
Awesome Sentinel 里记录非常多的社区用户的一些扩展和解决方案,另外大家也可以将一些比较好的扩展实现添加进来。
Sentinel服务就到此为止了,至于令牌桶算法和漏桶算法,我们在具体服务的限流降级上再深入实践。
欢迎点赞,谢谢各位大佬了ヾ(◍°∇°◍)ノ゙