首页 / 资讯中心 / 文章详情

从HBase到Iceberg:列式存储演进与数据湖迁移实战

从HBase到Iceberg:列式存储演进与数据湖迁移实战 ★ FEATURED ARTICLE
做大数据架构选型的朋友对 HBase 应该不陌生从 2010 年前后开始它几乎是互联网公司存储实时写入、海量随机读写场景的标准答案。但最近两三年不管是技术社区还是实际招聘需求里Iceberg 出现的频率越来越高很多团队甚至在把 HBase 里的核心业务数据往 Iceberg 上搬。这个演进背后不只是“换一个存储组件”那么简单而是列式存储技术在整个大数据生态里的位置发生了根本变化。这篇文章我想抛开官方文档式的功能介绍从一个实际做过迁移、也踩过不少坑的从业者角度聊聊从 HBase 到 Iceberg 这条演进线路到底意味着什么。适合正在做技术选型、或者准备把 HBase 上的宽表数据迁到数据湖体系的架构师和数据工程师参考内容会涉及存储模型差异、核心能力拆解、迁移实操步骤以及一些文档里不会写的坑。1. 技术演进的整体脉络与设计思路1.1 为什么演进的主线落在 HBase 和 Iceberg 上聊演进之前得先把 HBase 的历史定位说清楚。HBase 诞生于 Hadoop 生态早期当时 HDFS 只能解决海量文件的存储问题却没有办法提供高效的随机读写能力。HBase 在 HDFS 之上抽象出一张稀疏的、按 RowKey 排序的大表用 MemStore 加 HFile 这套 LSM 架构换来了极高的写入吞吐也是它后来称霸实时数仓和在线服务场景的根本原因。Iceberg 的历史比 HBase 短很多它属于数据湖表格式那一代产品核心目标不是解决随机读写而是让 HDFS 或对象存储上的数据文件具备“表”的语义。它通过三层元数据体系元数据文件、清单列表、数据文件把一批 Parquet/ORC 文件组织成一张可以并发读写的表并且支持 ACID 事务、快照隔离和 Schema 演进。这两个东西放在一起看恰好代表了大数据存储演进的两个方向HBase 解决“在线服务怎么扛住高并发”Iceberg 解决“离线分析怎么让数据表更可信、更好管”。它们的演进关系不是替代而是承接——业务对实时性和分析性的要求越来越高单靠 HBase 这类列族数据库不够用了需要把列式存储的优势以更开放的姿态释放出来。1.2 列式存储到底“列”在哪里很多刚接触的人会把 HBase 和 Iceberg 都归为“列式存储”但这里的“列式”含义差别很大。HBase 的列式是指列族Column Family维度的物理存储隔离同一个列族下的所有列会落到同一组 HFile 中。但你如果按某个列单独做扫描HBase 并不会只读取那一列的数据它依然要遍历这一列所在列族的全部 StoreFile。真正的列式存储是指 Parquet、ORC 这类文件格式在文件内部按列存数据块查询只读取涉及的列块即可IO 开销和参与扫描的数据量成比例下降。Iceberg 本身不是文件格式它只是表格式Table Format默认搭配 Parquet 或 ORC 文件。所以 Iceberg 获得的列式能力是从文件格式层面继承来的而它自己把精力放在如何管理这些文件、如何让表的语义完整。理解这个区别很重要因为它决定了你选型时该怎么判断如果任务是 KV 点查get/put列式文件格式其实帮不上太多忙HBase 反而更合适如果任务是宽表扫描、聚合分析和日内增量更新Iceberg 搭配列式文件才能把优势发挥出来。1.3 演进背后的推动力从“能存”到“好用”HBase 时代的数据使用模式很清晰业务方在写入时把 RowKey 设计好查询时按 RowKey 点查或按前缀扫。这种模式对存储很友好但对分析很不友好。你需要做聚合、Join、按时间窗口过滤时只能自己写 MapReduce 或 Spark 作业去全表扫描然后还要面对 HBase 扫描时产生的热点、Region Server 负载不均等问题。Iceberg 推动的演进本质上是把大数据存储的使用模式从“以 RowKey 为中心的存取模型”转向“以数据为中心的表格模型”。在 Iceberg 上你可以用 SQL 直接改表结构、隐藏分区、做时间旅行查询多个计算引擎共享同一张表而不用互相锁表。它把过去需要多个系统配合才能做到的事情收敛到一个统一的存储表格式上。我个人的判断是这个演进不会让 HBase 消失也不会让 Iceberg 短时间成为所有场景的默认选项但列式存储的重心正在从“底层文件格式”上移到“表和元数据的管理层”这是整个生态发展的核心趋势。2. 核心机制对比与关键技术点拆解2.1 HBase 的存储模型为什么不适合做数据分析HBase 的物理存储模型可以简化为一张表按 RowKey 字典序分区每个分区Region内有多个列族每个列族在内存里对应一个 MemStore刷写后生成 HFile。底层文件 HFile 的内部是按 RowKey 有序排列的 KV 结构每个 Key 包含 RowKey、列族、列限定符、时间戳和操作类型。这套模型有非常强的地方写入不落盘直接进内存批量刷写合并小文件读的时候通过 BlockCache 和布隆过滤器快速跳过无关文件。但代价也很明显数据按 RowKey 排序按非 RowKey 列做过滤需要全表扫描。列族内所有列一起存储无法做到按列裁剪。表的 Schema 本质上只是“列族 列限定符”的约定没有强约束。HBase 的 Scan 接口虽然支持设置 StartRow/StopRow、Filter但对复杂分析型查询的支持程度很低。我还记得第一次尝试在 HBase 上做多维度聚合分析时的场景一个 200 亿行的业务表按天统计某个维度的去重数量写出来的 Spark 作业跑了将近两个小时其中大部分时间浪费在 Scan 和序列化上。后来同样的数据落到 Iceberg 上按分区裁剪加列裁剪二十分钟以内就能跑完。这不是说 HBase 不行而是它被用在了不合适的场景里——但在真实业务里这种错配非常常见。2.2 Iceberg 如何解决表管理的核心难题Iceberg 的三个核心设计目标快照隔离、Schema 演进的可靠性、查询引擎的可插拔性。快照隔离是 Iceberg 最硬核的特性。每一次数据提交都会生成一个新的快照Snapshot快照之间互相隔离读取端在拿到某个快照的引用后可以不受并发写入影响地完成一次一致性读。这也意味着 Iceberg 天然支持时间旅行查询你可以直接读取某个历史时间点的表状态不需要任何备份或恢复操作。Schema 演进在 HBase 时代几乎是靠“约定”和“双写”来完成的而 Iceberg 的 Schema 演进是原子性的。增加列、删除列、调整嵌套字段类型都能被记录进元数据并且查询端不需要重写任何数据文件。列的位置和类型变化对老查询完全透明。可插拔性体现在 Iceberg 不绑定 Java API。它定义了一套 REST Catalog、JDBC Catalog 和文件级约定Spark、Flink、Trino、Presto 都可以用它提供的连接器直接操作同一张表且互相之间跳过表锁。这意味着你的数据工程师用 Spark 跑批实时团队用 Flink 写入分析团队用 Trino 查询看到的始终是一致的数据视图。2.3 一张表讲清楚核心差异对比维度HBaseIceberg数据组织方式RowKey 有序的 KV 结构数据文件 清单 元数据快照文件格式HFile内部 KV列族内混合Parquet / ORC列式存储随机点查能力很强毫秒级弱需要走文件裁剪列裁剪秒级或更慢分析型扫描弱全表扫描为主强支持谓词下推与列裁剪ACID 能力仅行级原子性跨行需借助外部方案表级 ACID快照隔离Schema 变更动态加列但无统一元数据约束原子性 Schema 演进记录完整变更历史时间旅行原生不支持依赖备份或工具原生快照支持任意历史时间查询事务读写范围延迟批量写为主不支持更新删除分析支持 Merge-On-Read 更新删除与流批一体写入这个表基本反映了选型时的分界线。如果你手里是订单、风控、用户画像这类需要高频点查和实时写入的场景HBase 依然有价值但如果你在做的是数据仓库层面的明细层、汇总层需要 ACID、Schema 演进而又不想引入厚重数仓组件Iceberg 明显更有优势。2.4 为什么 Iceberg 能在不替换 HDFS 的情况下完成演进Iceberg 演进的关键在于它不强制你更换底层存储。HDFS、S3、OSS 以及各种兼容对象存储的路径Iceberg 都能直接把数据文件写进去。它把“元数据”和“数据文件”分离数据文件是标准 Parquet/ORC元数据以 JSON 和 Avro 格式存在表目录下。这意味着你可以保留原有 Hadoop 集群不需要像 HBase 迁移到其他系统那样引入新的分布式存储节点。做过架构的人都知道存储层的切换是最伤筋动骨的。Iceberg 这一点设计得非常聪明——它没有重造一个存储引擎而是定义了一套“文件如何构成表”的标准让所有计算引擎按照这个标准来读取和提交。落地的复杂度就集中在表格式的适配层而不是存储层。3. 从 HBase 迁移到 Iceberg 的实操记录3.1 迁移前的评估与表结构改造我先说结论不要试图把 HBase 的表结构原封不动搬到 Iceberg那样大概率会得到一个性能很差的结果。HBase 的 RowKey 通常是业务主键或拼接主键比如“用户ID_订单ID_时间戳”。这个设计在 HBase 里是合理且高效的但到了 Iceberg你的第一优先级应该变成分区字段和排序键。需要重新考虑聚类方式、标识主键字段、过滤字段和更新字段。我经历的某模拟订单系统迁移原始 HBase 表的 RowKey 是“user_id(4字节) order_id(8字节) create_time(4字节)”列族包含 30 多个列。迁到 Iceberg 时我们做的关键调整是拆分了组合 RowKeyuser_id、order_id、create_time 分别作为独立列。分区策略选为 days(create_time)对应 HBase 里按时间范围的 Scan 需求。排序键设置为 user_id 和 create_time 的组合保留原有点查模式的分析适配。列族直接扁平化为普通列列限定符里原来的冗余字段做了梳理。表结构出来后建表语句类似CREATE TABLE ods_order_iceberg ( user_id BIGINT, order_id BIGINT, create_time TIMESTAMP, status STRING, amount DECIMAL(10,2), ... ) USING iceberg PARTITIONED BY (days(create_time)) TBLPROPERTIES ( write.format.default parquet, write.parquet.compression-codec zstd, write.sort.order user_id,create_time );注意这里我不会把 HBase 的全部列都建模成 Iceberg 的列尤其是那些只有写入没有读取、或者读取频率极低的大字段可以考虑归档或单独存放避免拖累列式文件的压缩比和扫描效率。3.2 批量迁移SnapShot 导出与增量补偿HBase 迁移最常见的方式是使用其自带的 Snapshot 导出机制。这个方式不占用线上 Region 读写导出的是 HFile 层面的快照副本。流程大概如下对源表执行 snapshot 操作记录快照名和创建时间。用 ExportSnapshot 命令把快照导入到一个临时 HDFS 目录。写一个 Spark 作业读取这批 HFile转成 DataFrame 后统一写入 Iceberg。注意 HFile 转 Parquet 时最容易踩的坑是类型推断。HBase 里所有值都是字节数组导出时如果不做显式类型转换Spark 会默认推断成 Binary 类型写进 Parquet 后下游查询全都得 cast性能很差。实践中的处理方式是在转换阶段明确以 StructType 定义每列的字段类型并且在写入前做一次空值、非法值清洗。比如把空字节转换成 null把数字类字段的前置零处理掉日期字段统一格式。这些清洗在 HBase 时代通常不会被严格约束但在 Iceberg 和 Parquet 里Schema 是强类型的脏数据会在写入阶段直接失败或者产生错误结果。批量迁移的时间窗口要选业务低峰。快照本身虽然不锁表但从快照导出到写入 Iceberg 的过程中源表数据是持续变化的所以一定需要配合增量补偿。3.3 增量同步与双跑验证增量同步我推荐使用 Binlog 类方案直接对接比如把 HBase 的 WAL 或复制机制解析出来接入消息队列再消费写入 Iceberg。如果你用的 HBase 版本对复制机制支持不够好另一种可行做法是定期做增量 Snapshot按时间戳过滤写入但延迟会更高。我们当时采用的方式是复用已有的离线同步链路每天凌晨从 HBase 导出一份近 24 小时的增量数据写入 Iceberg 的当日分区同时保留一份全量快照表。这样查询端可以结合快照表与增量表做合并视图等增量数据完整后再把增量合并进全量表。双跑验证是整个迁移中最不能省的一步。我在实际操作中会分三层验证数据量验证对比同一天两个表的行数、主键去重数、关键字段求和值。抽样对比每组抽样 10 万行按主键 Join逐字段比对新旧源的值差异。业务结果验证把典型报表查询日活、GMV、状态分布跑在新表上和原 HBase 上的历史结果做比对。这层验证周期至少要覆盖一个完整业务周期比如 7 天或者一个月。有的问题不是当天能看出来的例如分区边界处理和跨天数据的一致性只有跑过一段完整时间才能暴露。3.4 写入性能调优与实际效果迁移时我们遇到了一个典型问题Spark 写入 Iceberg 时产生大量小文件。默认情况下每个 Spark 分区写一个 Parquet 文件如果分区数没调好一天的存量数据会拆成几万个小文件后续查询和元数据管理都会变慢。几个我自己验证有效的参数组合spark.sql.shuffle.partitions200 spark.sql.files.maxRecordsPerFile2000000 spark.driver.memory8g spark.executor.memory8g spark.executor.cores4关键在于write.target-file-size-bytes我建议直接设置到 512MB 或 1GB这样 Spark 写入 Iceberg 时会自动控制在目标大小附近减少小文件数量。另一个重要参数是write.distribution-mode切换到 hash 模式可以保证相同分区内的数据不跨文件乱序对后续使用排序键优化查询有直接帮助。调整完这些参数之后我们 3 亿行规模的一天的业务数据写入耗时从原始的 40 多分钟降到 18 分钟左右文件数量从 2.3 万降到 800 左右。这个优化效果非常明显。4. 常见问题与排查技巧实录4.1 小文件膨胀与元数据目录越来越大冰berg 表跑一段时间后最容易出现的问题就是小文件膨胀。原因无外乎写入频率高、每批数据量小、分区字段粒度细。尤其是小时级或分钟级任务每次提交生成的数据文件只有几 MB日积月累元数据里记录的清单文件越来越多查询计划阶段扫描清单的时间就会拉长。解决办法要分两层第一层是写入端控制尽量提高单批次数据规模和减少提交频率第二层是定期做 Compaction 任务。# 通过 Spark 执行 Iceberg 的动态文件合并操作 CALL iceberg.system.rewrite_data_files( table ods.orders_iceberg, strategy binpack, target_file_size_bytes 536870912 );这个命令会把小于目标大小的小文件合并成大文件。在我实际操作中一般会配合一个定时任务每天凌晨先做一次rewrite_data_files再执行expire_snapshots清理历史快照最后执行remove_orphan_files清理废弃文件三步走完元数据目录能瘦身 70% 以上。4.2 快照数量过多导致时间旅行变慢和存储浪费Iceberg 的每一条写路径都会生成新快照如果一个表长期高频写入不做清理快照目录里的文件会积累到几千甚至上万份每次查询元数据时要加载的快照清单就很重。这里有一个误区很多人觉得开了时间旅行功能就应该把历史快照都保留结果让存储成本和查询耗时成倍上升。实际上业务需要的时间旅行窗口通常只有 7 天或 30 天超过这个范围的快照完全可以清理。-- 清理 7 天前的快照保留当前快照 CALL iceberg.system.expire_snapshots( table ods.orders_iceberg, older_than TIMESTAMP 2024-11-01 00:00:00 );需要注意的是expire_snapshots的默认行为会保留当前快照同时你也可以指定retain_last数量。在做清理前我建议先跑一个 dry run 查询看看哪些历史快照要被删除以及会释放多少存储确认无误后再执行正式清理。4.3 Schema 演进过程中的默认值陷阱Iceberg 的 Schema 演进虽然很灵活但有个细节容易踩坑新增列的时候如果带默认值历史数据文件在读取时不会自动填充默认值。具体来说老文件里的数据行没有这个新列如果你设置了 DEFAULT 值新写入的数据会带上该值但是历史查询返回的行里该列会是 null 而非默认值。这在指标统计时会造成数据口径不一致。我自己踩过这个坑之后现在处理新增列的流程是先不加默认值等数据回填完成再用 update 操作补上默认。或者干脆把 DEFAULT 约束交给应用层处理Iceberg 层面只保证 Schema 兼容性。4.4 分区演进与历史分区数据的兼容处理Iceberg 支持分区演进你可以从days(create_time)改成months(create_time)但这里要注意历史数据文件依然保留旧的分区布局新的写入才使用新布局。查询端会自动根据元数据判断每个文件所属的分区布局但当涉及跨分区演进边界的查询时实际扫描的文件数可能比预期多。我建议在做分区演进前先用rewrite_data_files配合REWRITE策略把历史数据重写到新布局。虽然重写会带来一定的计算成本但换来的是后续查询计划稳定尤其是分区粒度变化后比较容易出现查询扫描范围扩大的情况。# 重写数据文件以适配新分区策略同时压缩小文件 CALL iceberg.system.rewrite_data_files( table ods.orders_iceberg, strategy sort, sort_order user_id,create_time );4.5 迁移后的查询性能回退与排查思路从 HBase 迁到 Iceberg 后有一部分查询确实会变慢主要集中在高频点查场景。比如按 user_id 查某条记录HBase 可以毫秒级返回Iceberg 即使通过分区裁剪和列裁剪也只能到秒级或几百毫秒级。这不是 Iceberg 有缺陷而是它的存储模型定位就是分析型。如果业务里还有高频点查需求务实做法是保留一条 HBase 或宽表 KV 服务将 Iceberg 分析结果定期回写而不是削足适履把在线查询全部压到 Iceberg 上。我们的方案是 Iceberg 负责数仓明细与指标加工Redis 或者 HBase 负责前端在线服务查询两者通过定时任务同步关键结果。排查查询变慢时首先看执行计划是否命中分区裁剪其次看snapshot-id是否被固定在一个较老的快照上最后看文件数是否过多。这三个问题基本覆盖了绝大多数 Iceberg 查询性能瓶颈。5. 迁移后的运行体验与几点额外建议整个过程跑下来我对从 HBase 到 Iceberg 的演进有了比较清晰的体感。HBase 像一个训练有素的专项运动员在随机读写赛道上很强但很难覆盖分析场景的全部需求Iceberg 更像一个综合管理平台它不追求单点性能极限而是把表管理、多引擎访问、数据可靠性这些基础能力打磨扎实。我在实际运维中比较受益的几个习惯可以分享一下第一Iceberg 的元数据监控要接入到现有监控体系里重点盯快照数量、孤儿文件数量、平均文件大小三个指标。这三个指标直接决定了表健康度和查询性能。第二写入任务尽量集中到少数几个高频任务中不要让大家各自建写入作业否则小文件问题会从根上失控。我们后来统一收敛到一套数据写入服务无论实时还是离线都走同一入口文件治理成本大幅降低。第三如果团队之前习惯 HBase 的开发模式迁移前期一定要花时间培训“查询思维转换”。在 HBase 里写 filter 的人到 Iceberg 后会下意识拒绝重写 SQL结果就是把 HBase 的全扫描习惯带了过来性能崩了还怪表格式不行。实际上 Iceberg 的优化空间很大前提是得按它的方式去设计表、分区和查询。如果你现在正面临从 HBase 走向 Iceberg 的选型我的建议是先梳理清楚业务里到底有多少比例的查询是点查多少是分析扫描再决定迁移力度。全量替换不见得是正确答案但把数仓分析这条线完整搬上 Iceberg让列式存储发挥真正的分析价值是值得投入的一条路。
阅读完成 · 觉得有帮助?
咨询建站