从 MDC 说分布式链路追踪的前世今生 - 今日头条

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

我们可能需要一个日志脱敏操作,即在日志中不直接打印用户 username 或 phone 等敏感信息,取而代之的是打印一个 UUID,既能追踪用户,又能保

我们可能需要一个日志脱敏操作,即在日志中不直接打印用户 username 或 phone 等敏感信息,取而代之的是打印一个 UUID,既能追踪用户,又能保密。

或者为了能够快速定位问题,通常需要在日志中记录请求 url,请求方法,用户 ID,请求 ID 等等等等。硬编码的形式 log.info(“requestUrl:{}, userId: {}……”, requestUrl, userId);显然是无法满足要求的,这样实现工作量大,易出错,改动也极其不便。

这个时候,可能就需要 MDC 了。

MDC 是酸辣粉日志框架(戏称,正经叫法是 slf4j)提供的一种机制,全名 Mapped Diagnostic Contexts ,映射诊断上下文,主要用在做日志链路跟踪时,动态配置用户自定义的一些信息,比如 requestId、sessionId 等等。

使用 MDC 只需要几行代码就能轻松应对上述需求。实现一个 Filter,使用 MDC.put(key, val) 写入需要打印的参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws
            IOException,
            ServletException {
        
        try {
            MDC.put("requestUri", request.getRequestURI());
            MDC.put("uuid", UUID.randomUUID().toString().replace("-", ""));
            MDC.put("ip", request.getRemoteAddr());
            User requestUser = (User) request.getAttribute("user");
            if (requestUser != null) {
                MDC.put("uid", String.valueOf(requestUser.getId()));
            }
        } catch (Exception e) {
            logger.error("init MDC error.", e);
        }
        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

接下来注册这个 bean 即可。当然,也可以用 AOP 的方式来向 MDC 里塞东西。最后,只需要配置下 pattern 即可,pattern 里使用使用 %X{} 取值。比如 %X{requestUri} %X{uuid} %X{uid}。

很容易想到,MDC 能在一个请求里实现透传,用的应该是 ThreadLocal。MDC 应该是 Java 里最早使用的全链路方案,最简单,也最有效。

MDC 有一个问题,它是单体的,做不到分布式的链路追踪。现代分布式链路追踪最早的实现应该起源于 Google 的 Dapper。此后,业界有名的追踪系统,无论是国外 Twitter 的 Zipkin 、Naver 的 Pinpoint ,或者大众点评的 CAT 、已进入 Apache 基金会的 SkyWalking,都源于 Dapper 的影响。

为了有效地进行分布式追踪,Dapper 提出了 “追踪” 与“跨度”两个概念。从客户端发起请求抵达系统的边界开始,记录请求流经的每一个服务,直到到向客户端返回响应为止,这整个过程就称为一次“追踪”(Trace)。由于每次 Trace 都可能会调用数量不定、坐标不定的多个服务,为了能够记录具体调用了哪些服务,以及调用的顺序、开始时间、执行时长等信息,每次开始调用服务前都要先埋入一个调用记录,这个记录称为一个“跨度”(Span)。Span 的数据结构应该足够简单,以便于能放在日志或者网络协议的报文头里;也应该足够完备,起码应含有时间戳、起止时间、Trace 的的 ID、当前 Span 的 ID、父 Span 的 ID 等能够满足追踪需要的信息。每一次 Trace 实际上都是由若干个有顺序、有层级关系的 Span 所组成一颗“追踪树”(Trace Tree)

https://p3.toutiaoimg.com/origin/pgc-image/8c3cf3f74f3c4240b5af57df7043acfe?from=pc

Dapper 原理

追踪系统根据数据收集方式的差异,可分为三种主流的实现方式,分别是基于日志的追踪(Log-Based Tracing),基于服务的追踪(Service-Based Tracing)和基于边车代理的追踪(Sidecar-Based Tracing)

MDC 则属于基于日志的追踪,可以看出,MDC 只有 Trace,缺少了 Span,虽然可以通过改造实现,但是如果在微服务中,比如 Java 生态,Spring Cloud Sleuth 则是一个开箱可用的工具。基于日志的追踪显然侵入性比较低,性能损耗少,但依赖日志归集,实时性可能并不够,通常需要搭配 EFK 来使用。

基于服务的追踪是目前最为常见的追踪实现方式, Zipkin、SkyWalking、Pinpoint 等主流追踪系统使用此种方案。服务追踪的实现思路是通过某些手段给目标应用注入追踪探针(Probe),针对 Java 应用一般就是通过 Java Agent 注入的。这种方式同样侵入性低,但性能消耗略大,但可追踪的信息量大,也更精确。其实,基于服务的追踪可以认为是早期 AMP 的延续。

基于 SideCar 的追踪是服务网格的专属方案,也是最理想的分布式追踪模型,它对应用完全透明,无论是日志还是服务本身都不会有任何变化,它与程序语言无关,而且有独立的数据通道,当然缺点就是运维复杂,还不够普及. 而且基于 SideCar 的追踪方案其实也需要依赖 Zipkin 或 SkyWalking 等具体方案做 UI 和存储。

为了规范各种追踪产品,让其实现兼容和适配,2016 年的时候,CNCF 弄了个 OpenTracing 规范,主流的追踪系统都很快支持 OpenTracing。现如今,OpenTracing 规范已经升级为 OpenTelemetry。

分布式追踪系统前世今生我们讲完了,至于技术选型,则看各位量体裁衣了。