资讯动态

计算存储分离实战:从HDFS迁移到对象存储构建云原生大数据底座

发布时间:2026/9/20 7:07:42 来源:尧图企业网站定制
从 HDFS 到对象存储计算存储分离如何重塑云原生大数据底座最近几年我明显感受到身边越来越多团队在讨论一个话题能不能把 Hadoop 里的 HDFS 干掉做大数据平台的同学都知道HDFS 这套存储从诞生到现在陪我们打了十多年仗稳定、可靠、生态完善可一旦碰上云原生改造、容器化调度、资源弹性伸缩这些新需求它就开始显得笨重了。我自己在带着团队把一套基于 CDH 的大数据平台迁到 Kubernetes 上的过程中踩了一堆坑也把 HDFS 的读写链路、元数据机制、扩容方式翻来覆去想了个透。今天这篇就围绕从 HDFS 到对象存储、计算存储分离这条主线把技术选型、迁移实操、架构改造和个人踩坑记录一并分享出来。先说一个核心判断对象存储不是来替代 HDFS 的它是来替我们卸下存储负担的。HDFS 擅长的是大文件一次性写入、流式读取、吞吐优先但它把数据副本散落在计算节点本地磁盘上导致计算和存储被牢牢绑死。对象存储则把数据统一放在远端、按桶组织、通过 HTTP 协议访问天然适合云原生环境里计算和存储各自独立扩缩容的诉求。这篇文章适合正在做大数据平台云原生改造的架构师、运维工程师也包括刚接触大数据生态、想搞清楚 HDFS 读写流程和对象存储差异的初学者。为什么 HDFS 成了云原生路上的阻力1.1 HDFS 的架构本质决定了它的边界理解 HDFS 为什么在云原生场景下不好使得先回到它的设计初衷。HDFS 是模仿 Google GFS 实现的分布式文件系统核心是 NameNode 管理元数据、DataNode 存数据块一个文件被切成若干 128MB 或 256MB 的 block每个 block 默认三副本分布在不同的 DataNode 上。写数据时客户端向 NameNode 申请 block 位置列表再按 pipeline 方式把数据依次写入几个 DataNode。读数据时客户端同样先问 NameNode 拿到 block 的位置再就近读取。这个设计在物理机时代非常合理因为那时候网络带宽宝贵、本地磁盘便宜把计算挪到数据旁边能大幅减少网络传输。但随着集群规模变大问题也来了NameNode 要维护全量文件、目录和 block 的映射关系元数据只能放在内存里。单台 NameNode 的内存上限基本就决定了整个集群能存多少文件。我们之前有个集群NameNode 堆内存调到 64GBInode 数量还是逼近千万级别每天凌晨跑全量同步任务的时候NameNode 的 GC 停顿肉眼可见直接拖慢所有 RPC 请求。1.2 计算与存储耦合带来了一大堆连锁反应HDFS 把数据存在 DataNode 的本地磁盘上这个本地就是问题的根源。集群扩容的时候你不可能只加计算不存盘也不可能只扩存储不加 CPU因为每一台节点上既有磁盘又有计算资源。业务高峰来了想快速扩充计算能力结果新加的节点没有数据任务调度上去之后还是得从老节点跨网络拉数据性能自然打折。这种耦合还影响到了 Kubernetes 化改造。在大数据平台容器化的过程中我们想用 K8s 的调度能力动态申请 Pod 跑 Spark 或 Flink 作业但 HDFS 要求计算节点和 DataNode 网络拓扑尽量近否则数据本地性变成空话。一旦 Pod 被调度到没有 DataNode 的节点上Spark 的 Task 只能远程读数据吞吐量直线下降。我们实测过远程读和本地读的吞吐差距能到 3 到 5 倍。这个数字直接浇灭了很多团队无脑容器化的热情。还有一个容易忽视的点数据副本机制在云上变成了浪费。云厂商的磁盘本身已经是三副本存储了你在云主机上用 HDFS 再写三副本等于底层存了三份、上层又存了三份存储成本乘了九倍。这不是夸张是实际的账单教育了我们——存 1TB 数据最终计费的其实是 9TB 的空间成本。1.3 小文件问题是压垮 NameNode 的最后一根稻草HDFS 对小文件极不友好这是所有大数据老兵的共识。文件小于 block 大小不会占满一个 block 的空间但会在 NameNode 内存里占一条完整元数据记录。比如你有 1 亿个小文件NameNode 至少要管理 1 亿条 Inode 和对应的 block 映射光元数据就得几十 GB 内存。而且 Spark 写分区数据时如果每批次生成的文件特别多NameNode 的 RPC 处理能力会被打满出现大量NameNode is in safe mode或者 RPC 超时。我们用对象存储之后小文件的处理逻辑彻底变了。对象存储没有 Inode 概念文件即对象桶里的对象数量可以做到海量元数据压力不在客户端侧的目录树上而在服务端的索引里。S3 官方宣称单桶对象数无上限虽然实际会有一些性能拐点但比 HDFS 那个内存上限宽松太多了。做一个不算严谨但直观的对比HDFS 单集群文件数在千万级别就需要非常小心地调优而对象存储上亿对象仍然可以靠分页列举正常管理。对象存储凭什么能扛起大数据底座2.1 从文件系统的思维转变成桶对象的思维刚开始接触对象存储的人最容易犯一个错还把它当文件系统用。对象存储没有目录树所谓的目录只是对象 key 里的公共前缀。比如你上传一个对象key 是logs/2025/06/01/app.log在对象存储的底层实现里它就是一个完整的字符串 key并没有真正的logs目录实体。你可以列举前缀为logs/2025/06/的所有对象但你不可以像在 HDFS 里那样对一个目录执行 rename 或 chmod。这个差异看起来小影响却很大。Hadoop 生态的很多组件默认依赖文件系统的 rename、list、mkdir 等语义比如 Spark 的_temporary目录机制、Hive 的分区目录切换、Flink 的 checkpoint 恢复。迁到对象存储之后这些操作要么走服务端的 copy 接口要么需要在业务代码里规避。以 OZONE 或者 S3A Connector 的实现来看社区已经做了不少适配但你在使用时还是得心中有数哪些操作是廉价的哪些操作会触发真实的网络拷贝。对象存储的另一个核心特点是读写模型简单PUT、GET、DELETE、LIST。没有 append 操作没有随机写对象一旦写入就是不可变的。要更新一个对象本质上是重新上传一个同名对象覆盖它。这和大数据场景里常见的写一次、读多次模式天然匹配反而规避了 HDFS 在并发写、文件锁上的复杂性。2.2 S3 协议成了事实标准各厂商的兼容性百花齐放对象存储的协议实现中AWS S3 协议算得上行业事实标准。国内云厂商的 OSS、COS、OBS 也都兼容 S3 的大部分接口同时提供自己特有的 SDK 和扩展头。这个局面带来了一个好处我们可以基于 S3 协议做一套统一的数据访问层底层存储根据成本、合规、性能需求灵活切换。之前我们用过 MinIO 做私有化部署也用过公有云的对象存储切换时基本只需要改 endpoint 和认证信息业务代码无感。当然兼容性不是百分之百完美的。以 Hadoop 生态接入为例S3A Connector 是 Apache Hadoop 官方提供的访问 S3 协议存储的客户端它支持s3a://bucket/path这种 URI。但在处理目录标记directory marker、MPU 分片上传、ETag 弱一致性校验等方面不同对象存储的细节处理会有差异。MinIO 对 S3 的兼容度很高但它在 list 性能上对极深前缀的支持不如公有云服务好公有云对象存储大多没有 list 延迟焦点的烦恼但可能会在请求频率上做限流。选型时要结合自己的访问模式不要只看兼容 S3这几个字。2.3 数据本地性不再重要计算拓扑回归简单对象存储对计算侧最大的解脱是数据在哪里已经不重要了。所有计算节点通过同一个 endpoint 访问数据吞吐量不再依赖节点间的物理距离而是取决于网络的带宽和延迟。这意味着 Spark、Flink 跑在任何一台有网络可达性的节点上性能表现是一致的。这一点对 Kubernetes 环境尤其重要。你可以放心地把 Spark Driver 和 Executor 调度到任意 Pod 上不用再关心这个 Pod 是否和 DataNode 在同一个物理机架。我们也因此在 K8s 上使用 Spark Operator 时可以大胆地设置 executor 的 request 和 limit让它随意漂移而不影响数据读取性能。计算存储分离之后任务跟着数据跑变成了数据在远处等着计算来取虽然单次读的网络开销大了但整体调度能力和资源利用率显著提升。在大规模批处理场景中实测作业耗时并没有明显变长很多任务反而因为资源充足、并行度拉满而变得更快。迁移实操从 HDFS 到对象存储的完整路径3.1 盘点现状到底有什么、该迁什么、先迁什么迁移的第一步不是摸工具而是摸数据。我们当时对集群里的所有目录做了分层盘点按数据的重要程度和访问频率把数据分为三类核心资产数据包括经过 ETL 加工的主题表数据、用户画像标签、风控特征数据等这类数据是业务的命脉必须保证迁移零丢失且要做到新旧双写一段时间的校验。中间临时数据比如 SQL 任务里的临时表、Spark 作业的 shuffle 中间结果、测试环境的临时文件这类数据可以大胆放弃直接重跑任务生成不用迁。日志和原始数据埋点日志、业务原始表这类数据体量大、访问频率低适合做生命周期管理可以先迁到低频存储甚至归档存储里。我们最终的策略是临时数据一律不迁跑任务时自动重新生成核心数据搬迁移原始日志数据采用先双写、再批量回填的方式处理。这个分类思路帮我避免了很多无效迁移工作。你得记住一个原则迁移的最终目标是让业务正常跑不是为了把 HDFS 上的所有字节都搬到新地方。3.2 工具选型distcp 为主、自研脚本为辅HDFS 到对象存储的迁移工具第一选择永远是 distcp。它是 Hadoop 自带的分布式拷贝工具天然支持在集群内发起 MapReduce 作业来完成海量数据转移。使用 distcp 迁到 S3 或 OSS 时基本原理是在集群的一个节点上提交一个 MapReduce 作业让多个 mapper 同时读取源 HDFS 上的文件然后写入到目标对象存储的 bucket 里。我推荐的核心命令大致长这样hadoop distcp \ -Dfs.s3a.access.keyyour_access_key \ -Dfs.s3a.secret.keyyour_secret_key \ -Dfs.s3a.endpointoss-cn-hangzhou.aliyuncs.com \ -Dfs.s3a.path.style.accesstrue \ -Dfs.s3a.connection.maximum1000 \ -Dfs.s3a.multipart.size128M \ -Dfs.s3a.fast.upload.bufferdisk \ -Dfs.s3a.threads.max32 \ -DskipCRC \ -m 200 \ -pugp \ /data/core/* s3a://your-bucket/data/core/几个参数我要特别解释一下。-m 200表示同时跑 200 个 mapper这个数字要根据集群可用资源来定太大容易把集群资源打满影响线上业务太小则迁移速度慢。-pugp表示保留用户、组和权限信息这个对 Hive 和 Ranger 权限模型很重要。我一开始漏了这个参数迁完后发现所有文件的 owner 都变成了跑 distcp 的用户下游任务读取权限报错排查了半天。另一个重要的参数是-Dfs.s3a.fast.upload.bufferdisk。对象存储的上传走的是 multipart upload先把数据写到本地临时缓冲再分批传到远端。如果你的文件很大缓冲模式用 memory 容易 OOM改成 disk 会更稳。我们始终强调迁移不是使劲调并发就能提速的瓶颈往往在源端 NameNode 的 RPC 能力、目标端对象存储的单桶 QPS 上限、还有你所在网络的带宽。合理的做法是先小规模试跑用 20 个 mapper 测出基准速度再逐步加大。3.3 元数据迁移和权限体系迁移文件数据本身只是一部分还有 Hive 表的元数据、HDFS 上的 ACL 权限、目录的 quota 配额这些看不到的数据要处理。Hive 表迁移的场景下我们是在新集群上重建了 Hive 表结构把 location 指向新的s3a://地址然后用msck repair table或者直接跑一遍MSCK REPAIR TABLE table_name同步分区。如果分区特别多用msck会比较慢也可以自己写一个并发脚本扫描分区目录去批量添加分区。权限这块要注意对象存储的权限模型和 HDFS POSIX 权限模型完全不一样。HDFS 用 user/group/other 加 ACL对象存储用 bucket policy 和 IAM 角色。我们当时对接 Ranger 做统一权限管理通过 Ranger 的 S3 插件把 Hive 层面的表权限映射到底层 S3 路径的访问控制。如果你没有这套体系迁完后要尽快在 OSS/COS 侧配置好 bucket 的访问控制策略不然容易出现数据都在、但谁都不敢动的管理真空。3.4 双跑验证老平台和新平台并存一个周期数据迁移完之后直接切流量是危险的。我们采用的做法是双跑验证周期为两周。期间旧的 HDFS 集群继续保持线上运行新的对象存储集群作为影子链路同步跑关键作业。每天对比两份数据的表行数、关键字段的和值、分区数量确认完全一致后才逐步切流量。有同学会问双跑是不是等于迁移期间要维护两套资源对成本确实会增加但这是最稳妥的过渡方式。我们没有做先迁完马上拆旧集群这种激进操作而是让新平台先承担非核心报表任务等这些任务连续一周无差错后再把核心任务切过来最后才对旧集群进行下线操作。这个过程里HiveServer2 的配置从指向 HDFS 的 location 平滑切换到指向对象存储的 locationSpark 作业里的输入路径也一样靠的是配置中心统一管理而不是逐个作业手工改。云原生底座改造计算存储分离的架构实践4.1 从数据不动计算动到计算随意动传统大数据集群里Spark、Hive、Flink 跑在固定节点上数据存在同一个节点的磁盘里。计算存储分离后数据处理引擎和数据存储之间变成了纯粹的网络上的服务调用关系。这给基础设施层的改造带来了巨大的自由度。我们的新架构里底层是 K8s 集群上面用 Spark Operator 管理 SparkApplication用 Flink Kubernetes Operator 管理 FlinkDeployment。作业需要的数据全部从对象存储读取计算资源池可以按业务优先级拆分高峰期扩到几十个 Pod低峰期缩到个位数完全由 HPA 和自定义调度策略控制。这么做的一个副产品是大数据平台的环境概念被弱化了。以前区分开发、测试、生产环境的成本很高因为每个环境都要一套 HDFS。现在只用不同的 bucket 或不同前缀即可计算资源是共享的环境隔离成本几乎为零。4.2 访问协议的选择S3A 与 OSS 原生的取舍在实际接入层设计上我们遇到了一个选择用 Hadoop 社区通用的 S3A Connector还是用云厂商提供的高性能 SDK 如 Aliyun OSS SDK for Hadoop、腾讯云 CHDFS 协议等。这里有一个很现实的考虑。S3A 的优点是生态通用性强代码里写s3a://前缀将来从一个云厂商迁到另一个云厂商或者切换到 MinIO改动成本低。但它也有弱点就是某些性能优化可能不如云厂商自有的 SDK 激进。我们用 OSS 的场景里如果走 S3A 兼容协议遇到大量小文件高并发上传的场景性能会比原生的 OSS SDK 差一些。反过来如果你用的是云厂商 SDK将来被绑定在某个特定云上的概率就增加了。我们的折中方案是所有数据写入走统一的 Data Hub 服务该服务底层根据目标 bucket 的类型自动选择是否使用原生 SDK而离线分析读路径统一走 S3A/OSS 的 Hadoop Connector。这样既保证写数据的性能又保留读路径的统一性。如果你是一个小团队没有那么多人手去维护多层抽象直接用 HDFS 的s3a方案就够了性能差异在多数离线场景下可以接受。4.3 Spark 和 Flink 在对象存储上的性能调优Spark 跑在对象存储上第一个要调的是输出文件的提交方式。我强烈建议开启spark.sql.sources.commitProtocolClassorg.apache.spark.sql.execution.datasources.SQLHadoopMapReduceCommitProtocol的替代方案或者用社区针对对象存储做的Iceberg、Hudi这类数据湖框架。直接用 Spark 默认的 FileOutputCommitter 在对象存储上会极其痛苦因为它的 task commit 依赖 rename 操作而对象存储的 rename 是 copydelete代价高昂。我们在测试中对比过同样的 TPC-DS 查询在 HDFS 上跑和对象存储上跑最核心的差距不在 scan 阶段而在 shuffle 和 commit 阶段。shuffle 的中间数据默认写在本地磁盘这个问题不大但 final task 写数据到对象存储时如果任务数量特别多、并发 commit对象存储的请求数会暴增容易出现限流。解决办法是开启 Spark 3.x 引入的spark.sql.adaptive.enabled和动态 coalesce让最终生成的输出文件数不要太多。比如目标文件大小控制在 256MB 到 512MB一个 1TB 的表最终输出 2000 到 4000 个文件commit 的压力就小得多。Flink 的适配也有讲究。Flink 的 StreamingFileSink 和 FileSink 默认会写临时文件再在 checkpoint 完成时 rename 到最终路径。在对象存储上这个 rename 变成了对远端对象的拷贝频繁 checkpoint 时开销不小。我们的经验是稍微调大 checkpoint 间隔比如从 1 分钟改成 5 分钟并且开启 checkpoint 的 incremental 模式避免每次快照全量提交。如果你用的是 Flink Iceberg那 Iceberg 本身会管理数据文件的可见性Flink 只负责写数据文件commit 由 Iceberg 的 metatable 控制这样会舒服很多。4.4 数据湖方案成了自然的演进方向对象存储之上最顺理成章的大数据底座就是数据湖方案。用 Iceberg 或 Hudi 这类表格式在对象存储上维护一张支持 ACID 的表同时兼容 Spark、Flink、Presto/Trino 的查询。这里不再有 HDFS 目录的物理分区概念而是通过表的 manifest 文件管理数据文件列表和快照。换句话说HDFS 时代目录结构即元数据分区目录需要手动维护和 repair而在 Iceberg 表里分区是一个隐藏的字段写入时自动根据分区 spec 生成数据文件并记录到 manifest 中。这大大减轻了运维负担比如之前几天就要跑一次MSCK REPAIR TABLE的场景不复存在了。这也是我特别推荐迁移对象存储时顺势引入数据湖表格式的原因。常见问题与排查技巧实录5.1 认证和权限问题迁移刚开始时我们遇到最多的是认证错误。HDFS 用 Kerberos对象存储用 AK/SK 或临时 STS TokenSpark 作业提交到 YARN 上再转到 K8s 上各种凭证传递链路非常容易弄错。常见错误是The AWS Access Key Id you provided does not exist排查时可以按这个顺序检查core-site.xml 里的fs.s3a.access.key和fs.s3a.secret.key是否正确环境变量是否被某个进程额外覆盖提交作业的机器上是否有~/.aws/credentials干扰是否配置了fs.s3a.aws.credentials.provider例如org.apache.hadoop.fs.s3a.TemporaryAWSCredentialsProvider时还需要额外携带 session token。5.2 小文件还是无处不在对象存储对大文件友好但如果在写入时还是沿用 HDFS 时代那种每个 task 各自写一堆小文件的习惯对象存储的性能同样会崩。我们踩过的坑是某张 ODS 表由 5000 个 task 并发写每 task 写 1 到 2 个小文件结果下游一次性 list 和读取时就出现严重的延迟因为每个文件都要发一个 GET list 请求。后来我们强制在写入层做了合并策略加一个REPARTITION(200)或者COALESCE(50)文件数立刻降下来查询性能翻倍。清理小文件的工具可以自己写也可以依赖 Iceberg 的rewrite_data_files动作。Apache Iceberg 对文件数过多的情况可以用spark.action.rewriteDataFiles触发数据文件合并这比我们自己写一个 scan-and-copy 脚本靠谱得多。5.3 distcp 迁移中断后的续跑数据量一大distcp 作业总会因为网络、源端 NameNode 抖动、目标端限流等原因失败。好在 distcp 支持断点续跑同一个命令加一个-update参数就能跳过已拷贝且大小一致的文件。hadoop distcp -update -append ...这个组合经常被误解。-update会检查源文件和目标文件的大小不一致才重新拷贝-append则是把源端新增的 block 追加到目标文件后面。但对象存储不支持 append 语义所以-append对 S3A 目标端没有意义。我们实际操作时一般只加-update和-delete先同步新增文件删除目标端多余的文件保证两边完全一致。这里要小心-delete的用法它会删除目标端存在但源端不存在的文件如果你目标端还有别的并发写入的数据文件会被一并删掉所以只在确认目标端没有其他写入时使用。5.4 对象存储上的目录和 Hive 分区发现对象存储的 list 操作是按前缀扫描的。Hive 在查询分区表时Metastore 里的分区信息是独立的不会主动列举目录但如果你用 Spark 的insert into新建分区后忘了更新 Metastore查询就会看不到新分区。解决办法有三种一是跑msck repair table二是在 Spark 写完后手动spark.sql(alter table xx add partition(...))三是使用 Iceberg 这类自带元数据管理的表格式让分区管理自动化。5.5 请求频率限制与成本意识几乎所有的公有云对象存储都会对单桶的 QPS 做限制。比如一个 bucket 的 GET 请求限制可能在某些规格下是几千 QPS看起来很多但如果你在 Spark 里开了 200 个 executor每个 executor 同时读 100 个文件瞬间就打满了。解决方法是尽量让读取操作本地化使用大的 split减少小请求数量。比如用 Spark 读 Parquet 时把spark.sql.files.maxPartitionBytes调大到 1GB 或更多避免一个文件被拆成七八个 partition 分别发请求。成本也是一笔账。对象存储的请求费用虽然单价低但海量小文件场景下请求费会占比很高。我们有一次某张表月请求量破亿请求费占了存储总费用的三成。优化方案还是回到了合并文件、减少文件数这条路上。所以无论你是从 HDFS 迁移到对象存储还是直接新建对象存储计算底座都要记住一个核心原则对象存储的数据布局要以少对象、大对象为美。迁移后的日常运维与架构演进心得迁完之后日常运维的维度也发生了变化。HDFS 时代运维最关注的是 NameNode 堆内存使用率、DataNode 磁盘水位、block 副本健康状态、机架感知配置对象存储时代这些都不需要操心了但这些能力变成了云厂商的 SLA 范围。反而新的关注点出现了bucket 的生命周期策略、访问日志分析、跨区域复制、数据加密和合规审计。生命周期管理是非常实用的一个功能。我们给不同的 bucket 设置了不同的规则比如日志类数据 30 天后自动转低频存储180 天后转归档存储一年后自动删除。这套规则在 HDFS 时代实现起来要自己写定时任务扫描目录、调用 distcp 搬到冷节点或者直接 rm稍微复杂和容易出错。对象存储把这些全部变成配置项节省了大量人力。容灾备份的思路也变了。以前 HDFS 做异地容灾要搭一套 DistCp 定时同步任务偶尔还要担心 Snapshot 机制和副本的完整性。对象存储天然支持跨区域复制配置好源 bucket 和目标 bucket 的复制规则数据就自动同步到异地。这个能力在大数据平台容灾架构中是加分项但对 RPO 的要求要提前评估清楚因为跨区域复制是异步的有秒级到分钟级的延迟。另外在架构层面如果你正打算从 HDFS 迁到对象存储我建议你把目光放远一点迁移本身不是终点而是数据平台重塑的起点。把 Hive 表慢慢过渡到 Iceberg 表把 Spark/Flink 作业都通过 K8s 动态调度起来把数据权限全部收口到统一认证中心这些才是真正意义上的云原生大数据底座。我个人在实际操作中最大的体会是技术选型和迁移方案不能追求一步到位更不能照搬别人的架构图。每个团队的数据规模、业务特征、人员熟悉度都不一样。比如我们团队对 Hadoop 生态很熟所以先走 distcp 迁移再谈表格式改造而有的团队从零起步直接在对象存储上用 Iceberg 建数仓绕开了 HDFS 的重担。两种路径没有优劣之分关键是搞清楚自己的存量包袱和增量诉求。最后再分享一个小技巧无论用什么工具做数据迁移都建议写一份详细的迁移检查清单把每一批次的数据表、文件数、总大小、校验结果、切换时间点记录清楚。我们当时用这套清单保证了上百张表大迁移过程零事故排查问题时也能快速定位是哪个批次出了问题。迁移这种事准备得越细运行得越稳。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价