一、背景
1、小红书 Metrics 发展情况
2、面临的问题
二、演进历程
1、采集
2、高可用改造
3、查询优化
4、高基数治理
5、跨云多活
三、总结与展望
在当前云原生时代,随着微服务架构的广泛应用,云原生可观测性概念被广泛讨论。可观测技术建设,将有助于跟踪、了解和诊断生产环境问题,辅助开发和运维人员快速发现、定位和解决问题,支撑风险追溯、经验沉淀、故障预警,提升系统可靠性。可观测技术主要包括 Metrics、Logging、Tracing、Profiling 等,其中 Metrics 是最重要的基石,它不仅为业务监控和系统监控提供基础数据,还是构建告警系统、性能优化和容量规划的核心,这些都对 Metrics 体系的稳定、实时、高效提出了更高的要求。
基于此背景,小红书可观测技术团队在过去一年内,对原有 Metrics 体系进行了全面技术升级。目前在时序数据的采集、查询上实现了数十倍的性能提升,大幅降低计算、存储、带宽成本;同时显著提升可用性,具备全链路的单实例、单集群故障容忍能力,实现了分钟级的快速扩缩容;通过产品化配置变更、自动化集群迁移、标准化监控接入流程等方面,实现运维人力成本节约。
一、背景
随着小红书业务的持续发展,推荐、搜索、广告、电商和基础服务等各个领域的 Metrics 规模不断扩大。以推荐系统为例,单个核心应用在单次采集周期内所产生的指标量已达到亿级别,整个推荐系统的监控数据源指标量更是接近十亿级别。
云原生时代,Prometheus 逐步成为 Metrics 的事实标准。然而,最初基于开源 Prometheus 构建的系统,在规模迅速扩大的情况下,暴露了越来越多的问题。无论是资源占用,还是迭代效率,都已经无法支撑现有的数据规模和业务需求。作为小红书可观测团队,我们迫切需要对现有系统进行技术优化和成本压缩,以支撑业务的快速、高效发展。
依托开源 Prometheus 解决方案,小红书 Metrics 整体架构如图所示:
1)主要特点:
单副本架构:全链路从采集、写入、存储到查询都采用单副本方式。
分散部署:Prometheus 作为采集端部署在对应的 K8s 集群中,每个 K8s 集群中往往会部署多个 Prometheus 实例,负责采集不同业务指标,并将其写入不同的远端存储中。此外,还有一部分 Promethues 实例是以虚拟机方式部署,用于采集一些个性化的指标需求。
高可用性:Prometheus 不仅作为 Tsdb,还存储两个小时的数据供 Thanos 查询进行应急查询。
2)存在的问题:
稳定性差:由于全链路采用单副本方式,稳定性较差。日常变更和故障都可能导致 Metrics 丢失,进而影响告警、监控的准确性和可靠性,用户对此有明显感知。在指标量突增时,容易出现指标端到端写入延迟过大,进一步影响监控和告警功能。
资源成本高:Prometheus 的资源消耗较大,内存使用率经常超过 85%,频繁触发 Prometheus 资源告警;同时增加了实施多活/多副本的成本压力。
运维复杂:部署方式分散且管理成本高,甚至存在一些未知的、无主的 Prometheus 实例,导致日常运维需要耗费大量时间。大多数的虚机部署实例在进行变更时,属于黑屏操作状态,无法追溯。此外,系统还不具备全链路快速弹性扩缩容能力。
使用体验差:Metrics 的查询速度慢,经常出现慢查询问题。用户反馈大盘打开速度较慢、Panel 加载失败。作为应急备用查询方式的 Thanos 查询速度慢,经常超时,并且需要较高的资源成本。
二、演进历程
我们主要从以下几个方面,逐步做了架构演进:
初步云原生化:尽可能将虚机部署容器化,并迁移到发布平台进行发布,实现变更白屏化的目标。
采集端重构:采用新的采集架构替换 Prometheus,同时下线 Thanos,以解决采集过程中的各种问题,并实现采集-存储的彻底分离。同时,我们对Push 类需求进行了统一的支持和管控。
高可用改造:我们从采集、写入、存储和查询等方面,设计并实现了整体的高可用方案,使系统在端到端具备高可用能力,能够容忍单机群内少量实例故障、单机群整体故障等常见故障场景。
查询优化:我们对查询进行了限流和资源使用的限制,并对存量查询进行分析,重构了查询流程,将聚合运算下推到存储节点执行,从而加速大多数查询操作。
高基数治理:核心集群具备高基数治理能力,防止业务不规范使用导致的时间序列膨胀问题。
跨云多活:进一步优化部署方式,通过单元化部署实现机房故障隔离,并降低跨云带宽。
存储优化:目前我们进行的工作,存储支持降采样,以在不增加资源的情况下支持更长时间的指标数据存储。
技术选型:
我们基于开源 Victoriametrics (以下简称 Vms ) 作为整体架构基础,针对小红书内部的业务场景和痛点进行定制开发,以适配公司内部组件。全链路选择 Vms 的原因主要是:
出色性能:相比于 Prometheus ,Vms 在性能方面表现更出色。以 Prometheus Agent 模式为例,经测试发现,在相同的指标量下,Vms 所占用的资源约为其的 1/3 左右。
兼容性好:Vms 在采集侧基本兼容 Prometheus,这使得我们能平滑地迁移现有的 Prometheus 采集组件。同时,当前小红书 Metrics 存储组件已经基于 Vms 构建,全链路切换到 Vms 的成本和风险较低。
可扩展性:Vms 具有清晰简洁的代码结构,易于理解和维护,具备良好的可扩展性,在此基础上进行二次开发更加高效;此外,开源社区的版本更新速度、Issue 讨论、Bug fix 都非常迅速,为我们提供了好的支持。
演进后,当前架构图如下所示:
包括以下核心组件:
Vms: 包括Vmagent、Vminsert、Vmselect、Vmstorage 等开源组件。
Push service gateway: 负责接收业务主动推送的 Metrics,并根据租户将其转发到对应的监控采集集群。
管理组件
配置中心:负责指标采集的配置、Kube-config 的维护。
Meta service:负责服务发现、主备集群切换等功能。
运维平台:提供集群注册、扩缩容等常规运维支持,以及查询切流、写入限速/停写等多种应急处理能力。
接下来我们将从采集端、全链路高可用方案、查询优化、高基数处理和跨云多活等方面来详细介绍。
在最初的采集端部署方案中,我们按照每个 K8s 集群进行 1:1 的部署。例如,推荐集群在多达 15 个 K8s 集群中进行部署,整个公司的 Prometheus 部署组超过两百个。在资源使用方面,普遍采用的是 1:8 的大内存机型,其中某些 Prometheus 实例的内存配置接近于 512GB;即便如此,几乎每周都会出现多次内存超过 85% 的告警。扩容时由于大内存资源无法及时释放,导致故障时间延长。采集变更通过上线变更实现,当业务需求发生变化时,需要对十几组 Prometheus 分别进行上线,集群运维复杂烦琐。为了快速迭代上线,不同部署组的 Prometheus 配置逐渐分裂,增加了配置管理的负担。
1)整体架构
针对以上这些问题,我们重新设计实现了采集的架构,整体架构如下:
基于 vmagent 的开发方案:我们选择使用 vmagent 替代 Prometheus,并通过全链路的双活机制(后文介绍)替换和下线 Thanos。放弃使用 Prometheus agent 的原因是,在原型验证阶段发现了该代理存在的必现 Bug,且社区无相应的解决方案。经过测试,我们发现 vmagent 在性能方面明显优于 Prometheus agent,且其代码结构更加简洁直观,可扩展性更好。
指标采集配置动态生效:采集配置通过配置中心下发,vmagent 接收并保存到本地,周期性地检查配置变化,并通过重新加载(Reload)生效。这样,日常业务采集需求的变更可以通过产品化修改配置下发,避免了 Prometheus 变更每次需重新发布或者黑屏手动 Reload 的复杂操作,大幅度提高了运维效率。
分片&平滑扩缩容:考虑到实际生产环境中集群规模庞大,无法通过单个实例采集所有指标的情况,我们提供了简单配置能力以支持采集分片和快速平滑扩缩容。我们在采集配置中新增全局的 Shard_count 配置,用于表示实际的采集分片数。通过配置中心下发 Shard_count 参数后,每个采集分片会 Reload config 并更新采集对象 (Scrape targets) 列表。在更新 targets 列表时,我们使用 Pod_index 和 Shard_count 进行哈希计算,以保留相应的 targets。
对于扩容场景,先扩容实例,再修改并下发 Shard_count 参数:
对于缩容场景,先修改并下发 Shard_count 参数,等采集流量迁移完成后,再缩容实例:
采集过程保护:在业务生产环境中,经常会出现由于 Metrics 异常使用造成时间序列膨胀问题,进而导致采集进程资源占用不断飙升、甚至崩溃的情况。因此,采集过程的限制和保护是非常必要的。我们目前在单次采集的 Sample 设置上限、Sample 内容长度两个层面进行校验。
Sample limit 增强:支持在一个采集 Job 内单独配置 Sample limit,为重点应用额外设置 Sample limit,并优先保留核心 Metric 指标和重点集群的 Metric 白名单。
Metric、Label 长度校验:开源的 vmagent 并未对 Metric 和 Label 的长度做校验。然而,在实际生产环境中,经常出现业务不规范使用造成的 Metric 和 Label 内容过长问题,导致 CPU 突然飙升,需要紧急扩容大量资源来应对。因此,在处理采集到的指标内容时,我们增加了对 Metric 和 Label 内容长度的校验,并对超长的指标进行告警,同时做截断处理。
性能优化:在采集性能方面,我们发现当 vmagent 面对大量采集对象时存在一些问题,对此我们进行了针对性的优化,包括:
大量采集对象的延迟启动:当 vmagent 在进程启动或运行时新增大量对象采集时,会出现 CPU 和内存的抖动,相对日常运行时的 CPU 内存使用率明显增加,甚至可能导致 OOM。通过延迟初始化的方式,将大量对象分批启动,启动资源消耗降低到日常运行的消耗水平,使整体消耗降低一半。
删除大量采集对象时的 OOM:在删除大量对象时,通过使用对象池和并发限速的方式,解决资源占用过多的问题。
资源使用率不均衡:在实际生产环境中采集大量对象时,vmagent 的 CPU 使用率通常较高,而内存使用率很低(有时甚至低于 10%)。通过性能分析,我们发现 CPU 的大部分消耗来自垃圾回收(GC)。从 Golang 1.19 版本开始,提供了 SetMemoryLimit 功能,可以提高 GC 的阈值,降低 GC 的频率。通过调整内存限制,内存使用率得到提升,通过内存换 CPU 的方式,CPU 使用率普遍降低近一半,从而降低整体采集过程的资源消耗。
我们完成几十个 K8s 集群、数百个 Prometheus 虚拟机采集实例的治理,并成功将整体资源统一迁移到了新 Metrics 采集架构上。迁移后,采集模块的变更统一到小红书内部的发布平台和配置中心进行管理,使得采集变更更加高效便捷,同时实现可灰度、可回滚和可追溯,大幅降低了日常运维的复杂性,解放了运维人力。在资源消耗方面,新的 Metrics 采集架构相对于原有架构,采集性能提高近十倍,降低成本折合数万核 CPU、数百 TB SSD 。此外,在机器资源规格方面,我们不再依赖于大内存的机器,使得扩缩容更便捷,同时显著降低内存等资源告警量。
2)多种指标的标准化采集
K8s 基础指标的采集:
当前我们使用 cAdvisor、Node_exporter、Kube-state-metrics 等 K8s 组件来采集每个 K8s 集群的基础指标。最初 Kube-state-metrics 部署方式为单机,但由于部分 K8s 集群的节点数量达到近 5000个,Kube-state-metrics 暴露的指标量超过 500 万,单次采集响应的数据量超过 700 MB。每个K8s集群都部署了一个独占的、高规格的 Prometheus 来确保采集不会超时。然而,这种部署方式浪费了大量资源,且存在稳定性差难维护、升级扩容困难、单机扩容存在上限隐患等问题。
为了解决这些问题,我们将 Kube-state-metrics 调整为 Statefulset 的分片部署方式。每个分片只上报自身指标,并自动感知总分片数,从而降低单个分片暴露的指标量。在多分片进行扩缩容时,我们通过在采集配置中配置 Relabel 规则,将 Instance label 覆盖为固定值,以解决由于 Instance label 变化导致的指标抖动问题。此外,我们在变更时增加一个额外副本,并结合 Vmstorage 的去重能力,来实现用户无感的 kube-state-metrics 升级、扩缩容等运维操作
在实现分片部署后,我们使用现有的 vmagent 集群进行 Kube-state-metrics 的抓取,并下线了之前独占部署的 Prometheus。
虚机监控采集:
小红书的主机监控依赖 Consul 作为服务发现,由于公司逐步下线 Consul,且主机监控面临多种问题:Consul 的性能衰减导致注册超时,导致大量新机器的监控缺失;退机时 Consul 解注册接口失败,导致已下线的主机仍然尝试采集指标,触发误告警,日均引发 2 个以上的相关客服工单。此外,除了 Consul 之外,部分服务通过静态 IP 配置抓取,而地址信息的更新需要通过 Metrics 研发团队同学发布才能生效,缺乏白屏化管理。
针对上述问题,我们上线了新的主机监控系统:首先,我们定制了新的注册和服务发现方式。在主机信息维护机制上,使用服务树权威数据来源同步,替换了原有的基于变更维护的方式,以解决监控数据不一致的问题。此外,我们使用“局部同步+兜底全量同步“的方式提高同步性能。
同时,我们上线配置平台,提供了标准化产品化的接入方式,基本的配置变更无须研发团队介入。目前,已有 50+ 个主机监控采集任务由用户自主接入,包括网关、数据库、etcd、zookeeper 等,共抓取 10 多万个监控对象。此外,开关机注册的监控成功率也大幅提升,客服工单数量从日均 2 个以上,降低到两个月1 个,释放出运维人力。
业务数据推送通道:
针对各个数据源普遍存在少量的推送需求,例如 Python 类服务,我们提供统一的业务 Push 数据链路,通过推送服务网关,接收各个业务方的推送数据,并根据租户信息将数据分发到实际的业务数据源。
多 K8s 集群管理:
为简化采集端部署方式,并支持同时采集多个 K8s 集群的应用指标,我们采用了集中式抓取方式。由于公司内部常常新增 K8s 集群,因此采集端支持实时新增 Kube-config 配置,以便快速生效新集群的监控采集。
3)埋点 SDK 增强
小红书内部的 Java 服务使用基于 Micrometer 的 Prometheus 埋点 SDK,Micrometer 提供了丰富的度量类型、自动化配置以及默认的指标采集,并与 SpringBoot 等流行框架有良好的集成。然而,面对小红书核心服务的高复杂度,以及 Metrics 埋点的高 QPS 和时间序列基数,Micrometer 的使用逐渐暴露出一些问题:
性能开销:Micrometer 在每次进行指标埋点时都会对 Label 进行排序,这一过程消耗了部分 CPU 资源。
应急恢复能力不足:核心服务中的某些非预期埋点经常触发采集端的 Sample Limit 阈值。虽然临时解决方案是提升采集限制上限或丢弃部分低优先级指标,但这并没有从根本上解决非法指标上报问题,采集资源的压力依然存在。
低频率时间序列的处理:存在大量低频率甚至是一次性出现的时间序列指标。这些指标在 Micrometer Registry 中累积,不仅浪费采集端资源,随时间推移还可能触发 Sample Limit,影响系统性能。
针对核心业务 Prometheus 埋点性能和稳定性问题,我们对 SDK 功能进行了增强:
性能优化:引入了 Meter Cache 机制,在每次获取 Meter 时优先从 Cache 中加载,减少了每次创建时的排序操作,从而提升性能。
应急止血策略:为解决 Sample Limit 引发的问题,SDK 新增了指标上报的禁用功能。这一策略允许通过管理平台动态配置,禁用的指标会从 Micrometer Registry 中移除并停止注册,从而快速控制问题并稳定采集水位。
低频率时间序列清理:在 Meter Cache 中设定了全局过期时间。当条目过期时,会同时清理 Micrometer Registry 中的相关记录。
此外,为了降低用户 PromQL 的使用成本,我们在埋点 SDK 侧收集了 Metrics 元数据,包含 Metric 名称、Metric 类型、Label 列表等信息。产品层根据 Metrics 元数据,默认生成业务基本的 PQL 语句,以实现用户开箱即用的体验。这一举措不仅提升了用户体验,同时也减少了产品配置工作。总体结构如下图所示:
1)背景
在之前的架构中存在以下问题:
全链路单副本:系统只有单个实例,导致在单实例故障、多实例故障和变更等场景下缺乏基本的高可用性,造成监控数据丢失、大量误告警,严重影响用户体验。当集群不可用时,恢复时间较长。
集群雪崩:多次出现单个数据源雪崩现象,特别是当集群负载较高时(如内存使用率超过40%)。
静态 IP 不变依赖/弹性扩缩容:系统在服务发现方面依赖静态 IP 不变的特性进行写入和查询操作,导致扩缩容、集群切换等变更非常困难。云原生化后受到静态 IP 保持特性的限制,只能选择提供静态 IP 保持的云产品。此外,由于 Pass 平台提供的静态 IP 不变特性存在 bug,也会导致故障。
实例数据安全:大多数实例数据存储在本地盘上,部分实例出现数据文件被整体误删无法恢复的情况。
基于以上问题,我们的目标如下:
故障容忍:系统需要容忍一个集群内的部分实例故障和一个集群整体故障,降低集群雪崩的概率。
快速扩缩容:在资源可用的情况下,能在几分钟内实现平滑的水平扩容,无须通过应用发布上线。
数据安全:在数据误删等场景下,能够进行数据恢复。
快速迁移能力:加快机房迁移速度,对于单节点存储量低于 1TB 的实例,在一天内完成迁移。
2)整体架构
高可用的整体架构如下:
针对上述问题,我们依次提出解决方案:
全链路多活
为确保高可用性和数据备份,我们采用全链路双活部署。在采集侧,我们部署两套完全相同的采集集群,并将数据同时写入两个远端存储集群。在存储侧,我们同样部署两套存储集群。每个存储集群正常情况下会收到两份采集数据,为避免数据重复,存储集群在数据合并时进行去重操作,确保在同一时间窗口内相同的指标数据只保留一份。通过这种部署方式,系统可以容忍单个集群完全故障的情况。
集群雪崩预防
在单副本模式下,线上会遇到存储集群雪崩故障的情况。经过分析,我们发现这主要发生在指标量快速增长、集群整体负载较高的情况下,同时伴随着存储节点实例故障或者少量扩容。在此情况下,Vms 使用 Reroute 重定向机制,通过一致性 Hash 将原本写入问题节点的流量,分摊到其他可用节点上。虽然这种方式确保数据的持续写入,但是剩余存储节点在短时间内收到大量新序列数据后,需要创建索引和缓存,致使 CPU 和内存的快速上涨。如果剩余存储节点本身负载较高,无法及时响应写入请求,将进一步触发新的 Reroute,形成雪崩效应,导致整个集群无法进行写入和查询操作。
因此,考虑到 Reroute 机制作为实例级别的高可用机制,在某些情况下存在一定危险性。在已经对存储构建了双活集群的情况下,我们考虑替换 Reroute 机制,以避免整个集群发生雪崩现象。为了确保同一份数据写入固定的下游存储节点,我们引入了本地队列机制。具体而言,当下游存储不可用时,Vminsert 将数据写入本地队列, 等存储恢复正常后,在消费队列中的数据将写入下游存储。两种机制对比如下:
本地队列方案与存储双活结合后,当主存储集群中的实例发生故障时,Vminsert 会产生时序数据积压,并向 Meta service 上报数据情况。一旦积压超过阈值,Meta service 通知查询集群自动切换至备用存储集群。当主存储集群中的故障实例恢复后,Vminsert 逐步消费积压的快照数据。积压恢复后,Vminsert 会再次上报给 Meta service,随后 Meta service 通知查询集群切换回主集群。通过这种自动化的故障容忍和处理机制,我们实现了对集群故障的自动化处理。
静态 IP 不变依赖/弹性扩缩容
Vms 仅在商业版中提供了服务发现功能,而开源版本需要手动更新 -storageNode 启动参数来调整 Vmstorage 地址,运维成本较高。在云原生场景中,实例迁移时 IP 会发生改变。最初的 Vms 存储集群部署依赖底层云服务商提供的静态 IP 特性,但由于各种原因,IP 特性失效导致 IP 被释放的问题多次出现。此外,依赖云厂商的特性也限制了部署环境的选择。因此,我们开发了支持 Vmstorage IP 变化和服务发现的解决方案,具体如下:
新增 Meta service 支持服务发现机制,Vminsert 和 Vmselect 定期向 Meta service 轮询,以获取最新的 Storage 地址列表
新增 Meta service 作为服务发现提供者,而不直接使用公司提供的注册中心组件,以减少依赖。服务发现接口可根据需要进行扩展,具体实现可以替换为其他方式,如 Consul、HTTP 等。
服务发现的结果持久化在 Meta service内,并在 Vminsert/Vmselect 中缓存至本地文件,以避免外部依赖故障导致无法连接 Vmstorage。
支持查询切流:Vminsert 向 Meta service 上报 Storage 状态,Meta service 根据状态为 Vmselect 选择查询的地址。
调整节点分片 Hash 算法
Vminsert 组件与下游存储节点之间的分片方式采用一致性 Hash。最初的 Hash 算法是将存储节点的 IP 地址进行 Hash 计算,并将其放入 Hash 环中,再根据写入时序数据的 Hash 值确定数据落入哪个节点。
当存储节点的 IP 发生变化后,为了不改变时序数据在存储节点之间的分布情况,我们确保存储节点的实例名保持不变,同时将 Hash 算法中的“按 IP 进行 Hash“ 改为“按实例名进行 Hash“。这样在数据迁移等场景下,可以快速、平稳地启动。
灰度扩缩容
当 Vmstorage 节点进行扩缩容时,Vminsert 直接更新地址列表会导致各个存储节点的写入流量迅速发生重新分配,从而导致接收新时序数据的 Vmstorage 消耗大量资源来创建索引和缓存。
为了避免一次性切换分片可能引起的雪崩效应,Meta service 对 Vminsert 侧的分片扩容提供灰度机制。Vminsert 在获取配置时提供自身的实例名,并根据不同的实例名获取不同的灰度配置。
实例数据安全
使用云盘代替本地盘
初始阶段,存储节点部署在虚拟机上,并将数据写入本地盘。然而,在实例故障时,存在数据丢失无法恢复的风险。为了确保数据在实例故障和迁移等场景下不会丢失,我们选择将存储迁移到云盘上,以实现容器化部署和迁移。
Backup/Restore 机制
在极端情况下,底层存储 PV 可能会受到不良影响,例如由于错误的 PV 配置导致误删除,写入异常数据或误操作删除文件而导致 Vmstorage 无法启动等问题。为了解决这些问题,我们需要对存储进行备份。通过利用 Vmstorage 提供的快照能力,对 Vmstorage 的全量数据进行增量备份与版本管理,每小时、每天、每月将备份数据按需保存多份至对象存储,即可在发生故障时,回滚至正常状态。示意图如下:
此外,集群迁移也是一种重建方法。基于 Backup/Restore 机制,结合 Vminsert 实现的本地队列能力,我们可以无损将存储集群快速迁移到新的部署环境。近期,我们将 Vms 在云产品之间做迁移部署时,便利用了这一机制加快迁移过程,将迁移时间从以前的至少需要 30 天的双写缩短到半天,避免长时间双写带来的额外集群成本。总体流程如下:
全量数据备份与初始化同步:假设时间为 time 1,对原始集群的 Vmstorage 数据进行全量备份,上传到对象存储中;在完成数据备份后,迁移后的新集群从对象存储中获取到截止至 time 1 的全量数据。此时,Vmagent 的采集流量将不会写入迁移后的新集群。
增量数据备份与同步:Vmagent 开始将采集流量写入新集群,此时新集群的 Vmstorage 设置为 not ready 状态,即 Vminsert 无法连接 Vmstorage。Vminsert 将快照数据写入队列(queue)中。一旦 Vmagent 的采集数据稳定地写入新集群(假设时间为 time 2),原始集群开始增量备份数据(time 2 - time 1 的数据)。完成增量数据备份后,新集群再次从对象存储中获取增量备份数据,使得新集群具有截至 time 2 的数据。
数据回放与去重:新集群中的 Vmstorage 消费备份数据完成后,将其设置为 ready 状态(假设时间为 time 3)。Vminsert 开始回放 queue 中的数据(time 3 - time 2 的数据)并写入 Vmstorage 中。在 queue 的快照数据回放完成后,新的 Vmstorage 集群具有完整的全量数据。在整个过程中,时间范围上重叠的数据在 Vmstorage 中被 dedup 去重,以保证数据的一致性和准确性。
在指标查询方面,我们之前面临的主要问题是业务方反馈一些 PQL 查询速度较慢,并且存在一些高基数、高密度数据无法正常查询的情况。此外,从查询模块本身来看,多次出现大量数据的查询导致内存溢出(OOM)的问题。我们之前暂时通过扩展资源来部分缓解这些问题,但是随着业务指标量的不断增加,简单地堆积资源的方式已经无法持续发展,也不能从根本上解决问题。因此,我们决定从架构层面寻求更有效的解决方案。
1)计算下推
以一个常见的查询 sum(rate(http_request{code=200}[1m])) 为例,开源查询流程如下:
查询节点解析 PQL 后,提取出 Metric + Tag,并向存储节点发送请求。存储节点根据 Metric + Tag + time range,查询原始的 Timeseries 数据,并将 Timeseries 数据返回给查询节点。查询节点在接收到所有的存储节点返回的 Timeseries 数据,依次进行 rate、sum 等操作。在时间线特别多的情况下,单个存储节点返回的时间线原始数据量较大,导致查询节点接收到所有的时间线量更大,进而导致后续的 rate、sum 等操作需要更多的计算资源,增加了处理延迟。此外,为了防止查询节点因大数据量查询的影响而崩溃,查询节点会对时间线数量和单个查询的内存上限限制,从而使得部分 PQL 查询因触发上限而无法正常执行。
为了解决上述问题,我们考虑将聚合操作(如 sum、count、avg、max、min等)下推至存储节点执行,这样存储结点进行聚合后,可以极大地减少返回给查询节点的原始数据量,并且能够显著提高查询量的上限。
Vms 本身不支持计算下推功能。这是因为在存储节点对时间线进行聚合之前,要求一个时间线在查询的时间范围内只能存在于单个存储节点中,然而由于写入时的 Reroute 机制,一个时间线在存储节点中的分布是未知的。
为了解决这个问题,我们在高可用改造中去除了对 Reroute 机制的依赖。在非扩缩容的情况下,单个时间线是稳定地保存在一个存储节点中。此外,Metric 的特点是大多数查询都集中在最近的时间段内,而扩缩容操作相对低频且持续时间很短,因此在大多数情况下,我们可以跳过扩容的少数时间,依然能将聚合操作下推到存储节点。这一点改进对系统尤为重要,线上绝大多数的查询逻辑都包含聚合类的计算,非常适合进行下推操作。
计算下推后,新的流程如下:
在存储节点收到查询请求后,根据 Metric + Tag + Time range 查询到原始的 Timeseries 数据。参考查询节点中的类似处理逻辑,进行 rate、sum 操作,得到聚合后的 Timeseries 数据,返回给查询节点。在绝大多数查询 Case 中,聚合后返回的 timeseries 数据量,比原始的 Timeseries 数据量大幅下降。查询节点在收到各个存储节点的响应后,不再进行 rate 操作,而只对 Timeseries 数据进行 sum 操作。
在各个聚合类操作中,avg 操作需要进一步特殊处理。在各个存储节点中直接计算出 avg 是不准确的。对于 avg 操作,需要转换成 sum/count 并在查询节点中执行,即在查询节点中 avg = sum/count。以 avg(rate(http_request{code=200}[1m])) 为例,整体流程如下:
Vmstorage 节点在首次收到查询请求时,通过查询获得 Timeseries 原始数据,rate 后同时计算出 sum 和 count 数据。其中,sum 得到的数据返回给查询节点;同时存储 count 数据到 cache 中。查询节点在接收到第一次返回的 sum 数据后,会发送第二次查询请求以获取 count 数据。当查询节点收集到所有存储节点的 sum 和 count 数据,通过 sum/count 操作计算出最终的 avg 结果。
当前计算下推优化已经在推荐、社区业务监控落地。这一优化措施对于处理大数据量的慢查询,实现了几倍甚至几十倍的查询速度提升。同时,查询量上限也得到了显著提升,例如在推荐的排序服务中,一个查询支持的查询范围提升了 70 倍。
2)查询数据量限制
在 Vms 当前的查询逻辑中,查询节点将 Metric + Tag 传递给存储节点,存储查询到 Timeseries 后返回给查询节点;查询节点再将各个Vmstorage 返回的 Timeseries 进行加载处理。在存储节点数量多,且单次返回的 Timeseries 数量较大的情况下,可能会导致查询节点出现 OOM 的问题。根据我们对实际线上 case 的观察,一些大数据量查询的场景中,查询进程的单次内存使用量超过了 50GB。
因此,我们需要从多个角度对查询内存进行保护,以防止由于一些不合理的查询条件,导致查询节点出现 OOM。首先,针对单个 Query 的多个子查询,在加载处理 Timeseries 时,需要预估内存使用量,并对其超限的查询进行限制。对于单个查询内的多个并发执行的子查询,应进行统一计算和限制内存使用量。
此外,在计算下推的场景下,存储节点本身会在查询数据后,对查询结果进行计算。因此,存储节点本身也需要限制内部数据量的上限,以确保其正常运行。
高基数问题(Cardinality)一直是 Metrics 领域避不开的话题。在云原生生态中,例如 Kubernetes 自带标签和服务自定义标签设计的不合理、 Label 取值宽泛等问题常常导致高基数问题的出现,时间线爆炸影响存储和查询集群的稳定性。
在小红书内部,高基数问题尤为明显,经常出现由于用户使用不当、参数出现随机值等异常情况造成 Timeseries 数量激增的情况,进而导致数据采集和存储资源水位不断上涨。历史上出现的存储单节点崩溃现象,90% 以上的 Root Casue 均是由高基数问题引起的。
为了解决高基数问题,一般有以下几种思路:
通过产品化+自动化的方式,实现低成本的 Label 基数控制,满足用户差异化的合理基数需求,有效保障数据采集和存储的稳定性。
独立高基数指标的数据处理通道,提供更高能力的数据存储方式,不影响其他指标的稳定性运行
用户使用门槛增加,需要其理解基数管理的概念,并合理评估取值上限
采用独立高基数指标采集和存储方式后,很难无缝兼容原有的查询使用习惯
高基数问题没有一个完美的解决方案,需要在性能、稳定性和成本之间进行平衡。不同用户有差异化 Label 基数需求,例如基础组件、复杂业务场景的 Label 基数需求更高,不应该采取一刀切策略。
因此,我们的策略是在时序存储写入的容忍范围内,尽可能满足用户合理的指标基数需求,并通过上述计算下推的能力提升用户高基数的查询体验。
对于不合理的使用场景,严格抑制由随机取值造成的时序数据急剧膨胀问题。针对类似用户行为、商品订单和算法实验等真实高基数场景,建议采用日志或者独立 Push 通道进行采集和处理。综上所述,我们最终选择”差异化动态基数管理 + 高基数隔离“的解决方案,整体架构如下:
其中,Label_manage 是 Vminsert 中可插拔的核心扩展模块,它主要负责完成以下工作:
对接收的指标进行数据的统计、乱码检测和 Label Value 的采样检测等操作。对于高基数或乱码的 Label,会将其归类为特定的 Label Value。
Label_manage 会接收 Registry 下发的用户自定义 Label 配置,并动态生成 Label 检测配置,1 分钟内即可完成用户变更生效。
Label_manage 会定时将指标元数据、乱码、高基数等统计数据上报给 Registry 服务,提高问题定位的效率。
Label_manage 提供了分级的高基数检测策略,全局 Label 白名单检测优先,Label 基数超限后会触发指标级别的 Label 高基数检测,并支持不同服务指标的 Label 隔离。每个 Label 都会绑定一个 Bloomfilter,用户申请的 Metric 和 Label 配置会动态生成指标级别 Bloomfilter。超出基数限制的 Label Value 将被统一归类为特定的值,并对数据进行重组。具体检测逻辑如下所示:
小红书业务部署在多个云上,Metrics 最初的跨云部署情况如下:
在每个云环境的 K8s 集群内,我们部署了 Prometheus 来负责采集集群内的 Pod 指标和基础指标,并将所有采集到的指标统一写入到对应云环境的存储中。然而,在混合云部署中,这种设计带来了两个主要问题:
跨云传输问题:由于监控数据需要跨云传输,导致跨云带宽成本高。
单 Region 故障风险:所有数据都存储在一个云环境中,一旦该环境出现问题,整个监控系统将面临整体不可用的风险。此外,如果跨云专线发生故障,可能导致其他 Region 的监控数据无法访问。
为了应对业务监控数据在跨多云环境中的挑战,我们采取了一系列措施。
首先,我们对采集、写入和存储进行了单元化部署,确保每个云环境都能独立处理相关任务。其次,在查询阶段我们通过联合多个云环境的存储节点进行数据检索,此架构下,查询过程所需的日常带宽相对于数据采集和写入而言低很多。进一步进行计算下推改造后,存储节点返回到查询节点之间的数据量显著减少,从而进一步降低了查询带宽的占用。总体而言,跨云带宽相对之前大幅降低。以流量较大的推荐集群为例,迁移后的月度专线成本降低约 80%,显著降低资源成本。此外,通过调整后的架构,即使一个云环境发生故障,也不会对其他云环境上的监控可用性造成影响。
三、总结与展望
过去一年多的时间(2022.10~至今),我们对线上的各个 Metrics 集群进行了梳理、重构和迁移,覆盖了公司推荐、搜索、社区、广告、基础设施等几乎所有关键业务领域。目前,我们维护着近 30 个 Vms 数据源,单个集群的单采集周期指标量达到近十亿,整个集群资源规模达到数万核。在整个技术演进的过程中,我们取得了一些关键成果:
稳定性:大幅降低采集端出现 OOM 的概率;增强对高基数指标的治理能力;基本实现全链路的高可用性,并具备分钟级的快速扩缩容能力。
成本优化:采集端性能提升近十倍,降低成本折合数万核 CPU, 数百 TB SSD;通过自动化工具,我们大幅加快集群迁移速度,无须双跑等待,从而降低迁移成本;通过跨云多活改造,降低带宽成本超过 80%。
查询性能提升:通过内存保护机制大幅降低查询节点的崩溃频率,计算下推能力在查询速度和查询上限方面得到了数倍到数十倍的提升。
运维提效:日常升级变更实现白屏化操作,日常无须额外的运维人力。
在当前基础上,我们还有许多工作想要实现。未来计划支持更长时间的指标存储、容量运营、基于容量的自动扩缩容、开源版本升级同步等方面的升级迭代,以满足业务需求,欢迎有志之士一同加入我们。
作者介绍
韩柏(刘正峰):基础技术部/可观测技术组
小红书可观测技术工程师,毕业于上海交通大学,从事推荐架构、基础架构工作,在可观测、云原生、中间件、性能优化等方面有较为丰富的经验。
布克(章正中):基础技术部/可观测技术组
小红书可观测技术工程师,毕业于南京大学,从事基础架构可观测相关工作,熟悉监控基础组件、时序数据库相关的研发。
阿普(王亚普):基础技术部/可观测技术组
小红书可观测技术工程师,毕业于河海大学,在可观测、PaaS、云原生等基础技术领域有丰富的经验。
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721