highlight: a11y-dark
前言
本文将对使用Openfeign作为HTTP客户端时的分布式链路追踪进行设计与实现。
github地址:honey-tracing
正文
一. 怎么拦截feignClient的请求执行
相较于RestTemplate,feignClient的拦截器显得不是那么好用。feignClient的拦截器RequestInterceptor仅会在请求发起前,拦截到RequestTemplate并处理,然后feignClient基于RequestTemplate创建Request,最终完成请求发送,也就是RequestInterceptor只能在请求前生效一次,至于 请求异常或者 返回响应时,RequestInterceptor都是无能为力的。
既然feignClient的拦截器RequestInterceptor不好使,那么应该怎么办呢,其实可以参考8. Kafka分布式链路追踪实现设计一文中的实现思路,我们定义一个feign.Client的实现类,这个实现类会对feignClient真正使用的feign.Client做一层包装,同时这个实现类也会持有一个拦截器链,只有当拦截器链执行完毕后,才会最终调用到被包装的feign.Client,示意图如下。
基于上面这种思路,我们就可以很轻松的拦截到feignClient的请求执行,并且可以在 请求发起前,请求异常时和 返回响应时都执行相关操作,所以就很契合来实现分布式链路追踪。
二. 代码实现
pom中需要引入Openfeign的相关依赖,如下所示。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>11.8</version>
<scope>provided</scope>
</dependency>
因为不能使用feignClient的拦截器RequestInterceptor,所以我们需要定义拦截器接口,如下所示。
/**
* 适用于{@link HoneyFeignClient}的拦截器。
*/
public interface HoneyFeignInterceptor {
Response intercept(Request request, Request.Options options, HoneyFeignInterceptorChain interceptorChain) throws IOException;
}
拦截器的intercept() 方法的入参中,Request和Request.Options是feignClient执行请求需要的参数,HoneyFeignInterceptorChain是拦截器链,下面看一下HoneyFeignInterceptorChain的实现,如下所示。
public class HoneyFeignInterceptorChain {
private final Iterator<HoneyFeignInterceptor> interceptors;
private final Client client;
public HoneyFeignInterceptorChain(Iterator<HoneyFeignInterceptor> interceptors, Client client) {
this.interceptors = interceptors;
this.client = client;
}
public Response intercept(Request request, Request.Options options) throws IOException {
if (interceptors.hasNext()) {
return interceptors.next().intercept(request, options, this);
}
return client.execute(request, options);
}
}
只有当拦截器链的所有拦截器都执行完逻辑后,才会最终调用到feignClient底层的feign.Client来发起请求。
我们定义的feign.Client的实现类HoneyFeignClient如下所示。
/**
* 链路日志feign客户端。
*/
public class HoneyFeignClient implements Client {
/**
* 底层执行HTTP请求的feign客户端。
*/
private final Client client;
private final List<HoneyFeignInterceptor> interceptors;
public HoneyFeignClient(Client client, List<HoneyFeignInterceptor> interceptors) {
this.client = client;
this.interceptors = interceptors;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
HoneyFeignInterceptorChain honeyFeignInterceptorChain = new HoneyFeignInterceptorChain(interceptors.iterator(), client);
return honeyFeignInterceptorChain.intercept(request, options);
}
}
HoneyFeignClient主要的作用就是创建拦截器链来拦截请求的执行,并且HoneyFeignClient持有feignClient底层的feign.Client,在创建拦截器链的时候,HoneyFeignClient会把其持有的底层的feign.Client给到拦截器链,当拦截器链里面所有的拦截器都执行完毕后,最终请求由这个底层的feign.Client来进行发送。
我们再提供一个拦截器HoneyFeignInterceptor的具体实现,用于完成分布式链路追踪的处理,如下所示。
/**
* 适用于{@link HoneyFeignClient}的链路追踪拦截器。
*/
public class HoneyFeignTracingInterceptor implements HoneyFeignInterceptor {
private final Tracer tracer;
private final List<HoneyFeignTracingDecorator> honeyFeignTracingDecorators;
public HoneyFeignTracingInterceptor(Tracer tracer, List<HoneyFeignTracingDecorator> honeyFeignTracingDecorators) {
this.tracer = tracer;
this.honeyFeignTracingDecorators = honeyFeignTracingDecorators;
}
@Override
public Response intercept(Request request, Request.Options options, HoneyFeignInterceptorChain interceptorChain) throws IOException {
if (tracer.activeSpan() == null) {
return interceptorChain.intercept(request, options);
}
Span span = tracer.buildSpan(HONEY_DB_FEIGN)
.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
.start();
request = inject(span, request);
for (HoneyFeignTracingDecorator honeyFeignTracingDecorator : honeyFeignTracingDecorators) {
try {
honeyFeignTracingDecorator.onRequest(request, options, span);
} catch (Exception e) {
// do nothing
}
}
Response response;
try (Scope scope = tracer.activateSpan(span)) {
try {
response = interceptorChain.intercept(request, options);
} catch (Exception e) {
for (HoneyFeignTracingDecorator honeyFeignTracingDecorator : honeyFeignTracingDecorators) {
try {
honeyFeignTracingDecorator.onError(e, request, span);
} catch (Exception onErrorEx) {
// do nothing
}
}
throw e;
}
for (HoneyFeignTracingDecorator honeyFeignTracingDecorator : honeyFeignTracingDecorators) {
try {
honeyFeignTracingDecorator.onResponse(response, options, span);
} catch (Exception onErrorEx) {
// do nothing
}
}
} finally {
span.finish();
// 将代表下游的Span作为requestStack记录在parentSpan中
tracer.activeSpan().log(RequestStackUtil.assembleRequestStack((JaegerSpan) span));
}
return response;
}
private Request inject(Span span, Request request) {
Map<String, Collection<String>> headers = new HashMap<>(request.headers());
// 将链路信息注入到headers中
tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS,
new HttpHeadersInjectCarrier(headers));
// 基于headers重新创建Request
return Request.create(request.httpMethod(), request.url(), headers,
request.body(), request.charset(), request.requestTemplate());
}
}
上述拦截器做的事情概括下来就是请求发起前创建Span并注入链路信息到HTTP请求头中,然后在请求失败或者结束时记录Span。这里有两个需要关注的对象,第一个是装饰器接口HoneyFeignTracingDecorator,定义如下。
public interface HoneyFeignTracingDecorator {
void onRequest(Request request, Request.Options options, Span span);
void onResponse(Response response, Request.Options options, Span span);
void onError(Exception exception, Request request, Span span);
}
具体的实现类HoneyFeignTracingSpanDecorator实现如下。
public class HoneyFeignTracingSpanDecorator implements HoneyFeignTracingDecorator {
@Override
public void onRequest(Request request, Request.Options options, Span span) {
((JaegerSpan) span).setTag(FIELD_HOST, UrlUtil.getHostFromUri(request.url()));
}
@Override
public void onResponse(Response response, Request.Options options, Span span) {
try {
((JaegerSpan) span).setTag(FIELD_HTTP_CODE, response.status());
} catch (Exception e) {
((JaegerSpan) span).setTag(FIELD_HTTP_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
@Override
public void onError(Exception exception, Request request, Span span) {
((JaegerSpan) span).setTag(FIELD_HTTP_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
主要就是在请求发起前设置host,请求发起后设置httpCode。
第二个需要关注的对象是HttpHeadersInjectCarrier,这个是完成将链路信息注入到HTTP请求头中的carrier,之前在RestTemplate的分布式链路追踪的实现中,对应的carrier我们使用的是Opentracing提供的HttpHeadersCarrier,其能够往HttpHeaders添加请求头,但是在这里,我们无法从feignClient的Request中直接拿到HttpHeaders,所以我们只能自己写一个更方便的carrier,HttpHeadersInjectCarrier的实现如下所示。
public class HttpHeadersInjectCarrier implements TextMap {
private final Map<String, Collection<String>> httpHeaders;
public HttpHeadersInjectCarrier(Map<String, Collection<String>> httpHeaders) {
this.httpHeaders = httpHeaders;
}
@Override
public void put(String key, String value) {
httpHeaders.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
@Override
public Iterator<Map.Entry<String, String>> iterator() {
throw new UnsupportedOperationException();
}
}
上述代码都是在围绕着feignClient的请求如何被拦截所设计,我们设计了包装底层feign.Client的HoneyFeignClient,设计了拦截器和拦截器链,也设计了对应的装饰器,现在我们需要把这些组件注入到Spring容器中。
首先提供自动装配类HoneyFeignTracingConfig来注册拦截器和装饰器,如下所示。
@Configuration
@ConditionalOnClass(Client.class)
@AutoConfigureAfter({HoneyDefaultClientFeignConfig.class, HoneyHttpClientFeignConfig.class})
public class HoneyFeignTracingConfig {
@Bean
@ConditionalOnMissingBean(HoneyFeignTracingSpanDecorator.class)
public HoneyFeignTracingDecorator honeyFeignTracingSpanDecorator() {
return new HoneyFeignTracingSpanDecorator();
}
@Bean
@ConditionalOnMissingBean(HoneyFeignTracingInterceptor.class)
public HoneyFeignInterceptor honeyFeignTracingInterceptor(
Tracer tracer, List<HoneyFeignTracingDecorator> honeyFeignTracingDecorators) {
return new HoneyFeignTracingInterceptor(tracer, honeyFeignTracingDecorators);
}
}
然后需要注册HoneyFeignClient。假如工程中使用ApacheHttpClient作为feignClient的底层feign.Client,那么我们基于HoneyHttpClientFeignConfig来注册HoneyFeignClient,如下所示。
/**
* 当容器中有{@link ApacheHttpClient}的bean时该配置类生效。<br/>
* 用于基于{@link ApacheHttpClient}创建{@link HoneyFeignClient}。
*/
@Configuration
@ConditionalOnBean(ApacheHttpClient.class)
public class HoneyHttpClientFeignConfig {
@Bean
public Client feignClient(ApacheHttpClient httpClient, List<HoneyFeignInterceptor> interceptors) {
return new HoneyFeignClient(httpClient, interceptors);
}
}
但如果工程中没有显式的配置feignClient的底层feign.Client,此时我们需要基于Client.Default来创建HoneyFeignClient并注册,对应的自动装配类HoneyDefaultClientFeignConfig实现如下。
/**
* 当容器中没有{@link Client}的bean时该配置类生效。<br/>
* 用于基于{@link Client.Default}创建{@link HoneyFeignClient}。
*/
@Configuration
@ConditionalOnMissingBean(Client.class)
public class HoneyDefaultClientFeignConfig {
@Bean
public Client feignClient(List<HoneyFeignInterceptor> interceptors) {
return new HoneyFeignClient(new Client.Default(null, null), interceptors);
}
}
所有的自动装配类需要添加到spring.factories文件中,如下所示。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.honey.tracing.config.HoneyTracingConfig,\
com.honey.tracing.config.HoneyTracingFilterConfig,\
com.honey.tracing.config.HoneyRestTemplateTracingConfig,\
com.honey.tracing.config.HoneyKafkaTemplateConfig,\
com.honey.tracing.config.HoneyKafkaTracingConfig,\
com.honey.tracing.config.HoneyDbTracingConfig,\
com.honey.tracing.config.HoneyDefaultClientFeignConfig,\
com.honey.tracing.config.HoneyFeignTracingConfig,\
com.honey.tracing.config.HoneyHttpClientFeignConfig
最后使用到的常量如下所示。
public class CommonConstants {
......
public static final String HONEY_DB_FEIGN = "HoneyFeign";
......
}
工程目录结构如下。
三. 测试验证
在example-service-1中添加如下feignClient。
@FeignClient(name = "sned", url = "127.0.0.1:8081")
public interface SendClient {
@GetMapping("/receive")
void send();
}
再增加如下Controller。
@RestController
public class FeignClientController {
@Autowired
private SendClient sendClient;
@GetMapping("/feign/send")
public void send() {
sendClient.send();
}
}
最后需要在启动类上添加@EnableFeignClients注解,如下所示。
@MapperScan
@EnableAsync
@EnableFeignClients
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
example-service-1工程目录结构如下所示。
同时运行example-service-1和example-service-2,调用 http://localhost:8080/feign/send 接口,example-service-1中打印如下链路日志。
{
"traceId": "e9fbba1eb7795682884b8f8e9bc73a6b",
"spanId": "884b8f8e9bc73a6b",
"parentSpanId": "0000000000000000",
"timestamp": "1714833321812",
"duration": "163",
"httpCode": "200",
"host": "http://localhost:8080",
"requestStacks": [{
"subSpanId": "a11f702296bcccbb",
"subHttpCode": "200",
"subTimestamp": "1714833321817",
"subDuration": "152",
"subHost": "127.0.0.1:8081"
}],
"dbStacks": []
}
example-service-2中打印如下链路日志。
{
"traceId": "e9fbba1eb7795682884b8f8e9bc73a6b",
"spanId": "a11f702296bcccbb",
"parentSpanId": "884b8f8e9bc73a6b",
"timestamp": "1714833321897",
"duration": "34",
"httpCode": "200",
"host": "http://127.0.0.1:8081",
"requestStacks": [],
"dbStacks": []
}
可见使用feignClient请求下游时,可以正确打印链路日志。
总结
其实Openfeign的分布式链路追踪的实现,关键点和Kafka很相似,就是原生的拦截器不好用,我们需要自己对原生client做包装并引入自定义的拦截器来拦截 请求的执行,请求出错或 返回响应,只有这三个阶段能够被拦截到,才具备做分布式链路追踪的条件。