1. 旁路导入到底在解决什么问题
先说一个真实场景。我之前帮一个电商系统做分布式数据库选型的时候,客户反复问一个问题:一张已经在 OceanBase 上跑高并发交易的表,能不能在不中断业务的前提下,把几十 TB 甚至 PB 级的存量数据导进去?这个问题在几年前基本无解,因为传统导入方式意味着连夜的插入风暴、日志暴涨、和在线交易抢内存。直到把 OceanBase 的旁路导入真正用起来,才发现一条路:让导入数据不经过 SQL 层和事务链路,直接批量落成数据文件,这才真正做到一张表既扛高并发交易,又能快速吞下 PB 级数据。
先把这个概念说透。旁路导入,英文叫 Direct Load,核心思路就是不走常规 DML 的完整链路,直接把外部数据文件变成数据库底层的存储文件。很多人第一反应是“这不就是批量导入吗,LOAD DATA 不也能导大数据?”其实差别非常大。LOAD DATA 虽然语法上是一条语句,但底层仍然一条一条写数据,仍然要过事务、写日志、进内存。旁路导入相当于绕开了整条“生产线”,从源头就把数据封装成数据库可以直接挂载的文件格式。这也是它能在导入海量数据的同时,对在线交易影响较小的根本原因。
这篇文章适合谁看?如果你正在做数据迁移、大表初始化、离线批量同步,或者只是被“导大表导到半夜还没完”折磨过,那直接往下看。我会从底层原理讲到实操命令,再讲我踩过的坑,尽量让你少走弯路。
1.1 传统导入方式为什么顶不住
常规的导入方式,本质上是把源文件里的每一行数据当成一条 DML 语句来处理。哪怕你用多线程并发插入,也只是把并行的压力往后端堆,瓶颈并没有消失。一次插入在数据库内部要经历什么?语法解析、生成执行计划、走事务引擎分配事务 ID、写 redo 日志、写 undo 信息、更新索引,最后数据落到内存里的写缓冲区。等缓冲区到了一定阈值,还要触发转储,把增量数据合并到 SSTable 里。这套机制在正常业务写入下非常成熟,但拿来导入 PB 级数据时就显得太“重”了。
我打个比方。常规插入像你雇了一万个搬运工,每个人手里拎着一个文件袋,从大门口一趟一趟往档案室送。即使你把门口拓宽、把搬运工数量加多,档案室里的登记流程仍然一道都不能少,每个人都要签收、归档、登记。旁路导入则是直接叫了一辆卡车,把一整车已经整理好的档案箱拉到档案室门口,管理员只检查箱子编号,然后整箱上架。你说哪个快?
还有一点容易被忽略:传统导入的每一行数据都会产生 redo 日志和 undo 信息。PB 级的数据量意味着日志量也接近 PB 级,磁盘 IO 会被日志写放大严重拖累。更麻烦的是,导入期间这些日志还要参与高可用同步,每个副本都要应用一遍。很多人导数据导到半夜发现日志目录满了,就是这个问题。
1.2 旁路导入的快,快在哪里
旁路导入的快,不是单纯的“并发高”,而是链路短。它把大批数据文件按分区、按主键范围拆成多个分片,每个分片由独立的导入线程处理,数据直接经过解析、类型转换、排序、校验,然后在各节点上直接生成存储层的数据文件。这个过程不产生 redo,不经过内存写缓冲区,也不参与常规事务的锁竞争,所以 CPU、内存和磁盘 IO 的开销比传统插入低很多。
举一个直观的例子。同样导一张 5 TB 的表,常规并发导入可能需要十几个小时,而且期间业务写入明显变慢,偶尔还会出现锁等待。旁路导入如果并发设置合理,可能压缩到一两个小时,同时在线业务的 p99 延迟波动也小得多。这个差距不是靠硬件堆出来的,而是路径不同带来的结构性优势。
不过要提醒一句:旁路导入并不是“万能加速器”。它对数据质量、文件格式、目标表结构都有要求。你不能拿一个格式混乱的 CSV 指望导入速度起飞,也不能在主键冲突严重的数据上指望它自动帮你清洗。后续我会专门讲这些坑。
2. 一张表既扛交易又能吞下PB级导入,底层靠什么
很多人不理解:为什么同一张表,一边高并发交易在写,一边还能大批量导入,两者不打架吗?要回答这个问题,得先看懂 OceanBase 底层是怎么存数据的。
2.1 先说存储引擎的LSM-Tree
OceanBase 的存储引擎是 LSM-Tree 架构,和传统的 B+ 树存储引擎思路完全不同。传统 B+ 树是“就地更新”,数据写在页面上,更新的时候直接改磁盘上的数据页。LSM-Tree 则是“先写内存,再批量落盘”:所有写入先落到内存里的写缓冲区,也就是 MemStore,达到阈值后冻结,再通过转储生成磁盘上的 SSTable,后续再通过合并把多个 SSTable 归并成大文件。
这套设计最明显的好处是写路径短。随机写变成了顺序写,内存累积一批后一次性落盘,所以高并发交易场景下写入吞吐很猛,这也是 OceanBase 能扛超高并发写入的底气。但代价就是读取路径变长:查一条数据,可能先在内存里查,查不到再去磁盘上的分层文件里查,如果多个 SSTable 都有旧版本数据,还得做多版本合并。
LSM-Tree 架构里最贵的一步是数据合并。因为平时写入产生的增量数据是零散的,不合并的话,读取时要翻太多文件。所以系统会在后台不断做转储和合并,把数据整理成更紧凑的文件。这也是 LSM-Tree 数据库在写入量大时经常出现“合并风暴”的原因。
2.2 旁路导入如何绕开事务路径
既然 LSM-Tree 最贵的是合并,那如果能跳过“先写内存再转储再合并”这条链路,直接把外部数据生成好现成的 SSTable,是不是就意味着导入过程不再需要合并的算力?
旁路导入就是这么干的。它把数据文件在外部直接切分、排序,然后生成完整的 SSTable 文件,再把这些文件挂到目标表上。整个过程中,数据不需要进 MemStore,不需要等待转储,也不会成为后台合并的对象。等于别人要花很久才能完成的数据整理,你在导入阶段就已经完成了。
而且因为数据文件是直接生成的,导入时的 CPU 消耗主要是解析外部文件、做类型转换、排序和校验。这些操作很多可以并行,而且不依赖事务并发控制。所以把并行度拉高,导入速度可以接近磁盘和网络的物理上限。实测下来,在千兆网络加多节点分布式集群的配置下,吞吐量可以从每秒几万行跳到每秒百万行以上,这个差距是数量级的。
2.3 导入和在线交易在同一张表里怎么共存
这里要解释一个关键技术点:可见性。旁路导入生成的数据文件在一开始是不可见的,它并不属于当前活跃事务的快照范围。只有当导入任务提交之后,这批数据文件才在某个全局版本号下变为可见。也就是说,正在跑的交易不会看到半截导入的数据,这也避免了“读到导入一半的脏数据”这种问题。
同时,因为导入不产生 redo 日志,也就不会占用事务日志的同步带宽。在线交易的高频写入照样走它自己的链路,旁路导入走的是另一条数据通路。它们共享的是底层的 CPU、内存、磁盘和网络,所以资源冲突仍然存在,但没有锁竞争和日志竞争这种强冲突。实际操作时,可以通过控制导入并行度、限定 IO 带宽,或者干脆选择在业务低峰期导入,把资源冲突降到最低。
不过还有一点要坦白:旁路导入本身通常需要拿到表级或分区级的锁,才能安全地把数据文件挂载上去。所以它更适合“初始化导入”“存量迁移”这类场景,而不是让外部系统每秒钟持续往里灌数据。如果你的诉求是持续高频同步,那还是走正常写入更合适,或者把增量攒成一个批次,定时用旁路导入批量合并。
3. 实操:从数据文件到一张大表的完整导入流程
理论说再多,不如直接把命令跑一遍。下面我从导入前的检查、导入姿势、导入后的校验三个阶段,完整还原我在项目中用旁路导入做大表初始化的过程。
3.1 导入前的检查清单,少一项都容易翻车
先说最容易翻车的地方:磁盘空间。旁路导入不是把数据读进内存再写进数据库,而是直接生成一份完整的数据文件落盘,这期间不仅要存原始数据文件,还要存生成的临时文件。加上导入过程中如果启用了校验和索引构建,空间占用会更高。我一般建议预留目标表最终大小的 1.5 到 2 倍空闲磁盘。别小看这一点,生产环境磁盘快满的时候导入,大概率导到一半报错。
然后是文件格式和切分。源文件如果是 CSV,建议提前确认分隔符、转义符、字符集,尤其是字段里包含换行和逗号时一定要做转义处理。另外,文件最好按逻辑拆成多个大小相近的分片,比如每个分片 1 GB 到 4 GB,这样导入并行度才能拉开。单一大文件即使设了高并行,读取端也容易成为瓶颈,效果会大打折扣。
还要检查目标表结构。如果你要导入的表有唯一索引或者主键,数据里绝不能有大量重复键,否则导入过程中做唯一性校验时会抛错。一个可行的办法是先用外部工具对源数据按主键排序并去重,再启动导入。这个预处理看起来多了一步,实际上能省下后面排查的无数时间。权限也不要漏,导入账号至少需要目标表的 INSERT 权限,如果涉及建临时文件,还需要对应的目录写权限。
3.2 两种直连旁路导入的姿势
我常用的第一种方式是用官方命令行工具。工具的大致调用方式如下:
obloader -h 127.0.0.1 -P 2881 -u root@test -p your_password -D test \ --table orders --csv --direct \ --parallel 8 \ --file-path /data/orders_split/参数的含义很直白:--direct打开旁路导入模式,--parallel控制并发线程数,--file-path指向分片文件的目录。看起来简单,但这里有几个细节值得说一下。
第一,并行度不是越大越好。我遇到过一个案例,把--parallel调到 32,结果导入端 CPU 全部打满,磁盘 IO 也高达 99%,可目标库所在宿主机的业务 CPU 被打到告警。后来降到 8,总耗时只多了不到 20%,在线业务的延迟却恢复正常。所以不要盲目追求大并行,而是要看整体资源余量。一般先从 4 到 8 开始,观察一两个文件跑完后的耗时和资源占用,再逐步上调。
第二,目录里的文件切片大小要有策略。文件太小,比如几十 MB 一个,导入线程的调度开销会吃掉不少性能;文件太大,比如 100 GB 一个,单线程处理时间过长,一旦失败重试代价很大。我习惯把源数据切成 2 GB 左右的分片,既方便并行,也方便失败后只重跑部分分片。
第二种方式是直接在 SQL 里用LOAD DATA加直导提示词:
LOAD DATA /*+ DIRECT(true, parallel 8) */ INFILE '/data/orders.csv' INTO TABLE orders FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';这种方式的优点是可以少部署一个工具,直接在 SQL 客户端里发起。缺点是单条 SQL 处理超大文件时,如果中途断了,恢复起来没有命令行工具那么方便。所以我更推荐把文件先分片,然后再用多条 SQL 并发导入,或者直接用命令行工具。
这里还要提一个非常实用的变种:分区交换。如果目标是一张按时间分区的大表,你可以先把数据导入一张同结构的分区临时表,再通过分区交换操作把临时表快速挂载为正式分区。这种用法适合“每个月出一张新分区、一次性换入”的跑批场景,几乎不会影响其他分区的在线查询。
3.3 导入完成后的校验与收尾工作
数据导完,别急着欢呼。我最开始做导入的时候,就遇到过“提示成功但数量对不上”的情况。所以导入完成后一定要做几件事。
先做行数校验。最直接的办法是源端统计文件行数,和目标表SELECT COUNT(*)对一下。如果分片很多,可以用多个并发查询按分区汇总,效率更高。行数不对的时候,先查是不是文件里本身有空行、结尾换行符处理不一致,或者某些字段因为类型转换失败被过滤掉了。
然后是抽查数据质量。随便抽几条源文件里的记录,到目标表里按主键查出来,对比关键字段是否一致。批量导入最容易出问题的点是字符串截断、时区转换、浮点精度。订单金额、时间戳这类字段建议重点看。要么写一个简单的对比脚本,要么用数据库侧的校验函数对关键列做聚合校验。
最后一步是更新统计信息。旁路导入是直接生成数据文件,数据库的统计信息不会自动更新。统计信息一旦过期,查询优化器很可能选错执行计划,最典型的表现就是“导完数据之后简单查询变慢了”。我在 4.2 节会细讲这个问题。现在只需要记住,导入完成后要手动收集一次统计信息,必要的时候再做一次合并,把新挂载的数据文件整理得更紧凑。
4. 实战中反复踩到的坑,以及排查思路
这部分内容不是我翻文档看来的,是真实跑过好几轮导入之后攒下来的教训。我挑几个高频问题,给出一眼就能用的排查思路。
4.1 导入速度一直上不去,问题出在哪
如果是旁路导入还是慢,先别怪数据库。排查顺序通常是:源端读取 -> 网络传输 -> 数据解析 -> 目标端落盘。
源端读取是最容易被忽视的瓶颈。你文件放在机械硬盘上,却要求导入吞吐跑满万兆网卡,这不现实。先把文件放到 SSD 或者把读文件的方式改成内存映射,再测一次,往往有惊喜。如果你从对象存储拉数据,那瓶颈就更明显了,对象的 QPS 限制可能比数据库落盘还低。
其次看网络。小文件吞吐测试发现带宽不达标时,检查是不是只用了单条 TCP 连接。并行度上去了,但每个线程还是顺序读同一个大文件,网络层没有建立足够多的连接,同样跑不满。这时候把大文件多切几个分片,让不同的线程读不同的文件,网络利用率会立刻改善。
再看解析和类型转换。CSV 文本解析非常耗 CPU,尤其是字段多、字符串占比高的时候。同一个数据我用 CSV 导入比用 Parquet 导入慢了将近一半。如果数据链路允许,尽量把源文件转成列式格式再导入,能省掉大量文本解析开销。真的只能用 CSV 时,也要避免在导入语句里做表达式计算和类型转换,这些东西会在导入线程里逐个字段执行,非常拖速度。
最后看目标端落盘。如果磁盘写吞吐已经打满,并行度再高也没有用。此时要么压缩源文件体积,减少写入量,要么把导入分配到多个租户、多个节点上,把写入压力分散开。还有一个相对少见的坑:导入时如果有其他任务在做大合并,磁盘 IO 会被合并吃掉一大截,导致旁路导入速度骤降。遇到这种情况,最好查一下后台合并调度,给它设置一个低优先级时段。
4.2 导入完成后查询反而变慢
导入成功,行数也对,但业务方反馈“查询比之前慢了不少”。我排查过一次,发现原因很简单:统计信息没有更新。
正常写入一条条插入的时候,数据库会逐步更新统计信息,优化器对数据分布有基本感知。旁路导入直接生成一批数据文件,相当于凭空多出了一个“数据岛”,统计信息完全不知道新文件里有什么。比如,某张订单表的日期字段原来最大只到今天,现在导入了去年的历史数据,优化器还按照“只有几个月数据”的模型去算扫描行数,自然选错执行计划。
解决办法就是导入后立刻收集统计信息。在 OceanBase 里可以调用统计信息收集的存储过程,对整个表或者单个分区做一次统计,非常重要。收集完统计信息如果发现查询还是慢,再看数据文件的分布。旁路导入生成的文件未必是全局有序的,如果你的查询经常做范围扫描,可以主动发起一次合并,让数据文件更紧凑、索引效率更高。我一般把这一步排到发布流程里,避免上线后才被业务方找上门。
还有一个隐蔽问题:目标表如果有二级索引,旁路导入在导入阶段会同步构建索引。如果构建索引时并行度不够,或者索引列的数据分布极度倾斜,导入完成后某些索引扫描路径会特别慢。这时要检查目标表所有索引的统计信息和数据分布,必要的时候重建一下倾斜严重的索引。
4.3 旁路导入和在线业务的资源冲突怎么解
旁路导入虽然不产生 redo、不争事务锁,但它毕竟要消耗 CPU、内存、磁盘 IO 和网络带宽。如果完全不控制,它完全可以把一台机器的 IO 打满,在线业务的写入延迟立刻飙升。
我的建议是第一做限流。命令行工具通常支持设置并发和带宽上限,先按业务低谷时可用资源的 50% 来配置,跑一轮看看效果。如果在线业务延迟没变化,再逐步往上加。不要一上来就用一个极高的并发去挑战极限,生产系统经不起这种冒险。
第二做时间窗。不是所有业务都 24 小时高负载。旁路导入尽量安排在业务低峰期,比如凌晨。把最重的导入任务放在窗口内完成,即使中途遇到问题,也有足够的时间人工介入,不影响白天业务。
第三是监控合并任务。导入生成的新文件会在后台触发调度,如果导入量很大,可能造成合并任务排队,并在某个时间集中执行。集中合并的 IO 压力不亚于导入本身。我会在导入完成后的半小时内盯一下合并相关的监控指标,如果出现合并流量突增,及时调整合并触发阈值,或者手动错峰。
下面这个表格是我整理的高频问题速查表,遇到问题可以直接对照排查。
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 导入速度上不去 | 源文件在机械盘或被单文件读取限制 | 切分文件、换 SSD、用列式格式导入 |
| 并行度越高越慢 | 瓶颈在网络或目标端磁盘 IO | 检查带宽和磁盘写吞吐,降并行度 |
| 导入报唯一约束冲突 | 源数据重复或边界数据未去重 | 按主键排序去重后再导入 |
| 导入成功但行数不对 | 空行、转义、类型转换被过滤 | 对比文件行数,抽查关键字段 |
| 导入后简单查询变慢 | 统计信息过期、生成文件不紧凑 | 收集统计信息、主动合并 |
| 导入期间在线事务变慢 | 磁盘 IO 或 CPU 被导入占满 | 限流、低峰导入、控制并行度 |
| 导入中断后重跑很痛 | 单文件过大,失败重试代价高 | 分片成多个小文件,断点续传 |
5. 写在最后:旁路导入的适用边界和我的个人体会
用了这么多次旁路导入,我自己的体会是:它解决的是“大规模初始化和批量同步”的问题,不是“每秒钟持续高频写入”的问题。一张表既扛高并发交易,又能快速导入 PB 级数据,这句话的真正含义是,可以在业务基本不停的情况下,把海量存量数据在很短时间内灌进去。但如果你把它理解成“可以一边跑实时交易,一边无限量持续灌数据”,那一定会碰壁。
综合来看,适合用旁路导入的场景是这几类:新集群上线时的全量数据迁移、历史库和冷数据归档批量入库、报表分析系统的大分区初始化、数据仓库每天的批量跑批。这些场景的共同点是单次数据量大,对导入吞吐要求高,并且导入窗口和在线业务可以错开。不适合的场景则是小碎数据高频实时写入,那会生成大量小的数据文件,反而拖累后续查询和合并。
最后再分享一个我自己的操作习惯:每次启动大导入之前,先用一小片数据做一轮完整演练,包括导入、校验、统计信息收集、在线查询验证。这个过程只要花十几分钟,但能提前发现绝大多数格式、权限、资源问题。别拿生产环境几 TB 的数据当试验品,演练阶段省下来的时间,会在正式导入的时候翻倍还给你。