本文最后更新于 2025年9月8日 下午
前情提要
发生该事故后,NcatBot 为了避免被卷入有关风波,进行了一些紧急操作以避险。由于是首次遇到此类事件,应对经验不足,导致丢失了约 6 h 的工作代码,同时也切实反映出数据安全的重要性。
本文就 321 原则,提供一个 Windows 操作系统下简易但切实可行的数据安全方案。
321 原则:三份数据、两块介质、一处异地,少一个都可能翻车!
321 原则是一种广泛应用于数据备份和数据安全领域的最佳实践。其核心思想是:
- 3:至少保留 3 份数据副本(包括原始数据和备份)。
- 2:将数据存储在 2 种不同的介质上(如硬盘、移动硬盘、云存储等)。
- 1:至少有 1 份备份存放在异地(如云端或物理隔离的地点)。
通过遵循 321 原则,即使遇到硬件故障、误操作、自然灾害或勒索软件攻击等突发事件,也能最大程度地保障数据的完整性和可恢复性。这一原则简单易行,适用于个人用户、小型团队以及企业级的数据安全需求。
情况简析
对于个人开发者来说,三种情况的概率排序为 误操作 >> 硬件故障 > 其他所有情况。如果是开源代码,那么数据的 safety(数据完整性与可恢复性)要求是远大于 security(数据保密性与防止未授权访问)要求的。
对于开发者来说,数据风险主要在于劳动成果的损失,基于这个原则,我设计了以下的备份方案。
第一道防线:VS Code 的 Local-History,秒级回血
Local-History 是 Visual Studio Code 编辑器的一个插件,它能够自动记录你对文件的每次修改,形成本地的版本历史。这是防范误操作的第一道防线,简单粗暴但非常有效。
插件简介
10 s 即可快速完成安装,极大提升代码安全性。在 VSCode 中搜索并安装 “Local History” 插件即可。
每当你阶段性的需要保存文件时(按下 ctrl+s),插件会自动拷贝一个带时间戳的副本存储在本地,简单粗暴。
这种方式虽然简单,但对于日常的误删、误改等操作具有极强的防护作用。尤其是进行 git 有关的操作时。
最佳实践
第二道防线:Restic 快照,防的是‘整盘蒸发’
有时候脑抽达到了一种地步————把整个项目文件夹都给扬了,这时候 Local History 就有些回天乏术了。
再或者,哪天物理机器突然抽风坏掉了,那又咋办?
我们需要一个更好的备份操作。
情况介绍
为了防止误删,我们需要在别处完整的建立一个备份区域。这个备份区域是不能采用 “同步” 策略的,而应该采用 “快照” 策略。
同步和快照分别是什么?


由于我们能够接受一定程度的数据损失(例如 1 h 的工作成果损失),所以采用时效性稍差的 “快照” 仍然可以有效的解决问题。
特别的,为了防止单盘损坏,我们更需要在与系统盘所在物理硬盘不同的另一个盘上做备份。这是所谓的两种介质。
另外,为了防止极端情况发生,我们还需要一份异地。这里我采用了 SMA 共享文件夹方案,这种方案仍然可以用 restic 实现。
操作过程
设置工作区变量
🚨 首先设置工作区目录变量,方便后续使用:
1 2 3 4 5 6 7 8 9 10
| $TOOLS_DIR = "C:\tools"
$BACKUP_DIR_LOCAL = "D:\restic\MyRepo"
$BACKUP_DIR_REMOTE = "\\192.168.1.111\share\MyRepo"
$PASSWORD = "YourStrongPassword"
$TARGET_DIR = "C:/Users/yourname/Desktop/Proj"
|
下载有关软件
1 2 3 4 5 6
| mkdir $TOOLS_DIR Invoke-WebRequest -Uri https://ghfast.top/https://github.com/restic/restic/releases/download/v0.17.1/restic_0.17.1_windows_amd64.zip ` -OutFile "$TOOLS_DIR\restic.zip" Expand-Archive "$TOOLS_DIR\restic.zip" -DestinationPath $TOOLS_DIR Rename-Item "$TOOLS_DIR\restic_0.17.1_windows_amd64.exe" -NewName "restic.exe" Remove-Item "$TOOLS_DIR\restic.zip" setx PATH "%PATH%;$TOOLS_DIR"
|
初始化有关仓库
1 2 3 4 5 6 7
| cd $TOOLS_DIR $Env:RESTIC_REPOSITORY_LOCAL = $BACKUP_DIR_LOCAL $Env:RESTIC_PASSWORD = $PASSWORD restic init $Env:RESTIC_REPOSITORY_REMOTE = $BACKUP_DIR_REMOTE $Env:RESTIC_PASSWORD = $PASSWORD restic init
|
写入密码
1
| cat $PASSWORD | Out-File -FilePath "$TOOLS_DIR\restic_password.txt" -Encoding ASCII
|
备份脚本
将脚本内容写入 $TOOLS_DIR
下的 backup.ps1
:
展开即可抄作业
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @" param( [string]`$Repo, [string]`$Pass ) `$Env:RESTIC_REPOSITORY = `$Repo `$Env:RESTIC_PASSWORD = `$Pass # 实际备份 restic backup `` --tag hourly `` --exclude-caches `` --exclude "*.log" `` --exclude "node_modules" `` $TARGET_DIR # 清理旧快照 restic forget --prune `` --keep-hourly 72 --keep-daily 30 --keep-weekly 16 "@ | Out-File -FilePath "$TOOLS_DIR\backup.ps1" -Encoding UTF8
|
设置 Windows 定时任务
展开即可抄作业
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) ` -RepetitionInterval (New-TimeSpan -Hours 1) ` -RepetitionDuration (New-TimeSpan -Days 3650) $action = New-ScheduledTaskAction -Execute "PowerShell.exe" ` -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\backup.ps1 ` -Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD" Register-ScheduledTask -TaskName "ResticLocal" -Trigger $trigger -Action $action ` -User "$env:USERNAME" -Settings (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries)
$trigger2 = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(15) ` -RepetitionInterval (New-TimeSpan -Hours 1) ` -RepetitionDuration (New-TimeSpan -Days 3650) $action2 = New-ScheduledTaskAction -Execute "PowerShell.exe" ` -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\backup.ps1 ` -Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD" Register-ScheduledTask -TaskName "ResticNas" -Trigger $trigger2 -Action $action2 ` -User "$env:USERNAME" -Settings (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable)
|
再一道保险
需要保证备份程序的正确设置。
试运行备份
创建一个测试文件:
1
| New-Item -Path $TARGET_DIR\test.txt -ItemType File
|
运行备份:
1 2
| echo $PASSWORD | restic snapshots -r $BACKUP_DIR_LOCAL echo $PASSWORD | restic snapshots -r $BACKUP_DIR_REMOTE
|
- 如果均显示了一张包含了备份时间、备份目录的表,则备份成功。
定期检查备份完整性
设置脚本:
展开即可抄作业
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| @' param( [string]$Repo, [string]$Pass )
$Env:RESTIC_REPOSITORY = $Repo $Env:RESTIC_PASSWORD = $Pass
function Write-EventLogError { param($Message) Write-Error $Message New-EventLog -LogName Application -Source "ResticVerify" -ErrorAction SilentlyContinue Write-EventLog -LogName Application -Source "ResticVerify" -EntryType Error -EventId 1 -Message $Message }
function Show-ToastNotification { param( [string]$Title, [string]$Message ) # 方法2: 系统托盘气球 Add-Type -AssemblyName System.Windows.Forms $notify = New-Object System.Windows.Forms.NotifyIcon $notify.Icon = [System.Drawing.SystemIcons]::Information $notify.BalloonTipIcon = "Error" $notify.BalloonTipTitle = $Title $notify.BalloonTipText = $Message $notify.Visible = $true $notify.ShowBalloonTip(5000) Start-Sleep -Seconds 6 $notify.Dispose() }
# 统一错误处理函数 function Invoke-ResticCheck { param( [scriptblock]$Command, [string]$StepName ) try { Write-Output "执行步骤: $StepName" & $Command if ($LASTEXITCODE -ne 0) { throw "restic 返回非零退出码 $LASTEXITCODE" } } catch { $errMsg = "$StepName 失败: $($_.Exception.Message)" Show-ToastNotification -Title "备份验证失败" -Message $errMsg Write-EventLogError $errMsg exit 1 } }
# 1. 基础 check Invoke-ResticCheck -Command { restic check } -StepName "restic check"
# 2. 抽检 10% Invoke-ResticCheck -Command { restic check --read-data-subset=10% } -StepName "restic check --read-data-subset=10%"
Write-Output "TestPass" '@ | Out-File -FilePath "$TOOLS_DIR\verify.ps1"
|
设置定时任务:
展开即可抄作业
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 04:30 $action = New-ScheduledTaskAction -Execute "PowerShell.exe" ` -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\verify.ps1 ` -Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD" $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable Register-ScheduledTask -TaskName "ResticVerifyLocal" -Trigger $trigger -Action $action -Settings $settings -User $env:USERNAME
$trigger2 = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 05:00 $action2 = New-ScheduledTaskAction -Execute "PowerShell.exe" ` -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\verify.ps1 ` -Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD" $settings2 = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable Register-ScheduledTask -TaskName "ResticVerifyNas" -Trigger $trigger2 -Action $action2 -Settings $settings2 -User $env:USERNAME
|
试运行验证
1 2
| PowerShell.exe -File $TOOLS_DIR\verify.ps1 -Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD PowerShell.exe -File $TOOLS_DIR\verify.ps1 -Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD
|
总结
Why 定期验证
定期验证的目的是确保备份的完整性,事实上,备份由于偶然因素损坏的概率和硬件损坏的概率大致相当,定期验证可以及时发现备份损坏的问题。
即使备份发生损坏,同时发生主数据损坏的概率极低。只需要重新修复备份即可。
该方案的优点
- 基本践行 321 原则:
- Local-History 作为第一道防线,能够秒级恢复工作区数据。
- Restic 作为第二和第三道防线,完成两种介质和一份异地备份。
- 一次配置,永久使用,备份操作无需任何手动介入。
该方案的不足