宝塔面板切云数据库后备份只剩 20KB:mysqldump 客户端版本错配、pipefail 缺席与 phpMyAdmin 的 2054
上周帮人救火:一套跑在宝塔上的 WordPress 站,三个月前把本地 MySQL 5.7 迁到了云数据库 MySQL 8.0,站点一切正常,老板很满意。直到上周误删了一张表要回滚,打开 /www/backup/database/ 一看——每天的备份文件整整齐齐躺在那里,每个 20KB 左右,用 gzip -t 一测全是 unexpected end of file。三个月,90 个"成功"的备份,全是空壳。
这不是宝塔的锅,是切云数据库这个动作把三处静默失效同时揭开了:备份管道的退出码、客户端版本、认证插件。下面按部署顺序走一遍,每一步都给可复现的命令。
一、先别急着在宝塔里"添加数据库"
宝塔「数据库」页面的列表,是面板去连本机 127.0.0.1:3306 执行 SHOW DATABASES 拿到的,它根本不认识你的云数据库。很多人切云库之后发现"数据库全没了",第一反应是重装 MySQL——千万别。
正确姿势是:
- 宝塔上不装 MySQL 服务端,或者装了但停掉。PHP 走的是 mysqlnd/pdo_mysql 扩展,直连云库不需要本地有 mysqld。
- 站点配置(
wp-config.php、config/database.php)里的 host 直接写云数据库内网地址。 - 宝塔的「数据库」页面从此列表为空——这是正常的,别再回来了。
然后确认出口 IP。宝塔这台机器如果有多块网卡或走了 NAT,你 curl 出去的 IP 和宝塔面板配置里显示的未必一致:
ip -4 route get 1.1.1.1 | awk '{print $7}' # 内核实际选源地址
curl -s https://ifconfig.me # 出口公网 IP
云数据库白名单加错 IP 是老问题。这里有个硬性建议:能用内网就别开公网。我们这台是轻云互联的云服务器,和数据库同区域走内网直达,3306 不出公网,白名单只放内网段,省掉后面所有的 TLS、限速、爆破噪音。
二、phpMyAdmin 卡在 2054:caching_sha2_password 的兼容矩阵
云数据库 MySQL 8.0 建账号,默认认证插件是 caching_sha2_password。你把宝塔的 phpMyAdmin 指过去,直接白屏 + 日志里一行:
mysqli::real_connect(): (HY000/2054): The server requested authentication method unknown to the client
原因:宝塔自带的 phpMyAdmin 4.8 系列通常配 PHP 5.6/7.0/7.1,这几个版本的 mysqlnd 压根没有实现 caching_sha2_password,握手阶段的 auth plugin 回包被丢弃。
先确认一下对面到底是哪个插件:
mysql -h rm-xxxx.mysql.rds.internal -u app -p \
-e "SELECT user,host,plugin FROM mysql.user WHERE user='app'"
两条路,选一条:
- 改账号插件(最省事,安全性靠 TLS 和他的强制加密补):
ALTER USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
代价是丢掉 SHA256 挑战,公网连接时一定要配合require_secure_transport=ON。 - 升 PHP:宝塔装 PHP 7.4 + phpMyAdmin 5.1,mysqlnd 才稳。7.2/7.3 虽然部分支持,但早期小版本有握手 bug,不建议踩。
顺手把 phpMyAdmin 的配置改对。宝塔路径随版本变,一般是 /www/server/phpmyadmin/phpmyadmin_<hash>/config.inc.php:
$cfg['Servers'][$i]['host'] = 'rm-xxxx.mysql.rds.internal';
$cfg['Servers'][$i]['port'] = '3306';
$cfg['Servers'][$i]['connect_type'] = 'tcp';
$cfg['Servers'][$i]['auth_type'] = 'cookie';
$cfg['Servers'][$i]['ssl'] = true; // 云库强制加密时必须开
$cfg['Servers'][$i]['ssl_verify'] = false; // 用自签 CA 时先关验证,跑通再收紧
注意一个坑:宝塔面板「数据库」页右上角那个 phpMyAdmin 图标,点进去会走面板自己拼的免密登录参数。你改了 config.inc.php 也没用,得直接访问 phpMyAdmin 的独立入口(宝塔「软件商店」里点 phpMyAdmin → 设置 → 里面有个访问地址)。
三、mysqldump 5.7 撞 8.0:三个必踩的报错
宝塔装 MySQL 5.7 时顺带的那套客户端,路径在 /www/server/mysql/bin/mysqldump,版本 5.7.x。拿它去 dump 云上 8.0 的库,第一个跟头是排序规则:
mysqldump: Couldn't execute 'show create table `wp_posts`':
Unknown collation: 'utf8mb4_0900_ai_ci' (1273)
5.7 客户端字符集字典里没有 0900 系列,直接罢工。反过来,如果你为了修这个装了 8.0 客户端去 dump 5.7 的库,会撞第二个坑:
mysqldump: Couldn't execute 'SELECT COLUMN_NAME, JSON_EXTRACT(HISTOGRAM, ...)
FROM information_schema.COLUMN_STATISTICS ...':
Unknown table 'COLUMN_STATISTICS' in information_schema (1109)
加参数绕过:
mysqldump ... --column-statistics=0
第三个坑,8.0 客户端连普通权限账号时必现:
mysqldump: Couldn't execute 'SELECT LOGFILE_GROUP_NAME, FILE_NAME, ... FROM INFORMATION_SCHEMA.FILES':
Access denied; you need (at least one of) the PROCESS privilege(s) for this operation
云数据库上你多半拿不到 PROCESS 权限,加 --no-tablespaces 即可。
结论很直白:宝塔备份脚本用的是哪个 mysqldump,备份质量就取决于它。要么把宝塔的 MySQL 客户端升级到 8.0,要么独立装一套客户端并显式指定绝对路径。独立装建议直接拿官方二进制包,别跟系统的 libmysqlclient 纠缠:
wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.36-linux-glibc2.28-x86_64.tar.xz
tar xf mysql-8.0.36-linux-glibc2.28-x86_64.tar.xz -C /usr/local/
ln -sf /usr/local/mysql-8.0.36-linux-glibc2.28-x86_64/bin/mysqldump /usr/local/bin/mysqldump80
mysqldump80 --version
缺 libaio / libncurses.so.5 的话 ldd 一眼能看出来,别硬猜。
四、20KB 空壳的真相:管道退出码 + cron 的精简 PATH
现在回到开头那 90 个空备份。技术链路是这样的:
mysqldump ... | gzip > backup.sql.gz
mysqldump 在 dump 到第 3 张表时因为 Lock wait timeout 或权限不足挂了,但 gzip 正常退出——shell 的 $? 取的是管道最后一个进程的退出码,也就是 gzip 的 0。于是宝塔的备份任务日志里干干净净写着"备份成功",文件大小 20KB,内容是被截断的半截 SQL。
验证方法不用等出事,现在就能试:
gzip -t /www/backup/database/appdb_20240115.sql.gz
# gzip: /www/backup/database/appdb_20240115.sql.gz: unexpected end of file
zcat /www/backup/database/appdb_20240115.sql.gz | tail -2
# 正常应该看到: -- Dump completed on 2024-01-15 3:00:12
# 空壳往往是表结构都没写完就断了
第二个隐藏雷是 PATH。宝塔的计划任务由 cron 拉起,环境变量是精简版:
# 宝塔计划任务里可以这么探一下
env | grep PATH
# PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
which mysqldump
# (空) —— /www/server/mysql/bin 不在 PATH 里
所以脚本里写裸的 mysqldump 会直接 command not found,而这种失败比你想象的更难发现:宝塔计划任务的输出在 /www/server/cron/<任务ID>.log,没人看就永远发现不了。养成习惯:
tail -n 50 /www/server/cron/*.log
五、把备份从宝塔手里接管过来
我给这套站的方案是:关掉宝塔「数据库」的定时备份(它压根不认识云库),换一个自己可控的脚本,丢进宝塔「计划任务 → Shell 脚本」。核心就三行:绝对路径、set -euo pipefail、事后校验。
#!/bin/bash
set -euo pipefail
DUMP=/usr/local/bin/mysqldump80
DB_HOST="rm-xxxx.mysql.rds.internal"
DB_USER="backup_ro"
DB_PASS="******"
DB_NAME="appdb"
OUT_DIR="/www/backup/dbcustom"
STAMP=$(date +%F_%H%M%S)
OUT="${OUT_DIR}/${DB_NAME}_${STAMP}.sql.gz"
mkdir -p "$OUT_DIR"
"$DUMP" \
--host="$DB_HOST" --port=3306 \
--user="$DB_USER" --password="$DB_PASS" \
--single-transaction --quick --skip-lock-tables \
--default-character-set=utf8mb4 \
--set-gtid-purged=OFF \
--no-tablespaces \
--hex-blob \
--routines --triggers --events \
"$DB_NAME" | gzip -6 > "$OUT"
# 1) 管道完整性
gzip -t "$OUT"
# 2) 结尾标记(mysqldump 成功才会写这句)
zcat "$OUT" | tail -n 3 | grep -q 'Dump completed' \
|| { echo "FATAL: dump incomplete"; rm -f "$OUT"; exit 1; }
# 3) 表数量对账
SRC=$(mysql -N -B -h"$DB_HOST" -u"$DB_USER" -p"$DB_PASS" \
-e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME'")
GOT=$(zcat "$OUT" | grep -c '^CREATE TABLE')
[ "$SRC" -eq "$GOT" ] \
|| { echo "FATAL: table mismatch src=$SRC dump=$GOT"; exit 1; }
# 4) 最小体积兜底,防"200 字节的成功"
SIZE=$(stat -c %s "$OUT")
[ "$SIZE" -ge 1048576 ] || { echo "FATAL: too small $SIZE"; exit 1; }
find "$OUT_DIR" -name '*.sql.gz' -mtime +7 -delete
echo "OK $OUT $SIZE bytes tables=$GOT"
几个参数的取舍说明:
--set-gtid-purged=OFF:云库强开 GTID 时,不带这个参数会往 dump 里塞SET @@GLOBAL.GTID_PURGED,恢复到一个没开 GTID 的实例直接报错。--single-transaction:只对 InnoDB 有效。如果云库里混着 MyISAM 老表(宝塔导入历史数据时很常见),这个参数等于没加。跑一次SELECT table_name,engine FROM information_schema.tables WHERE table_schema='appdb' AND engine!='InnoDB';确认。- 不要随手加
--master-data/--source-data:需要REPLICATION CLIENT权限,普通备份账号直接Access denied,而且 RDS 类实例的 binlog 位点走代理时未必是你以为的那个。 - 备份账号只给
SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER,别用 root。
六、恢复演练:30 秒判断一个备份是死是活
备份没验证过等于没有。每周挑一个 dump 往测试库灌一遍,这一步能暴露所有前面埋的雷。最常见的两个报错:
ERROR 1227 (42000) at line 1832: Access denied; you need (at least one of)
the SUPER or SET_USER_ID privilege(s) for this operation
原因是触发器/视图里带着 DEFINER='olduser'@'%',而目标实例没这个用户、你也没 SUPER 权限。剥掉就行:
zcat appdb_2024xxxx.sql.gz \
| sed -E 's/DEFINER=`[^`]+`@`[^`]+`//g' \
| mysql -h test-db -u root -p appdb_test
MySQL 8.0.30+ 的客户端有 --skip-definer 参数可以直接在 dump 阶段解决,更干净。
ERROR 1273 (HY000) at line 12: Unknown collation: 'utf8mb4_0900_ai_ci'
往 5.7 实例恢复 8.0 的库必现。要么目标也升 8.0,要么提前把 dump 里的 utf8mb4_0900_ai_ci 批量替换成 utf8mb4_general_ci——注意列级 COLLATE 也得跟着换,光改表选项不够。
最后把恢复验证也塞进宝塔计划任务,跑完写一行结果到固定文件,配个监控告警盯着这个文件的时间戳。真正出事那天,你至少不用再对着 20KB 的文件发呆。
顺带一提,这套方案里省心的部分是网络:数据库和面板机同区域内网直连,备份走内网不占公网带宽,一个百来 G 的库晚上跑完也就一两个小时,不用为了"备份别把网站带宽打满"去搞限速和错峰。