写点什么

APM 建设踩了哪些坑?去哪儿旅行分布式链路追踪系统实践

  • 2023-11-17
    浙江
  • 本文字数:8460 字

    阅读完需:约 28 分钟

# 一分钟精华速览 #

分布式链路追踪系统在企业的 APM 体系中扮演着重要的角色。本文分享了去哪儿旅行构建分布式链路追踪系统的实践经验。从 APM 整体架构设计入手,讲述了日志收集、Kafka 传输和 Flink 任务处理等环节的性能优化实践和踩坑经验。

同时,作者结合丰富的分布式系统架构经验,探讨了 APM 系统和 Trace 数据的价值。通过阅读本文,你将了解到去哪儿旅行在构建 APM 体系中所面临的挑战,并学习如何应对这些挑战,实现更高效的性能监控和管理。

作者介绍



 去哪儿旅行基础平台架构师——王 鹏 

TakinTalks 稳定性社区专家团成员。毕业于大连理工大学,10 年以上大型分布式基础架构经验,专注于大型分布式基础架构和大数据处理领域。曾就职于 58 集团,主要负责 58 到家基础架构工作。后进入去哪儿旅行,负责分布式链路追踪系统的建设以及 APM 体系的搭建。在大数据、高并发的场景有丰富的经验。


温馨提醒:本文约 7000 字,预计花费 10 分钟阅读。


「TakinTalks 稳定性社区」公众号后台回复 “交流” 进入读者交流群;回复“1012”获取课件资料;


背景

APM 并不是一个新的概念。从 2012 年到 2016 年,市场上涌现了许多开源的 APM 组件,如 SkyWalking、Jaeger 等。可以说,在过去几年中,随着技术的发展和系统的复杂性增加,分布式链路追踪系统和应用性能管理(APM)已成为许多公司不可或缺的工具。

大多数公司都会根据其技术栈和业务体系来构建自己的 APM 体系和分布式链路追踪系统。以去哪儿旅行为例,我们的主要业务涉及搜索查询和电子商务交易等领域。由于业务体系的不同,在技术选择、方案设计方面可能存在一些差异。

在构建和应用 APM 体系的过程中,去哪儿遇到了许多挑战。例如,在日志收集、Kafka 传输以及 Flink 任务处理等环节遇到了一些问题。这次会分享如何克服这些性能瓶颈,以及在这个过程中积累的经验和教训,希望这些经验对其他公司在解决类似问题时能够有所帮助。

一、APM 整体架构是如何设计的?

作为一个 OTA 交易平台,去哪儿旅行的业务系统分为搜索和下单交易部分。从流量来看不会特别大,OTA 的交易量无法与零售电商交易相比。因此,去哪儿在构建 APM 体系上与其他电商公司可能存在一些不同。例如,大多数公司 APM 的 Trace 部分采用采样策略,但对我们来说,每一次请求、每一次查询或者每一次交易,尤其是交易链路的 Trace 非常少,每天几百万的交易量,产生的 Trace 不会太多。因此,最终采用了交易链路全采样的技术方案。

此外,从 2020 年开始,全公司技术栈之前的虚拟机模式转向了云原生的开发架构。在这三年中,从应用层、开发层和容器层,都已经完成了迁移。在迁移完成后,需要将可观测性的相关体系迁移到云上,或者说适配云原生的开发环境。

在构建 APM 体系的早期,由于众多开源技术尚未出现,进行了大量的自研工作。例如,早期我们依赖自研的 Java 中间件记录 Trace。但现在,开源社区已经提供了许多优秀的方案,可以根据公司环境、阶段和技术路线做出合理的选择。

经过技术分析与选型后,去哪儿旅行的 APM 体系采用插桩 Agent 模式和中间件埋点并行的方式(如图)。其中,红色部分是重点改造部分。



(去哪儿旅行 APM 体系整体架构)

首先,左上部分主要是如何记录和获取 Trace 信息。部分中间件以硬编码的方式获取 Trace 数据,另外,一些开源的第三方组件通过 Agent 模式获取了 Trace。

其次,关于如何收集这些数据,去哪儿旅行采用了开源的 Apache Flume 日志收集组件,并对其进行了一系列改造。改造的原因是原生组件并未提供如配置管理功能、限流控制等一些必要的功能。

第三,处理层的任务非常繁重,选择了 Flink 作为数据处理任务框架。数据处理不仅需要实时处理 Trace 的详细数据,还需要存储并分析出的 Trace 异常、后置采样、链路分析等等。

最后,Metrics 部分则是一个标准的收集和处理流程,通过 Statsd 将数据存储到时序 DB,最终有 Prometheus + Grafana 去展示。

总的来说,去哪儿旅行的 APM 体系整体架构设计结合了开源技术和自研成果,以实现更高效的性能监控和管理。

二、遇到了哪些“坑”?

2.1  日志收集组件的性能瓶颈优化

在 Trace 数据收集过程中,确实存在许多挑战。由于许多业务和监控系统基于 Trace 和 Metrics 来判断是否发出告警,因此这种收集需要比离线日志更实时。然而,这也意味着它们需要在保持高性能的同时,保证不会影响宿主机的性能,这是一个非常大的挑战。

2.1.1 问题表现:Trace 数据中断



在对系统进行改造前,发现有大量的 Trace 出现了数据中断的问题。这种问题表现为 Trace 的某些部分(比如 Span ID 为 1.4 和 1.4.2 的部分)突然消失,而后面的部分(比如 Span ID 为 1.5)又突然出现。这种情况可能由两种原因导致,一种是中间件里没有这部分数据,另一种是中间件里有数据但是没有传送过来。无论是哪种原因,都需要对系统进行深入地分析。

2.1.2 抽样分析:日志组件和配置问题

尝试把以上问题拆解成可视化的指标,以便于理解和处理它。首先,查看了整个 Trace 的问题比例,可以通过图上的监控看到 Trace 中断率达到了惊人的 70%~80%。这个结果说明大部分的 Trace 数据都存在问题。



为了进一步确定问题的原因,我们对 100 个问题数据进行了抽样分析。分析结果显示,有一半的问题是日志收集组件的问题,也就是 Qflume 组件的问题;另一部分问题则是配置问题,即没有正确配置数据收集,导致数据没有被收集到。除此之外,还存在一些其他的问题。

对于配置问题,可以通过统一刷新配置来解决。

而对于日志收集组件的问题,则需要进行更深入的分析和改造。首先需要对其内部工作流程进行了解。从左到右,这个流程图展示了日志处理过程。



在最左侧,业务应用通过异步队列将日志输出到磁盘文件。这个队列的长度是有限的,用来控制内存占用。如果队列满了,新的日志就会被丢弃。这就是日志丢失的一个原因。

还有其他两个瓶颈——一个是系统通过单线程同步读取日志,这个读取速度跟不上日志生成的速度。另一个是系统使用同步发送 Kafka 来传输日志。

2.1.3 优化效果

为了解决这些问题,采取了两个措施。首先,增加了异步队列的长度,但是也不能让它过长,以免消耗过多的内存。其次,将单线程读取改为批量读取,每次读取一个批次的日志。此外,还将同步发送改为 Kafka 异步发送,发送完一批日志后立马发送下一批,这样可以大大的提高吞吐量。



这样优化后,分钟级传输的数据量有了显著的提升,可以达到每分钟 80 亿甚至 100 亿条数据量。

2.1.4 踩坑点 1:传输失败率大幅提高

问题描述:

尽管解决了日志组件的性能问题,可以快速地读取和发送数据。然而,随之而来的问题是 Kafka 的数据量非常不稳定,呈现出明显的波动。



原因分析:

在查看相关数据后,发现了一个严重问题:频繁的内存溢出(OOM)。这个问题的原因在于,优化后的日志组件以批量的方式读取数据,这会占用大量的内存。如果内存不足,就会发生 OOM。



一种解决这个问题的方法是增大内存,但这并不是一个长久之计,因为资源有限,不能无限制地增加内存。另外,Trace 日志的实时性要求并不像业务那么高,晚几秒甚至十几秒都是可以接受的,只要数据最终能够传输过去就可以。

如何解决?

解决思路是在保证高性能的前提下,对数据传输进行限流,让日志组件在一个可控的内存使用范围内高效的工作。



限流方案包括设定时间窗口和对单条日志大小做限制。在设定时间窗口的方法中,时间窗口是滑动时间窗口,也可以是一条日志的处理时间。比如设置时间窗口为一秒、两秒或五秒,然后规定在这个时间窗口内,限制数据传输的大小,比如不能超过 200M。

尽管设置了时间窗口,但日志组件仍可能会出现 OOM 的问题。原因在于有些日志非常大,一条日志就有几兆甚至几十兆。这种情况虽然不合理,但却确实存在。如果遇到这样的日志传输过来,很可能会导致服务瘫痪。因此,团队还需要对单条日志的大小进行限制,如果日志过长,就需要进行截断处理。

另外,如果出现 OOM,团队也需要进行断点续传的工作。不能重新传输已经传输过的 Trace,因为这样会造成大量的资源浪费,且重复传输本身也是有问题的。

通过这些限制,就可以保证堆内存的使用不会超过限制,同时保持日志组件的高性能。

2.1.5 踩坑点 2:接口耗时增长,吞吐量下降

问题描述:

在解决日志组件内存溢出问题之后,又遇到了新的挑战:业务线反馈接口耗时突然大幅上升,吞吐量大幅下降,甚至有些业务应用被操作系统直接终止,导致故障出现。这个问题在一开始是令人困惑的,因为已经解决了 OOM 的问题,并且对内存使用进行了严格的限制。怎会出现影响业务系统的情况呢?

原因分析:

在排查问题时,发现有问题的服务器上,CPU 利用率非常高,达到 197%,内存的使用率也非常高,几乎已经没有剩余空间。那么,这是由什么原因引起的呢?回顾之前做的优化工作,发现在优化日志传输的步骤中,将 Kafka 的发送操作变成了异步发送。而在异步发送过程中,会大量占用堆外内存。

之前针对堆内内存做了限制,但是对堆外内存并没有做限制。如果 Kafka 大量发送数据,但是由于某些原因传输不过来,那么这些数据就需要放在堆外内存中等待发送。这就是导致内存使用率高涨,最后操作系统终止业务进程。

如何解决?

那么,如何限制堆外内存的大小呢?或者说,如何限制进程使用的内存空间呢?

从 Docker 这个容器技术中找到了灵感。熟悉 Docker 都知道,Docker 容器中使用的资源是固定的,包括磁盘空间、CPU 以及内存等,不能超过容器在初始时分配的大小。那么,Docker 是如何实现这个功能的呢?

这就涉及到 CGroup 技术,也就是 Linux 内核提供的控制组(Control Group)技术。CGroup 技术主要用于限制和隔离进程组应用的物理资源,简单来说,可以通过 CGroup 技术将硬件资源切分成很多块,并对每一块资源设定使用限制,进程和其产生的子进程都不能超过这个限制。

通过 CGroup 技术,限制进程的堆外内存使用和 CPU 使用,保证它们都在一个合理的范围之内。

2.1.6 优化效果

CGroup 进行资源限制后,系统的效率得到了显著提升。Root 失败率从原来的 80%降低到了 20%,优化效果十分明显。



2.2  高并发下 Kafka 集群传输优化

在解决了日志收集问题,资源使用得到了限制之后,大量的日志被发送到了 Kafka 集群,然后 Kafka 集群再将日志传输给 Flink 任务进行处理。这就引出了新的问题:如何保证 Kafka 集群能够高效稳定地传输日志?

2.2.1 问题表现

随着日志收集客户端在全公司范围内的推广,大约有 1 万多个实例部署,Kafka 集群开始出现了不稳定的情况。Kafka 集群会出现大量的连接失败,整个的数据接收量和发送量都会急剧下降。在这期间并没有对集群做任何改动,收集组件也没有做任何改动,但是却频繁出现这种不稳定问题。



2.2.2 问题分析

通过查看了 Kafka 集群的监控,发现网络空闲连接和线程数急剧下降。这可能是导致 Kafka 集群连接失败,数据传输量下降的主要原因。



在 Kafka 的架构中,客户端首先与网络接收线程组进行连接,发送日志数据。网络接收线程组在接收到数据后,将任务转发给 RequestQueue 进行处理。



Processor 处理器的主要工作是将请求放入请求通道队列(RequestChannel)。理论上,处理器的空闲量应该较大,因为其工作并不复杂,仅仅是进行内存操作,将网络接收的任务提交到队列。然而,监控数据却显示处理器的空闲数急剧下降,变得非常繁忙,并没有足够的空闲链接去处理网络请求。

进一步分析,发现请求处理器(KafkaReuestHandler)从请求通道队列中取出任务进行处理,主要是将请求写入磁盘。如果请求处理器处理不过来,请求通道队列的数据就会逐渐增多。当队列满时,新的请求无法进入,导致网络连接空闲数急剧下降。

综合以上所述,问题可能出在 Kafka 的请求处理器这一环节。主要可能是内存不足或者刷盘速度不够快。进一步检查后,发现确实有些机器的内存不够,有些机器的刷盘速度有问题,甚至有些磁盘已经损坏,导致部分机器的性能急剧下降。

2.2.3 优化效果

将有问题的机器从集群中移除,并增大了内存。经过这些优化,整个 Kafka 集群恢复了正常状态。



(正常状态下的集群)

优化后的效果明显,Kafka 集群的收发状态保持在一个正常的水平。在约 1 分钟的时间里,可以收发 1.7 亿多条数据。而且,根据业务的波峰波谷,数据的收发呈现出稳定的状态,不再出现以前的陡增陡降的情况。

2.3  百万级 QPS 数据处理任务优化

在数据处理过程中,任务这一环节是最核心的部分。在对比了 Spark 和 Flink 的流式处理后,发现后者更适合 Trace 场景。Flink 任务本身也比较复杂,如何能保证它的高可靠和高性能?

2.3.1 数据处理任务

数据存储量的 QPS400w 左右,峰值 1000w。数据进入任务首先进行反序列化,之后开始实时计算业务线的拓扑、异常 Trace 拓扑、Metrics 和 Trace 关联关系等。



另一个重要的任务是存储。每秒几百 GB 的日志存储到存储介质中,有 HBase、ES、ClickHouse 等存储介质。这些存储还有一些关联数据,这些关联数据需要拆分。例如,有很多 Metrics 和 Trace 的关联数据,需要先将它们解析出来,然后做分布式的存储,将它们存入数据库。这些计算都是在 Flink 的任务里面完成的。

2.3.2 Flink 任务拆分



上图是一个大任务,整个数据打散后分给不同的子任务去处理。这种方式有一个问题,如果某个子任务处理速度较慢,会产生一些背压。背压会继续向上反映到总的任务分发环节。分发环节处理速度慢,所有任务的处理速度也会变慢。所以,一个小任务出问题,就导致整个链路出问题。



因此,将一些不关联的 Trace 任务进行拆分,而不是让它们耦合在一起。这样做可以大大降低问题的发生概率。

2.3.3 背压如何解决?

背压(Back Pressure)是流控制中的一种策略,主要用于保护系统在高负载情况下的稳定性。当下游处理速度跟不上,上游数据输入的速度时,就会发生背压,这就像水管出水口被堵住,压力太大后就可能会导致水管崩裂。

解决背压可以从以下几个方面着手:

  1. 观察 Flink 任务中子任务的消费是否均匀。Flink 任务会被分解为子任务,子任务会被分配到不同的机器上执行。如果某些高耗 CPU 或者高耗 IO 的任务集中在同一台机器,会导致该机器的处理能力不足,从而影响整个任务的处理速度。因此,需要关注子任务的消费均匀性,并尝试调整资源的分配,使其更加均衡。

  2. 关注上下游算子的内存是否充足。如果输出算子的内存不足,可能会导致输入算子的数据无法正常传递。因此,需要通过内存监控,并合理设置不同算子的内存大小。

  3. 尽量使用内存的 Map 来替代 Window。虽然 Window 可以保证数据的完整性,但在某些情况下,并不需要这么强的一致性,更多的时候只是对数据缓存,使用内存的 Map 可以极大的节省内存消耗。

  4. 善用 Shared Group。Filter 一定小心下游算子的拥堵导致全面的拥堵,Shared Group 可以将频繁进行网络传输的算子放到一个 JVM 内,这样可以极大的节省网络资源和计算资源。

2.3.4 优化效果

通过优化背压问题,平均写入达到 400 万 QPS,平均写入耗时在 600ms 左右,这是一个非常不错的性能表现。



三、如何看到 APM 和 Trace 数据的价值?

3.1  APM 系统有哪些作用?

当系统经过几年的发展,可能会变得杂乱无章,各个系统之间的联系混乱不堪。在这样的情况下,可能对系统的运行逻辑一头雾水,更别说从这个混乱的拓扑中找出问题所在。而 APM 系统的作用就是帮助理清这些混乱的联系,然后指出可能出现问题的地方。与仅有监控系统相比,APM 系统可以更清晰地定位问题所在,这是一个巨大的价值点。



(APM 系统让查问题变得更简单)

3.2  Trace 与 Metrics 怎么关联

在业界很多并没有关注 Metrics 和 Trace 的关联关系,常见的做法是根据时间进行随机关联。然而,这样的关联性并不强。因此,根据去哪儿的业务情况进行了一些改进。

针对三类指标进行分析:一类是 Time 类的指标,一类是 Count 类的指标,另一类是 Rate 类的指标。

1、Time 类指标:假如一种操作的耗时是最长的,那么这种操作肯定是存在问题的。所以这类指标我们只取 Top10 即可。

2、Count 类指标:处理方式会相对复杂一些,因为它涉及到的业务概念较多。采用了随机策略,比如在记录某个指标时,无法将所有的 Metrics 与 Trace 的关联关系全部记录下来,只需要随机抽取一部分进行记录就可以了。另外一种策略是:报警策略,假如某个指标出现报警,那么在这段时间内这个指标存在问题的概率是非常大的,所以会提高采样率,为了更可能命中存在问题的 Trace。还有一种策略是:关键词策略,比如定义了一些业务异常,这种情况下,会进行全采样,因为这种 Trace 的价值非常大,例如 ERRO FAILED EXCEPTION 等关键词。

3、Rate 类指标:处理起来会更为困难。因为 Rate 类指标往往是两个数的比值,比如成功率和失败率。这些指标上升还好,但是如果下降,那么可能是因为没有 Trace 关联。所以对于 Rate 类指标,只能关注上升的情况。

在实际操作中,发现研发同学非常喜欢使用这个功能,因为大家只要加报警就能找到问题的 Trace。以前要找这个问题的 Trace 就需要在日志和代码中反复查找,非常费劲。而有了这个功能,他们只需要设置一个报警,只要出现问题,他们就能找到对应的 Trace,这大大节省了排查问题的时间。

3.3  Trace 作为基础底座如何应用


作为一个基础功能,Trace 在很多公司都有广泛的应用,可以在其基础上进行许多工作。基于 Trace 的高连通性,通过上下文传递来降低整个调用量。例如,在用户中心的接口中,调用量通常非常大。如果不合理在上下层或同级之间进行多次调用,用户中心的调用量就会指数级增加。

如果能够获取 Trace 的上下文,那么在短时间内,一个 Trace 内的用户数据的变化可能非常小。绝大部分数据是固定的,例如 User ID。通过传递 Trace 的上下文,可以指数级别降低其调用量。在一个 Trace 内,调用次数可以被限制在几百次以内。如果通过上下文传递,只需要调用一次就够了。这种优化的方法在实际应用中是可行的,还可以结合其他的技术,比如将大量访问频繁的数据存储在缓存中,以降低信息在传输层的大小。

另外,Trace 的高连通性对于混沌工程和全链路压力测试非常重要。全链路压力测试是基于 Trace 的连通性的。在进行灰度环境的压力测试时,不允许将整个压力施加到线上,以免造成故障。如何确保只在灰度链路内进行测试而不影响线上环境?这就是基于 Trace 的高连通性。高连通性意味着的链路拓扑应该是一个全连通集的子集,不允许出现调用跳转到线上的情况,如果出现则表明压力测试的拓扑是有问题,需要终止。

此外,如果出现无法覆盖整个 Trace 的情况,可以在网络层面进行拦截。在整个网络拓扑中,不允许请求外部的线上机器或服务。如果有请求到线上机器或服务的情况,可以进行拦截,终止压力测试。

3.4  Trace 数据有哪些价值

基于 Trace 的连通性,通过分析 Trace 数据,可以了解整个链路的性能瓶颈和热点,从而给业务线提出优化建议。

  • 基于超时时间的链路拓扑分析,发现配置不合理的点,是否有环装调用

在分布式服务中,通常使用 RPC 框架(如 Double 或 GRPC)进行通信。在早期,这些框架需要手动配置节点的超时时间。例如,上游的超时配置为 1 秒,下游的超时配置为 3 秒,这是一个不合理的配置,因为上游已经在 1 秒之后超时了,下游的超时时间设置并没有意义。这种不合理的配置可能出现在分布式系统中的许多节点中,难以寻找和识别。

通过分析每个请求的耗时数据,可以将 RPC 框架的超时时间拿出来,并对上下游配置进行分析,从而发现不合理的配置。这样,可以确定哪些超时配置是不合理的,以前很难解决的问题现在可以通过链路分析发现。一旦我们找到这些点,可以避免由于超时配置不合理导致的故障。

  • 基于请求耗时占比,分析性能瓶颈

通常情况下,某个请求中会有一个或多个函数的耗时占比非常高。


通过观察图中的 Span ID,某个请求的耗时占比超过了 48%。如果某个函数的耗时占比过大,这可能是由于它本身的执行速度较慢,或者它的实现方式存在问题。这个函数可能成为整个链路的瓶颈,影响整体性能。在这种情况下,可以考虑将该函数改为异步操作,或者拆分为并行的请求,以帮助业务线提高性能。

  • 基于同层并发请求重复次数占比,分析代码不合理调用。

有时候我们会在同层中重复调用某个接口,例如在某个地方调用了应用中心,又在另一个地方调用了应用中心,然后在下一层又调用了应用中心,这种重复调用是完全不合理的。



在同层的情况下,这样的调用可能会达到几十次甚至上百次,这显然是没有必要的。可能有些同学会说,只是把编写好的代码复制粘贴过来,没有去分析它的逻辑,长此以往,这些重复调用会导致系统性能下降。

这些是我简单列举的一些 Trace 数据有价值的点,在实际过程中,它的应用价值远不止这些。也欢迎大家开放探讨和交流。

五、总结展望

在构建整个 APM 体系过程中,三个主要组件:日志收集组件、传输链路治理以及 Flink 任务性能优化。日志收集组件和传输链路治理主要解决日志大流量和并发的问题。在日志组件中,关注内部和外部内存的限制。在传输层,关注任务调度和集群性能优化。

最后,分析了 APM 系统的价值和意义。每家企业都建设自己的 APM 系统,同时更需要深入挖掘其价值。APM 系统的真正意义在于能够通过数据客观深入了解系统的性能。通过 APM 系统,可以优化系统的性能、提高用户体验、减少故障和降低潜在风险。

Q&A

1、直播迁移到 Docker 和使用 CGroup 技术的成本怎么评估的?

2、如何及时发现未被监控的指标项,避免未被观测的指标突变引发故障?

3、接口偶发性超时,调用链只能看到超时接口名称,看不到内部方法,无法定位根因,也难以复现,怎么办?

以上问题答案,欢迎点击“阅读全文”,观看完整版解答!



发布于: 刚刚阅读数: 4
用户头像

公众号:TakinTalks稳定性社区 2020-03-03 加入

聚焦SRE和故障的技术交流社区

评论

发布
暂无评论
APM建设踩了哪些坑?去哪儿旅行分布式链路追踪系统实践_TakinTalks稳定性社区_InfoQ写作社区