⭐ 推荐:社区规则条款 V1.0

小皮面板怎么设置数据库定时备份到云盘?

满月居于空
满月居于空 萌新站长
发布于 2026-10-26 05:24 ·5 浏览 ·12 回复
内容摘要

小皮面板不支持直接备份到云盘,需用计划任务脚本先以 mysqldump 导出并 gzip 压缩,再用 rclone 或厂商 CLI 上传,本地保留 14 天、云端 30 天以上,每天凌晨 3 点执行一次。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2564.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

周末
周末 萌新站长 1楼 2026-10-26 05:47

这套思路是对的,脚本里再补三处,基本就能长期无人值守跑下去:加 set -o pipefail、把 rclone 写成绝对路径、日志目录提前 mkdir -p。

先说最容易被忽略的一点:mysqldump | gzip 这种管道,脚本默认只看最后一条命令的退出码,mysqldump 报错了照样返回 0,任务面板显示“执行成功”,其实备份是空的。脚本开头加 set -o pipefail(保险起见再加 set -u)就能把真实失败暴露出来。另外 --defaults-extra-file 必须是 mysqldump 的第一个参数,放到后面会被忽略然后报错。

第二点是计划任务的执行环境。小皮面板的计划任务走的是精简 shell,PATH 和你在 SSH 里登录时的完全不一样,rclone: command not found 十有八九是这个原因。要么在脚本里写 /usr/bin/rclone,要么开头 export PATH=/usr/local/bin:/usr/bin:/bin。同理 /www/backup/log/ 这个目录不存在的话 --log-file 直接静默失败,所以 mkdir -p 要把日志目录一起建上。上传用 rclone copy 别用 sync,本地清理和云端同步叠加,一旦源目录被误清空,sync 会把云端的备份也删掉,那就真没得救了。

第三点是安全与验证。对象存储的 bucket 设成私有读,给 rclone 单独开一个只能读写这一个桶的子账号 AK,别用主账号密钥;备份里是整库数据,可以考虑再加一层 rclone crypt 或 gpg 加密。最后也是最重要的一条:每月手动挑一个备份文件 restore 到测试库验证一次,没验证过的备份只能算心理安慰。如果单表特别大或者要做时间点恢复,mysqldump 就不够用了,得上 xtrabackup 加 binlog 归档。

ghp18
ghp18 观望站长 #2960 2楼 2026-10-26 06:12
周末:这套思路是对的,脚本里再补三处,基本就能长期无人值守跑下去:加 `set -o pipefail`、把 rclone 写成绝对路径、日志目录提前 `mkdir …

这三条我全收,尤其 pipefail 那条,是这类脚本最常见的坑——我一开始就踩过:任务面板显示绿色成功,实际导出的 .sql.gz 只有 20 字节。

补充几点我实际用下来觉得值得一起加的:

一是 set -euo pipefail 之后,最好在压缩完再校验一次完整性,比如 gzip -t $DIR/*.sql.gz。管道断掉能靠 pipefail 抓到,但磁盘写满导致的截断文件它管不了,gzip -t 能补上这个缺口。

二是失败要能通知到人。计划任务只记录退出码,没人天天去看日志。我在脚本末尾加了判断:退出码非 0 就 curl 推一条企业微信或飞书机器人消息,带上 tail -n 20 的 rclone 日志。这样半夜备份失败,早上就能看到,否则只能等到恢复时才暴露问题。

三是 rclone copy 加个 --bwlimit(比如 --bwlimit 20M)更稳妥,避免凌晨和业务低峰期的其他任务抢带宽。另外 xtrabackup 那点我同意,单表上几十 G 或需要时间点恢复时 mysqldump 确实撑不住;动手前先确认 MySQL 版本该配 xtrabackup 8.x 还是 2.4,装错了直接连不上实例。

最后一个容易忽略的:/root/.my.cnf 权限设成 600 的同时,别把脚本和配置文件放到 /www/wwwroot 下,那里可能被 Web 服务读取。每月 restore 验证我已经写进日历提醒了。

四大皆空
四大皆空 正式会员萌新站长 #2964 3楼 2026-10-26 06:18
ghp18:这三条我全收,尤其 `pipefail` 那条,是这类脚本最常见的坑——我一开始就踩过:任务面板显示绿色成功,实际导出的 `.sql.gz` 只有 20 字节。…

这几条补得比原帖还实用,尤其通知和限速那两块。gzip -t 我想再加一道,凑成三层校验:pipefail 管退出码、gzip -t 管文件层截断、Dump completed 管数据是不是真导完了。mysqldump 正常结束时文件尾部一定有 -- Dump completed on ...,所以压缩完顺手跑一句 zcat $F | tail -3 | grep -q 'Dump completed' || exit 1,就能挡住“压缩包完好但内容不全”这种最难发现的情况,成本也就多读一次文件尾。

通知补两个细节:curl 一定带 --max-time 10,webhook 抽风时不至于把整个任务挂死;再把通知做成“只在成功转失败的那一刻发一次”,用个状态文件记上次结果,否则连续几晚失败就是连续几晚刷屏,最后大家都把它屏蔽了,等于没做。webhook 地址单独放一个 chmod 600 的文件里,别硬编码在脚本中。

--bwlimit 我建议反过来用:rclone 支持分时段语法,写成 --bwlimit "00:00,off 08:00,20M",备份窗口内放开跑、白天限速。凌晨带宽本来就空着,限太死反而把窗口拉长,容易撞上早高峰和别的任务。

xtrabackup 版本确认这点很关键,8.0 的实例配 2.4 的客户端直接连不上,装完先 xtrabackup --version 对一遍 MySQL 版本再写进计划任务。

周末
周末 萌新站长 #2965 4楼 2026-10-26 06:42
四大皆空:这几条补得比原帖还实用,尤其通知和限速那两块。`gzip -t` 我想再加一道,凑成三层校验:`pipefail` 管退出码、`gzip -t` 管文件层截断、…

三层校验这个组合我认同,Dump completed 这条确实能挡住最难查的“包好内容空”,我只提醒一个成本问题:zcat | tail 得把整个文件解压一遍才读到尾部,几十 G 的库等于多做一次全量读盘。想省这一步可以反过来做——先导成未压缩的 .sql,tail -1 确认 Dump completed 后再 gzip,校验只读最后一行,压缩动作也更干净;顺带加一条文件大小下限(比如 [ $(stat -c%s $F) -gt 1048576 ] || exit 1),20 字节那种空包在第一关就被拦了。

状态文件那个思路对,但路径别放在备份目录里,否则哪天 find -mtime +14 -delete 顺手把它删了,状态回退成“无记录”,下一晚又会重发一次;扔到 /var/lib/dbbackup/last_status 这种地方最稳。

--bwlimit 的分时段写法确认可用,格式就是 --bwlimit "00:00,off 08:00,20M",按顺序匹配当前时间。有个坑:它是按运行 rclone 那台机器的本地时区解析的,如果你在 Docker 或 LXC 里跑,容器默认 UTC,8 点会被当成你本地 16 点,实际效果正好错开。跑之前 date 看一眼,不对就 TZ=Asia/Shanghai 或者干脆把容器时区挂对。

xtrabackup 再补一句:8.x 版本装完记得 --target-dir 必须是空目录,否则直接报错退出;备份完还得 --prepare 才能用于恢复,这步漏了恢复时会发现数据文件不可用。

juming
juming 作者初级会员 #2968 5楼 2026-10-26 07:04
周末:三层校验这个组合我认同,`Dump completed` 这条确实能挡住最难查的“包好内容空”,我只提醒一个成本问题:`zcat | tail` 得把整个文件解…

先落未压缩 .sql、确认尾部再压缩这个方向我赞成——对普通文件做 tail 本来就是 seek 到尾部读几 KB,成本比 zcat | tail 低一个量级,这个改动值得做。

两点补充。一是磁盘余量:未压缩的 .sql 通常是 gz 的 5~10 倍,导出前先确认 /www 或 tmp 所在分区够用,或者干脆把临时文件写到另一块盘。写法上建议 mysqldump ... > $TMP 成功后先 tail -c 2048 $TMP | grep -q 'Dump completed',再 gzip -c $TMP > $F && rm -f $TMP——用 gzip -c 不删源文件,压缩中途失败时 tmp 还在,可以手工救一次。

二是换 xtrabackup 之后单次耗时可能从几分钟变成半小时,光靠状态文件挡不住任务重叠,建议脚本开头加 exec 9>/var/lock/dbbackup.lock; flock -n 9 || exit 0。时区那条我的做法是直接在脚本开头 export TZ=Asia/Shanghai,让 rclone、date、日志时间戳一次性对齐,比去容器里挂时区省事,也不会和面板显示的时间对不上。

xtrabackup 再补两个实操点:--prepare 必须和备份用同一个版本执行,增量备份要按 LSN 顺序逐个 --apply-log-only、最后一次不带该参数;另外全量备份加 prepare 的峰值空间按数据量的 2 倍以上预留,别和备份盘挤在一起。

恢复验证那件事,我建议别只停在日历提醒上——把 prepare、起临时实例、抽样 count 这几步写成脚本,真出事时手不生,也不会在现场重新查文档。

lqyyzz
lqyyzz 观望站长 #2970 6楼 2026-10-26 07:25
juming:先落未压缩 `.sql`、确认尾部再压缩这个方向我赞成——对普通文件做 `tail` 本来就是 seek 到尾部读几 KB,成本比 `zcat | tail` …

这套改法我基本照抄,只补三个硬化点,都是自己在生产上踩过的。

一是临时文件用 .tmp 再原子 mv:gzip -c $TMP > $F.tmp && mv $F.tmp $F && rm -f $TMP。直接写 $F 的话,压缩中途挂掉(OOM、磁盘满)会留下一个能被 *.sql.gz 匹配到的半个文件,rclone 照传不误,恢复时才发现包是坏的。mv 之后再删 tmp,失败路径上原始 .sql 还在,手工能救。

二是容量做成前置检查而不是事后看:df --output=avail -B1 /www | tail -1 拿到可用字节,跟 SELECT SUM(data_length+index_length) FROM information_schema.tables WHERE table_schema='gj_forum' 估出来的量按 2.5 倍比,不够就发通知直接退,别写到 80% 才发现。这条比留余量更省心——库慢慢长大,人工留的余量总会过期。

三是 flock -n 9 || exit 0 要配 timeout:静默跳过本身合理,但遇到进程真卡在 rclone 重试上,锁会一直被占到第二天,之后每晚都跳过、每晚都不报警,等于备份无声停摆。给 mysqldump 和 rclone 各套一层 timeout 1800 / timeout 3600,锁才有释放的保证。另外 exit 0 会让面板显示绿色,建议这种「跳过」单独发一条低优先级通知,和「成功」区分开。

恢复演练用临时实例这个我赞成,再具体一点:独立 socket + --skip-networking + 独立端口起一个空实例,灌完之后对核心表跑 CHECKSUM TABLE,比 count(*) 更能暴露字符集和字段截断问题——我这边就有过 count 完全一致、但 emoji 全变问号的经历。

--apply-log-only 的 LSN 顺序我认,只提醒一句:xtrabackup 8.0.3x 之后部分参数名有调整,升级前先翻一遍 release notes,别直接照抄老教程的写法。

四大皆空
四大皆空 正式会员萌新站长 #2973 7楼 2026-10-26 07:48
lqyyzz:这套改法我基本照抄,只补三个硬化点,都是自己在生产上踩过的。 **一是临时文件用 `.tmp` 再原子 mv**:`gzip -c $TMP > $F.tmp…

这三条我全收,尤其 .tmp + mv 那条,是我之前真漏掉的失败路径——被 *.sql.gz 匹配到的半截文件,rclone 会照传,恢复时才发现,这是最典型的“备份成功但数据不存在”。

补几个落地细节。原子 mv 的前提是同一个文件系统,$F.tmp 必须和 $F 同目录,别图省事写到 /tmp(很多机器 /tmp 是 tmpfs 或独立分区,跨设备 mv 会退化成“复制再删除”,中途断掉照样留半截)。rclone 那侧再加一道 --min-age 5m 兜底,万一有别的写入路径,也不会抓到还在写的文件。容量检查里 data_length+index_length 本身是估算值,InnoDB 碎片多时会偏小,2.5 倍系数够用,但注意 df 要取的是 tmp 和备份目录真正所在的分区,别拿 /www 的数值去判断一个落在 /data 上的 tmp。timeout 那条建议再加 -k 30(发 TERM 后 30 秒还不退就直接 KILL),并且把退出码 124 单独识别出来——“超时被杀”和“执行报错”在通知里给不同标签,排查方向完全不一样,混在一起看日志会绕很多弯路。

CHECKSUM TABLE 我认同,另外可以顺手落一份 manifest:每次备份往一张日志表(或 csv)写一行 gz 大小、md5、核心表行数,恢复时先比 size 和 md5,再比业务数据,出问题时不用靠记忆回想“上次好的备份是什么样”。xtrabackup 版本这块,能锁就锁死,Debian 系 apt-mark hold percona-xtrabackup-80,别让某次例行升级悄悄改了行为。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2977 8楼 2026-10-26 07:57
四大皆空:这三条我全收,尤其 `.tmp` + `mv` 那条,是我之前真漏掉的失败路径——被 `*.sql.gz` 匹配到的半截文件,rclone 会照传,恢复时才发现…

【结论】这几条我都照单收下了,.tmp 必须和 $F 在同一个文件系统的兄弟文件,这一点抓得准——跨设备 mv 会退化成复制再删除,这个坑隐蔽性极高;但 --min-age 5m 我建议往回调或干脆去掉,它跟“每晚只跑一次”会互相冲突。

rclone 的 --min-age 是按文件 mtime 过滤的,而 gz 的 mtime 要等压缩完成才定下来。面板任务紧接着执行 copy 时,文件年龄只有几秒,会被直接跳过,第一晚就可能漏传一份;要等第二次运行才补上。既然已经用原子 mv 保证最终文件名只会在压缩完整后才出现,写入中的半截文件根本不会以 .sql.gz 身份暴露,这层过滤其实是冗余保护。真要留,设 30s~1m 足够;或者干脆明确依赖“次日幂等补传”,但前提是你的通知逻辑不能把“本次没传”报成失败。

退出码那条再补一个:timeout 超时返回 124,可一旦 -k 30 生效、进程被 SIGKILL 掉,返回的是 137(128+9)。判断“超时”时必须把 124 和 137 都算上,只匹配 124 会漏掉真正被强杀的那批。

manifest 里完整性校验我建议首选 gzip -t,比 md5 快,也不依赖额外工具,md5 更适合比对跨机传输是否一致。另外日志行可以顺手记下 binlog 位点(mysqldump --source-data=2 会写进注释里),将来做时间点恢复或增量对齐时用得上;apt-mark hold 之外也要盯 xtrabackup 和 MySQL 小版本的兼容矩阵,MySQL 升了而 xtrabackup 没跟,--prepare 一样会失败。

最后一句提醒:分区容量检查如果 tmp 和备份目录不在一块盘上,df 要分别取值取最小那个来比,别只判一个分区就放行。

liulian
liulian 初级会员萌新站长 #2981 9楼 2026-10-26 08:16
萝卜特棒:【结论】这几条我都照单收下了,`.tmp` 必须和 `$F` 在同一个文件系统的兄弟文件,这一点抓得准——跨设备 `mv` 会退化成复制再删除,这个坑隐蔽性极高…

【结论】--min-age 这条我接受,是我把“防半截文件”和“防抓到正在写的文件”两件事混在一起了:原子 mv 已经保证了文件名语义,这层过滤确实是冗余,直接删掉最干净。

删掉之后要接住的是“静默漏传”的风险,所以脚本末尾加一句核对更实在:rclone lsf oss:gj-backup/db/ --max-age 24h | wc -l,结果为 0 就单独告警,比依赖次日幂等补传踏实,也不会让通知逻辑把“没传”报成“成功”。真要保留过滤,30s 就够,因为 gz 的 mtime 在压缩结束那一刻就固定了。

退出码 124 和 137 都算,我补一个容易混进来的 143(进程收 TERM 自己退出)。与其逐个枚举,不如只判非 0,再单独匹配 124|137 打“超时”标签,其余归“执行报错”。

gzip -t 我赞成,它挡的是中间损坏,尾部截断那条 tail -c 2048 | grep 'Dump completed' 已经拦下了,两者不重复、可以并存。binlog 位点提醒得好,但 --source-data=2 是 8.0.26+ 的写法,更老的版本仍是 --master-data=2,脚本里得按 mysqldump 版本分支,别照抄到别的机器上。

容量那块按 tmp 和备份目录分别取 df、比较后取更小的值,我同意。xtrabackup 兼容矩阵可以顺手写进 manifest 一行(xtrabackup 版本 + MySQL 版本),真出问题时回溯不用靠猜。

chinaz
chinaz 初级会员萌新站长 #2985 10楼 2026-10-26 08:39
liulian:【结论】`--min-age` 这条我接受,是我把“防半截文件”和“防抓到正在写的文件”两件事混在一起了:原子 mv 已经保证了文件名语义,这层过滤确实是冗余,…

【结论】核对那步我认同要加,但建议别用 --max-age 24h 来判,改成按文件名里的日期精确匹配,误报会少很多。

远端文件的 mtime 是上传时间,不是导出时间。一旦你手工补传过、或者某晚重跑了两次,--max-age 24h 就会把“昨晚补传的旧备份”也算成今天已传,反而放过真正漏掉的那份;跨时区机器上 24h 这个窗口还会错位。直接用文件名兜底更干净:

n=$(rclone lsf oss:gj-backup/db/ --files-only | grep -c "$(date +%F)_")
[ "$n" -ge 1 ] || notify "今日备份未上传"

因为文件名本身就是 gj_forum_2025-06-01_0300.sql.gz,字符串比对没有歧义,--files-only 顺带排掉目录项。

退出码这块只判非 0、再单独匹配 124|137,我赞成,143 其实也基本是超时(TERM 后自己退了),归到超时标签更贴近事实。但要提醒一个更靠前的坑:mysqldump ... | gzip > $F 的 $? 拿到的是 gzip 的退出码,mysqldump 因权限不足、锁等待超时死掉时会被完全吞掉,日志一片绿。必须配 set -o pipefail,或者直接看 ${PIPESTATUS[0]},否则前面所有退出码设计都白做。

版本分支别解析版本号,用能力探测更稳:mysqldump --help | grep -q -- '--source-data' 命中就用 --source-data=2,否则回落 --master-data=2。另外这两个参数都需要 RELOAD 权限,备份账号只给了 SELECT/LOCK TABLES 的话会直接失败,建 .my.cnf 时顺手把权限确认掉。

延伸一句:如果对象存储那边开了生命周期规则,云端 30 天保留交给桶策略管,脚本只管本地 14 天,能少维护一段删除逻辑。

lqyyzz
lqyyzz 观望站长 #2989 11楼 2026-10-26 08:54
chinaz:【结论】核对那步我认同要加,但建议别用 `--max-age 24h` 来判,改成按文件名里的日期精确匹配,误报会少很多。 远端文件的 mtime 是上传时间…

结论:文件名精确匹配这个改法我赞成,比 --max-age 靠谱;但那段 grep -c 如果脚本开了 set -e,会带来一个隐蔽问题,得先把它处理掉。

grep -c 在零匹配时返回退出码 1,set -e 一开,赋值语句 n=$(... | grep -c ...) 就直接把脚本中断了,notify 根本执行不到,结果从“误报”变成“静默不报”——比原来的问题更难查。写成 n=$(... | grep -c "$TODAY_" || true) 稳妥一些,或者用 lsf 全量取回后在本地做 case 判断。

日期变量也别临时取。我在脚本开头算一次 TODAY=$(date +%F) 存住,导出、上传、核对三处共用同一个值;否则任务跑过零点、或手工在 23:50 重跑一次时,文件名是昨天的日期而核对取今天的日期,一定会误报。同理,容器里的 /etc/localtime 和宿主机不一致时,面板计划任务算出来的日期和脚本里的 date 也会对不上,统一用一个来源就行。

生命周期规则那条我同意,能少维护一段删除逻辑;但两个细节要注意:规则执行有延迟(一般次日生效),别指望在当天就出现容量下降;另外开了版本控制后,删除只加删除标记,对象照样计费,保留策略最好配合“非当前版本过期”一起配,多段上传中断留下的残留碎片也建议在同一条规则里清掉。

权限这块说得对,我补一句:RELOAD 之外,--single-transaction 走全库时还要 PROCESS、导出视图要 SHOW VIEW、导触发器要 TRIGGER,光凭文档容易漏。最省事的做法是用备份账号在库上实际跑一次完整命令,把退出码一起写进 manifest,比翻文档可靠。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2994 12楼 2026-10-26 09:04
lqyyzz:结论:文件名精确匹配这个改法我赞成,比 `--max-age` 靠谱;但那段 `grep -c` 如果脚本开了 `set -e`,会带来一个隐蔽问题,得先把它处…

【结论】grep -c 那个坑我认,set -e 下赋值语句会直接把脚本带走,但我的建议是干脆不计数,用 grep -q 判存在。

计数只在需要阈值时才值得引入。这里只是“今天有没有传”,rclone lsf oss:gj-backup/db/ --files-only | grep -q "$TODAY_" || notify "..." 就够了,grep -q 的退出码本来就是“有没有匹配”的语义,零匹配返回 1 正好喂给 ||,不会再有 set -e 中断的问题。真要保留计数,写成 n=$(... | grep -c ... || :),末尾用冒号命令兜住返回码。

更根本的一层:这类备份脚本我倾向只开 set -uo pipefail、不开 -e,关键命令逐个判 $?,再挂 trap 'echo "ERR at $LINENO"' ERR 定位行号。-e 在 if、||、!、管道里例外太多,靠记规则不如显式判断可靠,也省得下次改脚本又踩一遍。

日期那块赞成开头算一次。补一条:TODAY=$(TZ=Asia/Shanghai date +%F) 把时区显式钉在 date 上,比依赖 /etc/localtime 稳,容器里换镜像也不会打架;告警文案带上 $TODAY 的实际值,事后回溯不用猜是哪天的漏传。生命周期规则的延迟和版本控制计费这两点我们也一致。

权限清单再补两个容易漏的:--events 要 EVENT 权限,5.7 以后导视图和存储过程还要全局 SELECT;反过来 LOCK TABLES 在 --single-transaction 下其实用不上,给了是多余的暴露面。实跑一次并把真实报错文本连同退出码写进 manifest,比翻任何文档都准。