backup-db.ps1 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. #Requires -Version 5.1
  2. <#
  3. .SYNOPSIS
  4. MES Cloud 数据库全量备份(gzip 压缩,不改 MySQL、不重启)
  5. .DESCRIPTION
  6. 放到数据库服务器上执行。每天 18:00 由计划任务触发。
  7. 每次导出完整库并压缩,按日期保存;超过保留天数的旧文件自动删除。
  8. 先写临时文件,成功后再替换,避免备份失败时把上一份可用备份写坏。
  9. 备份目录:
  10. E:\MES_DB_Backup\mes_cloud_yyyyMMdd.sql.gz
  11. E:\MES_DB_Backup\backup.log
  12. 手动用法:
  13. powershell -ExecutionPolicy Bypass -File .\backup-db.ps1
  14. powershell -ExecutionPolicy Bypass -File .\backup-db.ps1 -Install
  15. #>
  16. param(
  17. [switch]$Install,
  18. [string]$BackupDir = "",
  19. [int]$KeepDaysParam = -1
  20. )
  21. $ErrorActionPreference = "Stop"
  22. try {
  23. [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
  24. $OutputEncoding = [System.Text.Encoding]::UTF8
  25. } catch { }
  26. . (Join-Path $PSScriptRoot "db-config.ps1")
  27. $dbCfg = Read-MesDbConfig
  28. $DbHost = [string]$dbCfg.host
  29. $DbPort = [int]$dbCfg.port
  30. $DbName = [string]$dbCfg.database
  31. $DbUser = [string]$dbCfg.user
  32. $DbPassword = [string]$dbCfg.password
  33. # ========== 备份目录等(连接信息请改 db-config.json 或页面) ==========
  34. $BackupRoot = "E:\MES_DB_Backup"
  35. $KeepDays = 7 # 保留最近几天的备份,更早的自动删掉
  36. $TaskName = "MES_Cloud_DB_Backup"
  37. $TaskTime = "18:00"
  38. # 体积特别大、可接受不进备份的表,按需取消注释:
  39. # $IgnoreTables = @("mes_adw300_data")
  40. $IgnoreTables = @()
  41. # ========================================
  42. if (-not [string]::IsNullOrWhiteSpace($BackupDir)) {
  43. $BackupRoot = $BackupDir.Trim().TrimEnd("\")
  44. }
  45. if ($KeepDaysParam -ge 0) {
  46. $KeepDays = $KeepDaysParam
  47. }
  48. $LogFile = Join-Path $BackupRoot "backup.log"
  49. $CnfFile = Join-Path $env:TEMP ("mes_backup_{0}.cnf" -f $PID)
  50. $Stamp = Get-Date -Format "yyyyMMdd"
  51. $OutGz = Join-Path $BackupRoot ("mes_cloud_{0}.sql.gz" -f $Stamp)
  52. function Write-Log {
  53. param([string]$Message, [string]$Level = "INFO")
  54. $line = "{0} [{1}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Level, $Message
  55. Write-Host $line
  56. if (-not (Test-Path $BackupRoot)) {
  57. New-Item -ItemType Directory -Path $BackupRoot -Force | Out-Null
  58. }
  59. Add-Content -Path $LogFile -Value $line -Encoding UTF8
  60. }
  61. function Rotate-LogIfNeeded {
  62. if ((Test-Path $LogFile) -and ((Get-Item $LogFile).Length -gt 5MB)) {
  63. $bak = $LogFile + ".old"
  64. if (Test-Path $bak) { Remove-Item $bak -Force }
  65. Move-Item $LogFile $bak -Force
  66. }
  67. }
  68. function Find-MysqlTool {
  69. param([string]$Name)
  70. $cmd = Get-Command $Name -ErrorAction SilentlyContinue
  71. if ($cmd -and $cmd.Source) { return $cmd.Source }
  72. $binRoots = @(
  73. "C:\Program Files\MySQL",
  74. "C:\Program Files (x86)\MySQL",
  75. "C:\mysql",
  76. "D:\MySQL",
  77. "E:\MySQL",
  78. "C:\Program Files\MariaDB"
  79. )
  80. foreach ($root in $binRoots) {
  81. if (-not (Test-Path $root)) { continue }
  82. $hit = Get-ChildItem -Path $root -Filter ($Name + ".exe") -Recurse -ErrorAction SilentlyContinue |
  83. Where-Object { $_.Directory.Name -eq "bin" } |
  84. Select-Object -First 1
  85. if ($hit) { return $hit.FullName }
  86. }
  87. return $null
  88. }
  89. function Compress-DumpToGzip {
  90. param(
  91. [string]$MysqldumpExe,
  92. [string[]]$DumpArgs,
  93. [string]$DestGz
  94. )
  95. $tmpGz = $DestGz + ".tmp"
  96. if (Test-Path $tmpGz) { Remove-Item $tmpGz -Force }
  97. $psi = New-Object System.Diagnostics.ProcessStartInfo
  98. $psi.FileName = $MysqldumpExe
  99. $psi.Arguments = ($DumpArgs | ForEach-Object {
  100. '"' + ($_ -replace '"', '\"') + '"'
  101. }) -join " "
  102. $psi.UseShellExecute = $false
  103. $psi.RedirectStandardOutput = $true
  104. $psi.RedirectStandardError = $true
  105. $psi.CreateNoWindow = $true
  106. $proc = New-Object System.Diagnostics.Process
  107. $proc.StartInfo = $psi
  108. $errBuilder = New-Object System.Text.StringBuilder
  109. $handler = {
  110. if (-not [string]::IsNullOrEmpty($EventArgs.Data)) {
  111. [void]$Event.MessageData.AppendLine($EventArgs.Data)
  112. }
  113. }
  114. $errEvent = Register-ObjectEvent -InputObject $proc -EventName ErrorDataReceived -Action $handler -MessageData $errBuilder
  115. if (-not $proc.Start()) {
  116. throw "无法启动 mysqldump"
  117. }
  118. $proc.BeginErrorReadLine()
  119. $outStream = $proc.StandardOutput.BaseStream
  120. $fileStream = [System.IO.File]::Create($tmpGz)
  121. $gzip = New-Object System.IO.Compression.GZipStream($fileStream, [System.IO.Compression.CompressionLevel]::Fastest)
  122. try {
  123. $outStream.CopyTo($gzip)
  124. } finally {
  125. $gzip.Dispose()
  126. $fileStream.Dispose()
  127. }
  128. $proc.WaitForExit()
  129. Unregister-Event -SourceIdentifier $errEvent.Name -ErrorAction SilentlyContinue | Out-Null
  130. $errText = $errBuilder.ToString()
  131. if ($errText) {
  132. Write-Log ("mysqldump 输出: {0}" -f $errText.Trim()) "WARN"
  133. }
  134. if ($proc.ExitCode -ne 0) {
  135. if (Test-Path $tmpGz) { Remove-Item $tmpGz -Force }
  136. throw ("mysqldump 失败,退出码 {0}" -f $proc.ExitCode)
  137. }
  138. if (-not (Test-Path $tmpGz) -or (Get-Item $tmpGz).Length -lt 100) {
  139. if (Test-Path $tmpGz) { Remove-Item $tmpGz -Force }
  140. throw "备份文件过小,可能失败"
  141. }
  142. if (Test-Path $DestGz) { Remove-Item $DestGz -Force }
  143. Move-Item $tmpGz $DestGz -Force
  144. }
  145. function Remove-OldBackups {
  146. if ($KeepDays -le 0) {
  147. Write-Log "未启用过期删除(KeepDays=0)"
  148. return
  149. }
  150. $expire = (Get-Date).Date.AddDays(-$KeepDays)
  151. Get-ChildItem -Path $BackupRoot -File -Filter "mes_cloud_*.sql.gz" -ErrorAction SilentlyContinue | ForEach-Object {
  152. if ($_.Name -match "mes_cloud_(\d{8})\.sql\.gz") {
  153. $day = [datetime]::ParseExact($Matches[1], "yyyyMMdd", $null)
  154. if ($day -lt $expire) {
  155. Remove-Item $_.FullName -Force
  156. Write-Log ("已删除过期备份: {0}" -f $_.Name)
  157. }
  158. }
  159. }
  160. # 清理此前增量方案留下的目录(若有)
  161. foreach ($oldDir in @("full", "incr")) {
  162. $p = Join-Path $BackupRoot $oldDir
  163. if (Test-Path $p) {
  164. Remove-Item $p -Recurse -Force
  165. Write-Log ("已删除旧目录: {0}" -f $p)
  166. }
  167. }
  168. }
  169. function Install-BackupTask {
  170. $scriptPath = $PSCommandPath
  171. $arg = '-NoProfile -ExecutionPolicy Bypass -File "{0}"' -f $scriptPath
  172. $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $arg
  173. $trigger = New-ScheduledTaskTrigger -Daily -At ([datetime]::ParseExact($TaskTime, "HH:mm", $null))
  174. $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
  175. $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 6)
  176. Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
  177. Write-Log ("已注册计划任务 [{0}],每天 {1} 执行: {2}" -f $TaskName, $TaskTime, $scriptPath)
  178. }
  179. # ---------- 主流程 ----------
  180. try {
  181. if ($Install) {
  182. $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).
  183. IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
  184. if (-not $isAdmin) {
  185. Start-Process -FilePath "powershell.exe" -Verb RunAs -ArgumentList @(
  186. "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath, "-Install"
  187. )
  188. return
  189. }
  190. New-Item -ItemType Directory -Path $BackupRoot -Force | Out-Null
  191. Install-BackupTask
  192. return
  193. }
  194. Rotate-LogIfNeeded
  195. New-Item -ItemType Directory -Path $BackupRoot -Force | Out-Null
  196. Write-Log "======== 备份任务开始 ========"
  197. $mysqldump = Find-MysqlTool "mysqldump"
  198. if (-not $mysqldump) { throw "找不到 mysqldump.exe,请把 MySQL 的 bin 目录加入 PATH" }
  199. Write-Log ("mysqldump: {0}" -f $mysqldump)
  200. Write-MesMysqlCnf -Config $dbCfg -CnfPath $CnfFile
  201. Write-Log ("连接 MySQL {0}:{1} 库 {2} 账号 {3}" -f $DbHost, $DbPort, $DbName, $DbUser)
  202. $baseArgs = @(
  203. "--defaults-extra-file=$CnfFile",
  204. "--single-transaction",
  205. "--quick",
  206. "--routines",
  207. "--triggers",
  208. "--hex-blob",
  209. "--default-character-set=utf8mb4",
  210. "--max-allowed-packet=1G",
  211. "--set-gtid-purged=OFF"
  212. )
  213. foreach ($t in $IgnoreTables) {
  214. $baseArgs += ("--ignore-table={0}.{1}" -f $DbName, $t)
  215. }
  216. $extraAttempts = New-Object System.Collections.Generic.List[object]
  217. $extraAttempts.Add([string[]]@())
  218. $extraAttempts.Add([string[]]@("--column-statistics=0"))
  219. Write-Log ("开始全量备份(gzip),目标: {0}" -f $OutGz)
  220. $lastError = $null
  221. $ok = $false
  222. foreach ($extra in $extraAttempts) {
  223. $dumpArgs = $baseArgs + $extra + @("--databases", $DbName)
  224. try {
  225. Compress-DumpToGzip -MysqldumpExe $mysqldump -DumpArgs $dumpArgs -DestGz $OutGz
  226. $ok = $true
  227. break
  228. } catch {
  229. $lastError = $_
  230. Write-Log ("参数尝试失败,将换一套参数重试: {0}" -f $_.Exception.Message) "WARN"
  231. }
  232. }
  233. if (-not $ok) { throw $lastError }
  234. $sizeMb = [math]::Round((Get-Item $OutGz).Length / 1MB, 2)
  235. Write-Log ("全量备份完成: {0} ({1} MB)" -f $OutGz, $sizeMb)
  236. Remove-OldBackups
  237. Write-Log "======== 备份任务结束 ========"
  238. exit 0
  239. } catch {
  240. Write-Log $_.Exception.Message "ERROR"
  241. if ($_.ScriptStackTrace) { Write-Log $_.ScriptStackTrace "ERROR" }
  242. exit 1
  243. } finally {
  244. if (Test-Path $CnfFile) {
  245. Remove-Item $CnfFile -Force -ErrorAction SilentlyContinue
  246. }
  247. }