Uber 如何为 PB 级数据湖构建 I/O 可观测性:从 HDFS/GCS 客户端埋点到 HiCam 高基数聚合

本文是对 I/O Observability for Uber’s Massive Petabyte-Scale Data Lake 的整理与翻译。

内容结构概览

本文主要分为八部分:

  1. Uber 为什么在 CloudLake 混合云迁移过程中必须建设数据 I/O 可观测性。
  2. 过去缺少统一 I/O 视角时,数据平台遇到了哪些具体问题。
  3. Uber 如何在 HDFS/GCS 客户端层面做透明埋点,实现高覆盖、零业务代码改造。
  4. 为什么这种细粒度观测会引入高基数时间序列问题。
  5. HiCam 如何作为实时聚合层,降低指标规模和存储成本。
  6. 这些 I/O 指标如何进入 Pinot、Hive、DataCentral 和 Grafana,服务于工程师和 on-call。
  7. 这套系统对 CloudLake 迁移、成本归因、数据分层和动态路由有什么价值。
  8. 从 Uber 的实践中,可以总结出哪些大规模数据平台可观测性经验。

一、背景:CloudLake 迁移让数据 I/O 变成一等公民

Uber 的数据基础设施正在向 CloudLake 演进。CloudLake 可以理解为 Uber 的混合云数据湖架构:一部分计算和存储能力仍然在自建数据中心,一部分则逐步迁移到云上。随着云上计算和存储容量持续扩张,原有本地容量也会逐渐下线。这个过程中,数据访问路径变得更加复杂,跨云厂商、跨区域、跨存储系统、跨计算引擎的数据读写会越来越常见。(Uber)

在这种架构下,I/O 可观测性不再只是“看一下读写吞吐”的辅助能力,而是影响云迁移成本、任务性能、网络拥塞、数据放置策略和存储分层策略的基础能力。一个 Spark 作业到底从哪里读了多少数据?一个 Presto 查询是否产生了大量跨云出口流量?某个数据集分区是不是经常被云上任务访问?哪些应用正在制造高额 egress 成本?如果这些问题回答不了,混合云数据湖就很难高效运转。

原文开头列出了 Uber 在 CloudLake 迁移中遇到的三类关键挑战。第一,跨服务提供商的网络链路可能成为瓶颈。第二,为了提高执行效率,计算任务最好和数据集放在同一个位置,但大量实验性任务没有固定读取模式,导致数据放置很难提前规划。第三,Uber 的数据规模极大,出于成本考虑必须做高效的数据分层,而做分层的前提是能看到真实的数据访问热度,也就是需要一张准确的 I/O heatmap。(Uber)

Uber 每天运行数百万个 Apache Spark、Presto 和 Anyscale Ray 工作负载,数据分布在 Apache HDFS 和云对象存储中。但在建设这套系统之前,Uber 并没有一个统一机制能回答“每个作业读写了多少数据、从哪里读、写到了哪里”这样看似基础却非常关键的问题。(Uber)


二、问题本质:不是没有日志,而是缺少统一、透明、细粒度的 I/O 视角

很多系统都有日志,Presto 有查询日志,Spark 有作业日志,HDFS 有访问日志,云对象存储也可能提供自己的访问记录。但这些日志通常是割裂的,它们只覆盖某个引擎、某种访问路径或某种粒度,无法形成统一的数据湖 I/O 图谱。

Uber 在原文中提到,过去 Presto 日志只能部分回答 SQL 读取问题,而且粒度主要停留在表级别,只覆盖单一查询引擎。对于 Spark、ad hoc 作业,或者直接访问 HDFS/GCS 的使用方式,就没有等价的统一视角。更重要的是,很多运维决策和迁移规划需要分区级别的数据,而不是只知道表级别统计。(Uber)

在 CloudLake 语境下,“表级别”往往还不够。一个大表可能有很多分区,不同分区的访问热度完全不同。有些历史分区很冷,适合放到低成本存储;有些最近分区被频繁扫描,适合靠近计算;有些分区会被云上任务反复读取,如果仍然放在本地数据中心,就会产生大量跨边界流量和延迟。

因此,Uber 需要的不是某个引擎自己的日志增强,而是一套满足以下条件的系统:

第一,它必须是实时或近实时的。网络拥塞和高额 egress 成本需要尽快发现,不能等几小时甚至几天后才离线分析。

第二,它必须和计算引擎无关。无论是 Spark、Presto、Ray,还是自定义作业,只要通过统一文件系统客户端访问数据,都应该能被观测到。

第三,它必须和存储厂商无关。HDFS、GCS、Amazon S3、Oracle Cloud Infrastructure 等不同存储后端,都应该尽量通过同一套机制接入。

第四,它必须能达到数据集和分区级别,而不是只停留在应用或表级别。

第五,它最好不要求业务代码改造。对于 Uber 这种规模的平台来说,如果要求每个 Spark 作业、每个 Presto 查询、每个自定义任务都手动加埋点,推广成本会非常高,也很难保证覆盖率。


三、方案一:在文件系统客户端层面做透明埋点

Uber 最终选择的切入点,是增强 Apache Hadoop 兼容的文件系统客户端,也就是 HDFS 和 GCS 客户端。具体来说,他们在负责读写数据的输入输出流上做拦截,包括FSDataInputStreamFSDataOutputStream。只要作业通过这些文件系统客户端读写数据,客户端就可以记录实际读写字节数。(Uber)

这个设计非常关键。它不是在 Spark 作业里埋点,也不是在 Presto 引擎里单独加逻辑,而是在更底层、更通用的文件系统访问路径上做观测。这样一来,只要上层系统使用 Uber 的 HDFS 客户端,就能自动获得 I/O 指标。Spark、Presto、自定义作业都可以不改业务代码而受益。原文称,最终可以做到每个 job container、每个 dataset 产生细粒度指标,并且对作业代码零改动。(Uber)

图 1 展示了这套观测路径的大致结构。应用侧包括 Spark、Presto 和其他数据平台服务;这些作业通过 Hadoop 文件系统抽象访问 HDFS、GCS 等存储;Uber 在文件系统客户端中加入观测逻辑,将读写字节数、延迟、调用次数和运行时上下文打包上报,最终进入后续指标管道。

Uber 记录的不是单纯的总字节数,而是“每个被访问文件的父目录”维度上的字节数。这个父目录通常对应数据集和分区,因此系统可以把读写量归因到具体 dataset/partition。与此同时,系统还会采集运行时元数据,例如 zone、application ID、operation type、engine,以及其他应用指标,用于后续归因。(Uber)

这类埋点看起来简单,但落地难度很高。因为 Hadoop 文件系统流在读写路径的关键链路上,任何额外逻辑都有可能影响作业性能。Uber 需要在不明显拖慢客户端性能的前提下,实时采集高维指标。因此,他们构建了 wrapped file system,把原本的文件系统调用包起来,通过方法拦截器记录延迟、调用次数等指标,然后再让原始调用继续执行。指标聚合通过 Uber 开源的 Tally 完成。(Uber)

这里的工程取舍很值得注意:指标采集必须“足够靠近真实 I/O”,否则就不准确;但它又不能太重,否则会影响数据平台本身。Uber 的做法是在客户端内先做局部聚合,再按固定周期批量发送,而不是每次 read/write 都产生一条远程指标事件。


四、为什么不能直接把所有指标打进时序数据库

客户端埋点解决了“能不能看见”的问题,但马上带来第二个问题:规模太大。

原文给出了一组数据:Uber 基础设施每天运行约 670 万个 YARN container,约 40 万个 Spark application,以及约 35 万个 Presto query。每个 container 可能读写多个路径,并且每分钟按路径发出指标。如果把这些指标直接写入时序数据库,每天可能产生数百亿级事件,足以压垮存储和可观测性基础设施。(Uber)

这就是典型的高基数时间序列问题。所谓高基数,指的是维度组合数量极大。比如一个 I/O 指标可能包含 application_id、dataset、partition、path、engine、zone、host、user、operation type 等多个标签。每个标签都有大量取值,组合起来后,时间序列数量会爆炸。

在传统监控系统里,我们通常会避免把pathapplication_id这种高基数字段直接作为指标标签,因为它会造成存储膨胀、查询变慢、聚合困难,甚至影响整个监控系统稳定性。但 Uber 这里的问题是:这些高基数字段恰恰是业务最需要的。没有 application_id,就无法知道哪个任务制造了高 egress;没有 path 或 partition,就无法知道哪个数据集分区应该迁移或复制;没有 zone,就无法分析跨区域流量。

因此,Uber 不能简单地丢弃这些维度,而是需要在指标进入最终存储之前,先做一个实时降维和聚合层。这就是 HiCam 的作用。


五、HiCam:面向高基数指标的实时聚合层

为了解决高基数指标规模问题,Uber 构建了 HiCam。它是一个轻量级的内部指标聚合器,作为远程 HTTP 服务运行,背后由 Apache Flink job 支撑。它位于客户端和下游存储之间,充当 reduction layer,也就是实时削减指标规模的中间层。(Uber)

客户端使用基于 Tally 的 HTTP metrics reporter,每隔几分钟批量发送指标。HiCam 接收来自数千个 container 的指标,在内存中按application_id等维度做聚合,然后每隔几秒发出合并后的事件。这样可以显著降低写放大,避免为同一个应用、同一个数据集存储大量重复指标。(Uber)

图 2 展示了 HiCam 的整体结构。客户端通过 Zookeeper discovery pool 发现 HiCam 服务,请求会被路由到某个 reducer,这个 discovery pool 在这里起到了类似间接负载均衡的作用。客户端把指标发送到聚合层后,HiCam reducer 进行归约,再把聚合后的数据发到 topic。这个 topic 后续会被 Apache Pinot 摄取,用于实时分析。(Uber)

HiCam 的价值在于,它没有牺牲关键维度,而是在进入存储前对重复和可聚合的数据做削减。对于高基数指标系统来说,这是一种很实用的思路:不要把所有原始点都直接写入数据库,而是在网络层和写入层之间增加一层流式聚合,把“可合并的噪音”提前处理掉。

原文还提到,HiCam 具备高可用能力,并通过 Apache Zookeeper 做服务发现。现在它已经成为 Uber 文件系统级可观测性的标准管道。更广泛地看,这个方案也不只适合 I/O 指标,只要是容器化或虚拟化环境里每天产生数十亿指标、又需要实时聚合的企业级场景,都可以借鉴类似架构。(Uber)

表 1 展示的是使用 HiCam 后,性能和存储规模上的削减效果。虽然不同场景的具体数值会变化,但它表达的核心很清楚:高基数指标如果直接写入下游系统,会造成极大的存储和查询压力;在入口处做实时聚合,可以显著降低后端压力。


六、观测事件包含哪些维度:不只是字节数,而是可归因的数据流

一个 I/O 可观测系统如果只记录“读了多少字节”并不够。真正有用的是把这些字节数归因到具体应用、用户、引擎、路径、数据集、zone、操作类型等维度上。

原文提到,最终系统可以在客户端侧约 5 分钟延迟内提供实时分析;对于关键场景,这个延迟可以降低到约 1 分钟。指标会被组织成观测事件,提供 owner、application_id、engine 等归因维度,从而帮助工程师定位网络使用来源。(Uber)

表 2 展示了观测事件中捕获的维度。它的作用不是让指标看起来更丰富,而是让后续问题可以被回答。例如:

当云出口费用突然升高时,可以按 application_id 找到主要贡献者。

当某条 on-prem 到 cloud 的网络链路拥塞时,可以按 engine、path、dataset 识别读写来源。

当某个 Spark 作业变慢时,可以查看它的文件系统延迟和网络读写量,判断瓶颈是在计算、存储还是网络。

当规划数据集迁移时,可以按分区访问热度判断哪些数据应该靠近云上计算,哪些数据可以继续留在低成本存储层。

这类归因能力,是 CloudLake 迁移能否精细化推进的基础。没有它,迁移只能按粗粒度经验推进;有了它,平台团队可以根据真实访问模式做数据放置、复制、分层和成本分摊。


七、结果展示:DataCentral、Grafana、Pinot 和 Hive 如何协同

有了客户端埋点和 HiCam 聚合之后,下一步是把结果真正交给工程师使用。Uber 把这些指标接入了内部数据可观测平台 DataCentral。用户可以在应用级页面查看自己的 Spark 应用详情,分析网络使用、文件系统延迟和性能瓶颈。(Uber)

图 3 展示的是 DataCentral 中的应用级洞察页面。对于数据工程师来说,这类页面比单纯的底层监控更直接,因为它从“我的作业为什么慢”“我的应用产生了多少网络流量”“我读写了哪些路径”这些问题出发,而不是要求用户自己去底层指标系统里拼查询。

同时,Uber 也把这些可观测性数据接入了聚合 Grafana dashboard,供工程师、on-call 和应用 owner 做实时故障缓解和分析。用户可以按读写模式、高 egress 应用、热点表、热点路径等维度切片,并查看每个维度上的准确网络吞吐。额外的 top users、top hosts 等指标也会提供给最终用户。(Uber)

图 4 展示了用于故障缓解和洞察的 dashboard。它的意义在于,当某个网络方向出现异常流量时,on-call 不需要从多个系统里手工拼接线索,而是可以直接看到哪些应用、哪些路径、哪些数据集正在制造流量。

图 5 展示了 on-prem 到 GCS 方向的出入流量。对于混合云数据湖来说,这类图非常关键,因为跨边界流量直接影响成本、延迟和链路拥塞。Uber 提到,这些 dashboard 支撑了多个高 egress 使用量告警;由于指标具备实时性,工程团队可以在数秒级获得降级风险感知和调试能力。(Uber)

在存储层面,实时分析数据会进入 Apache Pinot 表,保留有限时间窗口用于实时查询;同时,数据也会进入 Apache Hive 表,用于离线分析、成本分摊、长期可视化和战略规划。也就是说,Uber 不是只建了一个 dashboard,而是同时满足实时故障响应和长期规划分析两类需求。(Uber)


八、规模和效果:支撑 40 万 Spark 应用与 200 万 Presto 查询

原文提到,这套 I/O 可观测性能力现在支撑 Uber 每天超过 40 万个 Spark application 和 200 万个 Presto query,并且在 Pinot 中只需要不到数 TB 的存储需求。(Uber)

这个结果很有代表性。因为如果只看前面的原始规模,每天 670 万个 YARN container、数十万 Spark 应用、数百万 Presto 查询、每个 container 多路径读写、每分钟发指标,最终事件量会非常恐怖。如果没有客户端聚合和 HiCam 实时归约,这套系统很容易变成“为了观察系统而制造另一个更大的系统压力”。

Uber 的方案说明,在超大规模数据平台里,可观测性本身也必须被当作基础设施工程来设计。它不是简单加日志、加指标,而是要考虑覆盖率、性能开销、标签基数、存储成本、查询延迟、下游消费能力和用户体验。

原文还提到,这些指标会为 CloudLake 迁移策略提供关键决策依据,并会随着云迁移推进用于动态路由决策。也就是说,这套系统不仅用于“看见过去发生了什么”,还会逐步参与“决定未来应该怎么调度和放置数据”。(Uber)


九、这套系统解决了哪些实际问题

1. 云出口流量归因

在混合云环境里,网络 egress 是非常重要的成本项。一个作业如果频繁从云外读取数据,或者把大量数据跨云传输,就可能产生显著成本。过去如果没有统一 I/O 可观测性,平台团队只能看到总流量上升,却不一定知道是谁造成的。

现在,系统可以把字节数归因到应用、用户、引擎、路径和数据集。这样就能回答:哪些 workload 正在制造高 egress?哪个 owner 应该关注?是否需要调整数据放置?是否需要复制热点数据?是否应该限制或优化某类访问模式?

2. 跨 zone 流量监控

跨 zone 流量不只影响成本,也可能影响作业延迟和网络拥塞。尤其在数据湖架构中,计算任务和数据集不在同一个 zone 时,作业可能表现出更高尾延迟。

通过记录 zone 和路径维度,Uber 可以知道哪些任务产生了跨 zone 数据访问,哪些数据集经常被远程读取。这为 co-location,也就是计算和数据同位部署,提供了数据依据。

3. CloudLake 数据集放置

CloudLake 迁移不是简单把所有数据搬到云上。数据规模太大,访问模式复杂,迁移成本也高。平台团队需要知道哪些数据值得迁,哪些数据适合复制,哪些数据可以继续留在原位置。

I/O heatmap 可以告诉平台团队真实访问热度。热数据、跨边界高频访问数据、关键业务依赖数据,可能优先迁移或复制;冷数据或低频访问数据,则可以放在更低成本的存储层。

4. 存储分层

数据分层的核心是把不同热度、不同价值、不同访问频率的数据放到不同成本和性能的存储层。如果没有真实访问数据,分层策略就只能靠经验或静态规则。

Uber 的 I/O 可观测性提供了“按真实使用模式分层”的可能性。未来它还计划结合 storage class 和 object metadata,让分层策略更精确。原文在结论中也提到,这套基础能力将解锁基于真实使用模式的数据分层。(Uber)

5. 故障排查和性能瓶颈定位

当一个 Spark 应用变慢时,常见原因可能包括计算资源不足、输入数据过大、shuffle 异常、读取远程数据、文件系统延迟升高、云对象存储波动等。如果没有 I/O 维度,排查很容易只停留在计算框架内部。

现在工程师可以在 DataCentral 或 Grafana 上直接查看应用级网络使用、文件系统延迟和读写路径。这样可以更快判断问题是否来自存储或网络,从而缩短故障定位时间。

6. 成本分摊和 chargeback

大型平台往往需要把资源成本归因给业务、团队或应用。网络出口成本尤其需要精细归因,否则很容易变成平台团队整体承担的“黑盒成本”。

Uber 将这些指标接入 Hive 做离线分析,其中一个用途就是 chargeback,也就是成本分摊。通过 application_id、owner、dataset 等维度,可以更公平地把成本映射到真实使用方。


十、为什么客户端侧埋点比引擎侧日志更适合这种场景

这篇文章最值得注意的技术选择,是 Uber 优先在客户端文件系统层做埋点,而不是为每个计算引擎分别做日志增强。

引擎侧日志当然也有价值。比如 Presto 可以知道 SQL 查询计划,Spark 可以知道 stage 和 task 信息。但引擎日志的问题是覆盖范围天然有限。每个引擎都要单独实现一套逻辑,而且不同引擎之间的数据口径很难统一。

客户端侧埋点的好处是统一。只要访问数据最终经过 Hadoop-compatible file system client,就可以用一套逻辑记录读写。这个层级更接近真实 I/O,也更容易扩展到新存储后端。原文提到,这种 base file system 层面的修改使 Uber 可以跟踪所有继承它的基础设施,也能更容易支持 GCS/S3 等新 vendor,降低 vendor lock-in 风险。(Uber)

不过,客户端侧埋点也有代价。它位于关键路径,必须非常谨慎控制开销;它会产生极高规模的细粒度指标,必须通过聚合层处理;它需要考虑 feature flag、兼容性和运行时稳定性。Uber 的方案之所以能落地,是因为它不只是加了埋点,还同时设计了 Tally 聚合、HiCam reduction、Pinot 实时分析、Hive 离线分析和 DataCentral/Grafana 用户入口。

也就是说,客户端埋点只是入口,真正完整的是一条端到端数据可观测性链路。


十一、从 HiCam 看高基数指标治理

HiCam 这个设计对很多工程团队都有借鉴意义。现在很多平台都在做可观测性增强,但高基数指标常常是第一道坎。

在 Kubernetes、YARN、Spark、Flink、微服务、数据湖、云对象存储等场景中,指标维度很容易爆炸。比如容器 ID、pod 名、job ID、user、path、query ID、dataset、partition 等字段都非常有用,但如果不加控制地写入时序数据库,很快就会遇到成本和性能问题。

Uber 的做法不是放弃这些维度,而是在写入最终存储前构建一个聚合层。这个层有几个特点:

第一,它靠近数据入口,能在指标还没有落地前削减规模。

第二,它按业务上有意义的维度聚合,而不是盲目采样。

第三,它保留关键归因能力,例如 application_id、owner、engine、path 等。

第四,它把实时分析和离线分析分开,Pinot 服务短期实时查询,Hive 服务长期规划和成本分摊。

第五,它可以通过 Zookeeper discovery 和 Flink 后端保证可扩展性与可用性。

这类架构说明,高基数指标不是不能做,而是必须设计“从客户端到聚合层再到存储层”的完整路径。直接把所有原始指标打到时序数据库,通常不是可持续方案。


十二、未来方向:从 I/O 可观测到智能数据湖治理

原文在结论中提到,这套 I/O 可观测性基础能力将解锁多个未来方向,包括:基于真实使用模式的数据分层;把 storage class 和 object metadata 融入指标;扩展到 HTTP 流量以观测跨服务数据流;在用户或数据集级别提供实时 quota 告警。(Uber)

这些方向说明 Uber 的目标并不只是“看见数据读写”,而是让 I/O 可观测性变成数据湖治理的底座。

如果系统知道某个数据集热度很高,就可以自动建议更高性能的存储层,或者把数据复制到离计算更近的位置。

如果系统知道某个用户或应用突然产生异常 egress,就可以触发实时 quota 告警,避免成本失控。

如果系统知道某些服务之间有大量 HTTP 数据流,也可以进一步把文件系统 I/O 之外的跨服务数据访问纳入统一视图。

如果系统结合对象元数据和存储类型,就可以更精确地分析“性能、成本、热度、位置”之间的关系。

在云迁移早期,可观测性主要用于看清现状;随着平台成熟,它会进一步参与自动化决策,比如动态路由、数据放置、冷热分层和成本优化。


十三、总结:大规模数据湖可观测性的关键不是多打日志,而是找对观测层级

Uber 这篇文章讲的是 PB 级数据湖 I/O 可观测性,但它的核心经验并不局限于数据湖。它说明了一个很重要的工程原则:在大规模平台里,可观测性不能只靠各个系统自己打日志,而要找到一个既足够底层、又足够通用、还能表达业务语义的观测层级。

对于 Uber 来说,这个层级就是 Hadoop-compatible file system client。通过拦截FSDataInputStreamFSDataOutputStream,Uber 可以在不修改作业代码的前提下,覆盖 Spark、Presto 和自定义作业,记录每个应用、每个数据集、每个分区的读写行为。(Uber)

但仅有埋点还不够。因为 Uber 的规模会把细粒度指标放大成每天数十亿甚至更多事件。HiCam 的作用就是在客户端和下游存储之间建立实时聚合层,把高基数指标变成可承受、可查询、可分析的数据流。最终,这些数据进入 Pinot、Hive、DataCentral 和 Grafana,分别服务实时排障、长期规划、成本分摊和工程师自助分析。(Uber)

从结果看,这套系统已经支撑 Uber 每天超过 40 万个 Spark application 和 200 万个 Presto query,同时只需要不到数 TB 的 Pinot 存储。它帮助 Uber 实现云出口流量归因、跨 zone 流量监控、CloudLake 数据集放置、存储热力图、故障排查和成本规划。(Uber)

这篇文章给基础设施团队的启发可以概括为三点。

第一,混合云数据湖必须重视 I/O 可观测性。数据在哪里、谁在读、读了多少、跨了哪些边界,这些问题直接影响成本、性能和迁移策略。

第二,透明埋点比要求业务逐个改造更适合大规模平台。把观测能力下沉到统一客户端层,可以用更低成本获得更高覆盖率。

第三,高基数不是理由,架构不当才是问题。只要在入口聚合、实时削减、分层存储和归因维度之间做好设计,高基数指标也可以成为平台治理的重要数据资产。

最终,Uber 用这套系统替代了过去零散的、引擎特定的日志方式,构建出统一、引擎无关、低侵入的数据湖 I/O 可观测层。对于正在做云迁移、数据湖治理、存储成本优化或大规模数据平台可观测性的团队来说,这是一篇非常值得参考的工程实践。