
很多人觉得备份是“有空再说”的事直到某天误删了一张业务表、服务器磁盘突然报废或者被同事一条DROP DATABASE直接清库才追悔莫及。尤其是Linux服务器上的MySQL很多人平时连ssh都懒得登更别说天天盯着数据了。我就是吃过这个亏的人所以后来老老实实把crontabmysqldump这套定时备份方案搭了起来。这套方案适合绝大多数中小规模业务库能解决“数据丢了找不回来”这个核心痛点也适合刚接触Linux运维的同学照着抄作业。今天把完整思路、脚本、排障经验一次性讲清楚。1. 先从需求说起为什么我建议你认真做定时备份1.1 备份不是“要不要做”而是“怎么做得不后悔”很多新手以为备份就是把数据库导出一下手动执行一条命令然后把文件扔在服务器上就完事了。但真实场景里这个“简单”的做法会引出几个问题备份文件放在哪磁盘满了怎么办备份任务凌晨执行了但我不知道它到底成没成功数据库有100GB直接mysqldump会不会把线上业务拖垮最关键的万一真要恢复这份备份到底能不能用这些问题不提前想清楚等到事故发生时大概率会发现自己手里只有一份“看起来存在、实际上残缺”的备份文件。我在实际维护中见过太多次这样的局面有人把备份文件放在/root/下结果/分区写满数据库直接宕机有人用mysqldump不加任何参数导出到一半网络中断留下一个半截文件还有人配了crontab但因为脚本里写了相对路径定时任务一跑就报command not found备份从来没成功过。所以这篇文章不只是教你敲几条命令而是把整个备份闭环讲清楚方案选型、目录规划、脚本编写、定时调度、日志监控、恢复演练。你照着做一遍后面基本不会再为“备份了没”这种问题失眠。1.2 全量备份、增量备份与binlog先选对方案在动手之前得先明确你到底需要哪种备份。最常见的方案无非这三种方案原理适合场景缺点全量备份mysqldump把整个库的逻辑数据导出为SQL文件数据量不大单次导出可在十几分钟内完成备份窗口较长恢复只能恢复到备份时刻物理备份Xtrabackup/mysqlbackup直接拷贝数据文件数据量很大几十GB以上工具安装、权限配置复杂恢复对版本敏感全量备份 binlog增量定期全量备份配合二进制日志做增量恢复数据重要、需要恢复到任意时间点需要开启binlog恢复流程复杂我的建议很明确中小型业务、单库大小在10GB以内优先选择mysqldump全量备份 定期清理老备份文件。如果数据量到了几十GB再考虑Xtrabackup。如果你的业务要求“恢复到误操作前的几分钟”那必须提前开启log_bin并且把binlog文件也纳入备份体系。这里有个关键点很多人以为开启了binlog就等于有了增量备份其实不对。binlog文件本身会滚动、会过期如果你不做全量备份仅仅靠binlog是没法完整恢复的。正确做法是“全量备份 binlog归档”全量负责恢复基线binlog负责恢复基线之后到故障点之间的所有变更。我遇到过一个小伙伴他把expire_logs_days设成了7天却只做了周一一次全量备份。周三数据库坏了他手里只有周一的备份而binlog因为过期的原因中间两天的日志已经被MySQL自动清掉了。这个案例提醒我备份方案和binlog过期策略必须一起设计。2. 动手前的环境检查与目录规划2.1 确认MySQL安装方式与版本备份脚本不是拿来就能跑的首先你得知道你服务器上的MySQL是怎么装的。不同安装方式影响的是mysqldump命令的位置、socket文件的路径以及服务启动方式。常见的安装方式有几种apt/yum 安装例如apt install mysql-server或yum install mysql-server二进制文件通常在/usr/bin/mysqldump配置文件在/etc/mysql/。RPM安装CentOS上rpm -ivh mysql-community-server-*.rpm相关命令在/usr/bin/下。源码编译安装命令通常在你指定的prefix目录下比如/usr/local/mysql/bin/mysqldump。Docker部署MySQL运行在容器里备份需要docker exec进入容器执行或者使用容器内自带的mysqldump。排查的方法很简单which mysqldump mysql --version如果which mysqldump找不到命令但MySQL是能正常运行的大概率是因为PATH环境变量里没有包含MySQL bin目录。这时候可以用find / -name mysqldump -type f 2/dev/null全盘找一下。Docker部署的话用docker ps找到容器名再执行docker exec 容器名 which mysqldump。这里我特别强调版本一致性备份端和服务端的MySQL版本尽量保持一致。比如你用5.7的mysqldump去备份8.0的实例虽然大多数情况下能用但遇到特殊字符、权限视图等场景可能出现兼容性问题。最稳妥的方式是直接使用服务器上对应版本的mysqldump不要图省事从本机随便调一个。2.2 备份目录与账号规划很多人的备份脚本跑着跑着突然失败不是因为命令写错而是磁盘满了。所以目录规划这一步建议认真对待。我推荐的目录结构是这样/data/backup/mysql/ ├── daily/ # 每日全量备份文件 │ ├── dbname_2024-01-15.sql.gz │ └── dbname_2024-01-16.sql.gz ├── logs/ # 备份执行日志 │ └── backup.log └── scripts/ # 备份脚本 └── mysql_backup.sh把备份目录放在独立的/data分区而不是/root或/home是为了避免根分区被备份文件撑爆。你可以用df -h看一下各分区使用率优先选剩余空间最大的挂载点。MySQL账号方面不要用root去跑备份更不要把root密码直接写在脚本里。我的做法是单独创建一个备份专用账号CREATE USER backup_userlocalhost IDENTIFIED BY 你的强密码; GRANT SELECT, SHOW VIEW, EVENT, TRIGGER, LOCK TABLES, PROCESS, RELOAD ON *.* TO backup_userlocalhost; FLUSH PRIVILEGES;各权限的意义我简单说明一下SELECT、SHOW VIEW读取数据。EVENT、TRIGGER导出事件调度器和触发器。LOCK TABLES备份时锁表保证一致性。PROCESS、RELOADmysqldump --single-transaction和刷新日志时需要。权限最小化原则既保证了安全也避免备份账号误操作线上数据。密码不要明文放在所有人可见的脚本里可以给脚本设置chmod 700 mysql_backup.sh只允许指定用户读取。2.3 备份脚本需要哪些基础命令Linux定时备份脚本本质上就是一系列Shell命令的集合。你在写脚本之前最好确认以下命令都是可用的mysqldump核心导出工具。gzip压缩备份文件建议必装能节省大量磁盘空间。date生成日期标记。find用于清理过期备份。mailx或curl失败告警通知。检查命令command -v mysqldump gzip date find缺什么就补什么。比如CentOS没有mailx可以yum install mailx如果没有外网邮件服务可以用curl调用企业微信/钉钉机器人接口发告警。还有一个容易被忽略的点crontab默认的PATH非常简单只有/usr/bin:/bin。如果你的MySQL安装目录不在这个范围内脚本里直接写mysqldump会报command not found。解决方案有两种一种是在脚本开头export PATH/usr/local/mysql/bin:$PATH另一种是全路径调用/usr/local/mysql/bin/mysqldump。我习惯两种都做保险。3. 编写备份脚本一版可以直接改的脚本3.1 脚本主流程与变量设计下面这版脚本是我在线上用了很久的版本功能包括全量导出、压缩、过期清理、日志记录。你可以直接复制把变量改成自己的环境就能用。#!/bin/bash # # Description: MySQL全量备份脚本 # Author: 运维老兵 # Version: 1.3 # # ---------- 基础变量 ---------- MYSQL_HOST127.0.0.1 MYSQL_PORT3306 MYSQL_USERbackup_user MYSQL_PASS这里填你的密码 MYSQL_BIN/usr/bin # mysqldump所在目录 BACKUP_DIR/data/backup/mysql/daily LOG_FILE/data/backup/mysql/logs/backup.log RETENTION_DAYS7 # 保留7天备份 DATE_TAG$(date %Y-%m-%d_%H-%M-%S) BACKUP_FILE${BACKUP_DIR}/all_db_${DATE_TAG}.sql # ---------- 环境准备 ---------- export PATH$MYSQL_BIN:/usr/bin:/bin:$PATH # 如果目录不存在则创建 mkdir -p ${BACKUP_DIR} mkdir -p $(dirname ${LOG_FILE}) # 记录日志函数 log_info() { echo [$(date %Y-%m-%d %H:%M:%S)] [INFO] $* ${LOG_FILE} } log_error() { echo [$(date Y-%m-%d %H:%M:%S)] [ERROR] $* ${LOG_FILE} } # ---------- 执行备份 ---------- log_info 开始全量备份 # 1) 先备份MySQL所有库的结构和数据 if mysqldump \ --host${MYSQL_HOST} \ --port${MYSQL_PORT} \ --user${MYSQL_USER} \ --password${MYSQL_PASS} \ --single-transaction \ --routines \ --triggers \ --events \ --set-gtid-purgedOFF \ --all-databases ${BACKUP_FILE} 2${LOG_FILE}; then # 2) 压缩备份文件 gzip ${BACKUP_FILE} log_info 备份成功: ${BACKUP_FILE}.gz else log_error 备份失败请检查MySQL连接和错误日志 exit 1 fi # ---------- 清理过期备份 ---------- DELETED_FILES$(find ${BACKUP_DIR} -name all_db_*.sql.gz -mtime ${RETENTION_DAYS} -print) if [ -n ${DELETED_FILES} ]; then find ${BACKUP_DIR} -name all_db_*.sql.gz -mtime ${RETENTION_DAYS} -exec rm -f {} \; log_info 清理过期备份文件: echo ${DELETED_FILES} ${LOG_FILE} fi log_info 备份任务结束这个脚本有几个值得说的设计点。第一我把所有可控变量集中在脚本头部换服务器、换密码、调保留天数都只改一个位置不用满篇找。第二日志函数统一带了时间戳后面查问题很方便。第三备份先落盘、再gzip压缩这个顺序看起来多了一步但实际上比直接mysqldump | gzip file.sql.gz更容易排错——如果中间过程出错你能看到是一个完整的临时SQL文件失败还是压缩失败。3.2 使用mysqldump做一致性备份我脚本里用的几个参数每一个都有讲究这里拆开讲一下。--single-transaction是最关键的一个。它会在备份开始时开启一个可重复读事务通过InnoDB的MVCC机制拿到一致性快照。也就是说备份期间其他会话的增删改不会污染备份数据同时也不需要锁表。这对于不能停服的线上业务来说几乎是必选参数。但要特别注意--single-transaction只对InnoDB表有效。如果你的库里还有MyISAM表那就没办法了MySQL依然会对这部分表加上读锁LOCK TABLE ... READ这是引擎特性决定的。所以当你看到备份日志里有Locking tables的时候不用慌那可能是MyISAM表在锁。如果MyISAM表特别多建议优先把它们迁到InnoDB这不仅是备份体验问题更是数据安全性问题。--routines --triggers --events这三个参数分别导出存储过程/函数、触发器、事件调度器。很多人在备份时漏了它们导致恢复后发现应用大面积报错因为存储过程全没了。这里有个细节当后续需要导入的时候存储过程的定义者是原账号目标环境的账号不存在时恢复可能会报ERROR 1227这个我在后面恢复演练部分会详细说。--all-databases表示备份所有库包括系统库mysql、sys。我建议默认用这个参数。理由很直接如果有一天你要把整个实例迁移到新服务器只备份业务库是远远不够的授权信息、系统配置都在mysql库里。当然如果你只关心某个业务库也可以改成--databases dbname1 dbname2但请注意--databases后面一定要跟库名而不是裸的dbname这样导出的文件里才会包含CREATE DATABASE IF NOT EXISTS语句导入的时候才能自动建库。--set-gtid-purgedOFF这个参数主要给GTID环境用的。如果你的MySQL开启了GTID8.0默认支持导出时加这个参数可以避免备份文件里带着GTID执行记录减少后续导入时因为GTID冲突导致的报错。单机没开GTID的可以直接忽略。3.3 压缩、归档与保留策略导出的SQL文件往往很大一个500MB的库导出后可能接近1GB而不压缩的话每天一个文件硬盘很快就不够用了。gzip压缩率通常能到4:1到10:1也就是说500MB的SQL文件压缩后可能只有几十到一百多MB这个收益是非常可观的。使用上就一行gzip /data/backup/mysql/daily/all_db_2025-01-15_03-00-01.sql压缩完之后原始文件会被替换成.sql.gz记得确认一下。如果你担心gzip没执行成功可以在脚本里加判断if [ -f ${BACKUP_FILE}.gz ]; then log_info 压缩成功 rm -f ${BACKUP_FILE} else log_error 压缩失败保留原始文件 fi保留策略我用的是find -mtime N清理N天前的文件。这个N要结合你的数据量、磁盘大小、业务需求来决定。我用7天是因为这个实例每天全量备份一次7天内的备份文件足够应付绝大多数故障场景。如果你要做月度归档可以单独把每月1号的备份文件复制到另一个目录设置不同的保留策略。清理命令里的一个坑find用-mtime 7表示“超过7天前修改的文件”注意它是不含第7天当天的。如果你希望保留正好7天实际会保留8天的文件这个细节不算严重但如果你对保留天数要求精确建议改用-mmin 10080分钟。另外删除文件之前先-print打印出来、写入日志方便事后审计。3.4 日志与告警备份失败要第一时间知道没有告警的备份是不完整的。凌晨3点跑的备份如果失败了总不能等到第二天早上手动登录服务器才发现。我的经验是至少做到两级通知写日志 推送消息。日志的作用是事后排查在脚本里已经做了。推送是即时感知我常用的方式有邮件mailx -s MySQL备份失败 adminexample.com需要服务器提前配置好发信服务。企业微信/钉钉机器人用curlPOST一段JSON到机器人Webhook简单直接不依赖邮箱。比如推送企业微信机器人脚本里加一个函数notify_wecom() { local msg$1 curl -s -X POST https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key你的KEY \ -H Content-Type: application/json \ -d {\msgtype\:\text\,\text\:{\content\:\${msg}\}} }在备份失败的地方调用notify_wecom MySQL备份失败: $(hostname) $(date)。这样出问题第一分钟就能收到消息而不是第二天被业务方投诉了才知道。还有一个更“土”但非常有效的办法把备份日志里的成功记录和备份文件大小汇总成一条消息每天早上定时发送到运维群。哪怕只是一句“备份成功文件大小850MB”大家都安心。失败的时候消息自然会变成“备份失败”响应快的多。4. crontab定时任务配置与日志验证4.1 crontab语法速查crontab是Linux下的定时任务管理工具由系统的cron守护进程执行。它的语法不复杂分 时 日 月 周 命令 * * * * * command每一位的含义字段取值范围说明分0-59每小时的第几分钟时0-23每天的第几小时日1-31每月的第几天月1-12每年的第几个月周0-7每周几0和7都表示周日常用写法举例30 3 * * *每天凌晨3点30分执行。0 */6 * * *每6小时执行一次。15 2 * * 1每周一的凌晨2点15分执行。0 0 1 * *每月1日零点执行。有几个容易搞混的点*是“每一”的意思比如* * * * *就是每分钟执行一次*/5在分钟位表示每5分钟周和日同时有限制时是“或”的关系不是“与”。这个如果你没接触过可能觉得绕但实际用多了就自然记住了。4.2 配置定时任务并添加环境变量编辑当前用户的crontabcrontab -e加入这样一行0 3 * * * /bin/bash /data/backup/mysql/scripts/mysql_backup.sh这里我建议写全路径/bin/bash而不是直接写脚本路径。因为有些系统的curl初始环境不包含bash的绝对路径导致cron无法执行脚本。另外脚本本身需要可执行权限chmod x /data/backup/mysql/scripts/mysql_backup.sh配置好之后用crontab -l查看当前所有定时任务确认条目已经写入。这里有个重要提醒crontab -e是每个用户独立的。你在root用户下配置了任务它会以root身份执行你在普通用户下配置就以普通用户身份执行。脚本里的路径、权限都要匹配对应的执行用户。我见过有人用root配置了备份脚本脚本里却访问普通用户的家目录结果执行时权限不够磁盘写不进去备份一直失败。所以配置完任务后第一步就是手动执行一次脚本确认在目标用户身份下能跑通。有时候脚本在手动执行时一切正常但被crontab调用时却报找不到命令。原因就是我在前面提到的PATH问题。crontab执行时的PATH通常被精简为/usr/bin:/bin如果你的mysqldump在/usr/local/mysql/bin那就必须把PATH写进脚本。下面这段放在脚本开头无脑加上source /etc/profile export PATH/usr/local/mysql/bin:/usr/local/bin:/usr/bin:/bin:$PATH有人觉得加source /etc/profile有点多余但实测下来它能避免很多“环境变量没加载”的诡异问题尤其是在/etc/crontab里配置任务的时候。另外如果你配置了.my.cnf或者用密码文件还要注意Cron执行时的家目录和手动执行时可能不一样密码文件路径会找不到。最稳妥的做法就是在脚本里显式用--defaults-extra-file/root/.my.cnf指定配置文件或者干脆在脚本变量里写清楚账号密码。4.3 如何查看crontab执行日志配置完crontab之后很多新手做的第一件事是干等第二天然后打开备份目录看有没有新文件。这种办法太被动了。我一般配置完会立刻做两件事手动执行脚本一次然后查看执行日志。手动执行很简单bash /data/backup/mysql/scripts/mysql_backup.sh tail -f /data/backup/mysql/logs/backup.log如果手动执行正常说明脚本本身没问题。接着再验证crontab是否真的调用到了脚本。看crontab的日志不同的系统位置不太一样CentOS/RHEL 7/var/log/cronDebian/Ubuntu/var/log/syslog里过滤CRONsystemd杂志journalctl -u crond或journalctl -u cron比如在CentOS上grep mysql_backup /var/log/cron能看到类似这样的记录Jan 15 03:00:01 srv01 CROND[12345]: (root) CMD (/bin/bash /data/backup/mysql/scripts/mysql_backup.sh)如果看到了这条记录说明crontab确实执行了。接着重点看脚本自己的日志cat /data/backup/mysql/logs/backup.log如果脚本日志里也有了执行记录那整个链路就通了。接下来几天只需要每天扫一眼日志或等告警消息即可。这里再分享一个我常用的调试技巧临时把crontab里的执行时间改成下一分钟比如当前是14:35就写36 14 * * *等一分钟后观察/var/log/cron和脚本日志确认没问题后再把时间改回凌晨3点。这样不用等一晚上就能验证整个定时任务是否生效。5. 常见问题排查与恢复演练5.1 常见问题速查表根据我这些年对备份问题的排查经验把最常踩的坑和对应的解决办法整理成了下面的表建议收藏问题现象可能原因排查与解决手动执行脚本正常crontab执行后没有新备份PATH环境变量缺失脚本开头加入export PATH/usr/local/mysql/bin:/usr/bin:/bin:$PATH备份日志显示command not found: mysqldumpmysqldump不在crontab的PATH中使用全路径调用或者先source /etc/profile备份时提示Access denied; you need (at least one of) the PROCESS privilege备份账号权限不足给账号授予PROCESS和RELOAD权限--single-transaction后仍提示Locking tables库中存在MyISAM表MyISAM必然加锁建议把核心表转为InnoDB备份文件大小为0导出过程中异常中断检查磁盘空间df -h检查MySQL是否重启磁盘被备份文件写满保留策略未配置或清理失败配置find -mtime N -delete监控磁盘水位日志显示备份成功但恢复时报语法错误mysqldump版本与目标MySQL版本差异大保证导出端与导入端版本一致或兼容备份导出的SQL文件里有很多CREATE DATABASE重复使用--databases参数包含库名若只想导单个库且不建库则不加--databases收到备份失败告警但独立重跑成功定时任务执行瞬间业务高峰或锁冲突调整备份时间或加入--lock-wait-timeout参数邮件告警没收到邮件服务未配置或被限流改用企业微信/钉钉机器人检查发信日志这里的核心经验是不要等到备份真的出问题才开始学排查。每一条你都应该模拟过一次知道报错长什么样。比如权限不足的报错手动执行一次就能看到完全不用等crontab。5.2 恢复演练备份文件能不能用要演练过才算数很多人备份做得很勤但从来没恢复过。等到需要恢复的那一天才发现备份文件是坏的、缺了依赖库、或者SQL版本不兼容。所以我的建议是新备份方案上线后强制做一次恢复演练并且至少每个季度做一次随机抽查。恢复演练的完整流程分三步。第一步选一台测试机或者同一台服务器的测试库。如果有条件最好用一个新的MySQL实例完全模拟从零恢复。命令很简单mysql -uroot -p /data/backup/mysql/daily/all_db_2025-01-15_03-00-01.sql.gz但注意如果文件是.gz压缩过的需要先解压再导入或者用管道一步完成gunzip /data/backup/mysql/daily/all_db_2025-01-15_03-00-01.sql.gz | mysql -uroot -p第二步重点检查恢复后的数据是否完整。不要只是看mysql提示符有没有报错要实际查表USE 你的业务库; SELECT COUNT(*) FROM 核心业务表; SHOW TABLES;如果关键表行数和备份前一致说明这一份备份是可用的。如果行数对不上那就要排查备份脚本是不是漏了什么表或者导出过程中有报错被忽略了。第三步检查存储过程、触发器、事件有没有恢复成功SELECT ROUTINE_NAME FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA你的库; SELECT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA你的库;这里最常见的坑就是我在前面提到的ERROR 1227 (42000): Access denied; you need (at least one of) the SUPER privilege(s) for this operation。原因很简单备份文件里带有原库的DEFINER信息如果目标环境没有那个用户就会恢复失败。解决办法有两种一种是恢复前预先创建对应的用户另一种是导入前把文件里的DEFINER去掉sed -i s/DEFINER[^*]*\*/\*/g 备份文件.sql这个命令注意别在生产环境乱用它只用于恢复演练和灾难恢复场景。5.3 我的几点实操体会备份这件事做得越久越觉得它的重点其实不在“备份”本身而在“管理”。这里说几点我个人的实操体会。一是备份时间尽量安排在工作负载最低的时间段。比如凌晨2到4点大多数业务访问量低mysqldump对线上影响最小。但也不要盲目选凌晨3点如果你的业务有凌晨批量任务最好错开。我一般会看一下慢查询日志和监控面板挑一个CPU、IO都相对空闲的窗口。二是备份文件一定要定期抽检不能只看脚本日志写着“成功”就算完。有一次我的备份脚本连续三天都显示成功但第四天我随手解压了一个文件才发现里面内容全部是错误日志原因是磁盘IO异常导致mysqldump没有真正读取数据。这个经历让我养成了一个习惯每次手动检查备份时不仅看文件存在还要解压后抽查SQL内容确认里面有关键表的INSERT语句。三是脚本上线后不要在没监控的情况下放任不管。哪怕只是每天早上一句“备份成功/失败”的消息推送到工作群也是非常值得的。人总有忘的时候系统提醒才是最可靠的。四是如果业务变化频繁备份策略也要跟着调整。比如原来一天只有几千条写入后来某天业务量翻了几十倍原来的全量备份时间窗口可能就不够了这时候就要考虑分库备份、增量备份或者升级物理备份工具。没有一劳永逸的备份方案只有不断跟随业务调整的备份体系。最后再分享一个小技巧备份脚本里加一个备份文件完整性校验。压缩完之后用md5sum生成校验值文件后续无论做异地拷贝还是本地恢复都能先校验一遍避免因为文件损坏浪费几个小时。这个习惯不复杂但关键时刻真的能救命。我现在的工作流就是凌晨3点备份3点10分脚本自动推送一条消息到工作群里面包含备份文件大小和md5值我早上到公司扫一眼消息发现异常就立刻处理。每个月再挑一份备份文件做一次随机解压和恢复验证。这套流程运行到现在已经帮我避免过至少三次能想象得到的数据灾难。希望你也能搭起自己的这套备份体系而不是等到数据丢了再去看教程。