马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
Sybase ASE bcp 批量导入实战:从逐行日志暴涨到最小日志快灌与恢复兜底
一、具体的问题
上周接手一个历史数据迁移:把一套老系统的 4200 万行交易流水灌进 ASE 15.7 的一张事实表。同事用的是最普通的写法——目标表原样保留索引,bcp in 不带任何批量参数,日志选项也没动。跑到第三个小时,前端报 1105:日志段满了。查 sp_spaceused,事务日志已经吃掉 38GB,而且 log segment 上还挂着 Last-Chance Threshold,挂起阈值一触发,所有业务写事务全部挂起,等于整库停摆。
更麻烦的是后面这半截:为了腾日志,运维夜里手工 dump tran ... with truncate_only 腾了两次,第二天发现最后一次全量备份是四天前的——中间这几天的事务链断了,真出事的话恢复窗口完全对不上。这趟下来,4200 万行用了将近 9 个小时,日志烧了 40 多 GB,最后还是手工清的场。
同样的数据,换一套正确姿势重跑:4 小时 12 分钟灌完,日志几乎不涨,全程业务无感知。差距不在硬件,就在三件事上:数据库的 bulkcopy 开关、导入前的索引和触发器处理、bcp 的 batch 粒度。
二、核心原理
ASE 里 bcp in 有两条截然不同的写入路径,搞清楚这一条,后面所有参数都是顺着它来的。
慢路径(默认):每一行数据都作为一个 insert 记录进事务日志,每行都要走完整的日志缓冲、分配日志页、写 log record 的流程。它的好处是每一行都有日志,理论上可以配合 dump/load 做事务级恢复;代价是日志吞吐成了天花板——4200 万行就是 4200 万条 log record,日志段不够大就必然 1105。
快路径(fast bcp / 最小日志):当满足特定条件时,bcp 走 extent 分配级别的最小日志——它不再逐行记 log record,只记录"哪些 extent 被这个批量会话占用了",数据的落盘靠 checkpoint 和批结束时的刷新完成。触发条件是三个,缺一不可:
- 目标数据库打开了 select into/bulkcopy 选项(sp_dboption);
- 目标表没有索引(或导入前已删除)、没有触发器;
- bcp in 的 batch size 设置合理(-b),且导入粒度是整批提交。
最小日志的代价必须直说:fast bcp 产生的数据不参与事务日志恢复链。这批数据 load 进来之后,你没法用 dump tran 的方式把它包进增量恢复里——唯一安全的兜底是导入完成后立刻 dump database 做一次全量,把这段数据"固化"进备份链。这就是标题里"恢复兜底"四个字的含义:快灌省下的时间,要用一次全量备份补回去,账是平的。
另外两个支撑点:一是 truncate table 后紧跟 bcp,是和最小日志最搭配的开场——truncate 本身走日志但只记页释放,几乎不占空间;二是大 batch 让 ASE 一个事务管很多行,减少了提交开销和日志头开销,-b 给到 1 万到 5 万行通常就够,再大收益递减、内存压力上升。
三、实例参考(动手步骤)
步骤 0:确认现状基线
先摸清底子,不摸底就开跑等于闭眼开车:
- -- 目标库是否已开 bulkcopy 选项(1=开)
- select name, status2 from master..sysdatabases where name = 'factdb'
- dbcc traceon(3604)
- dbcc dbinfo(factdb)
- -- 更直接的看法
- use factdb
- go
- sp_dboption factdb
- go
- -- 目标表现状:索引、触发器、行数、空间
- sp_helpindex fact_trans
- sp_helptrigger fact_trans
- sp_spaceused fact_trans
- select count(*) from fact_trans
- -- 日志段余量
- sp_spaceused syslogs, 1
复制代码
把行数、日志余量、索引清单记下来,跑完做对比。
步骤 1:准备数据库开关
- master..sp_dboption factdb, "select into/bulkcopy", true
- go
- use factdb
- go
- checkpoint
- go
复制代码
注意 sp_dboption 在 master 里执行,改完必须回目标库 checkpoint 才生效。
步骤 2:卸索引、禁触发器
fast bcp 的硬前提。生产上的常规做法是生成一遍 DDL 备着,导入后原样建回:
- -- 导出建索引语句(用 ddlgen 或 sp_helpindex 抄录后手工整理)
- ddlgen -Usa -P*** -SASE01 -Dfactdb -TF -Nfact_trans -Oidx_fact_trans.sql
- -- 导入前删索引
- drop index fact_trans.idx_trans_date
- go
复制代码
触发器如果拿不掉(有审计要求),那就退回慢路径,并把预期日志量按行数×每行日志开销重新估算,备足日志段。
步骤 3:清表 + 快灌
- # 数据文件:4210 万行,字段分隔符选一个不会撞数据的,比如 |@|
- bcp factdb..fact_trans in trans.dat \
- -Uetl -P*** -SASE01 \
- -c -t "|@|" -r "\n" \
- -b 20000 -A 16384 -J utf8 \
- -e bcp_err.log -m 1000
复制代码
参数逐个说:-b 20000 每 2 万行一个事务;-A 16384 把网络包调到 16K,减少往返;-J utf8 显式声明字符集,别让服务端和客户端互相猜;-e 记格式错误行,-m 1000 最多容忍 1000 行坏数据,超了整批回滚。-r "\n" 在 Unix shell 里注意转义写法。
灌的过程中另开一个会话盯日志:
- while 1=1
- begin
- sp_spaceused syslogs, 1
- waitfor delay "00:01:00"
- end
复制代码
快路径下 syslogs 的页数应该几乎是平线;如果它开始线性爬升,说明 fast 条件被破坏了(最常见:有人中途建了索引),立刻停下来查,不要硬灌。
步骤 4:验证 + 恢复兜底(关键)
- -- 1. 行数对账:源 4210 万 = 表内行数 + bcp_err.log 坏行数
- select count(*) from fact_trans
- -- 2. 抽样对账:取 10 个固定主键,与源库逐字段比对
- -- 3. 重建索引(并行建,省时间)
- create clustered index ic_trans_date on fact_trans(trans_date) with parallel_degree = 4
- create index in_cust on fact_trans(cust_id) with parallel_degree = 4
- -- 4. 关掉 bulkcopy 选项,恢复常规日志策略
- master..sp_dboption factdb, "select into/bulkcopy", false
- go
- use factdb
- go
- checkpoint
- go
- -- 5. 立刻全量备份,把快灌数据固化进恢复链
- dump database factdb to "/backup/factdb_full_20261103.dmp" with compression = 1
- go
复制代码
第 5 步不是可选项。fast bcp 的数据不在日志恢复链里,不落全量,中间任何一次故障都会把这批数据打回原形。
治理前后对比
| 指标 | 改造前 | 改造后 | | 总耗时(4200 万行) | 9 小时 02 分 | 4 小时 12 分 | | 吞吐 | 约 1.3 万行/分钟 | 约 2.8 万行/分钟 | | 事务日志增量 | +40.3 GB(两次 1105) | +0.4 GB(近乎平线) | | 业务影响 | Last-Chance Threshold 挂起全部写事务 | 无感知 | | 恢复链完整性 | 备份链断裂风险(手工 truncate_only) | 导入后全量兜底,链完整 | | 人力 | 通宵两次手工清日志 | 计划内执行,零干预 |
四、实操检查清单
- 导入前确认 sp_dboption 库名 输出里 select into/bulkcopy 为 ON,改完执行过 checkpoint;
- 确认目标表无索引、无触发器,索引 DDL 已用 ddlgen 或手工方式存档;
- 导入前 truncate table,并确认 truncate 后做过 dump tran ... with truncate_only 让日志归零起点;
- -b 批大小在 1 万~5 万行之间,-A 网络包 ≥16K,-J 显式指定字符集;
- 分隔符选择不会出现在数据里的串(如 |@|),格式行错误落在 -e 指定的错误文件里;
- 导入期间旁路会话每分钟监控 sp_spaceused syslogs, 1,日志线性增长立即中止排查;
- 结束后行数三方对账:源行数 = 表内行数 + 错误文件行数;
- 重建索引时用 parallel_degree 提速,建完 update statistics;
- 导入完成立刻 dump database 全量兜底,归档到备份目录并登记;
- 恢复 select into/bulkcopy 为 OFF 并 checkpoint,防止日常误操作走最小日志;
- 把整个过程(行数、耗时、日志增量、备份文件名)写进迁移记录,下次直接复用。
几个容易踩的坑
- 以为开了 bulkcopy 就一定是 fast bcp。表上还留着索引,照样走慢路径,日志照涨。三个条件是"与"关系,逐条核。
- 快灌完就完事,忘了 dump database。这是 fast bcp 最大的隐性债务:数据在表里看得见,但不在恢复链里。中间出故障,load 回来的是快灌之前的状态。
- sp_dboption 改完不 checkpoint。选项实际没生效,bcp 悄悄退回慢路径,你还纳闷日志为什么还在涨。
- -b 贪大。开到 100 万行,批内任何一行格式错误都可能拖累整批处理,且 bcp 客户端内存吃紧。2 万左右是甜点位。
- 字符集不声明,-J 缺省猜。UTF-8 源文件灌进 cp936 库,中文变问号,对账时才发现——返工一次一天。导入前先 bcp 库..表 out 两行样本验证分隔与编码。
- 错误行处置不清爽。-m 上限内 bcp 会跳过坏行继续,但很多人忘了把 -e 错误文件里的行数加进对账公式,行数差几百行查一天。
- truncate 前不做全量。truncate 本身不可恢复,万一源数据文件是坏的,连后悔的底牌都没有。正确顺序是:全量备份 → truncate → 快灌。
- Last-Chance Threshold 挂起后硬杀会话。挂起时先 dump tran with truncate_only 腾日志让事务过,别上来就 kill——杀会话回滚 4000 万行的 insert,回滚时间比灌入时间还长。
把这套流程固化成迁移 SOP,下次同类灌数任务就是两小时一条命令加一条清单的事。 |