OpenFeign 实战:OpenFeign 组件有哪些高级玩法?
今天我们来进一步深入 OpenFeign 的功能特性,学习几个 OpenFeign 的进阶使用技巧:异常信息排查、超时判定和服务降级。
异常信息排查是我们开发人员每天都要面对的事情。如果你正在开发一个大型微服务应用,你经常需要集成一些由其他团队开发的 API,这就免不了要参与各种联调和问题排查。如果你是一个经验丰富的老码农,那你一定经常说这样一句话:“你的 Request 参数是什么?”这句台词在我们平时的 API 联调和线上异常排查中出镜率很高,因为服务请求的入参和出参是分析和排查问题的重要线索。
为了获得服务请求的参数和返回值,我们经常使用的一个做法就是打印日志。你可以在程序中使用 log.info 或者 log.debug 方法将服务请求的入参和返回值一一打印出来。但是,对一些复杂的业务场景来说就没有那么轻松了。
假如你在开发的是一个下单服务,执行一次下单流程前前后后要调用十多个微服务。你需要在请求发送的前后分别打印 Request 和 Response,不仅麻烦不说,我们还未必能把包括 Header 在内的完整请求信息打印出来。
那我们如何才能引入一个既简单又不需要硬编码的日志打印功能,让它自动打印所有远程方法的 Request 和 Response,方便我们做异常信息排查呢?
接下来,我就来给你介绍一个 OpenFeign 的小功能,轻松实现远程调用参数的日志打印。
日志信息打印
为了让 OpenFeign 可以主动将请求参数打印到日志中,我们需要做两个代码层面的改动。首先,你需要在配置文件中指定 FeignClient 接口的日志级别为 Debug。这样做是因为 OpenFeign 组件默认将日志信息以 debug 模式输出,而默认情况下 Spring Boot 的日志级别是 Info,因此我们必须将应用日志的打印级别改为 debug 后才能看到 OpenFeign 的日志。
我们打开 coupon-customer-impl 模块的 application.yml 配置文件,在其中加上以下几行 logging 配置项。
logging:
level:
com.geekbang.coupon.customer.feign.TemplateService: debug
com.geekbang.coupon.customer.feign.CalculationService: debug
在上面的配置项中,我指定了 TemplateService 和 CalculationService 的日志级别为 debug,而其它类的日志级别不变,仍然是默认的 Info 级别。
接下来,你还需要在应用的上下文中使用代码的方式声明 Feign 组件的日志级别。这里的日志级别并不是我们传统意义上的 Log Level,它是 OpenFeign 组件自定义的一种日志级别,用来控制 OpenFeign 组件向日志中写入什么内容。你可以打开 coupon-customer-impl 模块的 Configuration 配置类,在其中添加这样一段代码。
@Bean
Logger.Level feignLogger() {
return Logger.Level.FULL;
}
在上面这段代码中,我指定了 OpenFeign 的日志级别为 Full,在这个级别下所输出的日志文件将会包含最详细的服务调用信息。
OpenFeign 总共有四种不同的日志级别,我来带你了解一下这四种级别下 OpenFeign 向日志中写入的内容。
- NONE:不记录任何信息,这是 OpenFeign 默认的日志级别;
- BASIC:只记录服务请求的 URL、HTTP Method、响应状态码(如 200、404 等)和服务调用的执行时间;
- HEADERS:在 BASIC 的基础上,还记录了请求和响应中的 HTTP Headers;
- FULL:在 HEADERS 级别的基础上,还记录了服务请求和服务响应中的 Body 和 metadata,FULL 级别记录了最完整的调用信息。
我们将 Feign 的日志级别指定为 Full,并启动项目发起一个远程调用,你就可以在日志中看到整个调用请求的信息,包括请求路径、Header 参数、Request Payload 和 Response Body。
我拿了一个调用日志作为示例,你可以参考一下。
---> POST http://coupon-calculation-serv/calculator/simulate HTTP/1.1
Content-Length: 458
Content-Type: application/json
{"products":[{"productId":null,"price":3000, xxxx省略请求参数
---> END HTTP (458-byte body)
<--- HTTP/1.1 200 (29ms)
connection: keep-alive
content-type: application/json
date: Sat, 27 Nov 2021 15:11:26 GMT
keep-alive: timeout=60
transfer-encoding: chunked
{"bestCouponId":26,"couponToOrderPrice":{"26":15000}}
<--- END HTTP (53-byte body)
有了这些详细的日志信息,你在开发联调阶段排查异常问题就易如反掌了。
到这里,我们就详细了解了 OpenFeign 的日志级别设置。接下来,我带你了解如何在 OpenFeign 中配置超时判定条件。
OpenFeign 超时判定
超时判定是一种保障可用性的手段。如果你要调用的目标服务的 RT(Response Time)值非常高,那么你的调用请求也会处于一个长时间挂起的状态,这是造成服务雪崩的一个重要因素。为了隔离下游接口调用超时所带来的的影响,我们可以在程序中设置一个超时判定的阈值,一旦下游接口的响应时间超过了这个阈值,那么程序会自动取消此次调用并返回一个异常。
我们以 coupon-customer-serv 为例,customer 服务依赖 template 服务来读取优惠券模板的信息,如果你想要对 template 的远程服务调用添加超时判定配置,那么我们可以在 coupon-customer-impl 模块下的 application.yml 文件中添加下面的配置项。
feign:
client:
config:
# 全局超时配置
default:
# 网络连接阶段1秒超时
connectTimeout: 1000
# 服务请求响应阶段5秒超时
readTimeout: 5000
# 针对某个特定服务的超时配置
coupon-template-serv:
connectTimeout: 1000
readTimeout: 2000
从上面这段代码中可以看出,所有超时配置都放在 feign.client.config 路径之下,我在这个路径下面声明了两个节点:default 和 coupon-template-serv。
default 节点配置了全局层面的超时判定规则,它的生效范围是所有 OpenFeign 发起的远程调用。
coupon-template-serv 下面配置的超时规则只针对向 template 服务发起的远程调用。如果你想要对某个特定服务配置单独的超时判定规则,那么可以用同样的方法,在 feign.client.config 下添加目标服务名称和超时判定规则。
这里需要你注意的一点是,如果你同时配置了全局超时规则和针对某个特定服务的超时规则,那么后者的配置会覆盖全局配置,并且优先生效。在超时判定的规则中我定义了两个属性:connectTimeout 和 readTimeout。
- connectTimeout 的超时判定作用于“建立网络连接”的阶段;
- readTimeout 的超时判定则作用于“服务请求响应”的阶段(在网络连接建立之后)。
我们常说的 RT(即服务响应时间)受后者影响比较大。另外,这两个属性对应的超时时间单位都是毫秒。
配置好超时规则之后,我们可以验证一下。你可以在 template 服务中使用 Thread.sleep 方法强行让线程挂起几秒钟,制造一个超时场景。这时如果你通过 customer 服务调用了 template 服务,那么在日志中可以看到下面的报错信息,提示你服务请求超时。
[TemplateService#getTemplate] <--- ERROR SocketTimeoutException: Read timed out (2077ms)
[TemplateService#getTemplate] java.net.SocketTimeoutException: Read timed out
OpenFeign 降级降级逻辑是在远程服务调用发生超时或者异常(比如 400、500 Error Code)的时候,自动执行的一段业务逻辑。你可以根据具体的业务需要编写降级逻辑,比如执行一段兜底逻辑将服务请求从失败状态中恢复,或者发送一个失败通知到相关团队提醒它们来线上排查问题。
在后面文章中,我将会使用 Spring Cloud Alibaba 的组件 Sentinel 跟你讲解如何搭建中心化的服务容错控制逻辑,这是一种重量级的服务容错手段。
但在这篇文章中,我采用了一种完全不同的服务容错手段,那就是借助 OpenFeign 实现 Client 端的服务降级。尽管它的功能远不如 Sentinel 强大,但它相比于 Sentinel 而言更加轻量级且容易实现,足以满足一些简单的服务降级业务需求。
OpenFeign 对服务降级的支持是借助 Hystrix 组件实现的,由于 Hystrix 已经从 Spring Cloud 组件库中被移除,所以我们需要在 coupon-customer-impl 子模块的 pom 文件中手动添加 hystrix 项目的依赖。
<!-- hystrix组件,专门用来演示OpenFeign降级 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
<exclusions>
<!-- 移除Ribbon负载均衡器,避免冲突 -->
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
添加好依赖项之后,我们就可以编写 OpenFeign 的降级类了。OpenFeign 支持两种不同的方式来指定降级逻辑,一种是定义 fallback 类,另一种是定义 fallback 工厂。
通过 fallback 类实现降级是最为简单的一种途径,如果你想要为 TemplateService 这个 FeignClient 接口指定一段降级流程,那么我们可以定义一个降级类并实现 TemplateService 接口。我写了一个 TemplateServiceFallback 类,你可以参考一下。
@Slf4j
@Component
public class TemplateServiceFallback implements TemplateService {
@Override
public CouponTemplateInfo getTemplate(Long id) {
log.info("fallback getTemplate");
return null;
}
@Override
public Map<Long, CouponTemplateInfo> getTemplateInBatch(Collection<Long> ids) {
log.info("fallback getTemplateInBatch");
return null;
}
}
在上面的代码中,我们可以看出 TemplateServiceFallback 实现了 TemplateService 中的所有方法。
我们以其中的 getTemplate 方法为例,如果在实际的方法调用过程中,OpenFeign 接口的 getTemplate 远程调用发生了异常或者超时的情况,那么 OpenFeign 会主动执行对应的降级方法,也就是 TemplateServiceFallback 类中的 getTemplate 方法。
你可以根据具体的业务场景,编写合适的降级逻辑。
降级类定义好之后,你还需要在 TemplateService 接口中将 TemplateServiceFallback 类指定为降级类,这里你可以借助 FeignClient 接口的 fallback 属性来配置,你可以参考下面的代码。
@FeignClient(value = "coupon-template-serv", path = "/template",
// 通过fallback指定降级逻辑
fallback = TemplateServiceFallback.class)
public interface TemplateService {
// ... 省略方法定义
}
如果你想要在降级方法中获取到异常的具体原因,那么你就要借助 fallback 工厂的方式来指定降级逻辑了。按照 OpenFeign 的规范,自定义的 fallback 工厂需要实现 FallbackFactory 接口,我写了一个 TemplateServiceFallbackFactory 类,你可以参考一下。
@Slf4j
@Component
public class TemplateServiceFallbackFactory implements FallbackFactory<TemplateService> {
@Override
public TemplateService create(Throwable cause) {
// 使用这种方法你可以捕捉到具体的异常cause
return new TemplateService() {
@Override
public CouponTemplateInfo getTemplate(Long id) {
log.info("fallback factory method test");
return null;
}
@Override
public Map<Long, CouponTemplateInfo> getTemplateInBatch(Collection<Long> ids) {
log.info("fallback factory method test");
return Maps.newHashMap();
}
};
}
}
从上面的代码中,你可以看出,抽象工厂 create 方法的入参是一个 Throwable 对象。这样一来,我们在降级方法中就可以获取到原始请求的具体报错异常信息了。
当然了,你还需要将这个工厂类添加到 TemplateService 注解中,这个过程和指定 fallback 类的过程有一点不一样,你需要借助 FeignClient 注解的 fallbackFactory 属性来完成。你可以参考下面的代码。
@FeignClient(value = "coupon-template-serv", path = "/template",
// 通过抽象工厂来定义降级逻辑
fallbackFactory = TemplateServiceFallbackFactory.class)
public interface TemplateService {
// ... 省略方法定义
}
到这里,我们就完成了 OpenFeign 进阶功能的了解。针对这里面的某些功能,我想从日志打印和超时判定这两个方面给你一些实践层面的建议。
在日志打印方面,OpenFeign 的日志信息是测试开发联调过程中的好帮手,但是在生产环境中你是用不上的,因为几乎所有公司的生产环境都不会使用 Debug 级别的日志,最多是 Info 级别。
在超时判定方面,有时候我们在线上会使用多维度的超时判定,比如 OpenFeign + 网关层超时判定 + Sentinel 等等判定。它们可以互相作为兜底方案,一旦某个环节突然发生故障,另一个可以顶上去。但这就形成了一个木桶理论,也就是几种判定规则中最严格的那个规则会优先生效。