一、引言
HDFS 的设计出发点很清楚:大文件、批处理、高吞吐、廉价机器、故障常态化。在 HDFS 里,数据不是抽象地"存在存储服务里",而是落在 DataNode 的本地磁盘上。NameNode 管理命名空间和文件到 block 的映射,DataNode 负责读写请求和 block 的创建、删除、复制;用户数据不会经过 NameNode。

这套架构的核心优势是"数据本地性",问题也埋在这里。HDFS 的存储容量、计算容量、集群生命周期天然绑在一起:加算力往往也加磁盘,加存储也常常加机器;集群下线、扩缩容、跨业务共享数据,都要和 DataNode、NameNode、机架、权限、配额、数据均衡一起考虑。
二、容器化后的数据困惑
大数据系统容器化之后,最先暴露的不是计算引擎能不能启动,而是数据应该放在哪里。容器适合无状态、弹性、快速重建;HDFS 则假设一批长期存在的节点持有本地磁盘和 block 副本。
markdown
容器化计算的生命周期
Spark Executor / Flink TM / Trino Worker
|
| 可以快速创建、销毁、漂移
v
Kubernetes Pod / Container
|
| 本地盘不再天然等于长期数据资产
v
数据到底放哪里?
如果把 HDFS 原样搬进 Kubernetes,确实可以运行,但它并没有彻底解决云原生大数据的底座问题。因为数据依然绑定在存储节点上,计算弹性和存储弹性没有真正解耦;数据平台仍然要维护一套有状态分布式文件系统的生命周期。
对象存储给出的答案更符合云原生:数据放在独立、可横向扩展、通过 API 访问的存储服务里,计算集群按需启动,任务结束后释放资源。以S3(Simple Storage Service)为例, 它定义为对象存储服务,面向数据湖、备份、移动应用、网站和大数据分析等场景,提供可扩展性、可用性、安全和性能能力 。
三、计算存储分离的变化
计算存储分离听起来像一句架构口号,落到大数据平台里,其实包含四个具体变化:数据位置变化、扩缩容方式变化、故障域变化、性能模型变化。

HDFS 里,文件被切成 block 并复制到多个 DataNode;NameNode 根据心跳和 block report 掌握 DataNode 状态,并负责 block 复制决策 。对象存储里,应用面对的是 bucket、object、key,读写通过 HTTP API 完成,底层复制、纠删码、可用区布局由存储服务隐藏。
这种变化让平台具备了三个新能力。第一,计算集群可以短生命周期化,不再为了"数据还在本地盘上"而长期保留。第二,多套计算引擎可以共享同一份数据湖,减少跨集群复制。第三,存储可以独立做生命周期、归档、跨区域复制、权限与审计。
代价是性能直觉会改变。S3 通过 S3A 使用时不同于 HDFS:通信从 RPC 变为 HTTP GET/PUT/HEAD/LIST/COPY,请求延迟更高,数据本地性变成远端访问,rename() 由快速原子操作变成 COPY + DELETE 模拟,seek() 可能触发新的 HTTP 请求 。
四、对象存储不是文件系统
很多迁移失败,源于把对象存储当成"无限大的 HDFS"。S3A 可以把 S3 暴露成类似文件系统的接口,虽然连接器让 S3 看起来像文件系统,但它并不是文件系统,一些保持文件系统隐喻的做法会非常低效。
| 维度 | HDFS | 对象存储经 S3A 访问 |
|---|---|---|
| 访问协议 | RPC | HTTP GET/PUT/HEAD/LIST/COPY |
| 数据本地性 | 本地盘与机架感知 | 远端对象存储服务 |
| 一致性 | 数据与列表一致 | S3 自 2020 年后提供强一致 |
| 重命名 | 快速、原子 | COPY + DELETE 模拟,慢且非原子 |
| 删除目录 | 快速、原子 | 目录删除可能慢且非原子 |
| 读模式 | 本地 seek 较快 | seek 可能变成额外 HTTP 请求 |
| 扩缩容 | 计算与存储更耦合 | 计算与存储独立扩缩 |
S3 现在对对象的 PUT、DELETE 提供强读后写一致性,对覆盖写、删除、HEAD、对象元数据、标签、ACL 读取也提供强一致;LIST 也能反映 bucket 中对象的准确状态 ,这解决了早期云上数据湖中常见的"写完以后 list 看不到"的问题。
但一致性增强没有消除所有文件系统语义差异,即使 S3 已经一致,rename() 问题仍然存在:S3A 仍通过复制再删除来模拟重命名,这可能中途失败,也无法阻止集群中其他进程同时尝试重命名 。

这解释了"为什么容器化之后性能会变"。不是容器本身让大数据变慢,而是底层 I/O 模型从本地 block + RPC 变成远程对象 + HTTP 请求;过去依赖 rename、目录 listing、随机 seek 的代码路径,在对象存储上会暴露成本。
五、提交协议重写
大数据任务写结果时,经常采用"先写临时目录,再提交到最终目录"的模式。在 HDFS 上,这依赖目录 rename() 的原子性;在 S3 上,这条路不再安全。
标准的 FileOutputCommitter 依赖目录 listing 和 rename,而 S3 对象存储与 s3a:// 文件系统客户端无法满足这些要求;使用经典提交器向 S3 提交工作,存在生成数据丢失或损坏的风险 。
解决思路是绕开"rename 提交"。任务写出时使用 multipart upload,但不完成最终提交;到 job commit 阶段再统一完成 pending uploads,从而让提交协议适配对象存储的能力模型 。

所以,大数据云原生不是简单替换 URI:把hdfs://warehouse/table改成s3a://bucket/table只是开始。真正的迁移要检查写入协议、文件格式、分区策略、提交器、并发写控制、元数据刷新和缓存一致性。
六、一致性的边界
"对象存储已经强一致"这句话必须拆开理解,S3 的对象 PUT/DELETE/GET/HEAD/LIST 一致性已经很强,但这不自动等价于"所有数据湖事务都安全"。表级事务还需要回答另一个问题:多个 writer 如何竞争提交同一张表的新版本;对象存储的一致性解决的是"对象级读写可见性",湖仓表格式解决的是"表级快照与事务可见性"。
lua
一致性的三个层次
+-------------------------------+
| 表级一致性 |
| Iceberg/Delta/Hudi snapshots |
| commit log / metadata pointer |
+---------------+---------------+
|
+---------------v---------------+
| 对象级一致性 |
| PUT/GET/DELETE/LIST/HEAD |
| strong read-after-write |
+---------------+---------------+
|
+---------------v---------------+
| 物理持久性 |
| replication / erasure coding |
| availability zones / disks |
+-------------------------------+
这里最容易踩坑的是把"LIST 强一致"误读为"并发写事务安全"。对于 append-only 批写,风险较低;对于覆盖分区、merge、delete、upsert、并发 compaction,事务日志或表格式元数据才是关键。
七、缓存层的必要性
计算存储分离之后,缓存不再只是锦上添花,而是补齐性能模型的一层。因为计算节点不再天然拥有本地数据,远程对象存储访问会引入网络延迟、请求开销、带宽上限和对象存储限流。
S3 访问可能受到 bucket shard 的限流,增加 worker 有时反而会让情况变差;读取 Parquet、ORC 这类列式格式时,seek 操作可能触发新的 HTTP 请求,从而让随机访问变贵 。这正是缓存层要解决的问题:把热点数据、footer、索引、元数据或中间结果放到更靠近计算的位置。
lua
缓存层的位置
+--------------------+
| Query Engine |
| Spark / Trino |
+---------+----------+
|
| read hot ranges
v
+--------------------+
| Cache Layer |
| local SSD / daemon |
| footer / columns |
| hot partitions |
+---------+----------+
|
| miss
v
+--------------------+
| Object Storage |
| durable cold data |
+--------------------+
缓存可以分成三类。第一类是数据缓存,例如把热点 Parquet row group 或 ORC stripe 缓存在本地 SSD。第二类是元数据缓存,例如缓存文件列表、分区、表 schema、统计信息。第三类是执行侧缓存,例如 shuffle、本地 spill、broadcast、intermediate result。
八、云原生底座的组合

对象存储提供持久化和弹性,表格式提供事务与快照,Catalog 提供表发现和元数据指针,缓存层提供性能补偿,计算引擎提供执行能力。每一层都对应 HDFS 时代隐含在同一套系统里的能力:block 管理、命名空间、数据本地性、提交语义、权限和读写优化。
大数据云原生先改存储底座,是因为存储层承载了平台最难迁移的长期状态。计算可以弹性化,服务可以容器化,调度可以 Kubernetes 化,但数据一旦写入,就会长期影响成本、性能、权限、治理和多引擎兼容。
第一,数据生命周期长于计算生命周期。一个 Spark 集群可以一天创建十次,但一张事实表可能存在五年。先让数据脱离计算集群,平台才有真正的弹性。
第二,性能瓶颈来自 I/O 语义变化。对象存储上的 rename、seek、目录操作、HTTP 请求和限流都会改变性能表现 。如果不先处理存储访问模式,容器化只会把原来的瓶颈搬到新的调度平台上。
第三,正确性依赖提交协议和元数据。S3 强一致解决了对象级可见性,但表级事务仍依赖 Iceberg、Delta、Hudi 这类元数据与提交机制,存储系统是否提供互斥能力会直接影响事务安全 。
第四,多引擎共享需要统一数据底座。Spark 写、Trino 查、Flink 增量消费、机器学习训练读取特征数据,如果每个引擎都有自己的存储假设,平台会迅速碎片化。对象存储加开放表格式,是多引擎共享数据湖的通用路径。
九、迁移中的设计参考
存储底座改造不等于把 HDFS 数据复制到对象存储。更合理的顺序是先识别工作负载,再决定对象布局、文件大小、表格式、提交协议、缓存策略和元数据治理。
sql
迁移决策路径
+------------------+
| 工作负载画像 |
| batch / stream |
| query / ML |
+---------+--------+
|
+---------v--------+
| 文件与对象布局 |
| size / partition |
| format / prefix |
+---------+--------+
|
+---------v--------+
| 表格式与事务 |
| Iceberg/Delta |
| commit protocol |
+---------+--------+
|
+---------v--------+
| 缓存与元数据 |
| data cache |
| metastore cache |
+---------+--------+
|
+---------v--------+
| 观测与治理 |
| cost / latency |
| lineage / ACL |
+------------------+
批处理追加写可以优先关注大文件合并、提交器和分区目录规模。交互式查询要关注列式文件 footer、统计信息、manifest 裁剪、metastore cache、热点数据缓存。流式写入要关注小文件、checkpoint、commit 频率和 compaction。机器学习训练还要关注高并发小范围读取、样本 shuffle、本地缓存和跨可用区带宽。
对象命名也会影响性能和可运维性,对象存储存在请求限流和 prefix 写入速率问题,批量删除、rename、过度并发都可能带来反效果 。因此,云原生数据湖要避免"无约束小文件 + 高频目录改名 + 大量递归删除"的组合。