1. 为什么需要分布式调用链跟踪
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题。
全链路监控组件就在这样的问题背景下产生了。最出名的是谷歌公开的论文提到的Google Dapper。想要在这个上下文中理解分布式系统的行为,就需要监控那些横跨了不同的应用、不同的服务器之间的关联动作。
所以,在复杂的微服务架构系统中,几乎每一个前端请求都会形成一个复杂的分布式服务调用链路。一个请求完整调用链可能如下图所示:
那么在业务规模不断增大、服务不断增多以及频繁变更的情况下,面对复杂的调用链路就带来一系列问题:
- 如何快速发现问题?
- 如何判断故障影响范围?
- 如何梳理服务依赖以及依赖的合理性?
- 如何分析链路性能问题以及实时容量规划?
同时我们会关注在请求处理期间各个调用的各项性能指标,比如:吞吐量(TPS)、响应时间及错误记录等。
- 吞吐量,根据拓扑可计算相应组件、平台、物理设备的实时吞吐量。
- 响应时间,包括整体调用的响应时间和各个服务的响应时间等。
- 错误记录,根据服务返回统计单位时间异常次数。
全链路性能监控从整体维度到局部维度展示各项指标,将跨应用的所有调用链性能信息集中展现,可方便度量整体和局部性能,并且方便找到故障产生的源头,生产上可极大缩短故障排除时间。
有了全链路监控工具,我们能够达到:
- 请求链路追踪,故障快速定位:可以通过调用链结合业务日志快速定位错误信息。
- 可视化:各个阶段耗时,进行性能分析。
- 依赖优化:各个调用环节的可用性、梳理服务依赖关系以及优化。
- 数据分析,优化链路:可以得到用户的行为路径,汇总分析应用在很多业务场景。
2. 调用流程图
我们先看一张调用流程图的简单示例,这张图标识了请求开始到结束的整个流程,从左到右代表时间,字母和对应的“字母’”代表每个阶段的开始和结束。
- A 到 A’ 代表请求从开始到结束的整体时间。
- A 发起请求到 B 接收到数据总共花费了 50ms,B 在计算时总共花费了 250ms,最终 B 将数据聚合返回给 A,这个过程花费了 20ms。因为这一步会涉及内外网的数据传输,所以会有一定的时间损耗。
- B 在接受请求后,分别去请求了 R和 C 应用,在请求时又分别花费了 10ms 和 20ms。R 和 C 应用在接收请求后,又分别使用了 10ms 和 210ms 来处理业务逻辑并返回数据。
- C 和上面一样,通过 10ms 来进行 D 的调用和处理,这个流程总共花费了 210ms。
从这张图中我们可以清楚地看到每一个应用和别的应用进行交互时的总耗时和自身耗时,进而了解它们之间通信和自身处理的耗时、流程和数据走向。但是这还存在着一些问题,就是我们怎样将这个图以数字化的形式展现出来,然后通过这种形式去定位问题?这个问题的解决方式就是“链路图”。
分布式服务跟踪和监控的运行原理上实际上并不复杂,我们首先需要引入两个基本概念,即 SpanId 和 TraceId。
3. spanId
Span就代表在流程图中的字母和对应的“字母’”,他就代表了一个操作行为。一次链路调用(可以是RPC,DB等没有特定的限制)创建一个Span。Span 一般会由以下几个部分构成。
- 开始时间:代表这个操作是从什么时候开始的一个时间戳。
- 结束时间:和开始时间类似,它也是时间戳,代表操作执行完成的时间。通过开始时间和结束时间这两个时间戳,我们可以计算出这个操作的耗时时长。
- ID:主键。可以理解为在一个链路中,这个 ID 是唯一的。
- 父级 ID:一个 Span 中有自己的 ID 和父级 ID,可以理解为是一个树形的概念,父级 ID 是树干,ID 则是枝叶,通过树状图可以更方便地绘制图片和查询操作之间的依赖关系。
- 操作名称:用于指明你操作的内容名称,你可以快速了解是在进行什么操作。比如 HTTP 访问就可以使用访问路径。名称也会用在一些其他的地方,例如聚合数据时,就可以将同一名称的数据聚合在一起。
- 操作类型:用于指定当前操作的类型,一般可以分为下面 3 类,它们基本囊括了我们的各种操作类型:
- 入口:代表别的请求到达你的系统时,就可以认为是入口操作。比如 Tomcat 接收到外部的 HTTP 接口请求。
- 出口:代表你的系统向别的系统发出了请求,通常是获取数据等通信操作。比如你通过 RPC 调用了其他系统,或者你向数据库发起了一次查询,都可以认为是出口操作。
- 本地:本地进行了某些操作,不涉及任何需要远程通信的组件,是业务系统调用的本地处理。比如你通过 EhCache 查询了本地缓存,或是本地进行了一次文件操作。
业界一般使用四种关键事件记录每个服务的客户端请求和服务器响应过程。我们可以基于这四种关键事件来剖析一个 Span 中的时间表示方式,如下所示:
在上图中,cs 表示 Client Send,也就是客户端向服务 A 发起了一个请求,代表了一个 Span 的开始。sr 代表 Server Receive,表示服务端接收客户端的请求并开始处理它。一旦请求到达了服务器端,服务器端对请求进行处理,并返回结果给客户端,这时候就会 ss 事件,也就是 Server Send。最后的 cr 表示 Client Receive,表示客户端接收到了服务器端返回的结果,代表着一个 Span 的完成。
我们可以通过计算这四个关键时间之前的差值来获取 Span 中的时间信息。显然,sr-cs 值等于请求的网络延迟,ss-sr 值表示服务端处理请求的时间,而 cr-sr 值则代表客户端接收服务端数据的时间。
通过这些关键事件我们就可以发现服务调用链路中存在的问题。
4. TraceId
除了 SpanId 外,我们还需要 TraceId,也就是跟踪 Id。要想监控整个链路,我们不光需要关注服务 A 中的 Span,而是需要把请求通过所有服务的 Span 都串联起来。这时候就需要为这个请求生成一个全局的唯一性 Id,通过这个 Id 可以串联起从服务 A 到服务 D 的整个调用,这个唯一性 Id 就是 TraceId。
同时,我们也应该关注于各个 Span 之间的顺序关系。服务 A 位于服务 B 的上游,所以访问服务 A 所生成的 SpanId 应该是访问服务 B 所生成的 SpanId 的父 SpanId,服务 B 和服务 C 的调用关系以此类推。这样,我们通过获取请求的唯一性 TraceId,并通过各个父 SpanId 与子 SpanId 之间的关联关系就可以构建出一条完整的服务调用链路。
比如你运行的分布式大数据存储一次Trace就由你的一次请求组成。
每种颜色的note标注了一个Span,一条链路通过TraceId唯一标识,Span标识发起的请求信息。树节点是整个架构的基本单元,而每一个节点又是对Span的引用。节点之间的连线表示的Span和它的父Span直接的关系。虽然Span在日志文件中只是简单的代表Span的开始和结束时间,他们在整个树形结构中却是相对独立的。
5. 链路图
在了解了spanId和traceId后,我们看一下链路图
在图中,每一行的长方形都可以理解为是一个操作的基本单元,在链路中也叫作Span(跨度)。链路由一个 Span 的集合构成。其中 Span 中包含 4 个信息,在长方形中,从左到右依次是:SpanID、父级 SpanID、当前开始时间(从 0 开始)和当前 Span 的耗时。
- 假设第一行的 Span 代表在网页中发出请求,可以认定为是出口请求,所以 A 的 Span 是出口类型的操作。SpanID 从 1 开始,没有父级 Span, 所以 parentID 认定为 0,并且开始时间是 0ms,当前 Span 的总共耗时是 320ms。
- 在第二行中,B 接收到 A 传递来的请求,所以是入口类型的操作。由于网络损耗导致 B 在 50ms 时才接收到请求,所以当前操作的开始时间是 50ms。并且根据层级可以得知 B 是 A 的子节点,所以 B 的父级 ID 对应 A 的 ID,因此 B 的 parentID 是 1,并且 ID 是自增的,所以 B 的 ID 为 2。
- 在第三行中,因为 B 进行了一次 Redis 操作,而 Redis 需要连接别的数据源,所以这里的 Span 算为出口类型的操作。因为网络耗时和 Redis 处理各花了 10ms,所以总共的耗时是 20ms。
- 第四行则代表 B 向 C 应用发出了一个请求,所以同样是出口操作类型。这里需要注意的是,第三行和第四行的父级 SpanID 是一致的,这代表了它们的父级应用是一样的,都是 B 入口下面的操作。又由于它们开始的时间是相同的,所以代表它们分别启动了两个线程来进行操作。
6. 链路追踪的作用
- 链路查询:就算你接手了一个全新的项目,对这个项目一无所知,你也可以通过某个入口查看链路,快速了解当前程序的运行情况,并且可以通过很直观的图来展现:到底是哪里比较耗时,出现错误时是哪个操作导致的,等等。
- 性能分析:通过聚合链路中的数据,我们可以结合操作名称,快速得知系统的运行容量、耗时情况等。
- 拓扑图:通过对链路信息的聚合分析,我们可以分析得到的数据,形成拓扑图。拓扑图可以使你直观地了解整个系统的构成。
- 依赖关系:同样是链路的聚合分析,你还可以了解到操作之间的依赖关系,从而快速感知操作之间的重要等级。如果将依赖关系与限流熔断技术相结合,可以帮助更快地构建一个企业级的链路保护措施。
- 跨应用/语言:像我上面所说的每个内容,它们都是不限制语言的,每个语言都有它自己的实现。在一个大型的企业中,几乎不可能保证所有的系统都使用同样的语言,利用链路追踪不限语言的特点,我们可以将不同语言的代码串联到一起。
7. 链路追踪的短板
前面我们了解到链路追踪中最小的单位是 Span,每一个 Span 代表一个操作,但这样存在一个问题,粒度还是太粗了。如何解决粒度太粗的问题呢?在链路追踪的实现中,我们一般会有 2 种方式。
- 代码埋点:在代码中预埋点,通过侵入式的方式记录链路数据。
- 字节码增强:在字节码生成之后,再对其进行修改,从而给它功能。比如像 Java 这类语言,就可以通过字节码增强,而不是人工侵入式的方式记录链路数据。
这两点的区别在于,是否是侵入式的。埋点的方式虽然灵活,但是依赖性较高;字节码增强的形式不需要代码层介入,但仅能支持一部分应用框架。这两种方式能够实现链路追踪,并且可以大面积的使用 ,因为框架都是通用的。
但通用的链路监控方案的实现方式都逃不过面向切面编程,因为它们只能做到框架级别,而这又会导致我们可能会遇到程序执行缓慢或者不稳定的情况,却无法查询到原因。这时候我们一般只能通过在业务中手动增加埋点的方式来进行更细粒度的 Span 开发,这种方法也有几个缺点:
- 增加埋点成本高,很难全面覆盖。这样的方式只适用于具体的业务场景,如果其他的业务场景也存在类似的问题,就需要在其他的业务场景中再次埋点。如果一次没有把需要埋点的位置加全,还可能会涉及多次的上线,降低了系统的稳定性。 大量的埋点同时还会占用 CPU 和内存的资源。虽然每个埋点的性能损耗都不高,但是随着项目不停地迭代,埋点的数量会越来越多,性能损耗也会越来越多,长期下来就会对系统性能造成持续的影响。而且埋点后还需要开发人员定期移除掉不需要的埋点信息,极其浪费人员和时间成本。
- 动态增加埋点技术不可靠。既然人工难处理,那让系统自动处理呢?这便有了动态增加埋点技术。它是在某个特定的包下,对每个方法的执行都增加埋点,无须手动修改代码。但是这一技术的问题也很明显:因为会给所有的地方都增加埋点,性能的损耗可能比人工的方式更为严重,甚至因为埋点过多导致内存增长,最终造成系统崩溃,影响线上程序运行。
- 即使我们通过一个十分合理的方式解决了上面的两个问题,我们也只能在业务级别做埋点,没有办法细入 JDK 中的某个场景。因为如果要对 JDK 中的某个方法做埋点的话,可能会造成巨大的延迟风险。
为了解决上述问题,结合链路中的上下文信息,我们可以通过周期性地对执行中的线程进行快照操作,并聚合所有的快照,来获得应用线程在生命周期中的执行情况,从而估算代码的执行速度,查看出具体的原因。这样的处理方式,我们就叫作性能剖析(Profile)。
-
在编程语言中,基本所有的代码都是运行在线程中的,并且大多数的情况下都是单线程,比如 HTTP 或者 RPC 等框架接收到请求之后都会交给单独的线程去处理。
-
大多数的编程模型是基于线程的这一个概念去实现很多功能的,无论是现成的框架,还是底层的 JDK,比如 Dubbo 中的 RPCContext、Java 中线程安全的随机数生成器 ThreadLocalRandom。
8. 链路追踪还能做什么
8.1. 指标聚合
通过分析链路,部分指标无须再通过手动埋点的方式进行统计,比手动埋点获取更具优势,这个优势体现在如下 3 点。
- 更精准。链路追踪中的数据,会比手动在各个业务代码中编写的计算时间更加精准,因为它面向的是框架内部,相比在业务代码中编写,这样的方式覆盖面更广,也更精准。
- 更动态。在传统方式中,开发人员需要对每个开发的功能都进行埋点。随着功能迭代,开发人员在编写时肯定会有遗漏。分析链路不再需要开发人员手动埋点,程序可以自动解析链路中的数据信息,实现动态化。
- 更通用。链路追踪的概念是统一的,所有需要分析的链路都可以根据一套数据内容处理,生成多种相同的指标信息。这样的分析方式更加通用,无论你的代码在哪个层面,用的是哪个框架、哪个语言,都不会再被这些烦琐的内容局限。
在指标中,我们一般将数据分为 3 个维度。
- 服务:指具有一段相同代码、相同行为的服务。通常我们是将一个项目认定为一个服务的,但多个服务之间可能存在组合的数据关系。
- 实例:指服务在进行多进程、多机器部署时的运行实例。现在是微服务的时代,为了提高服务的吞吐率和服务在灰度上线时的稳定性,一般服务都不会单独部署,而是采用集群的形式。因此,一个服务往往对应两个或者更多的实例。
- 端点:是与实例平级的一个概念,一个服务下会有多个端点。这里的端点可以理解为我们在 span 中定义的操作名称,每个操作的操作名称就是一个端点,大多时候端点都是入口操作。
当然,通过链路来分析统计指标会有一些局限性。由于这些指标来源于链路数据,所以这个方法只能观测到通用的数据信息,而不能对指标进行定制化的统计,定制化的指标还是需要开发人员去代码层通过埋点统计。链路分析可以使你获取到通用的数据信息,代码埋点则可以帮助你收集定制化的指标数据,合理地使用这两种方式可以让你可观测的维度最大化,可以丰富你在分析数据、查看问题时的内容参考。
我们在链路分析中可以获取到以下数据:QPS、SLA、耗时、Apdex、Percentile、Histogram、延迟和 topN。
- QPS:在链路分析中最为常用,可以清楚地记录每个服务、实例、端点之间的请求量。我们还可以通过某个服务或者端点中实例之间的请求量,来查看负载情况是否均匀。
- SLA:根据 QPS 和具体端点的错误次数,同样可以统计出服务、实例或者接口的 SLA 情况。通过这样方式计算,可以统计到某个组件具体到服务的 SLA 执行情况。SLA 还会提供来源的情况,所以在评估问题影响时,这一指标会起到关键的作用。
- 耗时:Span 中的数据是包含开始时间和结束时间的,因此我们可以算出来耗时情况。同样,我们可以针对某个接口,统计出这个接口中每个 Span 在整体接口中的耗时占比,从而让性能优化聚焦在相对耗时较高的 Span 中。
- Apdex:有耗时情况,自然也有对应的 Apdex 值。通常,我们会在分析之前预定义不同类型服务的耗时基准时间单位,然后采用统一的标准设定。
- Percentile:基于耗时信息,同样可以计算出相应的百分位值。我们一般会依据长尾效应,根据 P95 或者 P99 中的耗时情况来针对性地优化。
- Histogram:通过直方图,我们可以根据耗时信息来指定多个耗时区间,辅助我们查看具体的分布范围。
- 延迟:Span 有出口和入口的理念,我们可以了解到相关的网络通信情况。比如 Dubbo 调用时,消费者发起请求的时间是 A,提供者接收到请求的时间是 B,那么通过 A-B 就可以获取到相应的延迟时间。通过延迟时间我们可以来判定是否是存在网络通信问题,如果网络延迟相对较大,则可能会影响整体的服务效率。当然,也有可能是机器之间时钟不同步导致的,但是一般服务器中的时间差别都相对较少,部分的数据可以忽略掉。
- topN:我们也可以根据上面提到的几部分数据,分别来绘制出相对缓慢的服务、实例或者端点。通过这样的数据,我们可以优先优化相对缓慢的数据,从而提高我们整体的效率。
8.2. 绘制拓扑图
除了追踪链路,链路分析的第二个功能,就是根据从链路中分析出的数据关系绘制拓扑图。它可以让我们了解服务之间的关系、走向究竟是什么样子的。这里我会更详细讲解一下拓扑图。通过链路分析绘制出的拓扑图,具有可视化、数据化、动态性这 3 个优点。
- 可视化:通过可视化的形式,你可以以一个全局的视角来审视你的系统,从而更好地分析数据之间的依赖关系和数据走向。哪些点是可以优化的,系统的瓶颈可能在哪里,等等,这都是可以通过拓扑图了解到的。
- 数据化:将拓扑图中的数据和统计指标相互结合,可以将两者的数据放在同一个位置去展示。比如通过 QPS 就可以看出哪些服务可能产生的请求更多,这些请求又是来自哪里;当出现问题时,我们还可以通过 SLA 看出来受影响服务的范围有多大。通过服务、实例、端点之间的相互引用,我们可以快速分析出相应的依赖占比。通过一个接口中的依赖占比,快速分析出哪些是强依赖,哪些是弱依赖,从而更好地进行熔断降级。
- 动态性:在功能迭代时,拓扑图会通过链路数据进行动态分析,所以无须担心它是否是最新的。拓扑图的这一特性也允许我们通过时间维度来查看演进的过程。
与指标一样,我们也会将拓扑图中的数据分为3个维度,分别是服务、实例和操作。每个维度的数据显示的内容不同,作用也相对不同。
- 服务:你可以通过服务关系拓扑图了解整个系统的架构、服务和服务之间的依赖,还可以通过全局的数据内容提供“下钻点”,从大范围的一个点切入,直到发现问题的根源。通过拓扑图,我们还能以全局的视角来优先优化相对依赖度高,耗时更高,也更为重要的服务。
- 实例:当两个服务之间存在依赖关系时,我们可以再往下跟踪,查看具体的进程和进程之前的依赖关系。再在实例之间加上统计指标数据,我们还可以看到两者在相互通信时的关系是怎么样的。通过实例之间的拓扑图,你可以确认你的实例是否都有在正常地工作,出现问题时,也可以根据这张图快速定位到是哪一个实例可能存在异常。比如我们在 Dubbo 请求调用时,可以通过依赖和指标数据,查看负载均衡器是否正常工作。
- 操作:操作和端点是一样的,它与实例是一个级别。通过拓扑图,你可以了解到操作之间的业务逻辑依赖,并且可以根据统计指标了解到延迟和依赖的程度。通过这部分数据,你能够看到具体的某个接口在业务逻辑上依赖了多少个下游操作,如果依赖数据越多,程序出现错误的概率也会越大。
8.3. 定制链路与数据
链路追踪的最后一个功能,就是链路与数据的定制化。链路与我们的程序代码息息相关,我们可以让链路得知一些业务中的数据,从而更好地辅助我们理解链路。
链路中最基本的数据单位是 span。一个 span 中记录的可能是接收到的某个请求,也可能是与其他第三方组件的一次通信,比如 Tomcat、Dubbo。但这个时候可能会遇到了一个问题,就是这个 span 只知道你的操作名称,而不知道例如来源 IP 地址、访问 UA 信息这样的信息。这个时候我们就可以考虑给这个 span 增加自定义的数据,像上面提到的 IP 地址、UA 信息,来丰富链路数据。其中可以添加的数据内容可以分为 3 类。
- 通信数据:通过框架中的相关数据内容,我们可以了解到业务在进行通信时的数据有哪些,如 MySQL 中可以存储相关的 SQL 语句,Redis 处理时可以记录与处理相关的命令。通过这部分数据,你可以了解到与组件进行交互时发送了哪些请求。比如在出现“慢查询”时,我们可以通过这部分通信数据获取到请求的 SQL,然后对“慢查询”进行优化。
- 业务数据:我们可以将自己业务中的数据保存到链路中,比如操作时的用户 ID、操作目的等。添加这部分数据让你更好地了解这个链路中的数据的内容,你可以根据这个内容模拟出相对应的真实场景,从而更好地定位问题产生的原因。
- 日志+异常信息:因为链路会在真实的代码中执行,所以我们可以将当时的日志框架与链路数据相结合。这种组合可以让你看到当时的执行日志有哪些,如果有出现异常信息,也可以在链路中快速地结合异常堆栈信息来定位问题。以拉勾教育为例,当获取课程信息出现了接口访问错误,比如查询 MySQL 数据库超时。我会观察出错时的具体链路数据,结合链路中出现错误的 Span 的堆栈信息,快速得知错误的原因。
通过增加这些数据内容,我们可以更容易地了解到代码的真实执行情况,包括其中的参数数据。当链路收集到这部分数据后,我们可以通过 2 个方式进行处理。
-
数据展示:在链路信息展示时,展示定制化的数据。这样在定位问题时,也会更加得心应手。
-
数据检索:通过业务数据和通信数据检索链路信息。比如最常见的,通过用户 ID 检索,查询出用户请求的链路。当有客服反馈问题时,你可以通过这样的方式,根据用户 ID,快速检索出具体业务和通信数据中的指定信息的数据链路。
9. 实现原理
常见的链路追踪的实现原理,可以拆解成 3 个部分:链路采集、数据收集、数据查看。
链路采集是实现原理的基础,没有它我们无法进行后面的步骤;数据收集是中间的衔接环节,我们可以存储采集到的数据;数据查看则是我们最终要达到的目的,可以快速查看链路数据信息,然后分析问题。
9.1 链路采集
链路采集是指从业务系统或者组件中采集实时的流量数据,将这些数据汇聚成统一的格式,然后发送到链路收集服务中。目前主流的实现方式可以分为 2 种:埋点和字节码增强,
9.2. 数据收集
从链路采集到数据之后,我们就可以对这些数据进行解析、分析等工作,并最终存储到相应的存储引擎中,常见的引擎有 ElasticSearch、HBase、MySQL 等。通常这时候数据会分为两类,统计数据和链路数据。统计数据可以让你了解数据的走向,链路数据则可以让你清晰地看到链路中的每一个细节。
9.3. 数据查看
数据已经存储到数据库后,我们就可以进行数据查询、基于这些数据进行告警以及其他的操作。
链路追踪中更加强调的是数据的可观测性,它可以通过图形化的形式展现出问题,因此对于可观测性有着很重要的意义,这也决定了链路追踪的数据展示是重 UI 的。
同样,我会对数据收集中的不同类型数据做简要的说明。
统计数据通常用于展示一段时间内的数值变化曲线、热力图、topN 样本数据等。此时可能需要使用图表、列表等形式来展示,前端中的 ECharts 就是目前比较常见的选择之一。
链路数据是基于请求链路的,数据与数据之间存在一定的依赖关系,此时就通常有树形图和拓扑图这 2 种展现形式。
- 树形图:链路中每一个 Span 都最少有 ID 和对应的父级 ID 信息,通过树形图的形式我们可以直观地看到一个链路是怎么执行的,服务与服务、接口与接口之间的调用关系。
- 拓扑图:拓扑图则可以为我们展现不同维度之间的依赖关系,包括服务、实例、接口之间的依赖关系。依据与此,我们可以快速梳理出这个链路依赖了哪些服务,在排查问题时可以依据此来辅助你排查问题的影响范围。
通过链路采集数据,对数据解析、分析后存储到数据库中,然后通过可视化的形式查看数据,至此,就构建了一个相对完整的链路追踪系统。
9.4. 如何跨进程传递 context
我们知道数据一般分为 header 和 body, 就像 http 有 header 和 body, RocketMQ 也有 MessageHeader,Message Body, body 一般放着业务数据,所以不宜在 body 中传递 context,应该在 header 中传递 context,如图示:
9.5. 采样率
现在已有的大量的链路追踪中,都会存在采样率的设定,其作用就是只采集一部分的链路信息,从而提升程序性能。在真正的环境中,没有必要采集 100%的链路信息,因为很多时候,大量的链路信息是相同的,可能需要你关注的只是其中相对耗时较高或者出错次数较多的。当然,有些时候也会为了防止漏抓错误而进行全量的链路追踪。是否需要进行全面的链路追踪,就看你在观察成本和性能中如何权衡了。
这样的采样频率其实足够我们分析组件的性能了,按 3 秒采样 3 次这样的频率来采样数据会有啥问题呢。理想情况下,每个服务调用都在同一个时间点(如下图示)这样的话每次都在同一时间点采样确实没问题。
但在生产上,每次服务调用基本不可能都在同一时间点调用,因为期间有网络调用延时等,实际调用情况很可能是下图这样:
这样的话就会导致某些调用在服务 A 上被采样了,在服务 B,C 上不被采样,也就没法分析调用链的性能。
SkyWalking 是如何解决的呢?如果上游有携带 Context 过来(说明上游采样了),则下游强制采集数据。这样可以保证链路完整。
10. 技术选型
skyWalking:
代码的本地调试复杂,入门有难度依赖Java探针,对日志的自由把控困难部署依赖ES集群
zipkin:
源代码只是负责trace的log展示,依赖采集组件只监控到接口级别,没法监控更细的粒度网络拓扑中没有DB、容器缺少规则报警缺少权限管理
市面上的全链路监控理论模型大多都是借鉴Google Dapper论文,本文重点关注以下三种APM组件:
- Zipkin:由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。
- Pinpoint:一款对Java编写的大规模分布式系统的APM工具,由韩国人开源的分布式跟踪组件。
- Skywalking:国产的优秀APM组件,是一个对JAVA分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统。
以上三种全链路监控方案需要对比的项提炼出来:
- 探针的性能,主要是Agent对服务的吞吐量、CPU和内存的影响。微服务的规模和动态性使得数据收集的成本大幅度提高。
- Collector的可扩展性,能够水平扩展以便支持大规模服务器集群。
- 全面的调用链路数据分析,提供代码级别的可见性以便轻松定位失败点和瓶颈。
- 对于开发透明,容易开关,添加新功能而无需修改代码,容易启用或者禁用。
- 完整的调用链应用拓扑,自动检测应用拓扑,帮助你搞清楚应用的架构。
10.1. 探针的性能
比较关注探针的性能,毕竟APM定位还是工具,如果启用了链路监控组建后,直接导致吞吐量降低过半,那也是不能接受的。对Skywalking、Zipkin、Pinpoint进行了压测,并与基线(未使用探针)的情况进行了对比。
选用了一个常见的基于Spring的应用程序,他包含Spring Boot,Spring MVC,Redis客户端,MySQL。监控这个应用程序,每个Trace,探针会抓取5个Span(1 Tomcat,1 Spring MVC,2 Jedis,1 MySQL)。这边基本和SkywalkingTest的测试应用差不多。
模拟了三种并发用户:500,750,1000。使用JMeter测试,每个线程发送30个请求,设置思考时间为10ms。使用的采样率为1,即100%,这边与生产可能有差别。Pinpoint默认的采样率为20,即5%,通过设置agent的配置文件改为100%。zipkin默认也是1。组合起来,一共有12种。下面看下汇总表:
从上表可以看出,在三种链路监控组件中,Skywalking的探针对吞吐量的影响最小,Zipkin的吞吐量居中。Pinpoint的探针对吞吐量的影响较为明显,在500并发用户时,测试服务的吞吐量从1385降低到774,影响很大。然后再看下CPU和memory的影响,在内部服务器进行的压测,对CPU和memory的影响都差不多在10%之内。
10.2. Collector的可扩展性
Collector的可扩展性,使得能够水平扩展以便支持大规模服务器集群。
Zipkin:
开发zipkin-Server(其实就是提供的开箱即用包),zipkin-agent与zipkin-Server通过http或者MQ进行通信,http通信会对正常的访问造成影响,所以还是推荐基于mq异步方式通信,zipkin-Server通过订阅具体的topic进行消费。这个当然是可以扩展的,多个zipkin-Server实例进行异步消费MQ中的监控信息。
Skywalking:
Skywalking的Collector支持两种部署方式:单机和集群模式。Collector与Agent之间的通信使用了gRPC。
Pinpoint:
同样,Pinpoint也是支持集群和单机部署的。pinpoint agent通过Thrift通信框架,发送链路信息到Collector。
10.3. 全面的调用链路数据分析
全面的调用链路数据分析,提供代码级别的可见性以便轻松定位失败点和瓶颈。
Zipkin:
Zipkin的链路监控粒度相对没有那么细,从上图可以看到调用链中具体到接口级别,再进一步的调用信息并未涉及。
Skywalking:
Skywalking 还支持20+的中间件、框架、类库,比如:主流的Dubbo、Okhttp,还有DB和消息中间件。上图Skywalking链路调用分析截取的比较简单,网关调用user服务,由于支持众多的中间件,所以Skywalking链路调用分析比Zipkin完备些。
Pinpoint:
Pinpoint应该是这三种APM组件中,数据分析最为完备的组件。提供代码级别的可见性以便轻松定位失败点和瓶颈,上图可以看到对于执行的SQL语句,都进行了记录。还可以配置报警规则等,设置每个应用对应的负责人,根据配置的规则报警,支持的中间件和框架也比较完备。
10.4. 对于开发透明,容易开关
对于开发透明,容易开关,添加新功能而无需修改代码,容易启用或者禁用。我们期望功能可以不修改代码就工作并希望得到代码级别的可见性。
对于这一点,Zipkin使用修改过的类库和它自己的容器(Finagle)来提供分布式事务跟踪的功能。但是,它要求在需要时修改代码。Skywalking和Pinpoint都是基于字节码增强的方式,开发人员不需要修改代码,并且可以收集到更多精确的数据因为有字节码中的更多信息。
11. 参考资料
《分布式链路追踪实战 》
《Spring Cloud 原理与实战 》
https://mp.weixin.qq.com/s/tzaSnz-8g62bGp_frKdbow
https://mp.weixin.qq.com/s/ZfyTKg22p_QFJwZcoJUTzw