1. 为什么HBase数据迁移总在“导出导入”这一步翻车
做过HBase运维的人大概都有过这种经历:集群要下线、机房要搬迁、业务要拆分,或者单纯想把测试环境的数据灌到预发环境,第一反应都是“导出再导入呗,能有多难”。结果真上手才发现,Export跑了一整夜没结束,Import进去之后RowKey分布全乱,Region热点集中到一两台机器上,查询延迟直接飙到秒级。更离谱的是有些表导完少了几十万行,翻日志才发现是WAL没刷盘或者Snapshot没打全。
HBase的数据迁移看起来只是两个命令的事,实际上它牵扯到数据一致性、Region分布、版本控制、压缩算法、权限继承这一整条链路。你导出的不只是一堆KeyValue,还有表的元数据、列族的属性、甚至预分区信息。任何一环没对齐,导入之后就是一堆隐性故障,可能过了一周业务才反馈“某些用户查不到数据”。
这篇内容我打算把HBase数据迁移这件事从头到尾拆开讲。核心围绕导出与导入这两个动作,但不会只给你两条命令就完事。我会说清楚什么场景该用Export/Import、什么场景必须换Snapshot、什么场景干脆用CopyTable更省事,以及每一步背后那些文档里不会写的坑。适合正在做集群迁移、数据同步、环境复制的运维和开发同学,也适合刚接触HBase、想搞明白“为什么导个数据这么麻烦”的新手。
先把结论摆前面:HBase迁移没有银弹,选错工具比不迁移还危险。下面我按实际决策顺序,一层层往下拆。
2. 迁移之前先想清楚:你要搬的到底是“数据”还是“表”
很多人一上来就问“Export怎么用”,其实这个问题本身就问偏了。你得先回答一个更根本的问题:这次迁移的目标是什么?是全量搬一张表到另一个集群,还是只搬某段时间的数据,还是两个集群之间做增量同步?目标不同,工具选型完全不一样。
2.1 三种迁移诉求对应的工具地图
我把常见的HBase迁移诉求归成三类,你可以直接对号入座:
| 迁移诉求 | 推荐工具 | 核心原因 | 典型耗时量级 |
|---|---|---|---|
| 全量搬整张表,目标集群可停机 | Snapshot + Clone/ExportSnapshot | 不经过HBase层,直接复制HDFS文件,最快 | 分钟级到小时级 |
| 全量搬整张表,需要跨版本兼容 | Export + Import | 走HBase API,版本差异容忍度高 | 小时级到天级 |
| 按RowKey范围或时间范围搬部分数据 | CopyTable 或 Export带范围 | 支持starttime/endtime和RowKey区间 | 取决于数据量 |
| 增量同步,源集群持续写入 | Replication 或 CopyTable增量模式 | 支持持续追平 | 持续运行 |
这张表里最关键的一行是Snapshot。如果你只是要把一张表从A集群搬到B集群,而且B集群能接受短暂停机,那Export/Import根本不是首选,Snapshot才是。ExportSnapshot直接操作HDFS文件,不经过RegionServer,不解析KeyValue,速度差了一个数量级。我见过一张2TB的表,用Export跑了14个小时还没完,换成Snapshot之后40分钟搞定。
那为什么还有这么多人用Export/Import?因为它的版本兼容性最好。Snapshot要求源集群和目标集群的HBase版本、HDFS版本基本一致,跨大版本经常打不开。而Export/Import走的是客户端API,只要两边都能跑MapReduce,版本差个一两代通常没问题。
2.2 一个容易被忽略的前置检查:目标表的预分区
不管你用哪种方式,导入之前一定要先建好目标表,而且预分区必须和源表对齐。这是我最想强调的一点。
Export导出的数据是带RowKey的,Import的时候如果目标表只有一个Region,所有数据会先涌进这一个Region,写满之后才split。split过程中会产生大量小文件,触发compaction,整个导入速度会被拖垮。更严重的是,如果源表本身是预分区的,导入后RowKey分布和原来不一致,后续查询的负载均衡就全乱了。
正确做法是:导入前用describe看一下源表的分区边界,然后在目标集群用同样的SPLITS建表。比如源表是按首字母分区的:
# 查看源表分区 echo "describe 'source_table'" | hbase shell拿到SPLITS之后,在目标集群建表时原样带上:
create 'target_table', {NAME => 'cf', VERSIONS => 3, COMPRESSION => 'SNAPPY'}, SPLITS => ['a', 'b', 'c', 'd', 'e']注意:列族的
VERSIONS、COMPRESSION、TTL这些属性也要和源表对齐。Export/Import只搬数据,不搬元数据,这些属性丢了之后业务可能直接报错。
2.3 版本与压缩算法的隐性坑
还有一个特别隐蔽的问题:压缩算法。如果源表列族用的是SNAPPY,目标集群没装SNAPPY原生库,导入的时候会直接抛UnsatisfiedLinkError。这个错误在MapReduce任务里往往被埋在几百行日志中间,不仔细看根本发现不了。
我的习惯是迁移前先在目标集群跑一条测试写入:
# 在目标集群建一张测试表,用源表的压缩算法 create 'compress_test', {NAME => 'cf', COMPRESSION => 'SNAPPY'} put 'compress_test', 'row1', 'cf:col1', 'test'能写进去说明压缩库没问题,写不进去就赶紧换算法或者装库。这一步花两分钟,能省掉后面两小时的排查。
3. Export/Import的完整实操链路与参数调优
确认了要用Export/Import之后,我们进入实操。这一节我把命令拆开讲,每个参数都说明白为什么这么设。
3.1 Export命令的组成与每个参数的实际含义
Export本质是一个MapReduce任务,它从HBase表里扫描数据,然后以SequenceFile格式写到HDFS上。基本命令长这样:
hbase org.apache.hadoop.hbase.mapreduce.Export \ <tablename> \ <outputdir> \ [<versions>] \ [<starttime>] \ [<endtime>] \ [<raw>] \ [<limit>] \ [<caching>]看起来参数不多,但每一个都能决定迁移的成败。我逐个说:
versions:导出多少个版本。默认是1,也就是只导最新版本。如果你的业务依赖多版本读(比如做历史回溯),这里必须设成和列族VERSIONS一致的值。我踩过一次坑:源表VERSIONS=3,导出时没指定,默认只导了1个版本,导入后业务查历史数据全是空。
starttime/endtime:按时间戳范围导出。注意这里是毫秒级时间戳,不是日期字符串。比如你要导2024年1月1日之后的数据:
# 2024-01-01 00:00:00 对应的毫秒时间戳 hbase org.apache.hadoop.hbase.mapreduce.Export \ source_table \ /tmp/export_output \ 1 \ 1704038400000这个参数在做增量迁移时特别有用,你可以先全量导一次,然后按时间窗口导增量。
raw:是否导出原始数据,包括删除标记。默认false。如果你要做的迁移需要保留删除语义,比如源表有大量Delete操作,那必须设true,否则导入后已删除的数据会“复活”。
caching:每次RPC扫描多少行。默认是2147483647,也就是不限制。这个默认值其实很坑,它会让每次RPC拉取过多数据,导致RegionServer内存压力大。我一般设成1000到5000之间,具体看单行大小。单行小就设大点,单行大就设小点。
# 推荐的Export命令模板 hbase org.apache.hadoop.hbase.mapreduce.Export \ source_table \ /tmp/export_output \ 3 \ 0 \ 9999999999999 \ false \ 20003.2 Import命令与“导入后数据不对”的排查顺序
Import同样是MapReduce任务,它读取Export生成的SequenceFile,然后Put到目标表。命令结构类似:
hbase org.apache.hadoop.hbase.mapreduce.Import \ <tablename> \ <inputdir>看起来简单,但导入后如果发现数据不对,排查顺序很重要。我一般按这个链路走:
- 先看行数:用
count 'target_table'对比源表行数。如果差得多,先查Export的输出目录里文件数是否完整。 - 再看RowKey分布:用
hbase hfile -p或者抽样scan,看RowKey是否和源表一致。如果RowKey变了,说明Export阶段就有问题。 - 最后看列族属性:
describe目标表,确认VERSIONS、COMPRESSION、TTL是否和源表一致。属性不一致会导致数据“看起来导进去了但读不出来”。
有一次我遇到导入后count对得上,但业务查不到数据。排查了半天才发现目标表的列族TTL设成了7天,而源表数据是30天前的,导入后直接被TTL过滤掉了。这种问题不看元数据根本发现不了。
3.3 MapReduce任务调优:让Export/Import跑得更快
Export/Import默认的MapReduce配置往往不是最优的。我一般会调整这几个参数:
# 增加Map任务数,提高并行度 -D mapreduce.job.maps=20 # 增加Reduce任务数(Import阶段) -D mapreduce.job.reduces=10 # 调整每个Map处理的数据量 -D mapreduce.input.fileinputformat.split.maxsize=268435456 # 开启压缩,减少HDFS写入量 -D mapreduce.map.output.compress=true -D mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec这里重点说mapreduce.job.maps。Export的Map任务数默认由输入分片决定,而HBase的TableInputFormat会按Region切分。如果源表Region少,Map任务就少,并行度上不去。手动调大这个值可以强制增加Map数,但也不能太大,否则每个Map处理的数据太少,任务调度开销反而拖慢整体速度。我的经验值是每个Map处理1GB到2GB数据比较合适。
Import阶段的Reduce数也值得调。默认Reduce数是1,所有数据都往一个Reduce里灌,速度极慢。设成10到20能明显提速,但要注意目标集群的写入能力,Reduce太多会把RegionServer打爆。
4. 比Export/Import更值得考虑的Snapshot方案
前面说了,如果目标集群能停机,Snapshot才是首选。这一节我把Snapshot的完整流程也讲清楚,方便你对比选型。
4.1 Snapshot的生成、导出与恢复三步走
Snapshot的核心优势是不复制数据。它只是在HDFS层面记录一个元数据指针,指向当前所有HFile。所以生成Snapshot几乎是秒级的,不管表有多大。
# 第一步:在源集群生成Snapshot hbase shell snapshot 'source_table', 'snapshot_20240101'生成之后,用ExportSnapshot把Snapshot文件复制到目标集群的HDFS:
# 第二步:导出Snapshot到目标集群 hbase org.apache.hadoop.hbase.snapshot.ExportSnapshot \ -snapshot snapshot_20240101 \ -copy-to hdfs://target-cluster/hbase \ -mappers 16 \ -bandwidth 200-mappers控制并行度,-bandwidth控制带宽上限(单位MB/s)。这两个参数根据你的网络情况调,内网千兆的话-mappers 16基本能跑满。
复制完成后,在目标集群恢复:
# 第三步:在目标集群恢复Snapshot hbase shell restore_snapshot 'snapshot_20240101'注意restore_snapshot会覆盖同名表,如果目标集群已经有这张表,会先删掉再恢复。所以执行前一定要确认目标表名。
4.2 Snapshot跨版本迁移的兼容性边界
Snapshot最大的限制是版本兼容性。它依赖HFile的格式,而HFile格式在不同HBase大版本之间可能变化。我整理了一个简单的兼容性对照:
| 源版本 | 目标版本 | Snapshot是否可用 | 备注 |
|---|---|---|---|
| 1.x | 1.x | 可用 | 同大版本基本没问题 |
| 1.x | 2.x | 通常可用 | 需要目标集群能读旧版HFile |
| 2.x | 3.x | 需验证 | HFile v3格式有变化 |
| 1.x | 3.x | 大概率不可用 | 建议走Export/Import |
如果跨了大版本,我建议先用一张小表做验证,别直接上生产表。验证方法很简单:在目标集群restore_snapshot之后,scan几条数据看看能不能正常读。
4.3 Snapshot方案下预分区和权限的继承情况
Snapshot相比Export/Import的一个巨大优势是元数据完整继承。预分区、列族属性、甚至ACL权限都会一起带过去。这意味着你不需要手动建表,也不需要担心分区不对齐。
但有一个例外:协处理器(Coprocessor)。如果源表挂了自定义协处理器,Snapshot恢复后协处理器配置会保留,但协处理器的jar包不会自动复制到目标集群。如果目标集群没有对应的jar,RegionServer启动时会报ClassNotFoundException。这个坑我在一次迁移中踩过,表恢复后整个Region起不来,排查了半天才发现是协处理器的问题。
解决办法是:迁移前先describe源表,看有没有Coprocessor配置,有的话提前把jar包放到目标集群的HBASE_CLASSPATH里。
5. 迁移后的验证:别等业务报警才发现问题
数据导完了不代表迁移成功。我见过太多案例,迁移当天一切正常,过了一周业务才反馈“某些查询变慢了”或者“部分数据查不到”。所以迁移后必须做一轮完整验证。
5.1 行数与RowKey分布的双重校验
行数校验是最基本的,但光看count不够。count在HBase里是遍历扫描,大表跑一次可能要几十分钟。我一般用两种方式结合:
# 方式一:用count精确统计(慢但准) echo "count 'target_table'" | hbase shell # 方式二:用HFile的元数据快速估算(快但粗略) hbase org.apache.hadoop.hbase.mapreduce.RowCounter target_tableRowCounter走的是MapReduce,比count快,但结果可能有微小误差。如果两者差异在0.1%以内,基本可以认为行数一致。
RowKey分布校验更关键。我一般抽样1000个RowKey,对比源表和目标表的分布:
# 抽样源表 echo "scan 'source_table', {LIMIT => 1000}" | hbase shell > source_sample.txt # 抽样目标表 echo "scan 'target_table', {LIMIT => 1000}" | hbase shell > target_sample.txt # 对比 diff source_sample.txt target_sample.txt如果RowKey顺序和内容一致,说明数据分布没问题。如果目标表的RowKey顺序变了,说明导入时发生了重分区,需要检查预分区配置。
5.2 用HBase Shell做抽样比对的具体命令
除了RowKey,列值也要抽样比对。我一般随机选10个RowKey,逐个对比所有列族和列:
# 在源表查一个RowKey echo "get 'source_table', 'rowkey_001'" | hbase shell # 在目标表查同一个RowKey echo "get 'target_table', 'rowkey_001'" | hbase shell对比的时候注意时间戳。如果业务依赖时间戳做版本控制,导入后时间戳可能会变。Export/Import默认保留原始时间戳,但如果你在Import时用了自定义的Put逻辑,时间戳可能被覆盖。
5.3 迁移后Region热点与Compaction的观察窗口
数据导入后,目标集群会经历一段“不稳定期”。大量新写入的HFile需要compaction,Region可能因为写入不均产生热点。我一般会观察24小时,重点看这几个指标:
- RegionServer的写入QPS:是否有个别RS明显高于其他
- Compaction队列长度:是否持续增长
- Region的split次数:是否频繁split
如果发现热点,可以用move命令手动迁移Region:
# 查看Region分布 echo "status 'detailed'" | hbase shell # 手动迁移热点Region echo "move 'encoded_region_name', 'target_regionserver'" | hbase shellCompaction方面,如果导入后写入量很大,建议临时调大hbase.hstore.blockingStoreFiles,避免写入被阻塞。等compaction追平后再调回来。
6. 那些文档里不会写的迁移经验
最后分享几个我在实际迁移中攒下来的经验,都是踩过坑之后才明白的。
第一,永远先拿小表练手。不管你觉得方案多成熟,先在测试环境用一张几百万行的表跑一遍完整流程。大表和小表的问题完全不一样,小表跑通不代表大表没问题,但小表都跑不通,大表肯定翻车。
第二,迁移窗口要留足buffer。我一般按预估时间的3倍来申请窗口。Export/Import受网络、磁盘IO、RegionServer负载影响很大,实际耗时经常是预估的两三倍。留足buffer比中途回滚强。
第三,源集群在迁移期间最好停止写入。如果做不到,至少要在Export开始前记录一个时间戳,迁移完成后用这个时间戳做增量补录。否则源集群在迁移期间的新写入会丢失。
第四,目标集群的hbase.hregion.memstore.flush.size可以临时调大。导入时写入量集中,默认的128MB容易触发频繁flush,产生大量小文件。临时调到256MB或512MB能减少flush次数,等导入完成后再调回来。
第五,别忘了清理Snapshot。Snapshot虽然不占额外空间,但会阻止HFile被删除。如果迁移后不清理,源集群的磁盘空间会一直不释放。清理命令:
echo "delete_snapshot 'snapshot_20240101'" | hbase shell迁移这件事,说到底就是把不确定性提前暴露。你在准备阶段多花一小时检查预分区、压缩算法、协处理器,就能在迁移阶段少熬一个通宵。我现在的习惯是每次迁移前写一份checklist,逐项打勾,哪怕已经做过几十次也不例外。因为HBase的坑往往就藏在那些“我以为没问题”的地方。