JAVA Dubbo 服务调用链不完整?配置 TracingFilter 打通链路追踪

Dubbo 服务调用链不完整?TracingFilter 打通链路追踪

大家好,今天我们来聊聊 Dubbo 服务调用链不完整的问题,以及如何通过配置 TracingFilter 来打通链路追踪。在微服务架构中,一个请求往往需要经过多个服务的协同处理才能完成。当出现问题时,如何快速定位到问题所在的服务,以及问题的根源,就变得非常重要。链路追踪系统应运而生,它可以帮助我们追踪请求在各个服务之间的调用路径,从而实现快速定位问题。

Dubbo 作为一款高性能的 RPC 框架,在微服务架构中被广泛应用。然而,在默认情况下,Dubbo 并没有提供完整的链路追踪功能。这意味着,如果你的应用依赖于多个 Dubbo 服务,那么在链路追踪系统中,你可能只能看到部分调用链的信息,而无法完整地追踪整个请求的生命周期。

为了解决这个问题,我们可以通过自定义 Dubbo Filter,也就是 TracingFilter,来实现链路追踪的功能。接下来,我们将详细介绍如何配置 TracingFilter,以及如何将其集成到现有的 Dubbo 应用中。

1. 链路追踪原理简介

在深入代码之前,我们先简单了解一下链路追踪的原理。一个典型的链路追踪系统主要包含以下几个核心概念:

  • Trace: 一个 Trace 代表一个完整的请求链路,它由多个 Span 组成。
  • Span: 一个 Span 代表请求链路中的一个操作,例如,一个服务调用、一个数据库查询等。每个 Span 都有一个开始时间和结束时间,以及一些相关的元数据信息。
  • Span Context: Span Context 包含了 Trace ID、Span ID 和 Parent Span ID 等信息。它用于在服务之间传递追踪信息,确保可以将不同的 Span 关联到同一个 Trace 上。

当一个请求进入系统时,链路追踪系统会生成一个全局唯一的 Trace ID。然后,在请求经过的每个服务中,都会创建一个 Span,并将其与当前的 Trace ID 关联起来。同时,每个 Span 也会记录其父 Span 的 ID,从而形成一个树状的调用链。

2. TracingFilter 的设计与实现

接下来,我们将设计并实现一个 TracingFilter,它可以自动地在 Dubbo 服务调用前后创建 Span,并将 Span Context 传递给下游服务。

首先,我们需要引入链路追踪相关的依赖库。这里我们以 opentracing-apijaeger-client 为例。

<dependency>
    <groupId>io.opentracing</groupId>
    <artifactId>opentracing-api</artifactId>
    <version>0.33.0</version>
</dependency>
<dependency>
    <groupId>io.jaegertracing</groupId>
    <artifactId>jaeger-client</artifactId>
    <version>1.8.1</version>
</dependency>

然后,我们可以创建一个名为 TracingFilter 的类,并实现 org.apache.dubbo.rpc.Filter 接口。

import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.propagation.TextMapAdapter;
import io.opentracing.util.GlobalTracer;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcException;

import java.util.HashMap;
import java.util.Map;

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER})
public class TracingFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        Tracer tracer = GlobalTracer.get();
        if (tracer == null) {
            return invoker.invoke(invocation);
        }

        String operationName = invoker.getInterface().getName() + "." + invocation.getMethodName();
        Span span = null;
        Scope scope = null;

        try {
            // Consumer 端,从上下文中提取 Span 信息
            if (RpcContext.getContext().isConsumerSide()) {
                SpanContextCarrier carrier = new SpanContextCarrier();
                tracer.inject(tracer.activeSpan().context(), Format.Builtin.TEXT_MAP, carrier); // 注入到carrier
                invocation.getAttachments().putAll(carrier.getCarrier());

            // Provider 端,从 RPC 上下文中提取 Span 信息
            } else {
                SpanContextCarrier carrier = new SpanContextCarrier(invocation.getAttachments());
                span = tracer.extract(Format.Builtin.TEXT_MAP, carrier).orElse(null);
            }

            if(span != null){
                span = tracer.buildSpan(operationName).asChildOf(span).start();
            }else{
                span = tracer.buildSpan(operationName).start();
            }

            scope = tracer.activateSpan(span);

            Result result = invoker.invoke(invocation);
            if (result.hasException()) {
                span.log(result.getException().getMessage());
            }
            return result;

        } catch (Exception e) {
            span.log(e.getMessage());
            throw e;
        } finally {
            if (scope != null) {
                scope.close(); // This will also close the span
            }
            if(span != null){
                span.finish();
            }

        }
    }

    // 用于传递 Span 上下文的 Carrier
    static class SpanContextCarrier {

        private final Map<String, String> carrier;

        public SpanContextCarrier() {
            this.carrier = new HashMap<>();
        }

        public SpanContextCarrier(Map<String, String> carrier) {
            this.carrier = carrier != null ? carrier : new HashMap<>();
        }

        public Map<String, String> getCarrier() {
            return carrier;
        }
    }
}

在这个 TracingFilter 中,我们首先获取 GlobalTracer 的实例。然后,根据当前的调用方是 Consumer 还是 Provider,执行不同的逻辑。

  • Consumer: 从当前线程的上下文 (通常是父 Span 的上下文) 中提取 Span 信息,并将其注入到 Dubbo 的 attachments 中,以便传递给下游服务。
  • Provider: 从 Dubbo 的 attachments 中提取 Span 信息,并将其作为当前 Span 的父 Span。

接下来,我们创建一个新的 Span,并将其设置为当前线程的激活 Span。然后,调用 invoker.invoke(invocation) 执行实际的服务调用。在服务调用完成后,我们需要关闭 Span,并将其从当前线程的上下文中移除。

3. SpanContextCarrier 的实现

在上面的代码中,我们使用了一个名为 SpanContextCarrier 的类来传递 Span 上下文信息。这个类需要实现 io.opentracing.propagation.TextMap 接口。

// 将SpanContextCarrier实现成TextMapAdapter
class SpanContextCarrier implements io.opentracing.propagation.TextMap {
    private final Map<String, String> carrier;

    public SpanContextCarrier() {
        this.carrier = new HashMap<>();
    }

    public SpanContextCarrier(Map<String, String> carrier) {
        this.carrier = carrier != null ? carrier : new HashMap<>();
    }

    public Map<String, String> getCarrier() {
        return carrier;
    }

    @Override
    public Iterator<Map.Entry<String, String>> iterator() {
        return carrier.entrySet().iterator();
    }

    @Override
    public void put(String key, String value) {
        carrier.put(key, value);
    }
}

这个 SpanContextCarrier 类实际上只是一个简单的 Map,它用于存储 Span 上下文信息。在 Consumer 端,我们将 Span 上下文信息放入这个 Map 中,然后将其添加到 Dubbo 的 attachments 中。在 Provider 端,我们从 Dubbo 的 attachments 中提取 Span 上下文信息,并将其放入这个 Map 中。

4. 配置 TracingFilter

要使 TracingFilter 生效,我们需要将其配置到 Dubbo 的配置文件中。Dubbo 提供了多种配置方式,例如,XML 配置、Annotation 配置和 API 配置。这里我们以 XML 配置为例。

<dubbo:reference id="demoService" interface="com.example.DemoService">
    <dubbo:consumer filter="tracing"/>
</dubbo:reference>

<dubbo:service interface="com.example.DemoService" ref="demoService">
    <dubbo:provider filter="tracing"/>
</dubbo:service>

在这个配置文件中,我们通过 filter 属性指定了要使用的 Filter。需要注意的是,我们需要同时在 Consumer 和 Provider 端配置 TracingFilter,才能实现完整的链路追踪。

除了在 XML 配置文件中配置 TracingFilter 之外,我们还可以通过 Annotation 或 API 的方式来配置。例如,我们可以使用 @Activate 注解来激活 TracingFilter

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER})
public class TracingFilter implements Filter {
    // ...
}

或者,我们可以通过 API 的方式来配置 TracingFilter

ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(20880);
protocol.setFilters("tracing");

ServiceConfig<DemoService> service = new ServiceConfig<>();
service.setInterface(DemoService.class);
service.setRef(demoService);
service.setProtocol(protocol);
service.export();

5. 初始化 Tracer

在使用 TracingFilter 之前,我们需要先初始化 Tracer。这里我们以 Jaeger 为例。

import io.jaegertracing.Configuration;
import io.jaegertracing.Configuration.ReporterConfiguration;
import io.jaegertracing.Configuration.SamplerConfiguration;
import io.jaegertracing.internal.samplers.ConstSampler;
import io.opentracing.Tracer;
import io.opentracing.util.GlobalTracer;

public class TracerUtils {

    public static void initTracer(String serviceName) {
        Configuration config = new Configuration(serviceName)
            .withSampler(new SamplerConfiguration().withType(ConstSampler.TYPE).withParam(1))
            .withReporter(new ReporterConfiguration().withLogSpans(true));

        Tracer tracer = config.getTracer();
        GlobalTracer.registerIfAbsent(tracer);
    }
}

在这个 TracerUtils 类中,我们使用 Jaeger 的 Configuration 类来配置 Tracer。我们需要指定服务名称、采样策略和 Reporter。这里我们使用 ConstSampler 作为采样策略,这意味着所有的请求都会被采样。我们还使用 ReporterConfiguration 来配置 Reporter,它可以将 Span 信息发送到 Jaeger Collector。

在初始化 Tracer 之后,我们需要将其注册到 GlobalTracer 中。这样,我们就可以在 TracingFilter 中使用 GlobalTracer.get() 方法来获取 Tracer 的实例。

6. 测试 TracingFilter

完成以上配置后,我们就可以测试 TracingFilter 是否生效了。我们可以启动 Dubbo 服务,并发送一些请求。然后,我们可以查看 Jaeger 的 Web UI,看看是否可以看到完整的调用链信息。

如果一切配置正确,你应该可以在 Jaeger 的 Web UI 中看到类似这样的调用链:

[Service A] -> [Service B] -> [Service C]

在这个调用链中,Service A 是请求的入口服务,Service BService CService A 调用的下游服务。每个服务调用都对应一个 Span,Span 中包含了请求的开始时间、结束时间和一些相关的元数据信息。

7. 注意事项

在使用 TracingFilter 时,需要注意以下几点:

  • 确保所有的 Dubbo 服务都配置了 TracingFilter,才能实现完整的链路追踪。
  • 初始化 Tracer 的代码应该在 Dubbo 服务启动之前执行。
  • 链路追踪系统会对性能产生一定的影响,需要根据实际情况进行评估和优化。

代码示例总结

以下是一些关键代码片段的总结,方便大家回顾和参考:

  • TracingFilter:
@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER})
public class TracingFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // ... (核心逻辑见上方完整代码) ...
    }
}
  • SpanContextCarrier:
class SpanContextCarrier implements io.opentracing.propagation.TextMap {
    // ... (核心逻辑见上方完整代码) ...
}
  • TracerUtils:
public class TracerUtils {

    public static void initTracer(String serviceName) {
        // ... (核心逻辑见上方完整代码) ...
    }
}
  • Dubbo XML配置:
<dubbo:reference id="demoService" interface="com.example.DemoService">
    <dubbo:consumer filter="tracing"/>
</dubbo:reference>

<dubbo:service interface="com.example.DemoService" ref="demoService">
    <dubbo:provider filter="tracing"/>
</dubbo:service>

配置 TracingFilter,可以打通 Dubbo 服务调用链,实现完整的链路追踪

通过配置 TracingFilter,我们可以有效地解决 Dubbo 服务调用链不完整的问题,实现完整的链路追踪。这可以帮助我们快速定位问题,提高问题解决效率,从而提升系统的稳定性和可靠性。希望这篇文章能够帮助大家更好地理解和使用 Dubbo 的链路追踪功能。

后续可能的优化方向

可以考虑使用更高效的 SpanContext 传递方式,例如使用二进制格式。此外,还可以集成 Metrics 系统,将 Span 的耗时等信息暴露出来,方便进行性能分析。同时,可以考虑使用异步的方式来发送 Span 信息,从而减少对业务代码的影响。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注