掘金 后端 ( ) • 2024-05-09 00:10

highlight: a11y-dark

前言

本文将对使用Openfeign作为HTTP客户端时的分布式链路追踪进行设计与实现。

github地址:honey-tracing

正文

一. 怎么拦截feignClient的请求执行

相较于RestTemplatefeignClient的拦截器显得不是那么好用。feignClient的拦截器RequestInterceptor仅会在请求发起前,拦截到RequestTemplate并处理,然后feignClient基于RequestTemplate创建Request,最终完成请求发送,也就是RequestInterceptor只能在请求前生效一次,至于 请求异常或者 返回响应时,RequestInterceptor都是无能为力的。

既然feignClient的拦截器RequestInterceptor不好使,那么应该怎么办呢,其实可以参考8. Kafka分布式链路追踪实现设计一文中的实现思路,我们定义一个feign.Client的实现类,这个实现类会对feignClient真正使用的feign.Client做一层包装,同时这个实现类也会持有一个拦截器链,只有当拦截器链执行完毕后,才会最终调用到被包装的feign.Client,示意图如下。

分布式链路追踪-Openfeign自定义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() 方法的入参中,RequestRequest.OptionsfeignClient执行请求需要的参数,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添加请求头,但是在这里,我们无法从feignClientRequest中直接拿到HttpHeaders,所以我们只能自己写一个更方便的carrierHttpHeadersInjectCarrier的实现如下所示。

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.ClientHoneyFeignClient,设计了拦截器和拦截器链,也设计了对应的装饰器,现在我们需要把这些组件注入到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";

    ......

}

工程目录结构如下。

Starter工程目录结构图

三. 测试验证

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工程目录结构如下所示。

Demo工程目录结构图

同时运行example-service-1example-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做包装并引入自定义的拦截器来拦截 请求的执行请求出错返回响应,只有这三个阶段能够被拦截到,才具备做分布式链路追踪的条件。