MySQL
本文范围
这篇我们不从概念开始背,直接按一台 MySQL 真正要上线的顺序来做。你可以把它当成一份部署运维带练:先把版本和安装源定下来,再把目录、配置、端口、账号、日志、备份、复制和巡检一段一段跑通。
这里的范围很明确,只讲 MySQL 部署运维:怎么选版本、怎么安装、怎么规划目录、怎么写 my.cnf、怎么开端口、怎么建用户、怎么做权限和安全、怎么开慢日志和 binlog、怎么备份恢复、怎么搭主从、怎么判断高可用边界、怎么做巡检,以及线上出问题时怎么排查。
本文不展开 SQL 优化、索引设计、事务隔离、锁、MVCC、执行计划和分库分表。慢日志会讲如何开启、如何定位入口、如何和排障结合;具体 SQL 怎么改,后续放到数据库开发和架构专题里单独写。
前置条件
截至 2026-07-09,MySQL 8.0 已随 8.0.46 在 2026 年 4 月进入 EOL。新生产环境不要再把 8.0 当成默认选择。本文以 MySQL 8.4 LTS 作为保守生产主线,补充 9.7 LTS 的当前官方方向;如果团队已经有 9.7 LTS 验证流程,可以把示例版本替换为 9.7。存量 8.0 环境建议先做补丁、备份和升级评估,不要把旧版本继续扩成新的生产基线。
一般需要准备三类环境。没有条件一次准备齐,至少先准备一台测试机和一份离线包目录,不然到了内网服务器上临时找依赖会很难受:
- 一台 Linux 测试机:Ubuntu 24.04 / Debian 12 / RHEL 9 / Rocky Linux 9 / AlmaLinux 9 均可。
- 一台可模拟生产的服务器:单独磁盘或独立挂载目录,至少 4C8G,磁盘按业务数据量预留。
- 一套离线交付目录:放 RPM/DEB 包、Docker 镜像包、校验文件、初始化脚本、备份脚本和恢复脚本。
MySQL 部署运维闭环总览
MySQL 部署不是“装起来能连上”就结束,而是一条从版本来源到备份恢复、复制和巡检的闭环。后续章节可以按这张图逐段落地,避免只完成安装却漏掉生产治理。
这张图不是为了画得好看,而是提醒你:每一步做完都要有验证命令。比如安装完要能看到版本,改完配置要能看到变量生效,备份完要能在临时实例恢复。只要某个环节没有验证命令、记录文件或恢复路径,就不要把它视为生产准备完成;数据库最怕的是平时能跑,故障时没人知道怎么还原。
中国大陆、内网和离线环境先想清楚
先别急着 yum install 或 docker pull。MySQL 在个人电脑上装起来很快,但一到公司内网、专线机房、等保环境,最容易卡住的不是命令,而是安装源、依赖包和镜像包。
MySQL 部署不能默认官网、Docker Hub、GitHub、国外 yum/apt 源都能直连。生产落地前先按网络环境选方案。
| 环境 | 推荐方式 | 风险点 | 验证命令 |
|---|---|---|---|
| 公网可访问 | 官方 Yum/APT 仓库或官方下载包 | 官方站点偶发慢,版本仓库可能切换默认版本 | mysql --version、systemctl status mysqld |
| 中国大陆公网 | 官方源优先;镜像站或云厂商镜像作为候选;企业内部最好缓存一份 | 第三方镜像可用性变化快,不能写死为唯一来源 | curl -I、dnf repolist、apt-cache policy |
| 企业内网 | 内部 yum/apt 仓库、制品库、文件服务器、私有镜像仓库 | 依赖包缺失、GPG 校验被跳过、版本不可追溯 | repoquery、dpkg -I、sha256sum |
| 完全离线 | RPM/DEB bundle、Generic tar 包、docker save/load 镜像包 | 漏带依赖、架构不匹配、无法回滚 | 离线机安装演练、恢复演练 |
公网安装只是第一种路径。真正稳定的团队做法是:外网机下载并校验包,进入内网后放到内部仓库,所有服务器都从内部仓库安装,版本和校验记录进入交付清单。这样做的好处是,今天装的、下个月扩容装的、故障重建装的,都是同一批可追溯的软件。
离线包闭环
外网机下载包后,先记录版本和校验值。这里的目标不是“形式上有校验文件”,而是以后任何一台机器拿到这批包,都能确认没有传坏、没有被替换、版本也对得上:
mysql --version 2>/dev/null || true
uname -m
gpg --verify mysql-*.asc 2>/dev/null || trueRPM 系机器可以在外网机准备依赖。如果你只下载一个 server 包,到了内网大概率会提示缺依赖,所以这里用 --resolve 一次把依赖带上:
sudo dnf install -y dnf-plugins-core
mkdir -p mysql-rpms
dnf download --resolve --destdir mysql-rpms mysql-community-server
sha256sum mysql-rpms/*.rpm > SHA256SUMS.txt
tar -czf mysql-rpms.tar.gz mysql-rpms SHA256SUMS.txt内网机解压后安装。执行 sha256sum -c 时如果有文件不匹配,先停下来重新传包,不要硬装:
tar -xzf mysql-rpms.tar.gz
sha256sum -c SHA256SUMS.txt
sudo dnf install -y mysql-rpms/*.rpmDEB 系机器可以准备 .deb 包。这里要注意,apt-get download 只下载指定包;复杂环境建议使用内部 APT 仓库、apt-offline 或提前把依赖补齐:
mkdir -p mysql-debs
cd mysql-debs
apt-get download mysql-community-server mysql-community-client
sha256sum ./*.deb > ../SHA256SUMS.txt
cd ..
tar -czf mysql-debs.tar.gz mysql-debs SHA256SUMS.txt内网机安装前也要先校验:
tar -xzf mysql-debs.tar.gz
sha256sum -c SHA256SUMS.txt
sudo apt install -y ./mysql-debs/*.debDocker 离线环境不要依赖现场直连镜像仓库。外网机或内部构建机先拉取固定版本,记录镜像 ID 和 digest,再导出成压缩包:
docker pull mysql:8.4
docker image inspect mysql:8.4 --format '{{.Id}} {{.RepoDigests}}'
docker save mysql:8.4 | gzip > mysql-8.4-image.tar.gz
sha256sum mysql-8.4-image.tar.gz > mysql-8.4-image.tar.gz.sha256内网导入后要能看到 mysql 镜像。看不到就说明压缩包、解压、导入某个环节出了问题:
sha256sum -c mysql-8.4-image.tar.gz.sha256
gunzip -c mysql-8.4-image.tar.gz | docker load
docker image ls mysql安装 MySQL
RPM 系安装
RHEL、Rocky Linux、AlmaLinux 这类系统可以使用 MySQL 官方 Yum Repository。这里先装仓库包,再禁用系统自带模块,最后安装 MySQL Community Server。仓库包名称会随系统版本和 MySQL 版本变化,落地时以官方下载页为准。
sudo rpm -Uvh mysql84-community-release-el9-1.noarch.rpm
sudo dnf repolist | grep -i mysql
sudo dnf module disable mysql -y
sudo dnf install -y mysql-community-server这一步执行完,dnf repolist 应该能看到 MySQL 相关仓库;如果看不到,先检查仓库包是否装对、系统版本是否匹配、网络或代理是否能访问。
启动并设置开机自启:
sudo systemctl enable --now mysqld
sudo systemctl status mysqld --no-pager
mysql --version看到 active (running),并且 mysql --version 能输出版本号,说明服务已经起来。RPM 安装后,root 初始密码通常写在 error log 中:
sudo grep 'temporary password' /var/log/mysqld.log
mysql -uroot -p如果 grep 不到临时密码,先看 journalctl -u mysqld 和 /var/log/mysqld.log,确认初始化是否成功。进入后立即改密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY 'Change_me_Use_Strong_Password_2026!';DEB 系安装
Ubuntu / Debian 可以使用 MySQL APT Repository。安装 mysql-apt-config 时会弹出版本选择,生产上不要随手选最新创新版本,要按团队基线选择 LTS:
sudo dpkg -i mysql-apt-config_*.deb
sudo apt update
sudo apt install -y mysql-community-server
sudo systemctl enable --now mysql
mysql --version执行完以后用 systemctl status mysql 看服务状态,用 mysql --version 看实际版本。如果系统自带的是 MariaDB 或 Ubuntu 默认 MySQL 包,要先明确团队到底使用哪个发行版。MySQL Community Server、Percona Server、MariaDB 的配置、权限、复制和备份工具有相似处,但不能在生产文档里混写。
Docker 安装
本地开发、测试环境、临时恢复环境可以用 Docker 快速启动。生产环境是否容器化,要看存储、资源限制、备份恢复、监控和故障切换是否配套。
先把配置、数据和日志目录建出来。这里用的是宿主机目录挂载,不要让数据库数据只留在容器可写层里:
sudo mkdir -p /data/mysql/conf /data/mysql/data /data/mysql/log
sudo chown -R 999:999 /data/mysql/data /data/mysql/log准备配置文件。配置文件挂到 /etc/mysql/conf.d/ 下,MySQL 容器启动时会自动读取:
sudo tee /data/mysql/conf/custom.cnf >/dev/null <<'EOF'
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
default_time_zone='+08:00'
slow_query_log=ON
long_query_time=1
log_bin=mysql-bin
binlog_format=ROW
binlog_expire_logs_seconds=604800
EOF启动容器。这里要注意三件事:密码用强密码,镜像固定版本,数据目录做持久化挂载:
docker run -d \
--name mysql84 \
--restart unless-stopped \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD='Change_me_Use_Strong_Password_2026!' \
-e TZ=Asia/Shanghai \
-v /data/mysql/conf/custom.cnf:/etc/mysql/conf.d/custom.cnf:ro \
-v /data/mysql/data:/var/lib/mysql \
-v /data/mysql/log:/var/log/mysql \
mysql:8.4验证。docker logs 里应该能看到 ready for connections,SQL 查询应该能返回版本、字符集和时区:
docker logs mysql84 --tail 80
docker exec -it mysql84 mysql -uroot -p -e "SELECT VERSION(), @@character_set_server, @@time_zone;"如果容器反复重启,先看三处:docker logs mysql84、挂载目录权限、配置文件权限。生产不要使用 mysql:latest。latest 是可变标签,今天拉到的镜像和一个月后拉到的镜像可能不是同一个版本。关键环境至少固定到 mysql:8.4,更严格的团队要记录 digest。
目录规划和权限
MySQL 最怕“能启动但不可维护”的目录布局。很多线上问题不是 SQL 写错,而是 binlog、slow log、备份文件和数据文件都堆在一个盘里,最后磁盘满了谁也不敢删。单机部署建议把数据、日志、备份、临时目录分清楚。
| 目录 | 用途 | 建议 |
|---|---|---|
/data/mysql/data | 数据文件 | 独立磁盘或独立分区,重点监控 |
/data/mysql/log | error log、slow log | 配合日志切割和保留策略 |
/data/mysql/binlog | binlog | 单独监控容量,避免撑满数据盘 |
/data/mysql/tmp | 临时文件 | 大查询、排序、临时表会用到 |
/backup/mysql | 备份文件 | 尽量异盘、异机、异地 |
/etc/my.cnf 或 /etc/mysql/conf.d/*.cnf | 配置 | 权限不能过宽 |
创建目录。如果没有 mysql 用户就新建;如果系统包已经创建过,这两条命令会安静跳过:
sudo groupadd -r mysql 2>/dev/null || true
sudo useradd -r -g mysql -s /sbin/nologin mysql 2>/dev/null || true
sudo install -o mysql -g mysql -m 750 -d /data/mysql/data
sudo install -o mysql -g mysql -m 750 -d /data/mysql/log
sudo install -o mysql -g mysql -m 750 -d /data/mysql/binlog
sudo install -o mysql -g mysql -m 750 -d /data/mysql/tmp
sudo install -o mysql -g mysql -m 750 -d /backup/mysql执行完用 ls -ld /data/mysql/* /backup/mysql 看 owner 是否是 mysql:mysql。如果目录 owner 是 root,MySQL 启动时很容易报 Permission denied。
如果系统启用了 SELinux,迁移数据目录后要处理上下文:
sudo semanage fcontext -a -t mysqld_db_t "/data/mysql/data(/.*)?"
sudo semanage fcontext -a -t mysqld_log_t "/data/mysql/log(/.*)?"
sudo restorecon -Rv /data/mysql配置文件权限过宽会被 MySQL 忽略。这个坑在容器和手工拷贝配置时很常见。看到类似 World-writable config file '/etc/my.cnf' is ignored,先查权限:
ls -l /etc/my.cnf /etc/mysql/conf.d/*.cnf 2>/dev/null
sudo chmod 640 /etc/my.cnf
sudo chown root:mysql /etc/my.cnf
sudo systemctl restart mysqldmy.cnf 生产模板
下面是适合中小型业务起步的模板。先把它理解成“生产起步配置”,不要直接复制到所有机器。真正落地前要按 CPU、内存、磁盘、连接池和业务写入量调整。
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
port=3306
bind-address=0.0.0.0
datadir=/data/mysql/data
tmpdir=/data/mysql/tmp
socket=/data/mysql/mysql.sock
pid-file=/data/mysql/mysqld.pid
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
default_time_zone='+08:00'
default_storage_engine=InnoDB
sql_mode=STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
max_connections=300
max_connect_errors=1000
open_files_limit=65535
table_open_cache=2048
max_allowed_packet=256M
innodb_buffer_pool_size=4G
innodb_flush_log_at_trx_commit=1
sync_binlog=1
log_error=/data/mysql/log/error.log
slow_query_log=ON
slow_query_log_file=/data/mysql/log/slow.log
long_query_time=1
log_queries_not_using_indexes=OFF
server_id=1001
log_bin=/data/mysql/binlog/mysql-bin
binlog_format=ROW
binlog_expire_logs_seconds=604800
gtid_mode=ON
enforce_gtid_consistency=ON
local_infile=0
skip_name_resolve=ON几个关键点要看懂。这里不是为了背参数,而是为了知道线上出问题时该从哪里下手:
bind-address=0.0.0.0只表示 MySQL 监听所有网卡,不代表任何人都能连。真正的访问控制还要靠用户 host、防火墙和安全组。utf8mb4是默认建议,避免 emoji、特殊字符写入失败。default_time_zone='+08:00'能减少应用、数据库、日志之间的时间差异,但跨国业务要统一评估 UTC。innodb_flush_log_at_trx_commit=1和sync_binlog=1更偏数据安全;导入大批量数据时可以临时放宽,但必须写明恢复步骤。binlog_expire_logs_seconds=604800是 7 天。保留时间要结合备份周期、恢复目标和磁盘容量。skip_name_resolve=ON可以减少 DNS 反查问题,但授权时要使用 IP 或网段,不要依赖主机名。
配置后验证。第一条命令用来看 MySQL 会读取哪些配置文件,第二条重启服务,第三条确认关键变量已经生效:
sudo mysqld --verbose --help 2>/dev/null | grep -E "Default options|my.cnf" -A 2
sudo systemctl restart mysqld
mysql -uroot -p -e "SHOW VARIABLES WHERE Variable_name IN ('port','datadir','character_set_server','time_zone','log_bin','gtid_mode');"如果重启失败,先不要反复重启。直接看 error log,通常会告诉你是参数不认识、路径不存在、权限不对,还是端口被占用。
端口、防火墙和连通性
MySQL 默认端口是 3306,但生产不应该暴露到公网。一般需要先确认“服务是否监听”,再确认“防火墙是否放行”,最后确认“账号来源是否允许”。至少做到:
- 只允许应用服务器、跳板机、备份机、监控机访问。
- 云服务器同时检查安全组和系统防火墙。
- 不用 root 做远程业务连接。
Linux 本机检查。ss 能看到 3306 监听,mysqladmin ping 返回 mysqld is alive,才说明本机服务层面没问题:
ss -lntp | grep 3306
systemctl status mysqld --no-pager
mysqladmin -uroot -p pingfirewalld 放行指定来源更安全。这里放行的是一个应用服务器网段,不是全网:
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.10.20.0/24" port protocol="tcp" port="3306" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --list-all客户端验证。访问后应该看到 SELECT 1 返回一行结果:
mysql -h 10.10.10.11 -P 3306 -u app_user -p -e "SELECT 1;"如果连不上,按顺序查。不要一上来就改密码或改 root 远程,先把网络、端口、服务、用户来源逐层拆开:
ping 10.10.10.11
nc -vz 10.10.10.11 3306
ss -lntp | grep 3306
sudo firewall-cmd --list-ports
mysql -uroot -p -e "SELECT user, host, plugin, account_locked FROM mysql.user;"用户权限和远程访问
MySQL 权限要按角色拆,不要一个 root 账号打天下。一个比较稳的办法是:业务账号只连业务库,只读账号只查,备份账号只备份,复制账号只复制。常见用户至少分四类:
| 用户 | 来源 | 权限 |
|---|---|---|
app_user | 应用服务器网段 | 业务库 DML,必要时执行存储过程 |
readonly_user | 报表或排查机器 | 只读 |
backup_user | 本机或备份机 | 备份所需最小权限 |
repl_user | 从库地址 | 复制权限 |
创建业务用户。这里的 10.10.20.% 表示只允许应用服务器网段访问,不是所有机器都能连:
CREATE DATABASE appdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'app_user'@'10.10.20.%' IDENTIFIED BY 'Use_Strong_Password_Here!';
GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON appdb.* TO 'app_user'@'10.10.20.%';
SHOW GRANTS FOR 'app_user'@'10.10.20.%';创建只读用户。给排查、报表、临时查询使用,不要让它有写权限:
CREATE USER 'readonly_user'@'10.10.30.%' IDENTIFIED BY 'Use_Strong_Password_Here!';
GRANT SELECT, SHOW VIEW ON appdb.* TO 'readonly_user'@'10.10.30.%';
SHOW GRANTS FOR 'readonly_user'@'10.10.30.%';不要为了省事写:
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%' WITH GRANT OPTION;这类授权会把业务账号变成半个 DBA 账号。出了问题,很难判断是应用误操作、脚本误执行,还是账号泄漏。生产环境里,权限越大,排障越难。
MySQL 8.4 及以后要注意认证插件。新环境优先使用默认的 caching_sha2_password。如果老客户端不支持,不要第一反应就把全库切回旧插件,先升级驱动;实在需要兼容旧系统,也要只对特定临时账号处理,并记录下线计划。
查看账号。执行后重点看三列:host 是否过宽,plugin 是否和客户端兼容,account_locked 是否误锁:
SELECT user, host, plugin, account_locked, password_expired
FROM mysql.user
ORDER BY user, host;锁定不用的账号:
ALTER USER 'old_user'@'10.10.20.%' ACCOUNT LOCK;删除账号前先确认授权:
SHOW GRANTS FOR 'old_user'@'10.10.20.%';
DROP USER 'old_user'@'10.10.20.%';安全加固
安全加固不要停留在“记得加强安全”这种口号上,直接做最小可执行清单:
- root 只允许本机登录,不开放
root@'%'。 - 业务账号按库授权,不授
SUPER、FILE、PROCESS、WITH GRANT OPTION。 - 跨网段访问优先走内网、专线、VPN 或堡垒机,不把 3306 暴露公网。
- 关闭不需要的
local_infile。 - 配置备份账号,不在脚本里明文写 root 密码。
- 定期检查空密码、过期账号、异常来源 host。
- 生产变更前导出授权,便于回滚。
检查弱边界。这里重点找 root 和 %,它们不一定都错,但必须能解释为什么存在:
SELECT user, host, plugin, account_locked
FROM mysql.user
WHERE host = '%' OR user = 'root';备份授权信息。生产变更前先导出一份授权,后面回滚会省很多时间:
mysql -uroot -p -N -e "SELECT CONCAT('SHOW GRANTS FOR ''', user, '''@''', host, ''';') FROM mysql.user" > show-grants.sql
mysql -uroot -p < show-grants.sql > grants-backup.sql如果需要 TLS,再开启证书和 require_secure_transport=ON。跨机房、跨云、跨公网链路不要裸连 MySQL。
慢日志、错误日志和 binlog
错误日志
错误日志是启动失败、崩溃、权限问题、表空间损坏的第一入口。MySQL 起不来时,不要先猜参数,先看最后 200 行日志:
sudo journalctl -u mysqld -n 200 --no-pager
sudo tail -n 200 /data/mysql/log/error.log容器环境:
docker logs mysql84 --tail 200
docker exec -it mysql84 sh -c 'tail -n 200 /var/log/mysql/error.log 2>/dev/null || true'慢日志
开启慢日志。这里用 SET GLOBAL 是临时打开,适合现场排查;长期生效还要写入 my.cnf:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';长期配置放进 my.cnf:
slow_query_log=ON
slow_query_log_file=/data/mysql/log/slow.log
long_query_time=1
log_queries_not_using_indexes=OFF执行完 SHOW VARIABLES,应该能看到 slow_query_log=ON,long_query_time=1。慢日志不是“看到慢 SQL 就立刻改索引”。运维侧先做四件事:
- 确认慢日志是否真的开启。
- 确认慢日志文件是否持续增长,避免打满磁盘。
- 关联应用发布时间、流量峰值、连接数、CPU、IO。
- 把慢 SQL 样本交给 SQL 优化专题处理。
binlog
binlog 用于复制和点时间恢复。简单说,误删之后能不能把数据追回来,很大程度上就看全量备份和 binlog 是否完整。只要业务对数据恢复有要求,就不要轻易关闭。
检查。log_bin=ON,并且 SHOW BINARY LOGS 能看到文件列表,说明 binlog 在正常工作:
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
SHOW BINARY LOGS;
SHOW BINARY LOG STATUS;常用配置:
server_id=1001
log_bin=/data/mysql/binlog/mysql-bin
binlog_format=ROW
binlog_expire_logs_seconds=604800
sync_binlog=1
gtid_mode=ON
enforce_gtid_consistency=ONbinlog 清理要先确认从库和备份恢复需要。不要看到磁盘满就直接删文件,直接删会让 MySQL 元数据和磁盘文件对不上。正确方式是让 MySQL 自己 purge:
SHOW REPLICA STATUS\G
SHOW BINARY LOGS;
PURGE BINARY LOGS BEFORE '2026-07-01 00:00:00';如果是主从环境,先确认从库已经追上,不然 purge 可能让从库断档。
备份恢复
备份没有恢复演练,就只是“心理安慰文件”。很多团队每天都在生成备份,真正出事时才发现文件坏了、缺触发器、缺存储过程、没有 binlog 位点,或者压根没人试过恢复。MySQL 备份至少要回答五个问题:
- 备份了哪些库和对象:表、视图、触发器、存储过程、事件。
- 备份文件在哪里,保留多久,谁能访问。
- 备份是否包含一致性位点或 GTID 信息。
- 恢复到哪里验证,验证哪些表和数据量。
- 数据误删、整库损坏、服务器丢失分别怎么恢复。
设计评审时可以先用下面这张图把“数据、日志、备份、恢复”串起来。它不是为了背 InnoDB 名词,而是为了确认:数据目录和表空间有没有被备份策略覆盖,redo / undo / binlog 的职责有没有混淆,PITR 需要的全量备份、binlog 归档、校验和恢复演练有没有形成闭环。
上线检查时按图逐个打钩:SHOW BINARY LOGS 能看到 binlog,备份包有压缩和 checksum 校验,mysqlbinlog 能在隔离实例回放,恢复后能用表数量、行数、关键业务查询和抽样 checksum 验证。少了其中任何一环,备份方案都只能算“生成了文件”,不能算“能恢复业务”。
逻辑备份
创建备份用户。不要让备份脚本拿 root 密码跑,脚本泄漏一次就等于数据库裸奔一次:
CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'Use_Strong_Password_Here!';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, RELOAD, PROCESS, REPLICATION CLIENT ON *.* TO 'backup_user'@'localhost';
FLUSH PRIVILEGES;使用 mysql_config_editor 保存登录信息,避免脚本里明文写密码。执行时会提示输入密码,密码会保存到当前用户的加密登录文件里:
mysql_config_editor set --login-path=mysql_backup --host=localhost --user=backup_user --password
mysql_config_editor print --login-path=mysql_backup备份单库。--single-transaction 适合 InnoDB 一致性备份,--routines、--triggers、--events 用来带上过程、触发器和事件:
mkdir -p /backup/mysql
mysqldump --login-path=mysql_backup \
--single-transaction \
--routines \
--triggers \
--events \
--set-gtid-purged=OFF \
appdb | gzip > /backup/mysql/appdb-$(date +%F-%H%M%S).sql.gz备份多库:
mysqldump --login-path=mysql_backup \
--single-transaction \
--routines \
--triggers \
--events \
--databases appdb authdb orderdb \
| gzip > /backup/mysql/mysql-databases-$(date +%F-%H%M%S).sql.gz备份后立即做三个校验。文件大小不是 0、gzip 能解压、里面能看到建表语句,才算过了第一关:
ls -lh /backup/mysql/*.sql.gz | tail
gzip -t /backup/mysql/appdb-*.sql.gz
zgrep -m 1 "CREATE TABLE" /backup/mysql/appdb-*.sql.gz定时任务先手动执行成功,再放入 cron。不要把一个没跑过的脚本直接塞进计划任务:
crontab -e20 1 * * * /usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1备份脚本要至少包含:失败即退出、创建目录、执行备份、校验压缩包、清理过期文件。
#!/usr/bin/env bash
set -euo pipefail
backup_dir="/backup/mysql"
db_name="appdb"
ts="$(date +%F-%H%M%S)"
file="${backup_dir}/${db_name}-${ts}.sql.gz"
mkdir -p "$backup_dir"
mysqldump --login-path=mysql_backup \
--single-transaction \
--routines \
--triggers \
--events \
--set-gtid-purged=OFF \
"$db_name" | gzip > "$file"
gzip -t "$file"
find "$backup_dir" -name "${db_name}-*.sql.gz" -mtime +14 -delete恢复演练
不要在原库上测试恢复。准备临时实例,这里用 Docker 只是为了演示恢复环境快速拉起,生产可以用同版本临时虚机或测试服务器:
docker run -d \
--name mysql-restore-test \
-e MYSQL_ROOT_PASSWORD='Restore_Test_Password_2026!' \
-p 3307:3306 \
mysql:8.4恢复。这个命令会把压缩包解开后直接导入临时实例:
gunzip -c /backup/mysql/appdb-2026-07-09-012000.sql.gz \
| mysql -h127.0.0.1 -P3307 -uroot -p验证。恢复后至少看库是否存在、表数量是否符合预期、数据体量是否接近原库:
SHOW DATABASES;
SELECT table_schema, COUNT(*) table_count
FROM information_schema.tables
WHERE table_schema = 'appdb'
GROUP BY table_schema;
SELECT table_schema,
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb
FROM information_schema.tables
WHERE table_schema = 'appdb'
GROUP BY table_schema;binlog 点时间恢复
典型场景:10:15 误删数据,凌晨 1:20 有全量备份,binlog 保留完整。恢复思路是先恢复全量备份,再回放 binlog 到误操作前。这里最重要的是先在临时实例演练,不要直接往生产原库回放。
查看 binlog。先人工确认误操作大概落在哪个文件、哪个时间段:
mysqlbinlog --base64-output=DECODE-ROWS --verbose /data/mysql/binlog/mysql-bin.000123 | less按时间截断回放。下面示例只回放到 10:14:59,避开 10:15 的误删:
mysqlbinlog \
--start-datetime="2026-07-09 01:20:00" \
--stop-datetime="2026-07-09 10:14:59" \
/data/mysql/binlog/mysql-bin.000123 \
| mysql -h127.0.0.1 -P3307 -uroot -p生产恢复要先在临时实例验证,再决定是否回写生产。误删恢复不要直接对原库执行,除非已经确认停写、备份、回滚方案和影响范围。
物理备份边界
mysqldump 通用、可读、跨版本能力强,但大库恢复慢。数据量上百 GB、恢复时间要求严格时,要评估物理备份工具,例如 MySQL Enterprise Backup、Percona XtraBackup、存储快照。这里要注意,物理备份追求的是恢复速度,但它更依赖版本、文件状态和工具链,不是简单复制数据目录。本文不展开物理备份完整教程,只给边界:
- 物理备份要和 MySQL 版本、存储引擎、redo/binlog 配套验证。
- 快照不是数据库一致性备份,快照前要考虑 flush、锁表、复制位点或存储一致性能力。
- 物理恢复必须定期演练,不然故障时才发现版本不兼容会非常被动。
主从复制和 GTID
主从复制解决的是读扩展、备份卸载、灾备基础,不等于自动高可用。可以把它理解成“主库写,从库跟着学”。主库挂了,从库不会自动变成新主库;业务连接也不会自动切换,除非你额外建设故障切换和连接路由。
复制链路和故障切换要分开评审。下面这张图适合放到上线检查或故障演练清单里:先看复制是否健康,再看读写入口是否能切,最后看旧主库回收和回滚验证有没有安排。
这里要特别注意两个边界。第一,半同步复制只能改变事务返回前等待副本确认的边界,不等于自动切换;第二,read_only 和 super_read_only 是防误写护栏,不是高可用系统。真正切换时要先冻结写入、确认 GTID 或 binlog 位点,再切 VIP / Proxy / Router,最后用 SHOW REPLICA STATUS\G、业务写入 smoke test 和旧主库只读状态验证回滚窗口。
主库配置
主库 my.cnf。这里至少要有唯一的 server_id、binlog、ROW 格式和 GTID:
[mysqld]
server_id=1001
log_bin=/data/mysql/binlog/mysql-bin
binlog_format=ROW
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_expire_logs_seconds=604800创建复制账号。来源 host 要写从库网段,不要直接写 %:
CREATE USER 'repl_user'@'10.10.40.%' IDENTIFIED BY 'Use_Strong_Password_Here!';
GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'10.10.40.%';
FLUSH PRIVILEGES;查看状态。GTID 模式下后面可以用自动定位,减少手工记录 binlog 文件和 position 的复杂度:
SHOW BINARY LOG STATUS;从库配置
从库 my.cnf。server_id 必须和主库不同,read_only 和 super_read_only 用来避免误写从库:
[mysqld]
server_id=1002
relay_log=/data/mysql/relaylog/mysql-relay-bin
read_only=ON
super_read_only=ON
gtid_mode=ON
enforce_gtid_consistency=ON配置复制。SOURCE_AUTO_POSITION=1 表示使用 GTID 自动定位:
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='10.10.10.11',
SOURCE_PORT=3306,
SOURCE_USER='repl_user',
SOURCE_PASSWORD='Use_Strong_Password_Here!',
SOURCE_AUTO_POSITION=1;
START REPLICA;
SHOW REPLICA STATUS\G重点看。看到 IO 和 SQL 都是 Yes,错误字段为空,延迟接近 0,复制才算基本跑起来:
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
Last_IO_Error:
Last_SQL_Error:
Retrieved_Gtid_Set:
Executed_Gtid_Set:如果 Replica_IO_Running 不是 Yes,优先查网络、账号、密码、端口、TLS。
如果 Replica_SQL_Running 不是 Yes,优先查 SQL 线程错误、表结构差异、主从数据不一致。
如果 Seconds_Behind_Source 持续升高,优先查从库 IO、慢 SQL、大事务、主库写入峰值和从库机器规格。
主从延迟排查
复制延迟不要只盯一个数字。先看复制线程,再看从库机器资源,最后关联业务写入峰值。常用命令:
SHOW REPLICA STATUS\G
SHOW PROCESSLIST;
SHOW VARIABLES LIKE 'gtid_mode';
SHOW VARIABLES LIKE 'binlog_format';
SHOW GLOBAL STATUS LIKE 'Threads_running';系统侧:
top
iostat -x 1
free -h
df -h常见原因:
| 现象 | 常见原因 | 验证方式 |
|---|---|---|
| IO 线程断开 | 网络不通、账号错误、主库 binlog 被清理 | Last_IO_Error、nc -vz |
| SQL 线程报错 | 从库数据和主库不一致、DDL 差异 | Last_SQL_Error |
| 延迟持续升高 | 大事务、从库 IO 差、从库配置低 | SHOW PROCESSLIST、iostat |
| 偶发延迟后恢复 | 主库写入峰值、备份任务抢 IO | 关联监控和任务时间 |
不要把 Seconds_Behind_Source=0 当成唯一健康标准。复制链路是否健康,还要看 IO/SQL 线程、错误字段、GTID 集合和业务读一致性要求。
高可用边界
MySQL 常见方案要分清。很多项目把“有一个从库”叫高可用,这是非常危险的说法;从库只是地基,不是完整切换体系:
| 方案 | 解决什么 | 不解决什么 |
|---|---|---|
| 单机 + 备份 | 成本低,适合开发测试和小业务 | 机器故障恢复慢 |
| 主从复制 | 读扩展、备份卸载、灾备基础 | 不自动切主,不自动改业务连接 |
| 主从 + VIP / Proxy | 简化连接切换 | 需要处理脑裂、探活、切换一致性 |
| InnoDB Cluster / Group Replication / MySQL Router | 官方高可用体系 | 部署和治理复杂度更高 |
| PXC / Galera 类方案 | 多节点同步复制能力 | 写入冲突、网络分区、运维复杂度 |
| 云数据库 | 运维托管、备份监控成熟 | 成本、权限、迁移和厂商边界 |
真正生产 HA 要单独设计:
- 故障检测由谁做。
- 谁有权提升新主。
- 旧主恢复后如何处理。
- 业务连接如何切换。
- binlog 和 GTID 如何保证不丢不重。
- 切换前后如何验证数据一致性。
主从复制不是高可用的终点,只是高可用的地基。
容量和巡检
MySQL 巡检要固定成命令清单。不要等磁盘 100% 才发现 binlog、slow log、Docker json log 都在增长。下面这些命令建议做成每日巡检脚本,至少保留最近一段时间的结果,方便对比趋势。
基础状态
先看服务活着没有、端口有没有、MySQL 自己是否响应:
systemctl status mysqld --no-pager
ss -lntp | grep 3306
mysqladmin -uroot -p ping
mysql -uroot -p -e "SELECT VERSION(), @@uptime, @@read_only, @@super_read_only;"这里 @@uptime 可以帮助判断是否刚重启过,read_only 和 super_read_only 可以帮助判断当前节点是不是只读节点。
容量
先看文件系统,再看 MySQL 目录。df 看哪个挂载点满了,du 看具体是谁占空间:
df -h
du -sh /data/mysql/*
du -sh /backup/mysql/*库大小:
SELECT table_schema,
ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2) AS size_gb
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
GROUP BY table_schema
ORDER BY size_gb DESC;表大小:
SELECT table_schema, table_name,
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb,
table_rows
FROM information_schema.tables
WHERE table_schema = 'appdb'
ORDER BY size_mb DESC
LIMIT 20;连接数
连接问题不要只看当前连接数,还要看历史峰值和异常连接:
SHOW VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Aborted_connects';按来源聚合:
SELECT user,
SUBSTRING_INDEX(host, ':', 1) AS host_ip,
db,
command,
COUNT(*) AS cnt
FROM information_schema.processlist
GROUP BY user, host_ip, db, command
ORDER BY cnt DESC;max_connections 不是越大越好。估算时至少看三件事:
max_connections ≈ 应用实例数 × 单实例连接池上限 × 1.2
max_connections ≈ 历史 Max_used_connections × 1.5
max_connections 受可用内存、单连接内存、open_files_limit 约束临时调整。这个设置重启后会丢,适合现场止血:
SET GLOBAL max_connections = 500;永久调整写入 my.cnf 后重启:
max_connections=500
open_files_limit=65535Linux 文件句柄也要跟上。否则 MySQL 连接数调上去了,系统层面也可能因为文件句柄不够而报错:
ulimit -n
sudo systemctl show mysqld -p LimitNOFILE日志和备份
巡检时顺手确认 binlog、慢日志和最近备份。不要等恢复时才发现最近一次备份是半个月前:
SHOW BINARY LOGS;
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';
SHOW VARIABLES LIKE 'slow_query_log%';find /backup/mysql -type f -name '*.sql.gz' -mtime -2 -ls
gzip -t "$(find /backup/mysql -type f -name '*.sql.gz' | sort | tail -n 1)"故障处理手册
远程连接失败
这是最常见的现场问题。先看现象,不同报错代表不同层次的问题:
Host 'x.x.x.x' is not allowed to connect to this MySQL server
Can't connect to MySQL server on 'x.x.x.x:3306'
Access denied for user排查。第一组命令看网络和端口,第二组 SQL 看用户来源和授权:
nc -vz mysql-host 3306
ss -lntp | grep 3306
sudo firewall-cmd --list-allSELECT user, host, plugin, account_locked FROM mysql.user ORDER BY user, host;
SHOW GRANTS FOR 'app_user'@'10.10.20.%';修复思路:
- 网络不通,先处理安全组、防火墙、路由。
- MySQL 只监听 127.0.0.1,调整
bind-address。 - 用户 host 不匹配,创建正确来源的账号。
- 密码或认证插件不兼容,升级驱动或调整特定账号。
这里要注意,不要把 root 改成 % 来解决业务连接问题。业务连不上,应该创建正确来源的业务账号,而不是扩大管理员账号边界。
启动失败
启动失败先看日志和磁盘,再看配置。很多时候 error log 已经把原因写清楚了:
sudo systemctl status mysqld --no-pager
sudo journalctl -u mysqld -n 200 --no-pager
sudo tail -n 200 /data/mysql/log/error.log
df -h
ls -ld /data/mysql /data/mysql/data /data/mysql/log常见原因:
| 现象 | 原因 | 处理 |
|---|---|---|
| 配置项 unknown variable | 版本不支持或参数拼错 | 查官方文档,删改参数 |
| Permission denied | 目录 owner、SELinux、AppArmor | 修权限和安全上下文 |
| World-writable config ignored | 配置文件权限过宽 | chmod 640 |
| No space left on device | 数据盘、binlog、日志满 | 扩容、清理、迁移 |
| InnoDB crash | 非正常断电、文件损坏 | 备份、强制恢复、导出重建 |
磁盘满
磁盘满时不要慌着删文件,先判断哪里满。df 看挂载点,du 看目录占用:
df -h
du -xh /data/mysql | sort -h | tail -n 30
du -xh /var/lib/docker | sort -h | tail -n 30MySQL 内部看 binlog。binlog 要用 SQL 命令清理,不要到目录里手工 rm:
SHOW BINARY LOGS;
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';处理顺序:
- 如果是 Docker 日志,先按 Docker 日志策略处理。
- 如果是 slow log 或 error log,先备份压缩,再截断或轮转。
- 如果是 binlog,先确认备份和从库位点,再
PURGE BINARY LOGS。 - 如果是数据文件增长,不能直接删
.ibd,要做容量评估、扩盘或归档。
紧急扩容前先保留现场。后面复盘需要知道到底是谁增长得最快:
date
df -h
du -sh /data/mysql/*
mysql -uroot -p -e "SHOW BINARY LOGS;"连接池爆满
应用常见报错是获取连接超时,数据库侧看到连接数接近上限。先看当前连接、历史峰值和线程状态:
SHOW VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW FULL PROCESSLIST;按来源聚合后,先判断是哪类连接:
- 大量
Sleep:应用连接池过大、空闲连接释放慢、连接泄漏。 - 大量长时间
Query:慢 SQL、锁等待、大事务。 Aborted_connects高:密码错误、探活异常、网络抖动、账号被扫。
临时止血可以扩连接数,但要同步看内存:
SET GLOBAL max_connections = 500;根因要回到应用连接池、慢 SQL、锁等待和资源容量。不要只把 max_connections 改成几千就收工;连接数越大,数据库被压垮时也可能更快。
导入 SQL 很慢
导入慢先确认瓶颈,不要上来就改一堆参数。先看文件大小、INSERT 数量、容器资源和磁盘 IO:
ls -lh dump.sql
grep -c "INSERT INTO" dump.sql
docker stats mysql84
iostat -x 1MySQL 侧:
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'max_allowed_packet';临时导入优化可以这样做,但导完要恢复。下面这些是会话级设置,只影响当前连接:
SET SESSION foreign_key_checks=0;
SET SESSION unique_checks=0;
SET SESSION autocommit=0;导入:
mysql --max_allowed_packet=256M -uroot -p appdb < dump.sql导入后:
COMMIT;
SET SESSION unique_checks=1;
SET SESSION foreign_key_checks=1;如果为了导入临时调低 innodb_flush_log_at_trx_commit 或 sync_binlog,必须记录原值并恢复:
SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog');忘记 root 密码
这是高风险操作。先确认是否还有其他管理员账号,有的话优先用正常管理员账号处理:
SELECT user, host FROM mysql.user;确实没有时,按官方流程使用 --skip-grant-tables 临时启动。操作要点:
- 停止 MySQL。
- 使用跳过授权表方式临时启动。
- 本机连接后刷新授权表。
- 修改 root 密码。
- 停止临时实例,恢复正常启动。
- 验证登录,并检查是否留下异常账号。
示例命令要按发行版调整。这里要特别加 --skip-networking,避免跳过授权表期间被远程连接:
sudo systemctl stop mysqld
sudo mysqld_safe --skip-grant-tables --skip-networking &
mysql -urootFLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'Change_me_Use_Strong_Password_2026!';恢复:
sudo pkill mysqld
sudo systemctl start mysqld
mysql -uroot -p -e "SELECT USER(), VERSION();"最后确认能正常登录,再检查是否留下异常账号。--skip-networking 很重要,避免跳过授权表期间被远程连接。
InnoDB 损坏或反复崩溃
看到 error log 中出现 InnoDB assertion failure、tablespace corruption、page corruption,不要直接删除文件。这里的目标不是“修好原文件”,而是尽量把还能读的数据导出来,然后重建实例。
先保全现场:
sudo systemctl stop mysqld
tar -czf /backup/mysql/mysql-datadir-before-recovery-$(date +%F-%H%M%S).tar.gz /data/mysql/data再尝试按级别递增强制恢复。不要一上来就用 6,级别越高,限制越多,风险也越大:
[mysqld]
innodb_force_recovery=1启动后如果能读,立即导出:
mysqldump -uroot -p --all-databases --single-transaction --routines --triggers --events \
> /backup/mysql/all-databases-force-recovery.sql导出后重建实例再导入。强制恢复模式下不要执行业务写入。修复完成后必须删除 innodb_force_recovery,正常启动并校验:
CHECK TABLE appdb.important_table;如果是磁盘坏道、文件物理损坏、频繁断电,数据库拉起来只是第一步,还要检查宿主机、磁盘、文件系统和电源。
落地工程深水区
MySQL 上生产后,真正难的不是 mysqld 能启动,而是把数据安全、容量边界和故障恢复变成可验证的工程约束。
| 维度 | 运维要落到的动作 | 验证命令 |
|---|---|---|
| RPO/RTO | 明确全量备份、binlog 保留、异地副本和恢复耗时,别只写“每日备份” | SHOW BINARY LOG STATUS;、恢复演练计时 |
| 存储边界 | 数据目录、binlog、错误日志、备份目录分盘或分卷,避免一个目录写满拖垮实例 | df -h、du -sh /data/mysql/* |
| 连接治理 | 应用连接池上限、max_connections、只读账号、迁移账号分别评审 | SHOW GLOBAL STATUS LIKE 'Threads_connected'; |
| 复制治理 | 主从延迟、只读状态、复制账号权限、故障切换手册要同时存在 | SHOW REPLICA STATUS\G |
| 变更治理 | DDL、参数调整、版本升级要能回滚,不能把生产当参数试验场 | SHOW VARIABLES LIKE 'innodb%';、变更前后 diff |
一个可落地的恢复演练不要只停在“备份文件存在”。至少要按下面顺序跑一遍:
# 1. 记录备份文件和 binlog 范围
ls -lh /backup/mysql/
mysql -uroot -p -e "SHOW BINARY LOG STATUS;"
# 2. 在隔离实例恢复
mysql -uroot -p < /backup/mysql/appdb-full.sql
# 3. 校验业务关键表和账号权限
mysql -uroot -p -e "SELECT COUNT(*) FROM appdb.important_table;"
mysql -uroot -p -e "SHOW GRANTS FOR 'app_user'@'%';"
# 4. 记录耗时、缺失对象和人工步骤生产现场还要特别盯住三个边界:
- 备份账号只给备份所需权限,不把 root 写进脚本。
expire_logs_days或binlog_expire_logs_seconds要和恢复窗口匹配,不能因为磁盘紧张随手清 binlog。- 参数模板进入配置管理,线上临时
SET GLOBAL调整要在事后写回配置并做重启验证。
上线清单
发布前:
上线后:
故障时:
常用命令速查
# 服务
systemctl status mysqld --no-pager
journalctl -u mysqld -n 200 --no-pager
mysqladmin -uroot -p ping
# 端口
ss -lntp | grep 3306
nc -vz mysql-host 3306
# 容量
df -h
du -sh /data/mysql/*
# Docker
docker logs mysql84 --tail 200
docker exec -it mysql84 mysql -uroot -p
docker inspect mysql84 --format '{{json .Mounts}}'-- 版本和核心参数
SELECT VERSION();
SHOW VARIABLES LIKE 'datadir';
SHOW VARIABLES LIKE 'port';
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'time_zone';
-- 用户
SELECT user, host, plugin, account_locked FROM mysql.user ORDER BY user, host;
SHOW GRANTS FOR 'app_user'@'10.10.20.%';
-- 连接
SHOW VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW FULL PROCESSLIST;
-- 日志
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'log_bin';
SHOW BINARY LOGS;
SHOW BINARY LOG STATUS;
-- 复制
SHOW REPLICA STATUS\G
-- 容量
SELECT table_schema,
ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2) AS size_gb
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
GROUP BY table_schema
ORDER BY size_gb DESC;