SQL Server备份连续失败九天:磁盘耗尽排查与恢复验证

发布时间:2026/11/3 4:57:14
SQL Server备份连续失败九天:磁盘耗尽排查与恢复验证

1. 从一次“例行巡检”说起:备份失败到底伤到了谁

每天早上到工位,我有个保持了快十年的习惯:先花四十秒扫一眼数据库作业的历史记录和磁盘剩余。这个动作看起来像是在摸鱼,但它替我拦下过好几次事故。那天早上打开 SQL Server 管理工具,订单库 OrderHub 的每日全量备份作业状态是红色的失败叉,再往上翻历史,前八天全是同一个叉。

也就是说,这套 SQL Server 每日备份已经连续失败九天,而没有任何人收到过通知。备份这东西有个很变态的特性——它每天成功的时候没人记得它,它连续失败九天也没人发现,直到真出事需要还原的那一刻,你才会知道手里那份“备份文件”是个 0 字节的壳子。所以这篇记录我不打算写成一份教科书式的“备份失败排查手册”,而是把这次从发现、定性、定位到修复、验证的完整过程摊开讲,包括中间走了哪些弯路、哪几个判断是错的、哪些地方现在回头看纯属侥幸。无论你是刚接手数据库运维的新人,还是已经把备份作业配好就再没管过的开发者,这个案例里的坑大概率你也会踩。

1.1 一个红色叉号背后的九天空窗

先还原现场。这套 OrderHub 库是某中型制造企业订单系统的核心库,数据量不算夸张,压缩后完整备份大约 18 GB 左右,跑在一台四年前上架的物理服务器上,实例是 SQL Server 2016 标准版。备份方案是运维同事三年前配的,内容很简单:每天 23:00 跑一次完整备份,目标目录D:\SQLBackup\,文件名固定为OrderHub_Full.bak,作业跑完之后由维护计划里的“清除维护任务”把三天前的备份删掉。

这套配置当年应该是能跑的,因为我在最早的备份文件列表里看到过正常的结果。但它有三个隐患,都在配置的时候就埋下了:

  • 备份文件用固定文件名,而且是追加模式而不是覆盖模式,文件只会一天天变大,永远不会变成新的一个;
  • 清理任务按“创建时间早于三天前”删除,可这个文件每天都在被追加写入,修改时间天天刷新,所以在清理任务眼里它永远是“新的”,从来没被删过;
  • 作业失败之后没有任何通知,维护计划也没勾“向操作员发送邮件”,等于这套机制只会干活,不会喊疼。

三条叠加,结果就是:文件从第一天的 18 GB 一路追加,第九天的时候已经膨胀到 90 GB 出头,把D:盘剩下的空间吃了个干净,第十天凌晨的那次备份在写入阶段直接撞墙,反而“稳定”地每天都失败。

1.2 影响范围的三层拆解

很多人处理这类问题会直接跳进“赶紧让它跑起来”,但我在动手之前先花十分钟把影响范围划清楚,因为这决定了你修复的优先级和要不要拉人。

第一层是数据层面的 RPO。九天没有有效备份,意味着一旦这台机器的磁盘彻底损坏,业务数据只能退回到十天前的状态。订单系统一天的增量是上万条单据,九天就是十万量级的业务数据,这个损失不是 DBA 一个人能扛的,必须先让业务方知道当前的风险敞口有多大,而不是悄悄修完假装无事发生。

第二层是备份链层面的完整性。完整备份失败,不代表日志备份和差异备份一定也失败。如果这台实例上还配了日志备份作业,日志备份有可能在正常跑,但日志链的起点是最后一次成功的完整备份,中间断了,还原的时候根本接不上。我查了一下,这套库是简单恢复模式,压根没配日志备份,所以这条暂时不用担心,但换个场景这就是个大坑。

第三层是机制层面的可信度。这次暴露出来的不是“备份失败”这一个孤立的点,而是“备份失败九天后才被人用肉眼发现”这个系统性缺陷。如果只把磁盘清出来让作业重新跑通,过三个月它还会以另一种形式再挂一次。所以真正的修复目标不是“让今晚的备份成功”,而是“让下一次失败在十五分钟内被人知道”。

把这三层想明白,后面的动作顺序就顺了:先止血、再治本、最后补监控。

2. 备份失败的定位逻辑:先搞清引擎在干什么

排查任何 SQL Server 备份失败,我自己的习惯是不要一上来就翻错误日志,而是先在脑子里过一遍“一次 BACKUP DATABASE 到底走了哪些步骤”。知道它有几步、每步可能在哪里断,看错误号的时候就能直接对上号,比大海捞针快得多。

2.1 一次备份在引擎内部走了哪几步

一次完整的数据库备份,底层大致分五个阶段。第一个阶段是打开备份设备,也就是在目标路径创建或打开那个.bak文件,这一步是操作系统层面的文件操作,会检查路径是否存在、服务账户有没有写权限、文件有没有被别人独占。第二个阶段是初始化和校验媒体集,SQL Server 会读取文件头,确认里面的媒体集信息能不能接着写,追加模式与覆盖模式的差异就发生在这一步。第三个阶段是读取数据库的元数据并计算备份规模,引擎会根据数据文件里已分配的页估算大概要写多少数据。第四个阶段才是真正的数据读取与写入,完整备份会把所有已分配的区扫一遍,差异备份则读取差异更改映射页来判断哪些区变过。最后一个阶段是收尾,更新msdb里的备份历史表,对日志备份来说还要标记日志可以被截断的位置。

这个拆解的价值在于:错误发生在哪个阶段,错误号会直接告诉你。3201 和 18204 基本都是第一阶段,文件打不开;3241 是第二阶段,媒体集对不上;3202 搭配操作系统错误 112 是第四阶段,写到一半空间没了;4305 则完全是日志链层面的问题。我先看了一眼作业历史里的那行报错信息,无法写入备份设备 ... 操作系统错误 112(磁盘空间不足),基本可以判断是第四阶段撞墙,跟权限、媒体集都没关系,直接去查磁盘就行了。

2.2 三类根因的快速分诊法

我把这些年遇到的 SQL Server 备份失败按根因归成三类,遇到问题先分诊,能省掉一大半无效排查。

第一类是“环境类”,占我遇到过的七成以上:磁盘满、目录不存在、共享路径断开、防病毒软件锁住备份文件、服务器被人重启后某个挂载盘没起来。这类问题的特点是报错里往往带操作系统的错误码,比如 5(拒绝访问)、3(找不到路径)、112(空间不足)、53(找不到网络路径)。看到这类错误码,直接去操作系统层面查,别在数据库里绕。

第二类是“权限与身份类”,隐蔽性最强:备份目录的 ACL 变了、SQL Server 服务的启动账户被换过、作业所有者是一个已经被停用的域账号、跨机器写到共享目录时服务账户没有网络访问权。这类问题的麻烦在于它会“突然”失败,明明昨天还好好的,因为你根本不知道 IT 那边改了什么东西。

第三类是“逻辑类”,最容易被误判成环境问题:数据库恢复模式被改成 SIMPLE 导致日志备份永久失败、日志链因为一次误操作断裂、数据库处于恢复中或可疑状态、差异备份依赖的基准完整备份被删掉了。这类问题看错误信息往往语焉不详,只能靠查系统视图和备份历史来判断。

这次的问题属于第一类,但修复过程中我又接连撞上了第二类和第三类的边角,后面会细说。

2.3 用 msdb 把第一现场捞出来

图形界面看作业历史有个毛病:它只显示最近几十条,而且信息被截断。真正要翻旧账,直接查msdb里的系统表最快。我用的第一条语句是这个,把指定作业最近五十次执行记录连同每步的报错信息一起拉出来:

SELECT TOP 50 j.name AS job_name, h.step_id, h.step_name, h.run_status, -- 0=失败 1=成功 2=重试 3=取消 4=执行中 h.run_date, -- 形如 20240612 的整数 h.run_time, -- 形如 230015 的整数 h.run_duration, -- HHMMSS 格式的耗时 h.message FROM msdb.dbo.sysjobhistory AS h JOIN msdb.dbo.sysjobs AS j ON j.job_id = h.job_id WHERE j.name = N'OrderHub-每日全量备份' ORDER BY h.instance_id DESC;

这里有两个小细节值得记一下。一是run_status = 0才代表失败,4是正在执行,别搞混;二是run_date和run_time都是整数而不是日期类型,直接按它排序会出错,我一般按instance_id倒序,那个是自增的,天然有序。另外,作业级别的汇总行step_id = 0只给出“作业失败”,真正的报错文本在具体的步骤行里,step_id > 0的那些。

拿到报错文本之后,第二条语句用来确认“上一次成功到底是什么时候”,这决定了你的数据风险敞口有多大:

SELECT TOP 20 bs.database_name, bs.type, -- D=完整 I=差异 L=日志 bs.backup_start_date, bs.backup_finish_date, CAST(bs.backup_size / 1024.0 / 1024 AS DECIMAL(18,2)) AS size_mb, CAST(bs.compressed_backup_size / 1024.0 / 1024 AS DECIMAL(18,2)) AS compressed_mb, bmf.physical_device_name, bs.is_damaged, bs.has_backup_checksums FROM msdb.dbo.backupset AS bs JOIN msdb.dbo.backupmediafamily AS bmf ON bmf.media_set_id = bs.media_set_id WHERE bs.database_name = N'OrderHub' ORDER BY bs.backup_start_date DESC;

这条语句的信息量比作业历史大得多。backup_size是未压缩的原始大小,compressed_backup_size是实际落盘大小;is_damaged标记这份备份在写入时是否出过错;has_backup_checksums告诉你当初有没有开校验。我在这张表里看到的结果很说明问题:最近九天全是空的,最后一条成功的记录在十天前,size_mb显示 18 GB 出头,physical_device_name全部指向同一个文件路径。

顺带说一句,这个文件路径字段经常被忽略,但它是排查“备份到底写到哪去了”的关键。我见过一次事故,作业配的是相对路径,结果文件被写到了 SQL Server 默认备份目录,而运维一直以为它在D盘,查了半天“为什么 D 盘空间没变”。

3. 核心细节拆解:四个最容易踩的坑

定位到“磁盘满”只是开始,真正决定这次修复质量的,是后面几件事怎么处理。这一节我把自己在这个案子里反复确认过的四个细节摊开讲,每一个都有明确的“为什么这么做”。

3.1 磁盘空间:不是看剩余多少,而是看够不够下一次

查磁盘剩余最直接的办法是xp_fixeddrives,它返回每个盘的可用空间,单位 MB:

EXEC master.dbo.xp_fixeddrives;

这个结果只能用作初步判断。真正要算的是“下一次备份需要多少空间”,而这个数字不等于数据库文件的大小。正确算法是:把数据文件里已经分配使用的页累加起来,再乘以一个压缩系数(如果你开了备份压缩)。我用的查询是这样的:

SELECT DB_NAME() AS db_name, SUM(CASE WHEN type_desc = 'ROWS' THEN size ELSE 0 END) * 8.0 / 1024 AS data_file_mb, SUM(CASE WHEN type_desc = 'LOG' THEN size ELSE 0 END) * 8.0 / 1024 AS log_file_mb FROM sys.database_files;

注意,size字段的单位是 8 KB 页,所以乘以 8 再除以 1024 得到的是 MB,这是文件分配的大小,不是实际写入的数据量。完整备份只备份实际有数据的页,所以真实备份大小一般会小于这个数字;但对于碎片率高、删过大量数据的库,被标记为已分配但实际空的页也会被读,差距就出来了。我一般按这个算出来的值再上浮 30% 作为空间规划依据,宁可多留。

更靠谱的做法是直接看历史备份记录里的backup_size,那是实测值。这次的案例里,历史完整备份是 18 GB,但磁盘上的文件已经 90 GB,一眼就能看出来这不是“单次备份需要 90 GB”,而是“追加了五次”。这就是文件名固定加上追加模式的经典后果,很多人第一次遇到会误判成“数据库突然变大了”。

提醒:磁盘规划时千万别只给备份目录留“一次备份”的空间。至少要按完整备份的 3 到 5 倍规划,因为你会同时存在多代备份;如果配上差异备份和日志备份,还得再加一截。空间紧张的机器,把备份目录和数据库数据文件、日志文件分到不同的物理盘上,出问题时至少不会互相拖累。

3.2 服务账户与目录权限:三个最容易“突然”失效的场景

磁盘清出来之后,作业还是失败。这次报的是 3201,操作系统错误 5(拒绝访问)。这就有意思了,明明目录存在、磁盘有空间,怎么会拒绝访问?

查服务账户的语句很简单:

SELECT servicename, service_account, status_desc FROM sys.dm_server_services;

这个视图需要VIEW SERVER STATE权限。看到结果我才想起来,这台机器的 SQL Server 服务账户在年初被 IT 统一改成过一个专用的低权限服务账号,而备份目录的 ACL 是更早之前按旧账户配的。改账户的时候只验证了服务能起来、业务能用,没人想到去验证备份作业——因为备份是半夜跑的,白天没人看。

这类“身份变化导致的隐性失效”有三个典型触发点,我在不同项目里都遇到过:一是服务账户被改,比如安全整改统一换成组托管服务账户,旧目录的 ACL 没跟着更新;二是写跨机器共享路径,SQL Server 服务账户如果不是域账户,访问\\fileserver\backup这类 UNC 路径时用的是机器账户,而共享权限里往往只授权了用户组,没有授权机器账户;三是目录被安全软件或存储策略重置了权限,尤其是那种挂载的存储卷,重新挂载后 ACL 会回到默认值。

修复的判定标准很简单:用服务账户的身份实际去目录里创建一个文件,能创建成功才算通。图形界面里“看到目录存在”和“能写进去”完全是两回事,很多人卡在这一步是因为只用管理员账号在资源管理器里点了一下,觉得没问题。

3.3 恢复模式与备份链:一个 SIMPLE 就能让日志备份永久失败

这次案子里没直接踩到,但排查过程中我顺手把这个实例上所有库的恢复模式和日志复用状态扫了一遍,这个动作后来救了我一次。

SELECT name, recovery_model_desc, log_reuse_wait_desc, state_desc, is_read_only FROM sys.databases ORDER BY name;

log_reuse_wait_desc这个字段值得单独说。它告诉你日志为什么不能被截断,常见取值有NOTHING(正常)、LOG_BACKUP(等着做日志备份)、ACTIVE_TRANSACTION(有长事务挂着)、REPLICATION、AVAILABILITY_REPLICA等等。如果某个库是完整恢复模式,而log_reuse_wait_desc长期显示LOG_BACKUP,说明日志备份作业根本没在跑,日志文件会一直涨,早晚把盘吃满——这跟本次的磁盘耗尽是完全同一类问题的两种表现形式。

至于“恢复模式被改成 SIMPLE 导致日志备份失败”,这是我在另一个项目里踩过的坑。开发同事为了省事,把一个测试库的恢复模式从 FULL 改成了 SIMPLE,测试完忘了改回来。日志备份作业每天准点失败,报的错是“在简单恢复模式下不允许执行 BACKUP LOG”,但因为那个库不重要,没人管。三个月后这个库被转成正式用途,备份策略没跟着重新评审,结果就是“看起来配了日志备份,实际上一条都没有”。

3.4 作业配置:超时、命名冲突与文件句柄

最后一个细节是作业本身的配置。这次我在改配置的时候顺手检查了几项,这里一并说清。

超时设置。作业步骤有个@timeout参数,单位是秒,0 表示不限时。维护计划生成的子计划里也有超时,默认值有时候短得离谱,比如某些版本里清理任务的默认超时只有几分钟。数据量大的库,备份跑两三个小时很正常,超时设短了就会出现“备份其实还在写,但作业已经被判定失败”的诡异现象,然后下次跑的时候文件还被占着,报出来的错完全是另一回事。

文件命名与覆盖模式。这是本次事故的根源。BACKUP DATABASE ... TO DISK = '...' WITH INIT表示覆盖,WITH NOINIT表示追加,不写的话默认是追加。用固定文件名加追加模式,就是每天往同一个文件后面接一段,文件只会越来越大。正确做法要么用带日期的动态文件名配覆盖模式,要么用固定文件名配覆盖模式(但只保留一份,没有历史版本)。我这次改成了前者,在作业里用 T-SQL 拼日期:

DECLARE @path NVARCHAR(400) = N'D:\SQLBackup\OrderHub_Full_' + CONVERT(NVARCHAR(8), GETDATE(), 112) + N'.bak'; BACKUP DATABASE OrderHub TO DISK = @path WITH INIT, -- 同名文件直接覆盖,杜绝追加膨胀 CHECKSUM, -- 写入时计算校验和,还原时可校验 COMPRESSION, -- 标准版 2016 SP1 起支持,压缩比通常 3~5 倍 STATS = 10, -- 每 10% 打一条进度,方便看是不是卡住了 NAME = N'OrderHub 每日全量备份';

COMPRESSION这个选项值得单独强调。标准版从 2016 SP1 开始支持备份压缩,我这套库实测压到了原来的四分之一左右,18 GB 变成 4.5 GB 上下,磁盘压力瞬间小了一大截。代价是备份期间 CPU 会多占用一些,对于备份窗口在深夜、CPU 本来就闲的库来说,这笔账怎么算都划算。

文件句柄被占用。这个坑很隐蔽。备份文件如果正被另一个进程打开——比如杀毒软件在扫描、某个备份软件在做二次归档、或者上一次失败的备份进程还没完全退出——就会报“无法打开备份设备”。排查办法是在服务器上直接看文件有没有被锁,或者用sys.dm_exec_requests看有没有正在跑的备份命令:

SELECT session_id, command, status, start_time, percent_complete FROM sys.dm_exec_requests WHERE command LIKE '%BACKUP%' OR command LIKE '%RESTORE%';

4. 完整修复过程:从止血到验证

前面三节讲的是原理和判断依据,这一节按时间顺序把实际操作串一遍。整个修复分四步,顺序不能乱:先止血保住当晚的备份,再治本解决空间和配置,然后验证备份真的能还原,最后补上告警机制。

4.1 第一步止血:先拿到一份能还原的备份

发现问题的当天上午,我做的第一件事不是改配置,而是先确认“现在磁盘上那个 90 GB 的文件到底能不能用”。这一步很关键,因为如果它可用,我就有了一个安全的回退点,后面怎么折腾都不慌;如果它不可用,那风险等级要立刻上报。

验证命令是RESTORE VERIFYONLY,它只读文件头和数据校验和,不会真的还原,速度快、不影响生产:

RESTORE VERIFYONLY FROM DISK = N'D:\SQLBackup\OrderHub_Full.bak' WITH CHECKSUM;

结果是“备份集有效”,说明这个 90 GB 的追加文件虽然臃肿,但结构完整,可以作为临时兜底。确认之后我才动手清理磁盘:把这个文件挪到一台有空间的归档存储上(不是删除,是先转存),然后删掉源文件释放空间。整个过程大概四十分钟,这期间业务无感知。

经验:任何“删备份文件”的操作之前,先想清楚“删完之后我手里还有什么”。哪怕那个文件是失败的、残缺的,只要它可能含有可用数据,就先转存再删。我见过有人为了腾空间直接删掉唯一一份备份,两天后磁盘阵列故障,数据全丢,这个教训太重了。

4.2 第二步治本:清理策略重写与目录规划

空间腾出来只是缓了一口气,不解决根本问题,第二天文件还会重新长起来。所以我做了三件事。

第一,把备份文件命名改成带日期,并用覆盖模式。也就是 3.4 节里那段 T-SQL。这样每天生成一个新文件,每个文件大小稳定,不会出现“一个文件吃掉整块盘”的情况。

第二,重写清理逻辑。原来的维护计划“清除维护任务”按文件创建时间判断,遇到追加写入就永远不删。我把它换成了按文件名里的日期判断,逻辑更直白,也更好排查:

DECLARE @cutoff DATE = DATEADD(DAY, -3, CAST(GETDATE() AS DATE)); EXEC master.dbo.xp_delete_file 0, -- 0 表示备份文件 N'D:\SQLBackup', -- 目录 N'bak', -- 扩展名,不要带点 CONVERT(NVARCHAR(30), @cutoff, 126); -- 只删这个时间点之前的

这里有个细节要注意:xp_delete_file的第四个参数是时间点,它会删除修改时间早于这个时间点的匹配文件,格式必须是 ISO8601 那种2024-06-09T00:00:00。很多人写成2024-06-09会不生效,因为格式不匹配。另外这个存储过程虽然官方文档里标注为“不建议使用”,但维护计划的清理任务内部用的就是它,一直很稳定,在自建脚本里用它没什么问题,只要参数写对。

第三,把备份目录和数据库数据文件、日志文件彻底分开。原来的布局是数据文件在E:、日志在F:、备份也在D:,但D:和F:其实是同一块物理盘的两个分区。这次我把备份改到了一块独立的外挂存储上,虽然只是逻辑上的隔离,但至少下次日志暴涨不会直接顶掉备份空间。

4.3 第三步验证:VERIFYONLY 只是及格线

改完配置当晚,备份跑成功了,4.5 GB,用了十二分钟。很多人到这一步就收工了,但我坚持多做一步:实际还原一次。

RESTORE VERIFYONLY只能证明文件结构完整、校验和没坏,它不能证明这份备份在还原到一台新机器上时能正常打开。能证明这件事的只有一个动作——真的还原。我的做法是在同一台实例上建一个临时库,从备份还原过去,跑几条基本查询看看数据对不对:

RESTORE DATABASE OrderHub_RestoreTest FROM DISK = N'D:\SQLBackup\OrderHub_Full_20240612.bak' WITH MOVE N'OrderHub' TO N'D:\RestoreTest\OrderHub_RestoreTest.mdf', MOVE N'OrderHub_log' TO N'D:\RestoreTest\OrderHub_RestoreTest_log.ldf', RECOVERY, STATS = 10; -- 还原完跑两条查询,确认数据到了某个时间点 USE OrderHub_RestoreTest; SELECT COUNT(*) AS order_cnt FROM dbo.Orders; SELECT MAX(created_at) AS latest_order FROM dbo.Orders;

几个实操要点。MOVE子句一定要写,不然它会尝试覆盖原库的物理文件,直接报错。RECOVERY表示还原完成后库可用,如果是准备继续叠加差异备份或日志备份,就用NORECOVERY。还原测试库建完之后别急着删,留着放一两天,万一同期发现数据有问题还能对比。

这个动作听起来很重,实际耗时不到二十分钟,但它把“我以为备份能用”变成了“我确认备份能用”。二者的差别,在真正需要还原的那一天,就是几小时停机和几小时停机加数据丢失的差别。

4.4 第四步兜底:让失败会喊人

最后一步是补监控。前面说过,这次事故真正的严重性不在于“备份失败”,而在于“失败了九天没人知道”。所以我加了两层通知。

第一层是作业级别的失败通知。在作业属性里勾上“在作业失败时发送电子邮件”,前提是实例上已经配好数据库邮件。这个配置很轻,但效果立竿见影,作业一失败就有邮件。

第二层是自建的巡检作业。光有失败通知还不够,因为如果作业被意外禁用、或者整个 SQL Server 代理服务挂了,你连失败通知都收不到。所以我加了一个每天早八点跑的小作业,查“最近 24 小时内有没有成功的完整备份”,没有就发告警:

IF NOT EXISTS ( SELECT 1 FROM msdb.dbo.backupset WHERE database_name = N'OrderHub' AND type = 'D' AND is_damaged = 0 AND backup_finish_date >= DATEADD(HOUR, -24, GETDATE()) ) BEGIN DECLARE @msg NVARCHAR(500) = N'OrderHub 最近 24 小时无成功完整备份,请立即检查。当前时间:' + CONVERT(NVARCHAR(30), GETDATE(), 120); EXEC msdb.dbo.sp_send_dbmail @profile_name = N'DBA_Alert', @recipients = N'dba-team@example.internal', @subject = N'[告警] OrderHub 备份异常', @body = @msg; END

这个“反向检查”的思路比单纯依赖失败通知可靠得多。失败通知是“作业跑了但失败了”,反向检查是“不管什么原因,只要没成功就告警”,覆盖面更广,包括作业被禁用、代理服务停了、备份文件被删这些情况。

5. 常见问题速查与经验清单

这一节把这次和以往遇到过的典型错误整理成两张表,方便下次遇到时对照。错误号我按自己遇到的频率排序,不追求穷举。

5.1 错误号与现象对照表

错误号典型报错片段最可能的根因第一步查什么
3201无法打开备份设备,操作系统错误 5服务账户对目录无写权限用服务账户身份实际写文件
3202写入设备失败,操作系统错误 112目标磁盘空间不足查磁盘剩余与单次备份大小
18204备份设备打开失败路径不存在或文件被独占查路径存在性与文件句柄
3241媒体集有 N 个媒体簇,但只提供了 M 个追加模式写到了别的媒体集检查备份文件是否被混用
3041BACKUP 未能完成命令通用收尾错误,真因在前一条往前翻一条具体错误
4305指定日志的起始 LSN 无效日志链断裂,中间缺备份查 backupset 里的 L 记录
4214没有进行日志备份恢复模式与作业不匹配查恢复模式与日志复用状态
15404无法获取有关 Windows NT 组/用户的信息作业所有者账号被停用或删除查作业 owner 与账号状态
3023备份校验和错误存储层或文件系统写坏换目标路径重试,查磁盘健康

表里这几条覆盖了我八成的实战场景。剩下的两成往往是组合问题,比如“权限没问题、空间也够,但备份就是慢到超时”,那多半是 IO 瓶颈或者锁等待,得去看备份期间的磁盘队列长度和sys.dm_io_virtual_file_stats里的数据。

5.2 几条用代价换来的经验

最后分享几个不写在文档里、但我觉得最值钱的判断习惯。

第一,看到备份失败,先问“上一次成功是什么时候”,而不是“这次为什么失败”。前者决定风险等级,后者决定技术方案。顺序反了,你会在一个已经九天没备份的库上慢悠悠地调试参数。

第二,备份大小突然变化,不要急着归因于“业务数据涨了”。先去看backupset表里的backup_size和磁盘上文件的实际大小是否一致,如果不一致,八成是追加模式在作怪。这个判断我做过三次,三次都是配置问题,没有一次是数据真的涨了。

第三,作业所有者用个人账号是个定时炸弹。这次排查的后半段又冒出个 15404,原因是作业的所有者是一个已经离职、账号被 IT 停用的域账号。作业本身配置得好好的,就是跑不起来。正确的做法是统一把作业所有者改成sa或者一个专用的服务账号,这个账号不允许登录交互、不允许改密码、不绑定具体的人。

第四,还原演练的频率应该跟业务重要性挂钩,不是跟心情挂钩。核心库我坚持每季度做一次完整还原演练,非核心库半年一次。演练不需要真停机,还原到一个临时实例上,跑几条数据校验就够了。这件事的价值不在于“验证备份可用”,而在于“验证你还原的步骤是对的”——很多人在真出事的那天才第一次执行还原,手忙脚乱之下漏掉NORECOVERY,白白多花几小时。

第五,别把监控做成“只监控你想到的东西”。这次加的“最近 24 小时有无成功备份”的反向检查,比任何正向的失败通知都管用,因为它不关心失败的原因,只关心结果。我现在的习惯是,凡是关键流程,除了正向的“成功即通过”,一定再配一条反向的“不成功即告警”,两条腿走路。

这套改完之后,OrderHub 的备份连续跑了三个月没再出问题,磁盘占用稳定在 20 GB 以内,压缩后的单份备份 4.5 GB,还原测试做过两次都正常。但说实话,我心里清楚这不是终点——配置会被人改、账号会被人停、磁盘会被人塞满,能做的只是让下一次失败被更早发现而已。