Qunar全链路跟踪及Debug

随着公司业务的发展,支持业务的程序也会逐步发展;随着业务的复杂化和流量的增加,一般都会通过拆分的方式来分解不同的业务,将流量分摊到更多的机器上,从而支撑更复杂的业务和更大的流量。

这种分布式的系统会带来很多好处,也自然带来了一些问题。分布式意味着需要通过网络来进行调用,比如RPC调用、HTTPAPI调用、消息队列等;同时,不只是内部开发的程序是分布式的,程序依赖的很多服务也是分布式的,比如数据库、缓存、HBase、ES等。大量的分布式导致服务间的调用关系越来越来越复杂,处于分布式系统中某个节点的程序无法方便的掌握全局结构。

为了方便掌握分布式系统的全局情况,出现了一种分布式追踪系统,它能够将请求所经过的各个系统的操作用一个唯一ID标识并记录下来,便于查看和分析系统全局结构。QTracer就是Qunar内部开发维护的一套分布式追踪系统。

下载PPT请直接拖到文章末尾。

一、QTracer简介

1、简介

QTracer是Qunar内部开发维护的一套分布式追踪系统;它会为每个请求生成一个全局唯一的TraceID,然后将TraceID不断传递给下游系统;同时,在每个系统中,它都会记录各个系统里的各项操作;最后,通过TraceID将各个系统里记录的操作整合起来,还原出一个请求在整个分布式系统中的详细执行流程。

2、功能介绍

下面简单介绍一些QTracer提供的功能,让读者能够快速了解QTracer的功能,直观的感受QTracer的作用。

2.1执行链路查询

1

链路查询是QTracer的基础功能之一,它能够将整个调用流程完整的展示出来。上图就是链路展示页面,从图中能看到请求所在机房、描述、类型、执行时间等信息。

链路查询能够起到很多作用:
1. 它能清晰展示整个请求链路,帮助使用者快速了解全局情况。
2. 能够了解请求经过了哪些服务、哪些机器、耗时情况、跨机房调用情况等。
3. 能够了解各个服务的执行情况,比如是否执行成功、是否进行了重试、失败是否对整个请求造成了影响。
4. 能够快速看出整个请求的耗时分布,快速了解请求的瓶颈。

2.2关联日志查询

2

除了查询执行链路,QTracer还提供了根据TraceID查询关联日志的功能。上图就是关联日志查询的效果,能够展示出请求经过的各个系统上对应的日志。当你不能从链路数据里看到足够的信息时,可以查看相关的日志,往往日志中记录了更详细的信息。

2.3 按条件搜索

3

前面说明的两个功能都是需要根据TraceID来查询,有时我们需要的却是查询TraceID的功能。上图就展示了QTracer提供的TraceID搜索功能,可以根据TraceID前缀、起始应用、时间段、关键字等来搜索出对应的TraceID。
搜索时的关键字就是业务在操作执行时记录到Trace数据里的;比如可以将订单号记录到Trace数据中,这样后面就可以根据订单号查询出对应的TraceID,从而还原订单的处理流程。

2.4 服务上下游关系

4

上面提到的功能都是直接查询、搜索原始的Trace数据,实际上QTracer通过对Trace链路的实时分析,提供了更多的功能。上图中的服务上下游关系就是一例,通过对Trace链路中的调用关系进行分析,可以得到服务间的依赖关系,也能得到服务调用的QPS、耗时等数据。

2.5数据库操作统计

5

6

除了对服务进行分析,QTracer还对数据库操作等进行了分析。上面的图就展示了表级、语句级的执行次数统计。实际上QTracer提供的数据库操作相关统计更多,一是提供了库、表、语句三个维度的QPS、耗时情况统计,方便了解执行情况;二是提供了最近最慢数据操作的统计,方面提前发现问题,比如某些索引没有正确建立。

2.6 透明数据传递

Trace链路记录的时候就要贯穿多个系统,它是否能够作为一个旁路来传递数据呢?QTracer据此提供了透明数据传递功能,利用Trace链路不断传递的特性将上游数据向下游不断传递,避免各个业务为了非业务参数而修改接口。

透明数据传递主要用来传递一些开关、标识,或者是一些非请求业务相关的数据。下面说几个例子:
1. ABTest时可以使用它传输分支标识,从而控制流程走向。
2. 单元化服务里利用它传递单元标识符,避免跨单元调用。

2.7 其它

当然,前面介绍的不是QTracer的所有功能,只是举了一些典型功能。除此之外,QTracer还提供了异常自动关联TraceID、服务调用QPS分析、最近最慢服务调用统计、操作失败情况统计等功能。

二、QTracer客户端核心设计

1、数据模型

7

上图展示了QTracer中一个Trace的基本结构,Trace由多个Span组成。
Trace是由多个Span聚合而成的树形结构,它表示一次完整的请求链路。
Span是Trace的基本组成单元,它表示请求涉及到的一个个单独记录的操作。Span中保存了服务描述、操作起始结束时间、操作结果、操作类型等信息。

2、基本概念

在Span中记录了许多不同作用的数据,它们都有不同的作用场景,下面笔者将逐个介绍Span中涉及到的这些基本概念。

2.1 TraceID

TraceID简单来说就是一个全局唯一ID,QTracer利用它来关联整个请求链路上的所有操作。可以看出,TraceID是一个需要在系统间不断传递的数据。

TraceID原则上来说只需要保证全局唯一即可,使用UUID这种也没有关系。不过QTracer里的TraceID做了一些设计,在TraceID里包含了更多的信息,比如起始应用、起始机器、生成时间、采样标识等,一方面TraceID更加规律,另一方面也方便调查Trace相关的问题。

2.2 SpanID

TraceID标识了整个调用链路,而SpanID则是标记了链路中的一个个操作。通过SpanID可以看出服务的执行顺序和调用关系。由于需要通过SpanID确定关系,所以SpanID也需要不断传递。

下面简单介绍下QTracer的SpanID设计方案:
1. 取SpanID为1的Span作为Root Span,表示一个请求的起点。
2. 标记同一级的顺序调用时,SpanID的本级不断增长,比如1.1、1.2、1.3这种。
3. 标记调用关系时,则需要增加SpanID的层级,比如1、1.1、1.1.1这样。

2.3 TimelineAnnotation

TimelineAnnotation用于记录一个Span内部的时序性信息。例如:
1. HTTP客户端的一次请求记为一个Span
2. 实际上一次请求会有多个步骤,比如建立连接、写入请求、读取回复等
3. TimelineAnnotation就是记录这种内部的时序性操作的

通过TimelineAnnotation可以看到各个阶段的具体起始时间,一个简单的应用场景就是当HTTP请求失败时,能通过它看到进行到哪个步骤失败了,方便快速定位问题。

2.4 KVAnnotation

KVAnnotation是最常用的一种,它用于记录业务自己关心的自定义数据,比如订单号、UID等。记录之后,查询Trace链路是能看到这些记录的数据,也能通过这些数据反向搜索出相关的TraceID。

2.5 TraceContext

TraceContext就是透明数据传递需要使用到的概念,它也是保存业务自定义数据的,但是TraceContext中的数据不会被收集,而是不断的随着链路向下游传递。

3、核心API

8

上图展示了QTracer的核心API使用方式,主要就是利用startTrace函数开启一个新的Span,然后利用各个add函数添加不同种类的数据。

实际上,用户基本不需要使用这些核心API,公司里的很多组件都默认添加了QTracer的埋点,直接使用默认埋点在配合一些快捷使用方式基本就足够了。

4、Trace的延续

为了关联多个系统的操作,必须要把上下文不断的传递下次,所以Trace的延续就是一个关键性的问题。

首先,我们先介绍一下单个系统内部如何延续Trace链路,它分为同步调用和异步及跨线程调用两种情况。
1. 同步调用。同步调用时,延续Trace链路非常简单,QTracer内部会利用ThreadLocal来保存上下文关系,每次开启新的Span时,直接从ThreadLocal里获取当前的TraceID和SpanId即可。
2. 异步调用及跨线程调用。这种情况下,ThreadLocal是无法生效的,只能显式的延续上下文关系。对于线程及线程池,QTracer提供了快速延续的包装方法,使用也非常方便;而异步调用则只得利用核心API进行延续。

其次,除了单个系统内部的传递,还有许多情况需要跨机器延续。分布式情况下跨网络请求时非常常见的,跨网络的情况和跨线程是非常相似的,都是需要手工进行延续。为了方便地跨网络传输上下文关系,QTracer在内部使用的Dubbo、HTTP、MQ等组件里都加入了自动传输上下文的功能,一般来说不需要使用方关注。

5、无侵入埋点

前面提到很少需要使用核心API,因为直接使用核心API记录对用户不够友好,太繁琐了。为了方便使用者,我们对各个组件都添加了无侵入的埋点,用户直接开启采样就能得到足够的信息,基本不需要自己记录过多的数据。
无侵入埋点主要包含两种情况:
1. 对于公司内部维护的组件,可以直接添加埋点代码,这样能够更精确的控制功能,记录更多信息。比如Dubbo、消息队列这种组件。
2. 对于不是公司内部维护的组件,由于无法修改源码添加埋点功能,所以采用了字节码修改的方式,能够在运行时为指定的类添加埋点功能。比如MySQL和PG的driver。

6、字节码插桩

字节码插桩功能在QTracer中使用非常广泛,这里简单介绍一下。字节码插桩就是一种运行时动态修改字节码的技术,能动态调整代码的行为。它和代理的作用很像,但是实现上完全不同。一般来说需要为JVM添加代理(agent)来启用字节码插桩功能。

QTracer实现了一套利用配置指定对某些类的方法进行插桩的功能。比如,针对MySQL和PG的操作的插桩就是通过在配置文件中指定驱动中的方法、字段等实现的。同时,当有有新的客户端需要插桩接入时,直接在配置中心添加新的插桩配置即可直接生效。

7、本地方法快速插桩

除了中间件、数据库driver等预先埋点的组件,有些业务系统还想要跟踪一些重要的本地方法。这种时候直接使用核心API ?核心API对业务来说使用起来比较麻烦,需要熟悉和API的使用,避免使用错误,同时也会加入很大业务无关代码。

为了解决这个问题,QTracer提供了一个方便的解决方案:注解。利用注解标记需要跟踪的方法和需要记录的数据。然后程序编译时自动生成一份本地的插桩配置,启动时QTracer载入这个本地配置即可自动对那些指定的方法进行插桩。

9

上图展示了几个注解的使用场景,@QTrace注解标记要跟踪的方法,@QP 标记要记录的参数,@QF 标记要记录的成员变量。

8、日志关联

QTracer提供了根据TraceID查询关联日志的功能,但是日志是如何关联到TraceID的呢?QTracer在实现时利用了MDC(Mapped Diagnostic Context)来保存TraceID和SpanID,MDC中的数据是可以直接输出到日志中的。

Span生成时将TraceID和SpanID保存到MDC中,等一个Span结束时将这两种数据清空;这样一来,在Span表示的操作期间,所有记录的日志都能够同时记录当前的TraceID和SpanID。使用方通过配置日志的log pattern,将QTracer的信息默认输出到日志中。后面等日志被实时日志收集系统收集上来的时候,利用实时任务分析日志即可获得TraceID到日志的关联关系。

三、QTracer系统架构

10

这是整个QTracer系统的简单架构图,包括数据记录与收集、数据处理分析、数据展现这三个部分。

1、Trace数据记录和收集

QTracer利用本地日志进行数据记录,将数据全部暂存到本地日志,然后利用实时日志收集将数据全部发送到专门的Kafka集群暂存。

数据记录时尤其重要的就是控制资源消耗,插桩时要尽量降低额外损耗,记录日志时也要小心对IO和磁盘空间的占用情况。为了尽量降低记录日志的损耗,QTracer内部实现了异步批量写日志;控制批量大小,避免占用过多内存;日志文件按照大小轮转而不是时间轮转,同时严格控制日志文件数量,这样能避免大量数据占据过多磁盘空间;同时在极端情况下,为了避免过多占用资源,QTracer会选择丢弃一定量的数据。

2、Trace数据处理与分析

我们内部选择了Samza框架作为分析Trace数据的实时任务框架,主要是因为:
1. Samza和Kafka集成程度高,配合良好
2. Samza使用非常简单,API简单直接,功能在Trace数据分析这个场景上足够使用
3. 提供了一个基于rocksdb的本地cache,提供了异常恢复功能,方便编写需要缓存很多数据做聚合的任务

11

上图展示了Trace数据处理的一个大致流程。数据处理主要分为两条线路,针对Span进行处理和针对Trace链路进行处理。

针对Span进行的处理主要有:
1. 直接保存到HBase中提供快速查询。Span保存到HBase时,直接使用TraceID构成rowkey,使用SpanID作为列名,这样能提供非常快速的查询。
2. 利用实时任务将Span数据聚合形成完整的Trace链路。
3. 分析最近最慢操作,比如HTTP请求、数据库操作等,这种数据能够根据单独的Span数据分析出来。
4. 统计操作的QPS、耗时等数据。

针对Trace进行的处理主要有:
1. 对完成的Trace链路数据进行精简,删除重复数据和无用数据,然后保存到ES中提供多维度搜索功能。适度的精简能降低数据量,不保存冗余的数据。
2. 分析统计调用链路上的上下游调用关系。通过对完整的链路进行拆解,能够得到链路涉及的各个服务的上下游关系。
3. 分析上下游调用关系的同时,也能得到服务调用的QPS、耗时情况等。
4. 通过大量调用关系的聚合,能够得到整体上各个服务之间的依赖关系。有依赖关系之后,查问题时更容易定位问题。

3、数据存储

QTracer存储方面主要使用了HBase和ES两种。它们各自都有不同的作用,下面简单介绍一下。

HBase首先是用于保存Trace链路数据,这点前面也提到过;实际保存时为了避免HBase region写入过热,对TraceID做了一些变形,将分布均匀的字段尽量放到rowkey前面。其次,HBase还保存了根据服务调用统计出来的分钟级QPS、调用时间等数据。

ES首先是用于保存精简的Trace链路,提供了按条件搜索功能。其次,还保存了服务的上下游调用关系、近期的整个服务依赖关系等。

四、QTracer Debug

1、简介

除了QTracer和各个组件提供的预先埋点以及手工提前埋点,有时会遇到想要直接获取代码运行到某个位置是调用栈的具体状态。QTracer Debug就提供了这样一种功能,它类似于IDE中提供的debug功能,通过在源码中设置断点,可以获取实际代码运行时断点处的所有变量、调用栈信息,而且,这不会暂停应用,同时额外损耗也非常小,可以直接在线上使用。

下面简单说下使用流程:
1. 在前端页面上选择项目,浏览代码,在代码行上标记断点
2. 选择应用的机器,启用断点
3. 访问能经过断点的URL,带上指定的参数
4. 等待数据收集完成即可自动展示所有数据

2、详细实现

2.1 概要

12

展示层面借助Gitlab的API实现代码浏览和按行设置断点功能。然后借助QTracer 的字节码插桩功能在指定位置加入断点代码,执行时进入断点就能记录调用栈状态。直接利用QTracer现有的数据处理路径进行数据的记录和收集。后面的实时任务会筛选出含有debug数据的Trace链路,并提取断点数据保存到HBase。最后在前端展示断点数据。

2.2 断点添加

断点添加是一个核心部分,它的主要流程是:
1. 分析应用所有的类,建立源文件+行号到类的映射关系。
2. 根据断点位置找到需要添加断点的类
3. 分析类,收集类中所有变量的作用域信息
4. 修改类的字节码,在指定位置插入收集所有作用域内变量等数据的代码

2.3 数据收集及记录

QTracer Debug的数据记录直接依赖QTracer自身的KVAnnotation功能,直接把数据存放到QTracer的Span中。数据收集直接通过QTracer的写本地日志+日志收集方式。实时任务对QTracer数据进行筛选,保存到HBase中。

PPT下载去哪儿全链路跟踪及Debug-去哪儿王克礼

【作者简介】王克礼,去哪儿平台事业部基础架构Java开发工程师,参与开发和维护去哪儿内部中间件,包括配置中心、消息队列、日志收集及链路跟踪系统QTracer等。本文来自王克礼在“携程技术沙龙——海量互联网基础架构”上的分享。

没看够?更多干货文章请搜索关注“携程技术中心”微信公号~

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Copyright © 1999 - 2014 Ctrip. Powered by WordPress.