SQL Server 事务日志暴涨、收缩不动实测:没做日志备份前 SHRINKFILE 只从 1736MB 缩到 1544MB,「切 SIMPLE 三步法」让 BACKUP LOG 直接报 4214
在 SQL Server 2022(16.0.4305.5)测试库 loglab(自造 orders 50 万行)上实测事务日志为什么只涨不缩:FULL 但没做完整备份时日志 3 轮都停在 776MB(伪简单模式);做完完整备份后每轮涨约 450MB 到 1736MB;不做日志备份直接 DBCC SHRINKFILE 只缩到 1544MB,要两轮「日志备份 + 收缩」才到 72MB;一个只插 1 行没提交的事务让日志备份只释放 7MB 并触发 9002,log_reuse_wait_desc 却还写着 LOG_BACKUP;切 SIMPLE 再切回 FULL 后 BACKUP LOG 报 4214。附只读排查 SQL、复现脚本与 CSV。
SQL Server 2022 实测(测试库、自造数据,2026-11-02,跑了两遍):日志文件大不是病,「日志备份之后 used_mb 不降」才是。我的判断:先按 RPO 二选一——要么 FULL + 定时日志备份 + 还原演练,要么明确用 SIMPLE + 完整/差异备份;最糟的是 FULL 却不做日志备份、再配上定时收缩或「切 SIMPLE 清日志」。本次切 SIMPLE 后 BACKUP LOG 报 Msg 4214,库悄悄回到伪简单模式,日志看着很健康(776MB 只用 3.6MB),其实已经不能按时间点还原。
适用对象
- 结论先说:日志文件只涨不缩,绝大多数是「FULL 恢复模式 + 没有定时日志备份」。本次做完完整备份后,每轮全表更新日志涨约 450MB:840 → 1288 → 1736MB,log_reuse_wait_desc = LOG_BACKUP。
- 反过来,FULL 但从没做过完整备份的库日志不会涨(3 轮都停在 776MB、只用 1.6–4.9%)——这是伪简单模式:sys.database_recovery_status.last_log_backup_lsn 为 NULL,日志在 CHECKPOINT 时自动截断,也不能按时间点还原。
- 不做日志备份直接 DBCC SHRINKFILE(log, 64):12ms 返回、不报错,日志只从 1736MB 缩到 1544MB;日志备份后再缩到 648MB(活动 VLF 在文件中段),第二次日志备份后才缩到 72MB。
- 一个只插 1 行、没提交的事务:日志备份只把 used 从 613.9MB 降到 606.8MB,MAXSIZE 640MB 下报 Msg 9002「full due to 'LOG_BACKUP'」。log_reuse_wait_desc 是上一次截断尝试时的原因,会过时——备份后 used 不降,就查 DBCC OPENTRAN。
- 「切 SIMPLE + CHECKPOINT + 收缩 + 切回 FULL」后日志确实变成 72MB,但接着 BACKUP LOG 报 Msg 4214;之后再写 450MB,日志仍是 776MB、只用 3.6MB——又回到伪简单模式。补一个差异备份(183ms)才接上日志链,中间这段无法时间点还原。
- 自动增长设成 1MB:同一个约 900MB 日志的大事务要 3.5–4.2s、1522 个 VLF;64MB 增长 2.1–2.5s、52 个 VLF;预分配 2GB 1.3–1.7s、16 个 VLF。本次 1522 个 VLF 对还原和联机耗时没有看出差别(都在 1.3–2.0s、90–165ms)。
测试库怎么造:1GB 数据文件、8MB 日志、FULL 恢复模式
实例是 Docker 里的 SQL Server 2022 Developer(RTM-CU27-GDR,16.0.4305.5)。loglab 数据文件 1024MB,日志初始 8MB、每次自动增长 64MB,建库后立刻 ALTER DATABASE … SET RECOVERY FULL——很多应用的建库脚本、以及 model 库默认值都是 FULL,这一步很有代表性。
每一步之后我都记一行状态:恢复模式、sys.databases.log_reuse_wait_desc、sys.dm_db_log_space_usage 的日志大小和已用、sys.dm_db_log_info 的 VLF 总数和活动 VLF 数,以及 sys.database_recovery_status.last_log_backup_lsn 是否为 NULL。下面的表都是这一行的摘录。
本节判断
- 「一轮更新」= UPDATE dbo.orders SET amount=amount+0.01, note=REPLICATE(…,400) 全表 50 万行,再 CHECKPOINT;本次 717–1269ms。
- 备份写到容器内本地目录,带 COMPRESSION;日志备份 .trn 约 78.6MB 对应约 1.4GB 日志。
先反直觉:FULL 但没做过完整备份的库,日志根本不涨
建库后不做任何备份,连跑 3 轮更新:日志从 392MB 长到 776MB 后就不动了,已用只有 12.5–38.4MB,log_reuse_wait_desc 一直是 NOTHING,last_log_backup_lsn 为 NULL。也就是说,这个库名义上是 FULL,实际行为和 SIMPLE 一样,CHECKPOINT 就截断。业内管这个叫「伪简单模式」。
做完一次完整备份(218ms,28474 页)之后,日志链开始了,情况立刻反转:同样 3 轮更新,日志 840 → 1288 → 1736MB,已用 495.6 → 953.3 → 1411.2MB,VLF 41 → 55,log_reuse_wait_desc 变成 LOG_BACKUP。所以「我们以前一直没事,最近突然涨」的库,常常是最近有人第一次给它做了完整备份,却没配日志备份。
| 步骤 | reuse_wait | 日志 MB | 已用 MB | VLF | last_log_backup_lsn |
|---|---|---|---|---|---|
| 建库 + 50 万行 | NOTHING | 392 | 8.1 | 28 | NULL |
| 无完整备份 · 第 1/2/3 轮 | NOTHING | 776 / 776 / 776 | 26.5 / 12.5 / 38.4 | 40 | NULL |
| 完整备份(218ms) | NOTHING | 776 | 38.5 | 40 | 有值 |
| 完整备份后 · 第 1 轮 | LOG_BACKUP | 840 | 495.6 | 41 | 有值 |
| 完整备份后 · 第 2 轮 | LOG_BACKUP | 1288 | 953.3 | 48 | 有值 |
| 完整备份后 · 第 3 轮 | LOG_BACKUP | 1736 | 1411.2 | 55 | 有值 |
直接收缩:12ms 返回、不报错,1736MB 只缩到 1544MB
接着我做大多数人的第一反应:DBCC SHRINKFILE (loglab_log, 64)。它 12ms 就返回了,没有任何报错,但日志只从 1736MB 变成 1544MB,已用 91.7%,活动 VLF 51 个——收缩只能砍掉文件尾部不活动的 VLF,前面 1.4GB 全是没备份的日志,一个字节都动不了。
做第 1 次日志备份(888ms,176468 页,压缩后 .trn 78.6MB)后已用降到 1.9MB,再收缩也只到 648MB:此时唯一的活动 VLF 落在文件中段,收缩不能越过它。第 2 次日志备份(61ms)让活动位置绕回文件开头,第三次收缩才到 72MB、8 个 VLF。第二遍复跑是一样的 1544 → 648 → 72MB。
本节判断
- 只读 SQL 第 6 段会算出「当前最多能缩到多少 MB」(最后一个活动 VLF 之前的总大小)。我在 776MB 的 loglab 上跑出来是 488MB,就是这个原因。
- SHRINKFILE 不报错不代表成功,缩完一定再查一次文件大小。
| 步骤 | 耗时 | 日志 MB | 已用 | 活动 VLF |
|---|---|---|---|---|
| 不备份直接 SHRINKFILE | 12ms | 1736 → 1544 | 1416.3MB(91.7%) | 51 |
| 日志备份 1 | 888ms | 1544 | 1.9MB | 1 |
| 再 SHRINKFILE | 44ms | 648 | 65.4MB | 2 |
| 日志备份 2 | 61ms | 648 | 1.4MB | 1 |
| 再 SHRINKFILE | 4ms | 72 | 0.5MB | 1 |
一个只插 1 行的事务,让日志备份也没用,还报 9002
这是我排查时最容易误判的场景。我在另一个会话里 BEGIN TRAN、往 job_log 插 1 行、不提交(模拟卡住的作业或没关的 SSMS 窗口),然后跑一轮更新,并把日志 MAXSIZE 限制为 640MB 模拟磁盘满。日志备份之后 used 只从 613.9MB 降到 606.8MB,下一轮更新直接报:Msg 9002, The transaction log for database 'loglab' is full due to 'LOG_BACKUP' and the holdup lsn is (257:63376:2)。
注意错误里写的是 LOG_BACKUP,sys.databases 里也还是 LOG_BACKUP——可我刚刚做过日志备份。真正的原因要用 DBCC OPENTRAN 看:OLDACT_SPID 77,开始 LSN (257:63464:2);只读 SQL 第 5 段显示 session 77、login sa、程序 SQLCMD、正在 WAITFOR,事务已开 5 秒,它自己写的日志不到 1KB。它之后产生的所有日志都不能截断。
我又单独做了一组(E3b):开着事务连做 3 次日志备份,第 1 次把 used 从 930MB 降到 467MB,之后两次只备份了 2 页和 5 页,used 一直 467MB,这时 reuse_wait 才变成 ACTIVE_TRANSACTION;CHECKPOINT 也没用。事务提交之后、下一次备份之前,它仍然写着 ACTIVE_TRANSACTION;再备份一次才变 NOTHING、used 20MB。
本节判断
- 我的读法:log_reuse_wait_desc 反映的是「上一次尝试截断时」卡在哪,不一定是现在。判断标准换成:做完日志备份,used_mb 有没有明显下降。没降,就查最老的活动事务。
- 第二遍复跑时日志起点更小,第 1 轮更新就撞上 640MB 报 9002,后续表现一致。
- 处理顺序:先找到这个会话的主人确认,再决定提交、回滚还是 KILL;KILL 一个大事务会触发回滚,回滚期间日志照样不能截断。
| 步骤 | log_reuse_wait_desc | 已用 MB | 活动 VLF |
|---|---|---|---|
| 开着事务 + 1 轮更新 | LOG_BACKUP | 930.4 | 38 |
| 日志备份 1 | ACTIVE_TRANSACTION | 467.2 | 14 |
| 日志备份 2 / 3(2 页、5 页) | ACTIVE_TRANSACTION | 467.2 / 467.3 | 14 |
| CHECKPOINT | ACTIVE_TRANSACTION | 467.3 | 14 |
| 事务已提交、尚未备份 | ACTIVE_TRANSACTION(已过时) | 467.6 | 14 |
| 日志备份 4 | NOTHING | 20.0 | 1 |
「切 SIMPLE 清日志」三步法:日志变小了,日志链也断了
网上最常见的「清日志」做法是:ALTER DATABASE … SET RECOVERY SIMPLE → CHECKPOINT + DBCC SHRINKFILE → SET RECOVERY FULL。我照做:一轮更新后日志 1280MB、已用 477.5MB,三步之后变成 72MB、8 个 VLF,看起来完美。
但紧接着 BACKUP LOG 失败:Msg 4214, BACKUP LOG cannot be performed because there is no current database backup,外加 Msg 3013。更隐蔽的是接下来:再跑一轮会产生 450MB 日志的更新,日志只长到 776MB、只用了 3.6MB,last_log_backup_lsn 又变回 NULL——库回到了 E1 的伪简单模式。监控只看「日志使用率」的话,这时一片绿。
补一个差异备份(183ms)后 BACKUP LOG 恢复正常。msdb.dbo.backupset 里能看到断点:切换前最后一个日志备份 last_lsn 是 331000003743200001,切换后的第一个日志备份 first_lsn 是 377000000452000001,中间不连续。从切 SIMPLE 到差异备份这段时间,任何时间点都恢复不了。
| 步骤 | reuse_wait | 日志 MB | 已用 MB | 结果 |
|---|---|---|---|---|
| 更新后(FULL) | LOG_BACKUP | 1280 | 477.5 | — |
| SIMPLE → CHECKPOINT + 收缩 → FULL | NOTHING | 72 | 0.5 | last_log_backup_lsn = NULL |
| BACKUP LOG | — | 72 | 0.5 | Msg 4214 + 3013 |
| 再 1 轮更新 | NOTHING | 776 | 3.6 | 伪简单模式,看着很健康 |
| 差异备份 + 日志备份 | NOTHING | 776 | 3.7 | 日志链接上,中间有断点 |
收缩之后的代价:自动增长 1MB,大事务慢 2–3 倍
收缩得太狠,日志就要在业务高峰重新长回来。我用同一个大事务(两轮全表更新、约 900MB 日志,一次提交)比较 4 种日志设置,每种重建库跑 3 次。SQL Server 2022 的规则是:每次增长 ≤64MB 只建 1 个 VLF,所以 1MB 增长长到 1.5GB 会留下 1522 个 VLF。
耗时差别很明显:1MB 增长 3907–4211ms,10% 和 64MB 都在 2.2–2.5s,预分配 2GB 只要 1268–1746ms。但我也要如实说:本次 1522 个 VLF 时,从完整备份还原(1.57–1.85s)和脱机再联机(126–132ms)没有看出比 16 个 VLF 慢(1.65–2.05s、144–151ms)——这个量级在 1.5GB 日志、热缓存下体现不出来,「VLF 多导致恢复慢」要到更大的日志和崩溃恢复场景才明显,我没测到,就不下结论。
本节判断
- 第二遍复跑(各 1 次):3511 / 2026 / 2069 / 1327ms,VLF 1522 / 59 / 52 / 16,顺序一致。
- 我的做法:日志按「两次日志备份之间的峰值用量」再留余量预分配,增长设固定 MB(如 512MB),别用 1MB,也别每晚缩回去。
| 日志设置 | 事务耗时 | 最终日志 | VLF | 还原 | 脱机→联机 |
|---|---|---|---|---|---|
| 初始 8MB · 增长 1MB | 3907–4211ms | 1523MB | 1522 | 1.57–1.85s | 126–132ms |
| 初始 8MB · 增长 10% | 2166–2297ms | 1544.6MB | 59 | 1.34–1.59s | 118–165ms |
| 初始 8MB · 增长 64MB | 2159–2512ms | 1544MB | 52 | 1.31–1.66s | 90–126ms |
| 预分配 2048MB · 增长 512MB | 1268–1746ms | 2048MB | 16 | 1.65–2.05s | 144–151ms |
先定 RPO,再选恢复模式;定时收缩和「切 SIMPLE」是最差的组合
写之前我列了五个角度:收缩前先看 reuse_wait;反对「切 SIMPLE 三步法」;日志文件大不是病、备份后不降才是;FULL 不做日志备份等于花 FULL 的磁盘买 SIMPLE 的恢复能力;1MB 增长让写入慢 2–3 倍。最后我选的判断是:恢复模式是 RPO 的决定,不是磁盘空间的决定。
理由很直接。FULL 模式唯一的价值是能用日志备份恢复到某个时间点;不做日志备份,你承担了 E1 里每轮 450MB 的日志增长,却拿不到这个能力。反过来,如果业务能接受「丢到上一次完整/差异备份」,就明确用 SIMPLE,并把完整/差异备份的频率按 RPO 定好——这不丢人。最糟糕的是两头都不要:FULL、不做日志备份、日志满了就切 SIMPLE 清一下,结果既有磁盘告警,又有 E4 那种自己都不知道的日志链断点。
我的建议具体到动作:要时间点恢复的库,日志备份间隔 ≤ RPO(例如 RPO 15 分钟就 15 分钟一次),每季度至少做一次「完整 + 日志链」还原到另一台机器;日志按峰值预分配后不再收缩。只有在一次性大操作(历史数据清理、重建索引)之后,才在备份完成后手动收缩一次。
| 可接受的数据丢失 | 恢复模式 | 必须做的 | 不要做的 |
|---|---|---|---|
| 几分钟到 1 小时 | FULL | 日志备份间隔 ≤ RPO;定期还原演练;监控 last_log 距今分钟数 | 切 SIMPLE 清日志;定时收缩 |
| 一天(丢到上次备份可接受) | SIMPLE | 每日完整或差异备份;还原演练 | 名义 FULL 却不做日志备份 |
| 说不清 | 先按 FULL 配好日志备份 | 跟业务确认 RPO 后再定 | 把恢复模式当磁盘空间开关 |
五分钟只读排查:在你的库上按这个顺序看
下载区的只读 SQL 全是 SELECT(不备份、不收缩、不改恢复模式、不 KILL),在本次测试实例上跑通。它按我排查时的顺序输出 6 段:每个库的恢复模式 / reuse_wait / 日志大小 / 增长设置 / VLF;FULL 库的最近完整、差异、日志备份和伪简单模式判断;最近 20 次备份的 LSN(看日志链断没断);当前库的日志使用率;最老的活动事务;当前最多能缩到多少。
本节判断
- 恢复模式 FULL 且 pseudo_simple = YES:日志链没建立或断过,先补完整/差异备份,再谈日志备份。
- last_reuse_wait = LOG_BACKUP 且 minutes_since_log_backup 很大:缺日志备份作业,先补作业,日志会自己停止增长。
- 做完日志备份 used_mb 不降:看第 5 段最老事务和 DBCC OPENTRAN,找会话主人确认后再处理。
- reuse_wait 是 AVAILABILITY_REPLICA、REPLICATION、ACTIVE_BACKUP_OR_RESTORE 等:问题在 AG 副本、复制或备份作业,本文没有复现,需要单独排查。
- 确定要收缩时,先看第 6 段 min_shrink_to_mb_now,目标设成峰值用量加余量,不要缩到 1MB。
觉得有用?转给正在做这件事的同事或朋友
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
SQL Server 日志文件很大,DBCC SHRINKFILE 为什么缩不动?
收缩只能释放文件尾部不活动的 VLF。FULL 模式下没备份的日志都是活动的,本次不做日志备份直接收缩只从 1736MB 缩到 1544MB;日志备份后还要看活动 VLF 位置,实测两轮「日志备份 + 收缩」才到 72MB。
做了日志备份,日志空间还是不释放,是怎么回事?
通常是有长时间未提交的事务。本次一个只插 1 行的未提交事务让日志备份只释放了约 7MB,并触发 9002;log_reuse_wait_desc 当时还显示 LOG_BACKUP。用 DBCC OPENTRAN 或 sys.dm_tran_database_transactions 找最老事务。
切到简单模式清日志再切回完整模式,有什么问题?
日志链会断。本次切回 FULL 后 BACKUP LOG 报 Msg 4214,库回到伪简单模式(日志自动截断、看着很健康),直到补完整或差异备份才恢复;断开期间无法按时间点还原。微软文档也要求切回后立即做完整或差异备份。
为什么我的库是 FULL 模式,日志却从来不涨?
很可能从没做过完整备份,或切过 SIMPLE 后没补备份。这时 sys.database_recovery_status.last_log_backup_lsn 为 NULL,库按伪简单模式运行:本次 3 轮共约 1.3GB 写入,日志一直停在 776MB。它不能做时间点恢复。
日志自动增长应该设多少?
按两次日志备份之间的峰值用量预分配,增长设固定 MB(如 256–512MB),不要用 1MB。本次同一个约 900MB 日志的事务,1MB 增长要 3.5–4.2s、1522 个 VLF,预分配 2GB 只要 1.3–1.7s。这是测试库热缓存结果,只看量级。
下一步
推荐动作
你提供只读 SQL 的输出(可脱敏),我们标出哪些库处于伪简单模式、日志链有没有断、日志备份间隔与 RPO 是否匹配、日志该预分配多大;备份作业和恢复模式的变更由你方授权执行。