前事不忘,后事之师,不忘国耻!

 找回密码
 用户注册
搜索
查看: 21|回复: 0

[开发应用] Sybase ASE bcp 批量导入实战:从逐行日志暴涨到最小日志快灌与恢复兜底

[复制链接][复制链接]
发表于 昨天 05:49 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
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:确认现状基线

先摸清底子,不摸底就开跑等于闭眼开车:
  1. -- 目标库是否已开 bulkcopy 选项(1=开)
  2. select name, status2 from master..sysdatabases where name = 'factdb'
  3. dbcc traceon(3604)
  4. dbcc dbinfo(factdb)
  5. -- 更直接的看法
  6. use factdb
  7. go
  8. sp_dboption factdb
  9. go
  10. -- 目标表现状:索引、触发器、行数、空间
  11. sp_helpindex fact_trans
  12. sp_helptrigger  fact_trans
  13. sp_spaceused    fact_trans
  14. select count(*) from fact_trans
  15. -- 日志段余量
  16. sp_spaceused syslogs, 1
复制代码

把行数、日志余量、索引清单记下来,跑完做对比。

步骤 1:准备数据库开关
  1. master..sp_dboption factdb, "select into/bulkcopy", true
  2. go
  3. use factdb
  4. go
  5. checkpoint
  6. go
复制代码

注意 sp_dboption 在 master 里执行,改完必须回目标库 checkpoint 才生效。

步骤 2:卸索引、禁触发器

fast bcp 的硬前提。生产上的常规做法是生成一遍 DDL 备着,导入后原样建回:
  1. -- 导出建索引语句(用 ddlgen 或 sp_helpindex 抄录后手工整理)
  2. ddlgen -Usa -P*** -SASE01 -Dfactdb -TF -Nfact_trans -Oidx_fact_trans.sql
  3. -- 导入前删索引
  4. drop index fact_trans.idx_trans_date
  5. go
复制代码

触发器如果拿不掉(有审计要求),那就退回慢路径,并把预期日志量按行数×每行日志开销重新估算,备足日志段。

步骤 3:清表 + 快灌
  1. # 数据文件:4210 万行,字段分隔符选一个不会撞数据的,比如 |@|
  2. bcp factdb..fact_trans in trans.dat \
  3.     -Uetl -P*** -SASE01 \
  4.     -c -t "|@|" -r "\n" \
  5.     -b 20000 -A 16384 -J utf8 \
  6.     -e bcp_err.log -m 1000
复制代码

参数逐个说:-b 20000 每 2 万行一个事务;-A 16384 把网络包调到 16K,减少往返;-J utf8 显式声明字符集,别让服务端和客户端互相猜;-e 记格式错误行,-m 1000 最多容忍 1000 行坏数据,超了整批回滚。-r "\n" 在 Unix shell 里注意转义写法。

灌的过程中另开一个会话盯日志:
  1. while 1=1
  2. begin
  3.     sp_spaceused syslogs, 1
  4.     waitfor delay "00:01:00"
  5. end
复制代码

快路径下 syslogs 的页数应该几乎是平线;如果它开始线性爬升,说明 fast 条件被破坏了(最常见:有人中途建了索引),立刻停下来查,不要硬灌。

步骤 4:验证 + 恢复兜底(关键)
  1. -- 1. 行数对账:源 4210 万 = 表内行数 + bcp_err.log 坏行数
  2. select count(*) from fact_trans
  3. -- 2. 抽样对账:取 10 个固定主键,与源库逐字段比对
  4. -- 3. 重建索引(并行建,省时间)
  5. create clustered index ic_trans_date on fact_trans(trans_date) with parallel_degree = 4
  6. create index in_cust       on fact_trans(cust_id)       with parallel_degree = 4
  7. -- 4. 关掉 bulkcopy 选项,恢复常规日志策略
  8. master..sp_dboption factdb, "select into/bulkcopy", false
  9. go
  10. use factdb
  11. go
  12. checkpoint
  13. go
  14. -- 5. 立刻全量备份,把快灌数据固化进恢复链
  15. dump database factdb to "/backup/factdb_full_20261103.dmp" with compression = 1
  16. 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,下次同类灌数任务就是两小时一条命令加一条清单的事。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-11-4 01:49 , Processed in 0.023681 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表