简介:SQL Server开启CDC后,若代理作业异常或出现大事务写入,很容易触发“数据库事务日志已满,原因为REPLICATION”的报错,导致数据操作无法继续。这份PDF围绕该故障展开,先用通俗方式说明CDC与复制的日志使用步骤,再通过一个512MB日志上限的测试库,实际演示SQL Server Agent被关闭后逐条插入数据直至日志写满、报错出现、收缩无效的过程,并对比代理开启后作业仍失败的原因。针对已堵塞的日志,资源给出了执行sp_repldone标记已分发、待日志截断后恢复写入的临时方案,同时提及由短时大批量事务引发的类似故障场景。适合DBA、运维人员以及希望深入理解事务日志机制的SQL Server学习者。资源包为单个PDF文件,约314KB,内容紧凑;目前已有1782人学习,可见该问题颇具代表性,下载后可作为日志异常排查的参考手册。
1. 开着 CDC 的表写入时报 "log full due to REPLICATION",问题出在日志状态没被释放
SQL Server 开启 CDC(变更数据捕获)后,对基础表执行 Insert/Update/Delete 本身不会立刻把日志空间吃掉,真正让日志文件被写满的,是日志里那些被标记为 Replication 状态、又迟迟没有被取走解析的日志记录。最典型的报错是:The transaction log for database 'TestDB' is full due to 'REPLICATION'。这个报错很容易被误判成“日志文件太小”或者“恢复模式不对”,实际上问题出在 SQL Server Agent 的捕获作业没有及时消费日志,导致日志重用机制失效。很多 DBA 遇到这个报错后第一反应是收缩日志或切简单恢复模式,结果发现都不生效,原因就在日志内部存在大量“待复制”状态的活动日志。这篇文章会用两个场景复现这个问题,给出可执行的排查和处置脚本,并解释为什么不能简单依赖收缩日志或重启服务来解决。
2. CDC 的日志标记机制:为什么日志空间被占用后简单恢复模式也救不了
2.1 被标记的日志记录
在开启 CDC 的表上执行 DML 操作时,事务日志写入的粒度其实和普通表没有本质区别,但在日志记录内部,CDC 相关的日志记录被标记为“等待捕获”(即 Replication 状态)。这是 SQL Server 为复制和 CDC 预留的日志管理机制,和数据库的恢复模式没有关系,即便你把数据库切到简单恢复模式,这条日志只要还处于 Replication 状态,checkpoint 也不能把它标记为可重用。
在 SQL Server 内部,每个虚拟日志文件(VLF)都有一个状态标记位,可以通过DBCC LOGINFO查看到。常见状态值包括:
| 状态值 | 含义 | 说明 |
|---|---|---|
| 0 | Nothing | 可重用状态,日志空间可以被覆盖 |
| 1 | Checkpoint | 检查点截断边界 |
| 2 | Replication | 等待复制代理或 CDC 捕获作业读取 |
| 4 | Active | 活动事务持有 |
| 8 | EOS | 虚拟日志文件末尾 |
| 16 | Precreate | 预创建状态 |
| 32 | Inactive | 非活动状态 |
| 64 | Targeted Replication | 定向复制标记状态 |
当 CDC 系统表cdc.dbo_test_cdc_CT写入完成之后,这些日志记录会被置为可重用。整个链路是:日志写入 → 捕获作业读日志 → 解析并写入变更表 → 标记日志可重用 → checkpoint 截断空闲 VLF。任何一环中断,都会让占用比例不断上涨。
2.2 查看日志等待类型和 VLF 状态
当 CDC 开启后日志出现积压,第一件事是确认等待类型。通过sys.databases的log_reuse_wait_desc字段能够最快定位问题类型。
SELECT name AS database_name, log_reuse_wait_desc, recovery_model_desc, log_size_mb = CAST(CAST(size / 128.0 AS DECIMAL(12, 2)) AS DECIMAL(12, 2)) FROM sys.databases WHERE name = 'TestLogFull';log_reuse_wait_desc字段如果返回REPLICATION,说明日志尾部存在被标记为 Replication 状态且未被捕获作业消费的日志记录。此时无论手动执行CHECKPOINT还是DBCC SHRINKFILE都无法释放这部分空间,因为在 SQL Server 的日志管理逻辑里,这类日志属于“不属于当前活动事务、但又不能重用”的中间状态。
接着用DBCC LOGINFO查看具体 VLF 分布:
DBCC LOGINFO('TestLogFull');输出里Status列为 2 的 VLF 数量如果占大多数,说明这些虚拟日志文件都被 Replication 状态占用。RecoveryUnitId和FileId用于区分不同日志文件下的 VLF,在多日志文件场景下需要结合这两列判断是哪个物理日志文件积压。
2.3 日志截断链路
简单恢复模式下,日志截断发生在 checkpoint 之后,但 checkpoint 并不能截断 Replication 状态的日志。日志重用条件有三个:该日志不是活动事务持有的日志、不处于 Replication 或 Targeted Replication 状态、不在备份或还原链路的保留区间内。所以当复制或 CDC 作业无法消费日志时,日志空间就变成了一块“死空间”,新事务无法覆盖这部分日志。
提示:开启 CDC 的数据库,日志增长的风格会从“事务性波动”变成“持续递增”,因为每次 DML 都会产生一条被标记的日志记录,直到捕获作业完成消费后才释放。
3. 完整复现:SQL Server Agent 未启动导致的日志积压与处理
3.1 搭建测试环境和启用 CDC
先创建一个限制日志最大大小的数据库。日志文件初始大小设小一点,是为了让问题更快暴露出来,生产环境如果日志文件被设了上限或者磁盘本身不足,现象是一样的。
USE master; GO CREATE DATABASE TestLogFull ON PRIMARY ( NAME = N'TestLogFull', FILENAME = N'D:\DBFile\TestLogFull\TestLogFull.mdf', SIZE = 500MB, MAXSIZE = UNLIMITED, FILEGROWTH = 100MB ) LOG ON ( NAME = N'TestLogFull_log', FILENAME = N'D:\DBFile\TestLogFull\TestLogFull_Log.ldf', SIZE = 1MB, MAXSIZE = 512MB, FILEGROWTH = 100MB ); GO这里把日志文件最大大小设置为 512MB,FILEGROWTH设为 100MB,意思是每次日志空间不足自动增长 100MB,但总大小不能超过 512MB。如果不设置MAXSIZE,在磁盘空间不设限的情况下,日志会一直增长到把磁盘写满,并不会触发 “log full” 报错,所以复现这个现象依赖日志大小限制。
接着启用数据库级别的 CDC,并创建一张测试表:
USE TestLogFull; GO EXECUTE sys.sp_cdc_enable_db; GO CREATE TABLE dbo.test_cdc ( id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50), mail VARCHAR(50), address NVARCHAR(50), lastupdatetime DATETIME ); GO EXEC sys.sp_cdc_enable_table @source_schema = 'dbo', @source_name = 'test_cdc', @role_name = 'cdc_admin', @capture_instance = DEFAULT, @supports_net_changes = 1, @index_name = NULL, @filegroup_name = DEFAULT; GOsp_cdc_enable_table里的@supports_net_changes设为 1 时,SQL Server 会额外维护一个dbo_test_cdc_CT表的合并更新机制,用来支持cdc.fn_cdc_get_net_changes_dbo_test_cdc这类查询接口,同时也意味着捕获作业需要处理两处元数据,日志消费路径会比只记录净变更时稍长。@capture_instance保持 DEFAULT 时,捕获实例的命名规则是架构名_表名,也就是dbo_test_cdc。CDC 开启成功后,会在系统表中生成对应的捕获实例信息,同时创建cdc.dbo_test_cdc_CT变更表。
3.2 关闭 Agent 制造日志积压现象
SQL Server Agent 是执行捕获作业的宿主进程。关闭 Agent 后,日志写入照常发生,但cdc.dbo_test_cdc_capture作业无法运行,Replication 状态的日志无法被消费。用一个循环插入脚本模拟持续写入:
USE TestLogFull; GO SET NOCOUNT ON; DECLARE @i INT = 1; DECLARE @batch INT = 5000; WHILE @i <= 100000 BEGIN INSERT INTO dbo.test_cdc (name, mail, address, lastupdatetime) VALUES (CONCAT('user_', @i), CONCAT('user_', @i, '@example.com'), CONCAT('address_', @i), GETDATE()); SET @i = @i + 1; IF (@i % @batch = 0) BEGIN CHECKPOINT; END END GOCHECKPOINT的作用是触发 checkpoint 截断逻辑。在简单恢复模式下,checkpoint 会刷新脏页并尝试截断可重用的日志空间,如果部分日志被标记为 Replication,则这部分仍然保留,而其他非 Replication 部分可以正常重用。在 Agent 服务关闭的场景下,日志会持续积压在 “状态为 2” 的 VLF 中,直到日志文件达到设定的最大大小 512MB,此时所有写入操作都会被阻塞,报错信息就会是 “The transaction log for database is full due to ‘REPLICATION’”。
此时执行查询确认日志等待状态:
SELECT log_reuse_wait_desc FROM sys.databases WHERE name = 'TestLogFull';返回结果是REPLICATION,就可以确认是捕获作业没有消费日志导致的。执行DBCC SHRINKFILE也不会有效果,因为日志尾部空间被活动日志占满,收缩操作本身就需要日志尾部可重用空间的支持。
3.3 用 sp_repldone 应急释放日志空间
启动 SQL Server Agent 服务后,你会发现捕获作业依然无法执行,原因很直接:捕获作业本身也需要写日志,而此时日志文件已经没有可用空间。这种情况下,只能手动将待复制的日志标记为已分发。
USE TestLogFull; GO EXEC sys.sp_repldone @xactid = NULL, @xact_seqno = NULL, @numtrans = 0, @time = 0, @reset = 1; GO@reset = 1表示将所有已复制但尚未标记完成的事务日志全部标记为已分发,@xactid和@xact_seqno设置为 NULL 时配合@reset = 1使用,表示不针对某个具体事务,而是重置整条日志分发标记。@numtrans = 0表示不限定具体事务数量。
执行完成后,日志空间的释放不会立即发生。此时执行一条写入语句(比如插入一行无关紧要的数据),触发 checkpoint 后日志空间就会被截断释放。验证方法:
DBCC LOGINFO('TestLogFull');执行后观察输出的Status列,如果大量 VLF 的 Status 已经是 0,说明空间已恢复为可重用状态。
提示:这个操作是应急方案,不是常规维护手段。手动标记为已分发的日志,对应的 CDC 变更记录并不会补写到变更表,CDC 数据链路会出现断裂,下游拿不到这部分变化数据。
4. 短时间大批量写入导致日志积压的第二个场景
4.1 大事务让日志写入速度超过捕获消费速度
第二个场景和 Agent 无关。即便 SQL Server Agent 正常运行,捕获作业也会因为调度频率、日志读取速度等原因,在高吞吐写入时出现消费速度跟不上产生速度的情况。捕获作业读取日志的机制是轮询,默认每 5 秒扫描一次日志,如果写入量远大于 5 秒内能处理完的量,Replication 状态的日志就会积累。日常生产环境最常遇到的问题其实就是这类问题,典型场景是数据迁移、批量导入、初始化同步等。
当日志文件大小被限制时,只要写入速度持续高于捕获作业的消费速度,日志文件就会在某个时间点被写满。后续表现和第一个场景一样,Agent 作业会因为日志空间不足而失败,进而是死循环:日志满 → 作业失败 → 日志无法释放 → 后续写入全部阻塞。
应对这个场景,添加日志文件或扩大日志文件上限是直接有效的办法,核心是让捕获作业先跑起来,把日志消费到安全水位后再做收缩或调整。
USE master; GO ALTER DATABASE TestLogFull ADD LOG FILE ( NAME = N'TestLogFull_log2', FILENAME = N'D:\DBFile\TestLogFull\TestLogFull_Log2.ldf', SIZE = 1024MB, MAXSIZE = UNLIMITED, FILEGROWTH = 256MB ); GO执行ADD LOG FILE后,日志文件空间立即生效,不需要重启服务。这里初始大小给 1024MB,是为了确保在捕获作业积压量比较大的情况下,作业有足够空间完成日志解析和写入操作。MAXSIZE建议在生产环境保持UNLIMITED,如果无法做到不限大小,至少要保证磁盘剩余空间大于日志文件当前大小的 1.5 倍,给缓冲留出余地。
执行完添加日志文件的语句之后,需要等待一段时间让捕获作业继续工作。日志解析是逐步进行的,积压的日志量越大,作业运行时间越长。观察以下查询的输出变化:
SELECT instance_name = capture_instance, start_time = start_time, last_commit_time = last_commit_time, row_count = row_count FROM cdc.dbo_test_cdc_CT;如果row_count持续增加,说明捕获作业正在消费日志。注意不要在这期间盲目执行SHRINKFILE,因为日志文件仍然包含尚未完全释放的 VLF,收缩动作会打断捕获作业的处理节奏。
4.2 日志文件增长被磁盘空间限制时的处理顺序
如果日志满的原因不是文件大小上限,而是磁盘剩余空间不足,先观察磁盘空间:
EXEC sys.xp_readerrorlog 0, 1, N'transaction log', N'full';这条命令会读取 SQL Server 错误日志中包含关键字 “transaction log” 或 “full” 的记录,能看到日志文件是否触发了自动增长失败。如果磁盘确实没有剩余空间,先压缩或迁移其他非必要文件腾出空间,再执行上面的ADD LOG FILE。
DBCC SQLPERF(LOGSPACE)可以快速查看所有数据库的日志文件使用率:
DBCC SQLPERF(LOGSPACE);返回结果中的Log Space Used (%)和Log Size (MB)用于判断哪些数据库的日志已经接近上限。如果某库的日志使用率长期高于 90%,并且log_reuse_wait_desc是REPLICATION,就需要优先排查 CDC 作业状态。
4.3 重启服务的边际效果说明
在 SQL Server 2014 SP2 及以上版本中,如果存放日志的磁盘有可用空间,重启 SQL Server 服务后日志文件会自动扩展一小段空间,物理大小可能超过MAXSIZE设置。这个现象是服务重启过程中日志文件初始化逻辑导致的,不能依赖它作为解决方案。
问题在于,如果 Agent 没有设置为随服务自动启动,重启服务后日志积压问题会重复出现。正确做法是在服务配置里确认 SQL Server Agent 的启动模式是“自动”,同时确保 CDC 捕获作业处于启用状态。
5. 巡检脚本和积压判断技巧:提前发现 REPLICATION 状态日志堆积
5.1 快速定位风险库
生产环境不建议等日志满后再处理,维护窗口内跑一遍巡检脚本,可以在日志占比达到 70% 到 80% 时提前发现风险。
SELECT d.name AS dbname, d.log_reuse_wait_desc, d.recovery_model_desc, usg.total_log_size_mb, usg.used_log_size_mb, usg.used_log_percent, d.is_cdc_enabled FROM sys.databases d CROSS APPLY sys.dm_db_log_space_usage(d.database_id) usg WHERE d.is_cdc_enabled = 1 ORDER BY usg.used_log_percent DESC;sys.dm_db_log_space_usage返回数据库日志文件的总大小、已使用大小和使用百分比,比DBCC SQLPERF(LOGSPACE)更精确且支持CROSS APPLY过滤。is_cdc_enabled过滤出所有开启 CDC 的库,避免在非 CDC 库上做无效排查。used_log_percent超过 80 且log_reuse_wait_desc为REPLICATION时,需要立即处理。
如果log_reuse_wait_desc是NOTHING,说明日志空间处于完全可重用状态,此时即便used_log_percent偏高,也只是物理文件大小问题,不在本文讨论范围内。
5.2 检查捕获作业本身的状态
CDC 作业卡死后,捕获作业的历史记录里通常会有错误信息。可以通过以下方式检查作业当前状态和最近运行时间:
SELECT j.name AS job_name, ja.start_execution_date, ja.last_executed_step_id, ja.stop_execution_date, ja.last_executed_step_date, CASE WHEN ja.job_id IS NULL THEN 'not running' WHEN ja.stop_execution_date IS NULL THEN 'running' ELSE 'not running' END AS job_status FROM msdb.dbo.sysjobs j LEFT JOIN msdb.dbo.sysjobactivity ja ON ja.job_id = j.job_id WHERE j.name LIKE N'cdc.%';sysjobactivity里的start_execution_date和stop_execution_date用于判断作业是否正在运行。如果某 CDC 作业一直处于 running 状态且长达数十分钟未结束,说明它在等待日志空间或者被阻塞。关闭 Agent 再启动后,作业大概需要 1 到 2 分钟才能重新调度,不要立即判断作业失效。
5.3 关联 CDC 变更表的数据增长确认流程
当日志积压时,CDC 变更表的数据量不会增长,这两者之间存在强关联。用这条查询确认积压是否已被消费:
SELECT capture_instance, COUNT(*) AS captured_row_count FROM cdc.dbo_test_cdc_CT GROUP BY capture_instance;如果写入基表的数据量明显增加,但captured_row_count没有变化,说明捕获作业积压仍未释放。正常状态下,基表写入后几秒内变更表就应该出现对应数据。在批量写入场景下,这个延迟会被放大,但延迟一般不会超过几分钟,长时间不增长就说明从日志读取到变更表写入的链路中断了。
一条完整的应急处置顺序是:先确认log_reuse_wait_desc是REPLICATION,然后确认 CDC 作业是否在空转或处于失败状态,接着确认磁盘剩余空间,按顺序执行:启动无效则添加日志文件,日志文件不足则用sp_repldone应急释放空间,最后等 CDC 作业恢复运行后再重新评估 VLF 分布和日志文件大小设置。
本文还有配套的精品资源,点击获取