阿里云下一代可观测时序引擎-MetricStore 2.0

徐昊(博澍)

背景

作为可观测场景使用频度最高的数据类型,Metrics 时序数据在可观测领域一直占有着重要地位,无论是从全局视角来观测系统整体状态,还是从大范围数据中定位某一个异常的位置,Metrics 数据总是处在整个可观测流程的第一步。在基础设施、云原生、中间件、IoT 设备、业务可观测、时序预测等众多场景,Metrics 都是被强依赖的核心。

近年来随着云原生、AI、边缘计算等技术的快速发展,传统面向偏静态、单一的监控方案逐渐被抛弃,转而使用更加动态、精细的监控方案,随之对时序引擎的要求也逐渐变化。时序场景“更新换代”的特征越来越明显,主要体现在:

  1. 观测粒度变细:从整体监控变为细节监控,例如主机从只关注 CPU 到关注每个 CPU 核心的每个维度利用率、APM 从只关注服务整体到每个接口甚至每个 Instance 的每个接口。整体而言观测的对象可能出现 10 倍甚至百倍的增长。

  2. 观测间隔变短:分钟级的指标虽然整体开销较小,但往往会抹平瞬时抖动,我们能看到越来越多的客户需要存储秒级甚至毫秒级的指标数据。即使从 1 分钟变为 5 秒也会有 12 倍的数据量增长。

  3. 更加动态的场景:随着容器化、训练任务、Serverless 服务等场景的广泛应用,Instance 这一观测对象的生命周期越来越短,我们面向的是一个越来越动态的场景。而动态的场景下传统时序引擎的性能极具变差甚至无法工作。

  4. 从查看到分析:由于存在了各类维度的细节观测数据,针对时序的查询方式逐渐从点查演变为多维度聚合分析,与 OLAP 分析的界限逐渐模糊。

  5. 从人工到自动:可观测发展的趋势是更容易、更自动化的定位问题,随着大模型、AIOps 技术的发展,自动化分析数据已经逐渐成为主流。自动化是典型的算力换人力,减少了人工“点”的操作,但对引擎而言查询压力会急剧增加。

分析上面的趋势,我们可以总结出,下一代时序引擎应该具有如下的能力:

  • 读写能力的稳定性,以应对实时在线场景的高 SLA 要求。
  • 灵活支持各种数据 Schema 与数据类型,具备在不同场景下的通用性。
  • 对高基数、不同精度的数据具有兼容性,避免在极端场景下性能大幅度劣化。
  • 高性能,可以支持越来越多的定时聚合、报警、分析需求。
  • 查询模式灵活,可以支持使用各种查询语言来支持大规模复杂分析。
  • 在大规模数据量下低成本的处理与保存能力。

MetricStore 1.0

MetricStore1.0 在 2020 年上线,对内承接了集团、ASI、等众多业务场景,对外为很多公有云大客户提供服务,日写入量数十PB,并且还在以每年 100% 的速度增加中,已经成为了不可或缺的基础设施。随着业务发展、支持场景的多样化及复杂化,当前 MetricStore1.0 在使用上遇到了一些瓶颈。

出于对各种数据模型的通用性考虑,MetricStore1.0 当前实现方式是采用通用列存模型,内部每一个数据点作为一行来保存,而并非时间线模式。所有的 LabelKey、LabelValue 对都作为一个列保存,每一个时间戳、数据值也都做为一个列来保存。

在存储层上,这样的方案在愈发复杂的线上场景中显现出一些不足:

  • 近期数据的压缩率较低,导致在有限的内存中, 缓存频繁失效,对查询的延迟和并发数量影响较大。
  • Labels 编码特性导致了压缩率并不是很高,查询、传输性能受限。
  • 没有能充分利用时间线的编码特性,每次查询时需要对数据点排序带来了较大延迟,同时压缩率也难以进一步提升。

Prometheus 作为一种广泛使用的开源协议,是目前 MetricStore 使用量最大的查询方式。从开发工作量和开源协议兼容性考虑,之前一直使用 Go 语言开发的开源 Prometheus 作为计算引擎,同时采用在外围做分布式改造的方式来进行查询加速,这些取得了很好的效果。随着业务的复杂化和数据量的增加,查询方式和数据量越来越大,对计算提出了更多的性能上的需求。在计算层面上,当前还存在实现上的提升空间:

计算效率

  • 开源 Prometheus 的计算引擎,实现上存在不少对象重复申请、传递和不必要的内存复制的情况,在计算的各个环节的资源使用上都存在较大的浪费,增加了计算开销和内存使用开销。
  • 在执行函数、聚合算子、二元算子、最终结果合并等流程均为串行执行,计算本身的实现效率存在提升空间,如果重新采用高效的方式实现,并且改为流式的并行计算的话可以很大程度上降低执行延迟。

内存效率与控制

  • 受限于语言,当前只能通过限制单次查询数据量和执行时间来实现近似内存控制,而且没有租户资源使用控制,对于复杂计算限制偏差较大,而大查询带来的内存申请->GC 过程也会同时增加当前进程内所有正在执行中查询的延迟。
  • 从效率和代码复用考虑,当前反序列化及结果预处理使用 C++ 来完成,而计算部分在 Go 语言侧,这样的跨语言数据交互,进一步增加了内存使用控制上的难度。

从以上现状和需求考虑,我们开发了 MetricStore 2.0 版本,从存储到计算进行了全面升级,致力于成为阿里云下一代可观测时序引擎。

MetricStore 2.0 技术方案

MetricStore 2.0 的核心技术升级共分为内存、文件、计算和协议交互四个部分。

内存存储模型升级

内存实时压缩

近期数据观测是时序数据的一个高频使用场景,在报警、Schedule SQL 执行预聚合等功能,都会对近期数据进行高 QPS 的访问,从线上情况来看,95% 以上的请求都是近期查询。随着硬件规格不断增加、价格逐年下降,在内存中缓存数据以尽量覆盖更多的近期查询成为了可能,我们可以通过性能与压缩率平衡的内存实时压缩来在内存中缓存更多的数据。

在一个标准的时序场景里面,数据特征上具有明显的规律:

  • 一个时间线会在一定周期持续存在,即使是高频更新的场景,一般也会维持 10 个采集周期。
  • 在同一种类时间线内部,LabelKey 和 LabelValue 也会存在很多重复。

以图中这几个来自容器 cadvisor 的时间线为例,只有标红的部分是变化的,其他部分都没有变化。

基于这个特征,我们做如下的编码:首先将这些 LabelPair 提取出来,按照 LabelKey 为维度,将 LabelValue 编码成独立的词典,接着编码后的 LabelsPairs 也会被进行再一次编码,提取出时间线这一单位,这样的两级词典化编码比较好的平衡了运行效率与压缩率。根据线上实际运行情况,对于容器场景下的数据集,与原始数据相比有近 10 倍的压缩率。

除了 Labels,在数值和时间列上,也存在着明显的数据特征:即同一时间线的时间和数值变化不明显。对于内存实时压缩,我们主要采用的还是优化版本的 Gorilla 压缩算法,在此基础上增加更加了细致动态内存管理,以适应动态观测对象的场景。

自适应 Block

内存在存储资源中,相对还是最紧张的资源,尤其是在面向多租的场景,如何根据每个租户特征来缓存数据、保证内存最优利用率是一个很重要的工作。为此我们引入了自适应的 Block 方案,动态统计数据特征,使 MemBlock 会随着各个 Labels 数据量自动调整参数与 Block 切换,保证数据量、内存开销与压缩率在合理范围内;如果数据维度继续增加,就不会再建立倒排索引,转而采用扫描模式来平衡效果资源占用;随着维度的进一步增加,整个 Block 会继续转变为成列存编码模式。

此外,在查询的场景下,会根据查询访问量及查询时间范围,在一定的区间和资源用量范围内动态调整 Cache 策略,保证近期数据尽量命中。为了减少 Block 失效带来的影响,MemBlock 在完成 Dump 后会直接被缓存,而不需要经过 Dump -> Block 失效-> 重新 Load 到 Cache 的过程。

在整体的 Block 自适应调整下,整个服务可以一直工作在比较均衡的状态,也尽力保证了读写的性能的稳定性。

文件存储模型升级

文件压缩

与内存压缩一样,Meta 数据的压缩也是使用时间线和 LabelKey、LabelValue 两级词典来压缩的,MemBlock 每次将时间线和词典数据增量落盘,然后分别进行 Compact。与内存中每个 LabelKey 保存一整个大词典不同的是,在文件部分把整个词典划分为一个个 16KB 的小词典进行保存,每个 Labels 列的 Segment 指向所需要的词典,加载的时候进行级联加载。这样做的好处是当查询部分 Label 的时候,只会加载所需要的词典,保证查询加载数据量可控。

在数据压缩中,乱序写入与标准列存的处理存在差异,尤其是在时序数据中,时间列通常需要进行排序。为了同时保持主键列(PK 列)和时间列的有序性,并减少计算开销及提升压缩率,我们在传统列存的概念上提出了一个新的方法:对 Time 和 Value 列引入了 Piece 单位作为每一组 Labels 子列的概念,以增强时间列与数值列的局部性,通过这种方式,我们可以更有效地保存数据。

当前支持 Double、Long、Bool、String 四种常见类型,在压缩时,会根据数值类型、数据特征与已知规则的比较、数据量、数据稀疏度等因素,使用 Bitmap、RLE、BitPacking、XOR、字典、Zstd 等多种算法,动态调整压缩参数和编码方式,来提升优化性能和压缩效果,同时在编解码上进行细致的内存控制及复用,并引入 SIMD 来尽可能提升压缩速度。

我们以 Long/Double 类型的数据为例,选取三个有代表性的数据集对压缩率(bytes/sample)进行测试,可以看到,相比于 1.0 版本也有着近一倍提升。

备注:VM 场景下的内存数据压缩率未包含指针等数据结构本身的空间,如果包含这些开销,压缩率还会降低 20%~30%。

由测试结果可以看出,在文件上,当前 Double 类型的压缩率还是比VM要低的,我们针对这种差别进行深入分析,总结了两点原因:

  • VM 的压缩方式为有损压缩
  • 有损压缩的失真率针对不同数据集表现情况不一,在对客场景解释成本过高,很难直接提供。
  • VM Compact 的级数比较多,最终生成的 BlockSize 比较大,整体压缩率会高一些
  • 大块方式压缩率高,但在很多查询场景会有比较严重的读放大和延迟增加,Compact 开销也会更大。
多种数据模型

在不同使用场景,根据业务不同,所需要的数据模型一般是不同的,为此我们除了支持 Dynamic 的 PK 列(即 Labels)外,还支持 Dynamic 的 Value 列,来满足不同场景需求。例如:

  1. APM 场景,主要关注点就是各个中间件的黄金三指标,多数情况下是比较确定的,一般使用 Schema 化的数据列。

  2. 在 K8s 监控场景,所监控的目标种类极多,统计维度不固定,因此采用动态 PK 列。

  3. 在 IoT 场景需要的设备标识是固定的,但指标数据可能动态增删,使用 Schema 的 PK 列和动态的数值列相对更优。

在灵活的 Schema 和广泛的数值类型支持下,可以做到兼容绝大多数场景,以及可以快速兼容各种数据源和新的协议,比如可以在 Prometheus 模式下增加一个 String 列,就可以比较容易得支持 Prometheus Exemplars 功能。

支持数据更新能力

通常情况下可观测数据来自于实时上报,符合 AppendOnly 的特点,并不需要修改。但部分场景下还是有更新需求,例如上报数据出错、离线回补订正等。MetricStore 2.0 在设计之初就考虑到数据更新、删除问题,目前已经支持数据更新操作,使用方式也比较简单,直接写入同一组 Label、同一个时间戳的新数据,后写入的新版本数据就会覆盖掉旧版本数据。

计算引擎升级

与传统监控场景单纯的点查与聚合不同,在业务使用场景越来越复杂的大背景下,MetricStore 在很多情况下实质上承担了一部分 OLAP 的分析任务。由于采集数据源是分散的,会把来自各个数据源的数据在存储上集中,在分析的时候就需要进行多数据源组合查询的需求,如果再叠加上业务属性,导致查询更为复杂,计算量也更大。

以一个来自容器场景的查询为例,这条语句是在计算采集端的数据齐全度状态信息,在执行过程中,这是一个类似多表 join 的计算,对近 10 个指标做 join 计算,从这个图中分析,在这种情况下,其中的二元算子都是在串行执行。

100 * (
(
(
(count(aliyun_prometheus_agent_heartbeat{agentId="0"}) or vector(0)) > bool 0
) * (
(
count(increase(aliyun_prometheus_agent_write_succeed_batch_total{}[4m]))
== bool (max(aliyun_prometheus_agent_replica_current_num) - 1)
) == 1
or (max(aliyun_prometheus_agent_replica_current_num) == bool 1) == 1
or vector(0)
)
) * (
absent_over_time(
(
sum(aliyun_prometheus_agent_memorylimit_alloc_mb{} / aliyun_prometheus_agent_memorylimit_limit_mb{} > 0.55) > 0
)[15m:15s]
or vector(0)
)
) * (
count(increase(aliyun_prometheus_agent_heartbeat[2m])) / max(aliyun_prometheus_agent_replica_current_num)
or vector(0)
) * (
absent_over_time(
(
abs(100 - 100 * sum(rate(aliyun_prometheus_agent_write_succeed_batch_total{}[4m])) / sum(rate(aliyun_prometheus_agent_write_succeed_batch_total{}[4m] offset 1m))) > 20
)[15m:15s]
or vector(0)
)
) * (
absent_over_time(
(
sum(sum by (podName)(aliyun_prometheus_agent_write_arms_duration_num) > 1000)
)[5m:15s]
or vector(0)
)
) * (
absent_over_time(
sum(aliyun_prometheus_agent_worker_series_num > 5000000)[15m:15s]
or vector(0)
)
) * (
(
(max(aliyun_prometheus_agent_all_targets_num) != bool 0) == 0
) or (
absent_over_time(
(
sum(aliyun_prometheus_agent_worker_targets_num) / max(aliyun_prometheus_agent_all_targets_num) < 0.9
or sum(aliyun_prometheus_agent_worker_targets_num) / max(aliyun_prometheus_agent_all_targets_num) > 1.1
)[15m:15s]
or vector(0)
)
)
)
)

为了充分发挥 CPU 多核、指令加速能力,以及对计算、内存资源的精细化控制,我们使用 C++ 来开发新的 Prometheus 计算引擎。新的引擎对于计算流程上及实现上进行了全面改造,实现了更高的性能、稳定性和 QoS 控制。此外在 PromQL 的兼容性测试中,Prometheus C++ 计算引擎与开源的兼容度为 100%。

计算的主要流程如下:

  • 整个过程从网络传输 -> 数据预处理 -> 迭代计算在一个 Pipeline 中进行计算,并行化进行传输计算过程。
  • 使用流式读取计算,流中的每个 Task 采用并行模式,并行读取进行预处理、反序列化 Meta、还原时间线&数值列的内存结构。
  • 每次计算只解压缩一条时间线,并将所使用的内存复用。
  • 计算下推到并行解析侧,除原生支持并行、减少流转代价外,解析也可根据计算逻辑进行特定优化。
  • 在各种函数实现上尽量高效,并使用 SIMD 进行加速。
  • 二元算子执行时,二元的子查询之间会按照一定的并行度放到多个协程中完成计算。

以下面的查询为例来看执行过程:

sum(rate(container_cpu_usage_seconds_total)[1m]) by (pod) * on(pod) group_left(node)
kube_pod_info{pod=~".+"}

在资源占用上,每次查询全链路的内存申请量和计算量做分租户准确控制,存在单查询用量控制和总量控制两级,可以避免单一计算占用过多影响其他用户与计算进程 OOM 的情况,确保查询的资源占用可预期。

在计算模式上,我们也做了进一步升级,当前支持三种形式:

这三种模式各有优势,在实际执行过程中,计算引擎会根据用户选项、原始数据量、查询参数、预估计算量等因素,动态选择执行方式,以获得更理想的效果。

除了这些技术上的优化,业务上的优化也一样重要。一般的用户不了解 PromQL 的原理,经常会出现想对较慢的查询进行优化但无从下手的情况。目前我们提供了 PromQL 的 Query Explain 功能,可帮用户把 Query 解析成树状结构,更好了解每层的执行行为。后续也会对该接口进行优化,透传出每层的执行延迟和代价,降低分析优化的难度,达到更好的自服务效果。

传输协议升级

通常在一个较大的查询里,会包含上百万组 Labels 字符串和数亿数据点,这些数据如果直接传输的话,即使在内网环境也会对有较大的开销。为此在存储和计算侧的传输协议上,我们进行了一定的优化:

  1. 在 Worker 处对查询的计算和 IO 过程进行分解,将 IO 和纯计算操作分别放到协程和物理线程中完成计算。

  2. 采用流式传输协议,数据分块流式传输到计算侧,计算侧整个加载、计算的过程也都是流式。

  3. 直接将高压缩率的原始 Block 响应给计算节点,存储层缓存效率大大提升,还可减少一次序列化与反序列化开销。

  4. 传输前基于查询分析,只生成并返回需要的 Labels 字段。

性能指标

写入性能
  • 当前单 Shard 写入速度最大可以支持 130M/s 写入,约 25w行/s;与 MetricStore 1.0 的 40M 相比,写入上限提升了 3 倍。
  • 以一些关键列为例,Time 列的单核压缩速度 430M/s,解压速度 870M/s;Long/Double 列单核压缩速度 940MB/S,解压速度 2510MB/S。
查询性能

在线上一些典型的查询场景,目前 MetricStore2.0 和 VM 的查询延迟如下:

从测试结果可以看出,MetricStore 2.0 的查询性能对比 VM 在各个场景下全面领先。

下述是分别是典型的短周期告警、分析型场景从 1.0 升级到 2.0 后的变化,后端延迟(单位为 ms)分别都有数倍的提升,尤其是在大规模瞬时分析的场景下,1.0 由于排队问题,延迟会急剧增加。

实时告警场景平均延迟
短周期分析型场景延迟
短周期分析型场景 P95 延迟
资源占用

MetricStore 2.0 在资源使用率上相较于之前的版本有较大优化,以使用量较大的容器场景数据为例,纯对比计算性能,可以看出 MetricStore 2.0 的资源开销是 VictoriaMetrics(VM)的 50%,比 MetricStore 1.0 提升 3 倍以上,整体的应对查询 QPS 能力上面也有着很大的提升。

备注:

总结

在高效的编码压缩与工程实现、公有云多租户资源复用下,MetricStore 2.0 现在有全面超越开源产品的性能,可以做到更低的单位成本与更高的资源利用率。

  • 相对于开源产品,灵活的编码模式切换、自适应的内存缓存调整下,可以保证写性能稳定,即便在数据状况不理想的情况下,也能尽量保持读取性能的平稳过渡;此外,存储和计算引擎全部使用 C++ 实现,细致的优化了计算过程,支持多种并行模式,有更好计算性能与 QoS 控制能力。
  • 通用性强,支持常见数据类型及强弱 Schema PK 和 Data 的支持,可以让用户在不同的业务场景下进行组合使用,同时具备快速对接其他读写协议的能力。
  • 在计算同时支持 PromQL、SQL、SPL 多种查询方式,用户根据使用场景和习惯选择合适的方式,避免计算方式单一而出现强行使用某种查询带来的学习成本与运行效率成本。

未来规划

MetricsStore 目前作为重要的基础设施,承载着丰富的业务场景,我们会在技术和产品上持续优化和演进,提供更加通用化解决方案、更强的性能和更好的产品能力,近期即将会上线的功能有:

动态多值列

相关介绍参见上述数据模型章节,动态多值列(DynamicValues)对于各类动态场景具有较好的适用性,在大大降低用户配置代价的同时也能兼顾查询、存储效率。

内置的降采样能力

当前 MetricStore 的降采样能力实现与很多开源产品一样,使用的是定时任务调度来完成的(Secheduled SQL),在数据量大的场景可能会出现任务延迟导致降采样不可用。在 MetricStore 2.0 项目中,我们设计初期就考虑降采样的诉求,目前可以做到 Compaction 过程中直接降采样,且不影响上层使用方式。

注意:降采样一定会带来精度缺失,建议合理配置降采样周期。

查询能力增强

Metric 数据在使用场景上经常会出现大量的高 QPS 访问,如盯屏、报警,或者当系统出现问题的时候,大量开发人员短时间内执行很多查询的情况。而存储层无法实现对历史数据的弹性。后续我们会提供类似一写多读的 ScaleOut 能力,实现查询能力的动态扩容。

数据删除

在精细化可观测的过程中,或多或少会遇到删除相关的诉求,例如错误数据删除、时间线异常发散的数据删除、法规要求抹除用户数据等。

注意:删除一直以来属于危险操作,且删除操作的代价比较大,需要进行一定的限制以避免在不恰当的场景下被使用,我们后续会对这一能力提供支持,并在产品和功能上做一些设计以防误用。

钉钉扫码入群↑

新版的 MetricStore 2.0 正在各个区域逐步上线,欢迎大家使用,如果有问题或者需求的话请使用钉钉扫码入群联系我们。


observability.cn Authors 2024 | Documentation Distributed under CC-BY-4.0
Copyright © 2017-2024, Alibaba. All rights reserved. Alibaba has registered trademarks and uses trademarks.
浙ICP备2021005855号-32