节约资源、提升性能,字节跳动超大规模 Metrics 数据采集的优化之道

新闻资讯   2023-07-02 10:17   74   0  

编辑 | 李忠良
嘉宾 | 刘浩杨
在 ArchSummit 2023 北京站上,字节跳动刘浩杨分享了《字节跳动超大规模 Metrics 数据采集的实践和探索》,他从字节跳动可观性平台的建设入手,讨论了字节跳动数据采集所面临的问题和挑战,介绍了在数据采集方面的内核优化和工程实践,为许多在数据采集方面的企业提供了可落地的参考思路,本文为分享文章整理~

字节跳动可观性平台的构建思路有两个主要方向。首先是 三位一体,“三位”指统一采集和分析指标、跟踪和日志数据,类似于融合分析的概念。除了这三类数据外,我们还采集事件和翻译数据,并进行同步使用。“一体”是指在一个平台上处理整个可观性的数据,并建设统一的引擎来处理日志、跟踪和指标数据。

其次是 数据融合处理。我们在不同层面上进行大量的数据采集,涵盖了基础设施层、容器层和中心应用业务层。例如使用 Trace ID 在业务应用和中间件层进行串联。我们对 Log ID 和 Trace ID 进查询之后,串联搜索和分析。

字节跳动可观性平台的架构

字节跳动可观性平台的架构,采用水平分层的构建思路。

最底层是数据底座或数据引擎,它包括统一采集、数据传输和清洗过程。针对数据,我们使用两种存储,即指标存储和日志存储。

在上层是分布式查询引擎,再上层则是我们定义的 APM 引擎层。该引擎类似于中间件,提供可观性所需的原子能力,如监控、报警、日志和链路等。这些能力按领域划分并封装为原子能力。在这些原子能力之上是我们的产品层。

目前,我们在三个方向上开发了多个产品。第一个方向是云产品观测,因为字节跳动有自己的云平台,如火山引擎。我们在该平台上开发了大量云产品的观测能力,包括存储网络、RDS 产品和云原生的 SaaS 平台等;第二个方向是开发运维,包括应用观测和日志分析,以及业务监控和事件等。我们还针对业务场景对链路进行了优化,通过链路数据进行性能排查和生成拓扑图,并在此基础上进行了架构治理工作,如强弱依赖和业务拓扑。

超大规模 Metrics 数据采集

在这样的情况下,我们面临着巨大的技术挑战。

首先,我们的采集 Agent 部署数量约为百万级别,而代理和 SDK 每秒接收的数据量达到了千亿级别。在代理和 SDK 层面进行聚合后,数据被写入后端的 TSDB 存储,目前每秒写入量超过 50 亿,TSDB 的查询 QPS 达到了 10 万级别。

我们支持的业务场景涵盖了字节跳动的 ToC 和 ToB 业务。在字节跳动内部,我们目前维护着数十万个服务集群,大约接近 20 万。微服务实例数量达到千万级别,这意味着我们需要观测数以千万计的微服务容器实例,从而产生大量数据。

另外,字节跳动使用了多种开发语言。我们的在线业务主要使用 GoLang,还有一些使用 Python,而对于对数据延迟要求较低的业务则 C++ 技术。此外,一些业务使用 Java 进行开发,还有其他一些相关的语言和 RPC 框架,大约有十几种。在这样的规模下,每天进行数万次的服务发布,每次变更都会产生大量观测数据。

如何支撑这样的规模呢?我们内部自研了一个实时数据库,称为 Byte TSDB,其架构与业界常见的实时数据库相似。前端是采集端,由 SDK 或代理将数据发送给一个全局的生产者,负责机房路由和租户路由。后端是我们多租户的 TSDB 架构。

与业界的其他 TSDB 相比,Byte TSDB 可能存在一些显著差异。在我们的 TSDB 中,组件比较多。首先,我们使用 MQ 进行数据写入,其中有两个消费链路。

第一个消费链路从 MQ 消费 Meta 数据,并将其放入热层。在热层中,我们默认缓存过去 28 小时的数据。这是因为我们知道在指标监测中,大部分查询通常关注最近几个小时的 CPU 和 QPS 等指标变化。因此,我们在热层使用纯内存方案来支持查询。

第二个消费链路是从 MQ 消费数据后进行降级,并将降级后的数据写入 MQ 的另一个 Topic。然后,一个流式消费组件负责从 MQ 读取数据,并将其存入冷层。在冷层中,我们设计了用于存储 Metrics 的格式,并将数据写入 SDK 和 HDFS 上。对于冷层,我们认为只要存储足够,数据的存储时间可以是无限的。

对于冷存储,它主要面向的场景不涉及新业务。比如,如果需要查询最近几个小时的指标,我们可能需要对 Metrics 中的数据进行数据仓库分析或离线分析。在冷层,我们可以支持内部的一些 BI 系统,以生成长期报表等。

数据库最下面是控制面,它包括两个组件,一个是配置服务器 (Config Server),用户的配置信息将存储在其中;另一个是协调器 (Coordinator),负责协调数据写入、路由信息和服务发现等组件之间的共享信息。

最后,查询层采用了分布式查询模型。我们在国内部署了四个机房,为我们的 TSDB 架构提供了一个跨机房查询的界面。这样用户可以通过跨机房查询来检索所需的数据,无论其位于哪个机房。不过,今天我们不会涉及更多与查询相关的细节。

字节的 TSDB 与社区提供的数据类型基本相同,包括 Counter、Meter、Histogram 和 Summary 等主要类型。我们提供了 SDK 来进行数据写入,并为几种主流语言提供了实现。在查询方面,我们目前兼容 OpenTSDB 的查询语法,并可以通过像 Grafana、Bosun 或 OpenAPI 等方式进行业务接入。

由于字节内部使用的语言众多,我们的 TSDB 最初支持 OpenTSDB 的数据协议。因此,在早期阶段,我们的 SDK 的功能相对简单,只是将用户的数据序列化成 TSDB 的文本格式,然后通过一个 Metrics Agent 进行聚合,最后发送到后端。

因此,轻量级的 SDK 没有执行太多的任务,类似于现在社区中的数据库 SDK,它只是负责数据采集和数据包转换。数据采集是在 Agent 上进行的,Agent 在 SDK 将数据发送过来后会进行 30 秒的聚合,然后通过 POS 方式进行上报。

基于这套架构,可以看出它的功能有一定的限制。第一个问题是是否支持灵活配置,因为现在大家关注秒级打点或者对数据有其他更多的配置需求,但在这套架构下是不支持的;另一个问题是,很多场景并不适用于 SDK 进行数据写入,可能需要进行主动采集。在这种情况下,许多业务团队会自行开发一个 Agent,并使用我们提供的 SDK 进行数据采集。但这种方式在字节的整体视角下会导致维护多个 Agent 的情况,这限制了场景的适用性。

另外,我们发现在 SDK 进行序列化时会引发一些问题。举个例子,我们提到一个极端情况,有一个使用 Flink 进行了一个离线任务,它的存储量大约是每个任务管理器每秒处理 200 万的数据。当它使用我们的 Java SDK 埋点后,发现 CPU 消耗非常高。经过一些分析,发现 Metrics SDK 中的打点消耗占了管理器进程的 36%。这个比例实际上非常高。在他们的机器上,仅仅进行打点,在这样的数据量下,就占用了每个进程超过一核 CPU。

此外,我们将所有的聚合工作都放在 Agent 上会带来一些问题。在社区中,很多方案,比如 Telegraph,允许在 Agent 上进行聚合,但在一些小规模的场景下,这是完全没有问题的。但在我们的场景中,我们的装机量已经达到了 100 万台以上。

上图可以看到在我们目前的打点量,比如从 SDK 到 Agent,每秒处理的打点数量超过 1000 亿,几乎每台机器上的 Agent 都处于负载状态。

海量 Metric 数据采集优化实践

为了解决引入的性能、成本和场景问题,我们采取了以下技术措施。首先,针对后端的 TSDB,我们每秒处理 50 亿个数据点,每天产生的指标数据大约有 4PB。为了优化数据量和成本,我们引入了多值 Metric 的概念。在单值情况下,我们默认每个指标打 10 个点。而在多值情况下,我们只保存一个数据点,但该数据点包含了 10 个字段,这带来了几个好处。

首先,这样做可以优化存储成本,通过标签的复用,我们可以将多个时间序列的数据点存储在一起,从而大幅降低存储成本;其次,在进行多字段查询时,我们经常需要同时查看 P99 和 P95 等延迟指标,如果是单值情况下,需要发起两次查询来获取数据。而在多值情况下,我们只需要进行一次查询,就可以获取到多个数据点的结果。

此外,多值还可以支持字段之间的计算。在单值情况下,例如 Prometheus 目前也是单值的,它需要先扫描所需的字段,然后在内存中进行二次计算。然而,在分布式查询的场景下,涉及到大量数据时,我们会从多个工作节点上导出数据,然后在一个节点上进行计算。在多值情况下,我们可以在单个工作节点内完成查询和计算,这对查询性能有很大优化。

当然,我们也设计了一个私有的序列化协议,他与普通的缓冲区或文本协议不同。传统协议存在一些问题,如压缩率低以及在处理整个 Json 数据时,需要将所有数据打包在一起或解包后逐个读取字段。相比之下,我们的协议具有流式编码和解码的优势。例如,在后端进行统计时,我们可以流式地编码和解码,以统计包中序列的数量以及 Metric 数据的大小。

在 SDK 方面,我们从整体架构的角度出发,将之前提到的每 30 秒的 Agent 聚合能力前置到了 SDK 上。首先,它减少了序列化的量。现在,我们在聚合后只需处理一个序列的点,然后进行序列化和发送;另外,SDK 还提供了基于业务进程的自定义打点精度配置。我们支持秒级监控,并且如果有更高的打点精度要求,也能很好地支持。此外,通过我们的优化措施,成功降低了 Agent 的负载。

然而,我们也意识到,当推广新的架构和新的 SDK 到业务线上时,会面临一些挑战。为了减少 Agent 使用的资源,我们对原有的聚合链路进行了许多优化工作。这些优化包括对租户数据的隔离,通过将不同租户的数据存放在不同的空间中,来减少不同业务之间的打点故障。

此外,我们还进行了一些 C++ 的优化工作。例如,在解析标签后,我们将字符串的处理方式改为使用池化方案,并通过使用单指令多数据指令集进行加速。对于压缩算法,我们对 Day Lab 和 DSTD 进行了权衡。

最后,我们在新的硬件上进行了探索。除了常见的 CPU 和 GPU 之外,我们还引入了一种名为 DPU(数据处理单元)的新型硬件。DPU 在字节内部的某些业务场景中得到了应用。我们与 DPU 团队合作,针对这种硬件场景对我们的 Agent 和 C++ 代码进行相关优化工作。

回到 SDK 层面,我们想将 SDK 内部进行聚合操作。我们采用了传统的分层设计方法,提供了 Low level API,允许声明 Counter 类型并进行打点操作。于此同时,我们还提供了 High levelAPI,针对 Java 和 Go 等语言,基于泛型实现了结构化的声音打点功能。我们还考虑兼容第三方集成,如目前流行 Java Spring Boot 内置的 Mac Meta。在内核中,我们采用了基于 Pipeline 的处理流程。每个打点生成一个数据序列点,我们可以进行自定义处理,例如标签重写、添加或删除相关标签。

此外,我们还有一个聚合器和服务管理模块,除了数据处理的 Pipeline 外,SDK 还包括租户管理、自监控和与配置中心对接的模块等。

下面介绍我们在 SDK 中用于序列聚合的数据结构。我们为什么要选择名为 KDTree 的数据结构呢?

KDTree 是一种对于多维度检索非常友好的数据结构,也是一种平衡的二叉树。对于每个 Metrics,都会生成 KDTree 结构。在每个结构中,可以看到根节点是该序列的 Tag value 的哈希,作为根节点。底下的子节点按照排序后的 Tag 进行分层。因此,当查询一个序列时,不需要进行更多的比较,只需在节点上比较 Tag value 的字符串内存即可。

其次是 Signing Collector,它是一种针对数据结构的优化。当层级过多时,查询性能会下降,且无法翻转不平衡的情况。我们对该数据结构进行了两种优化。首先是设置(setting),每个 Metrics 生成 1024 个 KDTree,超过 1024 后,根据哈希选择一个树,并将序列值乘以相应的权重。另一种优化是在发生较大倾斜时,将其退化为哈希 Map。

基于这些优化,我们设计了多段式的 Metrics 索引。举个例子,假设我们统计一个 RPC 框架或 HTTP 框架中每个 SCP 请求的几个值。我们可能使用 Counter 类型的 Metrics 来统计 QPS,使用 summary 数据结构来统计最新延迟,以及使用 Counter 来统计错误率。

在这种情况下,每个请求需要打三个点。如果我们使用类似 Prometheus 的系统,它会生成一组 Tag 对象,并对这三个 Metrics 分别进行打点。因此,在查询过去的数据时,需要对三个 Metrics 进行三次查询。

为了解决这个问题,我们设计了多段式索引。在 Metrics 中,有 prefix name 和 suffix name 两个部分,前两个部分用于定位到具体的 Metrics,而 suffix 的作用是在节点树上挂载一组值,它们可以通过 suffix 区分。在之前提到的场景中,一个请求要打三个点,我们可以仅通过内存中的 Tag 索引进行一次查询。查询到该节点后,我们可以对内存中存储的这三个点的值进行原子操作。这样的优化对于 SDK 给用户代码带来了显著的打点延时提升。

另外,还有一个高级结构化 API 示例,在声明 Tag 时,可以将 Tag 声明为 Go 的结构体,并使用标签指定 Tag 的名称和是否有默认值。在下面的 metric set 部分,可以基于泛型的方式声明一个 Counter 这样可以避免一些问题,例如对于某个指标,在定义 key 时写了一组字符串,然后每次打点时, 还需要确保这组值与 key 的顺序对应。

当我们基于结构化进行打点时,我们只需要创建一个标签对象,并将其传递给 API 中这样我们就不需要在各处维护字符串了。这样我们可以预先解析标签的元数据,并对其进行排序。这样,当进行打点时,我们就不需要重新对标签进行排序了。因为在打点时,像 Prometheus 这样的度量系统,对传入的标签进行排序是一个很大的开销,然后再进行哈希查找。我们省去了排序这一步骤。

然后,在将数据放入 SDK 进行聚合之后,可能会带来一些新问题。用户可能会发现某些点无法查询到,并担心这是 SDK 引入的问题,为此,我们设计了一个名为 Series Query Debug Server 的功能。它允许我们在 SDK 收到的某个端口上输入类似于 SQL 的 DSL 查询语句,然后在当前查询时对其内部存储序列进行快照。

完成快照后,我们可以从中提取当前序列中特定度量的标签以及其下包含的序列值,并为用户提供自助调试。我们计划将此功能作为标准功能开放给用户使用。此外,基于这个快照,我们还计划对度量序列进行转储,这样可以了解当前 SDK 或用户进程中那些 Metrics 正在打点,可以将该数据导出,并通过工具进行离线分析。

数据采集优化优化收益

在执行一个 Flink 的一个 ETL( Extract-Transform-Load)任务时,在相同的吞吐量下,通过引入新的匹配 SDK,CPU 的消耗从原来的 36% 降低到了 7% 左右。通常情况下,我们大部分都是在使用在线服务。我们选择了一些在线服务来观察,其中包括通过 Metrics 打点 SDK 带来的高负载和低负载的情况。

在第一行中,原先 SDK 占比 15% 的在线服务,其 QPS 大约为几万。经过优化后,这个占比降低到了 9.5%,大约有 5% 左右的优化。对于一些 QPS 较低的在线服务来说,Metrics 引入的埋点消耗从 5% 降低到了 3%。

实际上,我们可以近似地认为每个在线服务大约可以节约 3% 左右。虽然在单个服务上看起来这个数字并不算大,但是考虑到像字节 Go 有 800 多万个微服务实例的情况,如果所有的服务都升级到新版本的 SDK 后,每个进程内的节约大约为 3%,那整体的资源节省将达到约 24 万核。当然,这个评估可能不太准确,实际上需要在进行优化后才能进行资源的缩容。不过从整体上来看,在海量微服务场景下,这是一个非常可观的收益。

未来展望

接下来,我们将讨论 SDK 上所做的工作。首先,我们将整合采集所有数据的 Metrics track log。因为在最近两年的可观性领域,包括 CCF 底层社区,OpenTelemetry 是最受关注的项目之一。它在一个 SDK 框架下收集 Metrics、track log 和事件数据。我们将进行相应的整合,并考虑如何兼容相关 API 和协议。

其次,我们将支持主动上报数据和主动采集方式。我们提出了一个名为 Widget 的新项目,它负责 Metrics 数据的聚合与透传,包括许多主动采集的能力。主动采集将包括获取主机指标和其他指标等。此外,我们还可以通过 OpenTelemetry Magic SDK 直接通过代理进行数据上报。该代理具备强大的 Pipeline 能力,在用户的业务集群中进行一些边缘计算后。将数据上报到后端的实际数据库。除了将其作为采集器使用之外,我们还将同时在后端的连接网关和流计算消费上使用该代理进行开发。

我们通过开源的方式,在现有业界优秀的 agent 中选择一个合适的进行建设,而不是从零开始构建一个。我列举了一些开源协议友好的选项,如 Phabit、Telegraph、Vector、Message、Collecto。最终,我们选择了阿里他们团队开源的 ilogtail 作为我们的 Agent。我们为 ilogtail 提供了原生的 Metrics 和 trace 协议的支持,并通过代码生成的机制支持了插件功能。这样,我们可以在公司内部建立自己的插件仓库,并在构建过程中将核心项目和插件整合在一起。

我们还计划在 ilogtail 上提供运维控制面的支持,此外,我们还计划与阿里的同学合作,设计类似于 Flink 流计算的 pipeline 设计,包括在其上构建 SQL 引擎。

活动推荐

ArchSummit(深圳站)全球架构师峰会将于 7 月开幕!MySQL 之父、AWS 技术大咖、华润雪花啤酒 CDO、科大讯飞 AI 研究院副院长、腾讯、字节技术专家届时将分享架构演进、大数据、大模型实践案例,扫码或点击「阅读原文」查看全部专题。

九折购票最后 1 天,团购或储值享多重优惠,咨询购票请联系票务经理 18514549229(微信同手机号)。

文章引用微信公众号"InfoQ",如有侵权,请联系管理员删除!

博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。