【微服务专题】深入理解与实践微服务架构(二十三)之OpenFeign调用实现熔断降级

2,359 阅读17分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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分组:

image-20220712165459109

抓包数据为:

[
    {
        "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级别的资源):

image-20220712170220629

注意:这里因为没有在nacos配置文件中新增配置文件,因此重启项目后配置会消失。切记,API分组的路径是包括网关动态或静态路由的所有前缀,也就是说服务名不可省略,否则API分组的限流会失效

Github Issues参考:spring cloud gateway Integration sentinel 限流不生效 #2278

image-20220712125057968

2. 添加API限流规则

在控制台界面的 流控规则 选项中添加基于API为资源的限流规则:

image-20220713101846947

保存配置,可以看到成功创建了基于API分组的限流规则:

image-20220713101929365

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控制台界面出现了添加配置文件(由于配置和应用开始变多了,我们为了管理更清晰,进行了备注):

image-20220713131029344

参考基于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分组的限流规则

image-20220713131500005

API分组

image-20220713131635751

下面我们尝试测试基于API分组的限流规则:

5. 测试API分组限流功能

这里没有注释基于Route ID的限流规则(前缀为service-sentinel应用,范围更大),这是因为sentinel中同时配置了基于 路由IDAPI分组 的限流规则时,会优先触发基于 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-sentinelservice-provider-nacosservice-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。

参考:

下面我们采用第一种方式,从原理出发,来解决Gateway不支持OpenFeign的问题:

7. 解决OpenFeign不支持异步调用的问题

有以下几个需要注意的地方:

  1. 注入OpenFeignClient 必须使用 @Lazy
  2. FeignClient 必须要异步调用,使用Future方式,不能同步调用(即在异步转换层里加逻辑),不然会报不能在xxx线程执行的错。
  3. 使用 @EnableFeignClients ,开启 OpenFeign 功能
  4. 使用 @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;
    }
}

管中窥豹,可以知道异步调用的实现思路其实是:主线程用于接收请求并打上一个事件标识,然后另开一个线程去处理耗时的线程,最后按照调用顺序依次返回请求处理的结果给主线程。这样,主线程就无需阻塞处理耗时的请求调用,从而做到了同步(对应请求来说是同步的,线程之间是异步的)非阻塞的请求调用操作。异步线程池中不同线程处理的事件之间的流转处理,就称为事件循环,如下图所示:

img

根据鼠标事件的检测原理,可知:事件处理架构就是将需要处理的请求事件提前注册为事件函数,然后使用状态机等状态管理工具切换事件的状态。当事件的某个状态发生变化时,系统将会调用该事件函数的已注册的事件处理程序进行事件处理

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服务就到此为止了,至于令牌桶算法和漏桶算法,我们在具体服务的限流降级上再深入实践。

欢迎点赞,谢谢各位大佬了ヾ(◍°∇°◍)ノ゙