Skip to content
bitzorcas
中EN

Guide

Operations 数据库备份、恢复与活跃归档

解释 SQL Server 备份恢复、路径防护、审批与职责分离、pre-restore 快照、保留策略,以及租户事务归档和冷存储缺口。

Last updated

备份恢复与归档都改变数据生命周期,但目标不同:备份恢复保护整个数据库;活跃归档把租户热表中的历史行迁移到归档表。两者当前都绑定 SqlSugar + SQL Server 能力,不是多 ORM 等价功能。

1. 两条边界

租户活跃归档

固定 Retention Policy

DELETE OUTPUT INTO Archive

SysArchiveBatch

租户批次查询

数据库灾备

Create

.bak 文件

VERIFYONLY

pre-restore snapshot + RESTORE WITH REPLACE

备份文件不带租户边界;恢复覆盖当前数据库。归档必须有可信 TenantId,手写租户入口永远不会调用全租户执行。

2. 当前备份 Provider

唯一生产实现是 SqlSugarDatabaseBackupService。它直接使用 Microsoft.Data.SqlClient 执行 SQL Server T-SQL:

  • BACKUP DATABASE Full;
  • BACKUP DATABASE ... WITH DIFFERENTIAL;
  • BACKUP LOG;
  • RESTORE VERIFYONLY;
  • RESTORE DATABASE ... WITH REPLACE, RECOVERY。

EF Core 或其他数据库 Provider 没有等价实现。缺少服务时 API Host 注册 UnavailableBackgroundJobPorts 失败关闭。

3. 创建备份

CreateBackup.Command.Type 通过大小写不敏感 Enum 解析,只接受 Full、Differential、Log。无效值返回 Validation。

Production/Staging 根据审批 gate 校验 Incident;非强制环境放行。JobHost 定时备份直接调用 IDatabaseBackupService,不经过这个 HTTP 审批 Handler。

4. Log 备份的 skipped 成功

SQL Server 为 SIMPLE recovery model 时,Log 备份返回成功结果,但 Skipped=true、文件名为空、大小为零并附原因。

监控不能只统计 Result.IsSuccess;必须单独统计 skipped,否则会把长期没有日志备份误报为绿色。

5. 文件命名与目录

目录来自 DatabaseBackup:BackupDirectory,空值回退到应用目录下 Backups。数据库名优先配置,否则从连接串解析。

文件名形如 {database}_{full|diff|log}_{yyyyMMdd_HHmmss}.bak。同一秒同类型并发创建会计算相同文件名,并使用 WITH INIT,当前没有互斥锁。

6. 创建调用示例

正确处理 skipped
// ① 发起日志备份;审批票据是否必需由当前 Host 环境决定。
var result = await mediator.Send(
new CreateBackup.Command(Type: "Log", ApprovalTicket: ticket),
cancellationToken);
if (result.IsFailure)
return BackupDecision.Failed(result.Error.Code);
var backup = result.Value!;
// ② SIMPLE recovery 会返回成功但 skipped,不能当作已经生成文件。
if (backup.Skipped)
return BackupDecision.Skipped(backup.SkipReason!);
return BackupDecision.Created(backup.FileName, backup.SizeBytes);

7. 列表不暴露绝对路径

ListBackups 列举目录下所有 .bak,按最后修改时间倒序,公开 DTO 只含文件名、大小、时间和从文件名推断的类型。

类型推断顺序先 full/diff/log,再 prerestore;未知命名显示 unknown。列表没有分页,备份很多时会一次读取全部文件元数据。

8. 文件名路径防护

验证和恢复调用 ValidateBackupFileName:拒绝空值、..、路径分隔符、驱动器号,并在 GetFullPath 后确认仍位于备份根目录。

这可以阻断常见路径穿越。符号链接安全取决于部署文件系统和根目录治理;备份目录应由专用账户控制。

9. VERIFYONLY 的返回语义

文件不存在或路径非法返回失败。SQL Server 执行异常则返回成功 Result + IsValid=false + 异常消息。

Verify Handler 随后直接写活动审计;审计 sink 抛错会让请求失败,即使数据库验证已经完成。Create/Restore 的审计则被 try/catch 包裹,不会覆盖主结果,三条路径并不一致。

10. 恢复的门控顺序

Confirm == RESTORE

Validate approval

SoD when creator is known

Validate file name + exists

Create pre-restore backup

RESTORE WITH REPLACE

best-effort activity audit

Confirm 必须精确大小写匹配。审批实际只在 Production/Staging 强制;非强制环境 ValidateAsync 直接 true。

11. 职责分离会降级

只有审批 gate 能返回 Incident.CreatedBy,且当前用户 EffectiveUserId 可用时,才拒绝同一人创建工单并执行恢复。

无 OpsExtension Store、非强制环境、工单读取失败或身份缺失时,creator 为 null,SoD 放行。商业环境不能把这条 best-effort 校验描述为强制双人复核。

12. 恢复不会自动先验证

恢复服务确认文件存在后,直接创建当前数据库的 pre-restore 全量快照,然后执行 RESTORE ... WITH REPLACE, RECOVERY。

它没有先调用 RESTORE VERIFYONLY,也没有核对备份数据库名、链完整性、加密、版本兼容或 restore rehearsal。单独的 verify 端点由操作者自行调用,且结果没有绑定到后续恢复。

13. pre-restore 快照

快照名为 {database}_prerestore_{timestamp}.bak,创建失败就不会执行 restore。成功响应只公开快照文件名,避免泄露绝对路径。

快照与 restore 不在一个可回滚事务内;如果快照成功而恢复失败,快照保留。当前保留作业对 pre-restore 文件固定保留三天。

14. 备份保留策略

PruneExpiredBackupsAsync:

  • recent full 按 DailyRetentionDays;
  • 周日 full 按 WeeklyRetentionWeeks;
  • 每月 1 日 full 按 MonthlyRetentionMonths×30 天;
  • diff/log 简化为日窗口;
  • pre-restore 固定三天;
  • 单文件删除失败只写 warning,整体继续成功。

这是按文件时间与命名的近似策略,没有备份链图、legal hold、不可变存储或异地复制证明。

15. 活跃归档的两个固定策略

默认 registry 只有:

ResourceOnlineArchiveActionArchive table
NotificationInbox365 天1095 天ColdStorageSysNotification_Archive
LoginLog180 天730 天DeleteSysLoginLog_Archive

策略是代码内不可变列表,不是租户可配置规则。

16. 归档事务语义

SqlSugar 执行器仅支持 SQL Server。每个租户开启独立事务,以 1000 行为批次执行 DELETE TOP ... OUTPUT DELETED ... INTO archive,然后在同一事务写 SysArchiveBatch。

这样可保证单租户热表删除、归档表插入和批次记录共同提交。全租户 Job 是逐租户事务,不保证所有租户一起成功或回滚。

17. 租户边界与手动归档的 fail-closed 审计

手动执行从 ICurrentUser.User.TenantId 取租户;空或 "0" 一律拒绝(Archiving.Tenant.Required),即 Host 身份不能触发租户归档。批次查询也按当前租户过滤;PageIndex≤0 归一为 1,PageSize≤0 归一为 20,PageSize>1000 截断为 1000。PageWindow 的默认最大 offset 是 100000,超过后当前 Handler 返回空页(TotalCount=0、PageIndex=1、PageSize=20),这一回退会丢失请求页元数据,客户端不能把它当作真实无数据。

手动归档的守卫按固定顺序执行,审计现在是 fail-closed,不再是 best-effort:

  1. 租户校验(tenantId 空或 "0" → Archiving.Tenant.Required);
  2. 资源名、审批工单、原因长度校验;
  3. 确认令牌校验(必须精确匹配 "ARCHIVE"),在审批与任何副作用之前;
  4. 审批 gate 校验;
  5. 策略存在性,且策略 Action 必须是 Archive(如 ColdStorage 会以 Operations.Archive.ManualActionUnsupported 拒绝,同样在审计与执行之前);
  6. 意图审计:写 DataRetention.Archive.Intent。写失败返回 Operations.Archive.AuditUnavailable,归档不执行;
  7. 执行 ArchiveTenantAsync;
  8. 完成审计:写 DataRetention.Archive.Completed。执行成功但审计写失败时返回 Operations.Archive.OutcomeUnknown,而不是成功。

换种说法:归档执行器不再”完成后 best-effort 写审计、失败只告警”。意图审计写不进去就根本不会动数据;数据动了但完成审计写不进去,调用方收到的是失败结果而不是成功。这样审计记录与数据迁移在失败情况下保持一致——要么两者都成,要么两者都没有,不会出现”数据已经迁走却查不到操作记录”的灰色地带。批次表本身仍保存 ExecutedBy、执行时间、行数和时间范围,是更具体的操作证据。

18. ColdStorage 仍未落地

NotificationInbox 策略的 Action 是 ColdStorage,但 MoveToColdStorageAsync 固定返回 ColdStorageUnavailable。手动入口也会在执行前以 Operations.Archive.ManualActionUnsupported 拒绝非 Archive 的策略。活跃归档只先迁入数据库 archive table;冷存储阶段尚未接入对象存储。

不能把策略枚举值等同于已完成端到端冷存储、校验和、加密、legal hold 与删除证明。

19. HTTP 可达性

归档有手写路由并有集成边界测试。备份四个用例只有嵌套 [GenerateEndpoint] 声明;当前生成器遍历限制意味着仍需修复映射并增加真实 HTTP 合同测试。

20. 灾备验收清单

  • 在独立环境验证 Full/Diff/Log 链;
  • 恢复前强制 VERIFYONLY 并绑定验证 hash;
  • 演练恢复到隔离数据库,而不只原地 WITH REPLACE;
  • 验证 RPO/RTO、加密、访问控制和异地复制;
  • 并发备份不会覆盖同秒文件;
  • 审计不可用时有告警和补偿证据;
  • 清理基于备份链与 legal hold,而不只文件年龄;
  • 归档、冷存储、延迟删除和查询路径都完成演练。

21. 源码核查

Terminal window
# SQL Server 备份/恢复与归档实现。
rg -n "BACKUP DATABASE|RESTORE VERIFYONLY|WITH REPLACE|DELETE TOP" \
src/Framework/BitzOrcas.Infrastructure.SqlSugar -g '*.cs'
# 备份与归档边界测试。
dotnet test tests/BitzOrcas.Application.Tests \
--filter FullyQualifiedName~ArchiveTenantBoundaryTests

Operations 总览 · Schema 安全

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%