
本文是对 I/O Observability for Uber’s Massive Petabyte-Scale Data Lake 的整理与翻译。内容结构概览本文主要分为八部分Uber 为什么在 CloudLake 混合云迁移过程中必须建设数据 I/O 可观测性。过去缺少统一 I/O 视角时数据平台遇到了哪些具体问题。Uber 如何在 HDFS/GCS 客户端层面做透明埋点实现高覆盖、零业务代码改造。为什么这种细粒度观测会引入高基数时间序列问题。HiCam 如何作为实时聚合层降低指标规模和存储成本。这些 I/O 指标如何进入 Pinot、Hive、DataCentral 和 Grafana服务于工程师和 on-call。这套系统对 CloudLake 迁移、成本归因、数据分层和动态路由有什么价值。从 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 客户端。具体来说他们在负责读写数据的输入输出流上做拦截包括FSDataInputStream和FSDataOutputStream。只要作业通过这些文件系统客户端读写数据客户端就可以记录实际读写字节数。(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 等多个标签。每个标签都有大量取值组合起来后时间序列数量会爆炸。在传统监控系统里我们通常会避免把path、application_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。通过拦截FSDataInputStream和FSDataOutputStreamUber 可以在不修改作业代码的前提下覆盖 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 可观测层。对于正在做云迁移、数据湖治理、存储成本优化或大规模数据平台可观测性的团队来说这是一篇非常值得参考的工程实践。