跑数据库运维没几年,但我越来越确认一件事:装 MySQL 这件事,如果每次都靠手工敲命令,总有一天会因为一个参数写错而丢掉一个通宵。尤其到了 MySQL 8,初始化方式、认证插件、参数默认值都和 5.7 有差异,照着旧文档抄很容易翻车。后来我把自己负责的几十套 MySQL 8 环境全部收编成两套脚本:一套是生产可用的 MySQL8 一键安装脚本,另一套是一键巡检脚本。安装脚本负责把一台裸机变成标准化实例,巡检脚本负责每天凌晨把每套实例的底裤翻出来看一遍。今天这篇就把这两套脚本的设计思路、核心代码、参数计算方式、踩过的坑全部摊开讲,给同样被 MySQL 环境交付和日常巡检折腾过的 DBA、运维和全栈同学一点参考。1. 为什么我把安装和巡检都做成了脚本1.1 一次参数写错引发的通宵先说个真实经历。有一回给业务侧新交付一套 MySQL 8.0 实例,我自认为熟练,手速很快地把解压、初始化、改密码、配参数流程一路点完。结果第二天业务高峰,连接数直接被打满,数据库疯狂拒绝连接。上去一看,max_connections抄的是上一台小规格机器的 200,而这台机器内存是 64G,业务并发量也是按 64G 规格预估的。更尴尬的是,启动参数里我忘了加log_bin,导致那段时间主从复制根本搭不起来,所有数据只有本机一份,谁都不敢碰。凌晨两点在机房改参数、重启、补建复制,整个过程让我非常清醒地认识到:手工操作带来的不确定性,比机器本身的不确定性可怕得多。那次之后,我开始把 MySQL 8 的部署流程拆成标准化步骤,全部固化成脚本。安装脚本解决的是“每次装出来的实例都一样,不会因为手滑而埋雷”;巡检脚本解决的是“装完之后的每一天,不用等业务报障才知道数据库快不行了”。这两件事本质上是同一件事:让数据库环境变得可控、可预期。1.2 这两套脚本到底解决什么问题安装脚本解决的痛点非常具体:新环境交付时,把下载二进制包、创建系统用户、生成 my.cnf、初始化数据目录、配置 systemd、安全初始化这一长串手工步骤合并成一条命令。多实例部署时,不需要把整个安装流程重复操作 N 遍,传个不同的端口和目录就能再出一套实例。避免“这台的参数和那台不一样”这种玄学问题,所有实例共用一套基线配置。巡检脚本解决的问题更偏日常:每天自动检查实例存活、连接数水位、慢查询、主从延迟、锁等待、磁盘空间、binlog 积压这些关键指标。把数据库状态输出成一份带评分、带分级告警的报告,而不是靠人肉登录一台台看。给监控平台提供退出码和结构化结果,异常时第一时间被感知。这两套脚本不是我拍脑袋写的,而是在几十套生产环境上反复磨出来的。如果你的工作里也需要频繁交付 MySQL 8,或者你正在管理好几套 MySQL 实例却还没有一个像样的巡检机制,下面的内容应该对你有用。2. MySQL8 一键安装脚本:从下载到上线的完整链路2.1 脚本的输入参数与使用方式我不喜欢那种写死目录和密码的安装脚本,换个机器就得改半天。所以这个安装脚本把所有可变项都做成参数,默认值按照最常见的生产场景设置。bash install_mysql8.sh \ --port=3306 \ --data-dir=/data/mysql/mysql3306/data \ --base-dir=/usr/local/mysql \ --version=8.0.36 \ --memory=32G \ --character-set=utf8mb4核心参数如下:参数默认值说明--port3306实例监听端口,多实例场景改成 3307、3308 即可--data-dir/data/mysql/mysql$PORT/data数据目录,建议独立数据盘--base-dir/usr/local/mysql二进制安装目录,多实例共用一套--version8.0.36MySQL 小版本号,用于拼接下载地址--memory自动计算手动指定时按输入值计算 InnoDB buffer pool--character-setutf8mb4字符集,新项目一律 utf8mb4--root-password自动生成不传则随机生成并写入受限文件为什么不用 yum 或 apt 装?生产环境我更信任官方提供的 generic Linux 二进制包。用发行版仓库装,版本往往滞后,且编译参数、目录布局都是发行版定的,出问题后你很难对齐官方文档。用官方二进制包,解压出来拷到目标机器就能跑,离线部署也方便。安装脚本在执行前会做几项前置检查:当前系统用户是否为 root、目标端口是否被占用、数据目录是否已存在、磁盘剩余空间是否足够、物理内存是否能撑起指定的innodb_buffer_pool_size。这些检查全部放在下载和初始化之前,避免执行到一半才发现前置条件不满足。2.2 核心流程与幂等设计整个安装流程我拆成了 8 个阶段:参数解析与前置检查下载并校验 MySQL 二进制包创建 mysql 系统用户和目录结构生成 my.cnf 配置初始化数据目录注册 systemd 服务启动实例并做安全初始化输出实例摘要信息最需要注意的是幂等设计。所谓幂等,就是同一台机器上同一个脚本重复执行时,不会把已经存在的实例再初始化一遍,更不会把数据目录清空重建。我的处理原则是:如果$BASE_DIR/bin/mysqld已存在,跳过解压步骤。如果$DATA_DIR/mysql目录已存在(说明已经初始化过),跳过初始化步骤。如果 systemd 服务已存在且处于 active 状态,直接提示“实例已在运行”并退出,绝不重复启动。核心流程的伪代码结构大概是这样的:init_env() { # 创建 mysql 用户、数据目录,只有不存在时才创建 if ! id mysql &>/dev/null; then groupadd mysql useradd -M -s /bin/false mysql fi } prepare_binary() { if [ ! -x "${BASE_DIR}/bin/mysqld" ]; then tar -Jxf "${TEMP_DIR}/${TARBALL}" -C "${TEMP_DIR}/extract" --strip-components=1 mv "${TEMP_DIR}/extract/"* "${BASE_DIR}/" fi } init_datadir() { if [ ! -d "${DATA_DIR}/mysql" ]; then mkdir -p "${DATA_DIR}" chown -R mysql:mysql "${DATA_DIR}" su -s /bin/bash mysql -c "${BASE_DIR}/bin/mysqld \ --defaults-file=${CONF_FILE} \ --initialize-insecure \ --basedir=${BASE_DIR} --datadir=${DATA_DIR}" fi }为什么要用--initialize-insecure而不是默认的随机密码初始化?因为这步常常是自动化脚本翻车重灾区。默认初始化方式会把临时 root 密码写进错误日志,脚本要再去解析日志里的密码,很容易因为日志格式变化或者权限问题读不到。--initialize-insecure生成的是一个空密码 root 账号,启动后脚本立刻通过 socket 连接并ALTER USER设置新密码,整个流程完全可控。每个关键步骤都通过LOG()函数输出带时间戳的日志。生产环境需要审计,脚本日志统一落到/var/log/mysql_deploy.log,谁的服务器、什么时候装了什么实例,翻日志一目了然。2.3 参数自动计算与 my.cnf 模板既然是“生产可用”,参数就绝对不能是默认值裸奔。脚本里做了两件重要的事:第一,根据物理内存自动计算innodb_buffer_pool_size;第二,按 MySQL 8 的推荐实践生成一份基线 my.cnf。内存计算逻辑参考下面的公式:读取/proc/meminfo里的MemTotal,换算成 GB。innodb_buffer_pool_size = 物理内存 * 60%。如果计算结果超过 120G,封顶 120G,避免把内存全吃光。如果--memory参数显式指定,则以指定值为准。max_connections我建议单独传参覆盖,脚本默认按内存规模给基线值:内存小于 32G 给 200,32G 到 64G 给 300,超过 64G 给 500。这个值不是越大越好,连接数越大,线程栈、buffer pool 消耗都会上去,而且很多业务侧的坑反而是连接池把连接数拉满导致的。my.cnf 模板核心部分长这样:[mysqld] server-id = 23306 port = 3306 user = mysql basedir = /usr/local/mysql datadir = /data/mysql/mysql3306/data socket = /data/mysql/mysql3306/mysql.sock pid-file = /data/mysql/mysql3306/mysql.pid log-error = /data/mysql/mysql3306/error.log character-set-server = utf8mb4 collation-server = utf8mb4_0900_ai_ci # 复制与事务 server-id = 23306 binlog_format = ROW gtid_mode = ON enforce_gtid_consistency = ON log_bin = /data/mysql/mysql3306/binlog/mysql-bin binlog_expire_logs_seconds = 604800 max_binlog_size = 256M innodb_flush_log_at_trx_commit = 1 sync_binlog = 1 # InnoDB innodb_buffer_pool_size = 18432M innodb_buffer_pool_instances = 8 innodb_flush_method = O_DIRECT innodb_io_capacity = 1000 innodb_io_capacity_max = 4000 # 慢查询 slow_query_log = 1 slow_query_log_file = /data/mysql/mysql3306/slow.log long_query_time = 1 # 连接 max_connections = 300 max_connect_errors = 100000几个关键参数的选择逻辑:server-id我按端口 + 20000生成,保证同一套集群内不会碰撞。binlog_format=ROW是 MySQL 8 主从标配,虽然日志量比 STATEMENT 大,但数据一致性最好。gtid_mode=ON配上enforce_gtid_consistency=ON,后面做从库重搭、主从切换都会方便很多。binlog_expire_logs_seconds按秒设置,MySQL 8 已经废弃了expire_logs_days,这里用 604800 秒就是保留 7 天。innodb_flush_log_at_trx_commit=1和sync_binlog=1是双 1 配置,每次提交都刷盘,性能有一定代价,但生产环境的可靠性优先。long_query_time=1意味着超过 1 秒的 SQL 都会被记录下来。很多人觉得 1 秒太严格,但生产环境慢查询日志宁可多记不可漏记,后面可以按实际业务调整。2.4 安全基线:密码、权限与 systemd安装脚本最后一部分是安全初始化,这也是生产环境最关键的部分。默认安装完 MySQL 会有一个空密码 root、匿名用户、test 库,这些必须全部处理掉。我用 socket 连接 root 执行一段 SQL:ALTER USER 'root'@'localhost' IDENTIFIED BY '<生成的随机密码>'; DELETE FROM mysql.user WHERE User=''; DROP DATABASE IF EXISTS test; DELETE FROM mysql.db WHERE Db='test' OR Db='test\_%'; FLUSH PRIVILEGES;如果指定了业务账号,也会在这一步创建:CREATE USER 'app_user'@'%' IDENTIFIED BY '<独立密码>'; GRANT SELECT, INSERT, UPDATE, DELETE ON `appdb`.* TO 'app_user'@'%';这里特别提醒一句:root 账号只允许从 localhost 连接,业务账号权限按最小化原则给,绝不把整个实例的超级权限交给应用。随机生成的 root 密码会写入/etc/mysql/.mysql-secret,文件权限设为 600,只有 root 可读。systemd 服务配置也是标准化的一部分。生产环境必须让 MySQL 开机自启、异常退出自动拉起:[Unit] Description=MySQL 8.0 instance 3306 After=network.target [Service] Type=notify User=mysql Group=mysql ExecStart=/usr/local/mysql/bin/mysqld --defaults-file=/etc/mysql/mysql3306/my.cnf LimitNOFILE=65535 TimeoutStartSec=600 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.targetLimitNOFILE必须调大,MySQL 默认文件描述符限制在生产环境根本不够用。Type=notify是 MySQL 8 官方推荐的 systemd 模式,mysqld 会主动通知 systemd “我已经准备好接受连接了”,避免服务启动脚本误判启动失败。2.5 跑完一套安装的日志解读脚本正常跑完的输出大概是这个样子:[2025-01-12 23:10:01] [INFO] 检查系统环境:通过 [2025-01-12 23:10:01] [INFO] 检查端口 3306:未被占用 [2025-01-12 23:10:02] [INFO] 物理内存 32G,计算 innodb_buffer_pool_size=18432M [2025-01-12 23:10:05] [INFO] 下载 MySQL-8.0.36:通过 [2025-01-12 23:10:08] [INFO] 校验 SHA256:一致 [2025-01-12 23:10:20] [INFO] 解压二进制包:完成 [2025-01-12 23:10:21] [INFO] 创建 mysql 用户:完成 [2025-01-12 23:10:22] [INFO] 生成 my.cnf:完成 [2025-01-12 23:11:01] [INFO] 初始化数据目录:完成 [2025-01-12 23:11:02] [INFO] 注册 systemd 服务:完成 [2025-01-12 23:11:03] [INFO] 启动实例:完成 [2025-01-12 23:11:03] [INFO] 安全初始化:完成 [2025-01-12 23:11:04] [INFO] 实例 3306 安装成功看到最后一行“安装成功”不代表万事大吉,还要自己连上去确认一下变量和状态。我习惯在安装脚本跑完后补一句连接测试:mysql -uroot -p"${ROOT_PW}" -h127.0.0.1 -P3306 -e "SELECT VERSION();"能打出版本号,这台才算真正交付。3. 一键巡检脚本:不能只看进程还活着3.1 巡检项到底选哪些很多运维同学做巡检就是mysqladmin ping一下,能通就完事。但数据库这种东西,进程活着可比不上故意偷偷折磨人的。经常出现的情况是:实例 status 是 UP,但线程已经堆积、复制已经断了几小时、磁盘空间只剩 2%。所以巡检脚本必须覆盖到“实例还能不能承担业务”的层面。我把巡检项分成 6 大类:分类具体指标数据来源存活与基本状态连接数、uptime、版本、线程运行数SHOW GLOBAL STATUS性能关键指标InnoDB buffer pool 命中率、QPS、脏页比例SHOW GLOBAL STATUS复制健康io 线程、sql 线程、主从延迟、复制错误SHOW REPLICA STATUS事务与锁长事务、行锁等待、当前阻塞information_schema.innodb_trx容量风险磁盘使用率、inode 使用率、binlog 积压df、SHOW BINARY LOGS慢查询与错误慢查询数量、错误日志最近级别、Top 大表SLOW LOG、error log、information_schema.tables巡检项不能贪多,重点盯那些真正会让系统出问题的指标。比如Threads_connected高不代表危险,Threads_running高才是真正的危险信号;Innodb_row_lock_waits是累计值,连续两次差值大才说明锁冲突严重。3.2 登录凭据的安全处理巡检脚本需要直连 MySQL 拿状态,但不能用 root,更不能把 root 密码裸写在脚本里。我的做法是单独创建只读巡检账号:CREATE USER 'mysql_monitor'@'localhost' IDENTIFIED BY '<随机密码>'; GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'mysql_monitor'@'localhost'; GRANT SELECT ON performance_schema.* TO 'mysql_monitor'@'localhost'; GRANT SELECT ON sys.* TO 'mysql_monitor'@'localhost';PROCESS权限用来查SHOW PROCESSLIST和大部分状态变量,REPLICATION CLIENT用来查复制状态,performance_schema和sys的只读权限用来查锁等待和大表。这套权限足够巡检脚本用,但做不了任何破坏性操作。MySQL 客户端读取认证信息时,可以用--defaults-extra-file指定一个 600 权限的配置文件,避免密码出现在命令行进程列表中:[client] user=mysql_monitor password=xxx host=127.0.0.1 port=3306chmod 600 这个文件,设置属主为运行巡检脚本的账号,其他人都没有读权限。3.3 核心采集逻辑与关键命令巡检脚本用 MySQL 客户端连上实例后,会先取一次全量状态变量存到临时文件,后续指标从临时文件里解析,减少重复连接数据库的开销:mysql --defaults-file="${CNF_FILE}" -N -e "SHOW GLOBAL STATUS;" > "${TMP_STATUS}"然后从状态文件里取出关键值:threads_connected=$(grep -w 'Threads_connected' "${TMP_STATUS}" | awk '{print $2}') threads_running=$(grep -w 'Threads_running' "${TMP_STATUS}" | awk '{print $2}') max_connections=$(mysql --defaults-file="${CNF_FILE}" -N -e "SHOW VARIABLES LIKE 'max_connections';" | awk '{print $2}')复制状态这块需要特别注意 MySQL 版本差异。8.0.22 之前是SHOW SLAVE STATUS,之后改成了SHOW REPLICA STATUS。脚本里做兼容,先试新命令,失败再回退旧命令:set +e REPLICA_STATUS=$(mysql --defaults-file="${CNF_FILE}" -N -e "SHOW REPLICA STATUS\G" 2>/dev/null) if [ -z "${REPLICA_STATUS}" ]; then REPLICA_STATUS=$(mysql --defaults-file="${CNF_FILE}" -N -e "SHOW SLAVE STATUS\G" 2>/dev/null) fi set -e拿到输出后解析关键字段:IO_RUNNING=$(echo "${REPLICA_STATUS}" | awk -F': ' '/^(Replica_IO_Running|Slave_IO_Running):/{print $2}' | head -1) SQL_RUNNING=$(echo "${REPLICA_STATUS}" | awk -F': ' '/^(Replica_SQL_Running|Slave_SQL_Running):/{print $2}' | head -1) SECONDS_BEHIND=$(echo "${REPLICA_STATUS}" | awk -F': ' '/^(Seconds_Behind_Source|Seconds_Behind_Master):/{print $2}' | head -1)长事务检测用information_schema.innodb_trx直接扫,超过 30 秒的事务都值得关注:SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS running_seconds FROM information_schema.innodb_trx WHERE trx_started < NOW() - INTERVAL 30 SECOND ORDER BY running_seconds DESC LIMIT 20;锁等待我习惯看performance_schema.data_lock_waits,MySQL 8 里这个视图比老版本的INNODB_LOCK_WAITS更直接:SELECT COUNT(*) AS lock_wait_count FROM performance_schema.data_lock_waits;大表清单也放进巡检里,方便容量规划:SELECT table_schema, table_name, ROUND((data_length + index_length) / 1024 / 1024 / 1024, 2) AS size_gb, table_rows FROM information_schema.tables WHERE table_schema NOT IN ('mysql', 'sys', 'performance_schema', 'information_schema') ORDER BY (data_length + index_length) DESC LIMIT 10;3.4 报告输出、评分与监控集成巡检结果不能是一堆散落的数字,要能一眼看出问题。脚本会把采集到的所有指标整理成一份报告,并对每一项按危险程度打分。基本分级规则是:不满足阈值但是有风险,记WARN,扣 5 分。已经影响可用或者马上要出故障,记CRIT,扣 15 分。正常项记INFO,不扣分。总分 100 分,90 分以上算健康,70 到 90 算需要关注,70 以下就要拉响警报了。部分阈值参考:指标WARN 阈值CRIT 阈值连接数使用率大于 80%大于 95%磁盘使用率大于 70%大于 85%主从延迟大于 30 秒大于 300 秒InnoDB buffer pool 命中率小于 99%小于 95%长事务1 个以上超过 30 秒持续超过 10 分钟输出报告长这样:============================================================ MySQL 巡检报告 实例: 192.168.1.10:3306 巡检时间: 2025-01-13 02:00:00 ============================================================ 总体评分: 86 / 100,状态:需要关注 [WARN] 连接数使用率 93.5% (281/300) [INFO] InnoDB buffer pool 命中率 99.98% [INFO] 复制状态正常,延迟 0 秒 [CRIT] /data/mysql 磁盘使用率 87.2% [WARN] 存在 3 个超过 30 秒的长事务 [INFO] 慢查询数量较基线增加 18 次 ============================================================脚本退出码也按级别区分:0 表示正常,1 表示有 WARN,2 表示有 CRIT。这样接入监控平台时不用解析文本,直接看退出码即可。我这边每天晚上 3 点跑完巡检,如果退出码非 0,监控平台就会拉起一条告警,值班同学直接看报告定位问题。3.5 定时任务与告警迭代定时任务我放在 crontab 里,错开业务高峰和管理员休息时间:2 3 * * * /usr/local/bin/mysql_check.sh --port=3306 --report-dir=/var/log/mysql_reports --json >> /var/log/mysql_check_cron.log 2>&1巡检脚本同时支持多实例。生产环境往往是多端口多实例,可以为每个实例配一行 cron,也可以在脚本外做一个实例列表循环。我更喜欢后者:实例清单维护在一个/etc/mysql/instances.txt文件里,巡检脚本逐行读取并巡检,新增实例时只改清单,不用改 cron。关于告警有一条血泪经验:新脚本上线后的第一周,千万不要直接开邮件和短信告警。先把输出报告落盘,每天上班看一眼阈值有没有误报。等跑满一两周、阈值都验证过再接入告警渠道,否则你会被各种假告警骚扰到把告警当作噪音关掉。告警这种机制,宁可刚开始少一点,也不要让团队失去对它的信任。4. 真实环境里的使用记录4.1 一次安装日志实录我在一套生产用的订单库环境上重新跑了一次安装脚本,完整走一遍流程。服务器配置是 32G 内存、8 核 CPU,系统盘和数据盘分离,数据盘挂在/data。执行命令:bash install_mysql8.sh --port=3306 --data-dir=/data/mysql/mysql3306/data --memory=32G脚本先检查了端口、目录和磁盘空间,然后开始下载官方二进制包。这里要提醒一下,下载阶段非常依赖网络,如果机器不能直接访问官方 CDN,就得在脚本里预留一个镜像下载地址的变量,或者干脆预先把 tar 包放在本地目录。生产环境最好一次性先把安装包下载好,放到内网软件仓库,后续交付都走内网。初始化阶段值得关注的是数据目录权限。脚本里先mkdir -p并chown -R mysql:mysql,再切换到 mysql 用户执行初始化,很多手动初始化失败的案例都是因为忘了这一步,mysqld 报错Permission denied,错误日志里根本看不到实际情况。跑完后我特意验证了 root 密码文件/etc/mysql/.mysql-secret,权限确实是 600,只有 root 能读。然后我在业务侧建了一个只读账号和一个普通业务账号,分别录入权限最小化配置,确认应用可以通过 TCP 连接。整个安装到交付,脚本耗时不到 5 分钟,这还没算等待下载的时间。如果是手工装,光敲命令和等初始化就够喝一壶的。4.2 巡检发现了什么问题安装完成之后,我让巡检脚本每天凌晨跑,第三天就开始发现问题。第一眼看到的是磁盘使用率 WARN。数据目录所在分区用到了 78%,再细看巡检报告里的 binlog 信息,发现高峰期业务写入量很大,7 天的 binlog 积压下来占了将近 200G。原参数binlog_expire_logs_seconds=604800保留 7 天并没有错,但在这个业务模型下显然没有必要保留这么久,和业务确认恢复窗口后,我把保留时间调成了 3 天,也就是172800秒,磁盘使用率立刻降回安全水位。第二个问题是长事务。巡检脚本连续两天都报有超过 30 秒的事务,进去查information_schema.innodb_trx,发现是一个报表任务发起的事务,因为一条关联查询没有走索引,全表扫了 200 多万行,事务迟迟不提交。虽然还没造成锁阻塞,但已经让undo膨胀了不少。这个问题的价值在于,如果没有巡检,可能要等到它跟后续业务事务形成锁等待之后才会被发现。第三个问题是在慢查询 Top 榜单里看到的:某张订单流水表的COUNT(*)查询稳定排在慢日志前列。查了执行计划,发现这张表有 2.3 亿行,统计信息严重过时,导致优化器选择了错误索引。虽然跟巡检脚本本身没有直接关系,但它逼着我在巡检报告里增加了一项“Top 大表”信息,每次巡检都能盯着这些大表的容量变化。4.3 针对巡检结果做的参数调整巡检不是只看问题,还要驱动调优。针对那台订单库,我根据巡检报告做了三处调整:第一,innodb_buffer_pool_size从默认的 18G 调到了 40G。判断依据是巡检报告里 InnoDB buffer pool 命中率只有 97.8%,明显低于我常用的 99% 警戒线,说明热数据已经放不下了。机器有 128G 物理内存,调大 buffer pool 到 40G 不影响其他进程,命中率很快就回升到 99.9% 以上。第二,把long_query_time从 1 秒适当放宽到 1.5 秒。为什么放宽?因为巡检报告发现某些业务场景下 1 秒以上的查询虽然多,但执行计划正常、返回行数合理,属于业务本身复杂。如果不放宽,慢查询日志会越来越大,反而掩盖真实需要优化的慢查询。这里要强调的是,放宽阈值必须配合巡检的“慢查询数量偏离基线”提醒机制,不然退回盲区。第三,调了连接数。之前max_connections是 300,巡检发现峰值连接使用率稳定在 60% 左右,但连接池最大连接数设置存在放大效应,一有升级发布就会短时间冲到 85%。我不建议直接拉大max_connections,而是跟应用团队沟通把连接池上限压下来。这种做法比单纯调大数据库参数健康得多。参数调整要记住一个原则:临时调参和持久化必须同时做。比如动态改SET GLOBAL long_query_time=1.5之后,必须同步更新 my.cnf 里的对应值,否则 MySQL 一重启又回到旧参数,等于白调。脚本环境里我习惯于把特殊实例的覆盖参数放在独立的conf.d片段里,主模板保持统一,升级基线时不至于把特化配置冲掉。5. 常见问题与避坑经验5.1 问题速查表这两套脚本用下来,我整理了一张高频问题速查表,基本覆盖了我自己踩过或者同事踩过的大部分坑:现象常见原因处理方法mysqld: error while loading shared libraries: libaio.so.1系统缺 libaio 和 numactl 库Debian/Ubuntu 装libaio1 numactl,CentOS/RHEL 装libaio numactl-libs初始化报Permission denied数据目录属主不是 mysqlchown -R mysql:mysql $DATA_DIR安装后本地 root 登录不了root 默认 localhost,且密码文件没读到确认/etc/mysql/.mysql-secret内容和权限mysqld 启动后被 systemd 反复拉起数据目录不完整或 my.cnf 路径错看error.log,确认datadir里有mysql目录远程连接报Authentication plugin 'caching_sha2_password'客户端/驱动版本太老升级驱动到支持 MySQL 8;临时方案是建账号时指定mysql_native_password,但只建议过渡用巡检脚本连不上实例巡检账号权限不足或 .my.cnf 权限过大核对GRANT,确认配置文件 chmod 600复制状态一直显示 No主从复制账号没有起检查主库 binlog、从库START REPLICA,再看Last_IO_Errno磁盘使用率报警不停binlog 保留时间太长或 Xtrabackup 备份文件堆积确认binlog_expire_logs_seconds,清理过期备份慢查询日志里看不到预期 SQLlong_query_time只对普通语句生效,管理语句默认不记需要时开log_slow_admin_statements=ON单机多实例时端口冲突复制了旧 my.cnf 没改端口/socket每个实例独立配置目录,确认 port、socket、pid 不共用5.2 脚本迭代中的几条心得这套脚本不是一次写成的,是不断加需求、踩坑、改出来的。有几条心得写在这里,希望对正在做类似事情的人有帮助。第一,安装脚本写完之后,一定要在全新干净的系统上验证,最好用虚拟机或者容器起一个最小化系统跑一遍。最开始我在一个有历史 MySQL 环境的机器上测,很多问题被掩盖了,等换到干净机器立刻暴露了缺依赖、目录冲突的问题。干净环境能跑通,才能叫“一键”。第二,巡检脚本的阈值不能一次定死,要观察至少两个业务周期。不同业务的连接数、慢查询、主从延迟表现差异很大。比如一个 3D 渲染平台,凌晨任务一大堆,连接数常常很高,但这是正常规律;另一个偏 OLTP 的订单系统,凌晨要是连接数高,那就是异常。所以初版巡检脚本我会把阈值调得宽松一点,先收集一段时间的基线数据,再逐步收紧。第三,任何脚本改动都要走变更流程。这个听起来很重,但在生产环境里真的很重要。安装脚本一旦更新,可能影响之后所有新交付实例的初始化行为;巡检脚本一旦更新,可能改变告警判定逻辑。没有版本管理和变更记录,出了问题你都不知道是哪一版脚本干的。第四,巡检报告一定要留历史。我现在的脚本会把每次巡检的 JSON 报告按日期归档,然后每天跟昨天的报告做 diff。很多问题不是单看某一天的某一个指标就能发现的,比如Threads_running连续一周缓慢上升,这种趋势比单日告警更有预兆性。我看过太多“昨天还好好的,今天突然挂了”的故障,其实早就有迹可循,只是没人把前后的巡检数据拉出来对比。最后分享一个小技巧,也是我现在日常最依赖的一个动作:巡检脚本跑完后,把评分和各项关键指标写到一个固定的.txt文件里,每天早上用diff对比昨天的文件。新增出来的 WARN 往往比仓储里的汇总告警更容易激发人去查一次。数据库巡检这件事,怕的不是告警太多,而是告警太多之后没人看;让每一次变化都变得显眼,大家才会真的把巡检当回事。
跑数据库运维没几年,但我越来越确认一件事:装 MySQL 这件事,如果每次都靠手工敲命令,总有一天会因为一个参数写错而丢掉一个通宵。尤其到了 MySQL 8,初始化方式、认证插件、参数默认值都和 5.7 有差异,照着旧文档抄很容易翻车。后来我把自己负责的几十套 MySQL 8 环境全部收编成两套脚本:一套是生产可用的 MySQL8 一键安装脚本,另一套是一键巡检脚本。安装脚本负责把一台裸机变成标准化实例,巡检脚本负责每天凌晨把每套实例的底裤翻出来看一遍。今天这篇就把这两套脚本的设计思路、核心代码、参数计算方式、踩过的坑全部摊开讲,给同样被 MySQL 环境交付和日常巡检折腾过的 DBA、运维和全栈同学一点参考。1. 为什么我把安装和巡检都做成了脚本1.1 一次参数写错引发的通宵先说个真实经历。有一回给业务侧新交付一套 MySQL 8.0 实例,我自认为熟练,手速很快地把解压、初始化、改密码、配参数流程一路点完。结果第二天业务高峰,连接数直接被打满,数据库疯狂拒绝连接。上去一看,max_connections抄的是上一台小规格机器的 200,而这台机器内存是 64G,业务并发量也是按 64G 规格预估的。更尴尬的是,启动参数里我忘了加log_bin,导致那段时间主从复制根本搭不起来,所有数据只有本机一份,谁都不敢碰。凌晨两点在机房改参数、重启、补建复制,整个过程让我非常清醒地认识到:手工操作带来的不确定性,比机器本身的不确定性可怕得多。那次之后,我开始把 MySQL 8 的部署流程拆成标准化步骤,全部固化成脚本。安装脚本解决的是“每次装出来的实例都一样,不会因为手滑而埋雷”;巡检脚本解决的是“装完之后的每一天,不用等业务报障才知道数据库快不行了”。这两件事本质上是同一件事:让数据库环境变得可控、可预期。1.2 这两套脚本到底解决什么问题安装脚本解决的痛点非常具体:新环境交付时,把下载二进制包、创建系统用户、生成 my.cnf、初始化数据目录、配置 systemd、安全初始化这一长串手工步骤合并成一条命令。多实例部署时,不需要把整个安装流程重复操作 N 遍,传个不同的端口和目录就能再出一套实例。避免“这台的参数和那台不一样”这种玄学问题,所有实例共用一套基线配置。巡检脚本解决的问题更偏日常:每天自动检查实例存活、连接数水位、慢查询、主从延迟、锁等待、磁盘空间、binlog 积压这些关键指标。把数据库状态输出成一份带评分、带分级告警的报告,而不是靠人肉登录一台台看。给监控平台提供退出码和结构化结果,异常时第一时间被感知。这两套脚本不是我拍脑袋写的,而是在几十套生产环境上反复磨出来的。如果你的工作里也需要频繁交付 MySQL 8,或者你正在管理好几套 MySQL 实例却还没有一个像样的巡检机制,下面的内容应该对你有用。2. MySQL8 一键安装脚本:从下载到上线的完整链路2.1 脚本的输入参数与使用方式我不喜欢那种写死目录和密码的安装脚本,换个机器就得改半天。所以这个安装脚本把所有可变项都做成参数,默认值按照最常见的生产场景设置。bash install_mysql8.sh \ --port=3306 \ --data-dir=/data/mysql/mysql3306/data \ --base-dir=/usr/local/mysql \ --version=8.0.36 \ --memory=32G \ --character-set=utf8mb4核心参数如下:参数默认值说明--port3306实例监听端口,多实例场景改成 3307、3308 即可--data-dir/data/mysql/mysql$PORT/data数据目录,建议独立数据盘--base-dir/usr/local/mysql二进制安装目录,多实例共用一套--version8.0.36MySQL 小版本号,用于拼接下载地址--memory自动计算手动指定时按输入值计算 InnoDB buffer pool--character-setutf8mb4字符集,新项目一律 utf8mb4--root-password自动生成不传则随机生成并写入受限文件为什么不用 yum 或 apt 装?生产环境我更信任官方提供的 generic Linux 二进制包。用发行版仓库装,版本往往滞后,且编译参数、目录布局都是发行版定的,出问题后你很难对齐官方文档。用官方二进制包,解压出来拷到目标机器就能跑,离线部署也方便。安装脚本在执行前会做几项前置检查:当前系统用户是否为 root、目标端口是否被占用、数据目录是否已存在、磁盘剩余空间是否足够、物理内存是否能撑起指定的innodb_buffer_pool_size。这些检查全部放在下载和初始化之前,避免执行到一半才发现前置条件不满足。2.2 核心流程与幂等设计整个安装流程我拆成了 8 个阶段:参数解析与前置检查下载并校验 MySQL 二进制包创建 mysql 系统用户和目录结构生成 my.cnf 配置初始化数据目录注册 systemd 服务启动实例并做安全初始化输出实例摘要信息最需要注意的是幂等设计。所谓幂等,就是同一台机器上同一个脚本重复执行时,不会把已经存在的实例再初始化一遍,更不会把数据目录清空重建。我的处理原则是:如果$BASE_DIR/bin/mysqld已存在,跳过解压步骤。如果$DATA_DIR/mysql目录已存在(说明已经初始化过),跳过初始化步骤。如果 systemd 服务已存在且处于 active 状态,直接提示“实例已在运行”并退出,绝不重复启动。核心流程的伪代码结构大概是这样的:init_env() { # 创建 mysql 用户、数据目录,只有不存在时才创建 if ! id mysql &>/dev/null; then groupadd mysql useradd -M -s /bin/false mysql fi } prepare_binary() { if [ ! -x "${BASE_DIR}/bin/mysqld" ]; then tar -Jxf "${TEMP_DIR}/${TARBALL}" -C "${TEMP_DIR}/extract" --strip-components=1 mv "${TEMP_DIR}/extract/"* "${BASE_DIR}/" fi } init_datadir() { if [ ! -d "${DATA_DIR}/mysql" ]; then mkdir -p "${DATA_DIR}" chown -R mysql:mysql "${DATA_DIR}" su -s /bin/bash mysql -c "${BASE_DIR}/bin/mysqld \ --defaults-file=${CONF_FILE} \ --initialize-insecure \ --basedir=${BASE_DIR} --datadir=${DATA_DIR}" fi }为什么要用--initialize-insecure而不是默认的随机密码初始化?因为这步常常是自动化脚本翻车重灾区。默认初始化方式会把临时 root 密码写进错误日志,脚本要再去解析日志里的密码,很容易因为日志格式变化或者权限问题读不到。--initialize-insecure生成的是一个空密码 root 账号,启动后脚本立刻通过 socket 连接并ALTER USER设置新密码,整个流程完全可控。每个关键步骤都通过LOG()函数输出带时间戳的日志。生产环境需要审计,脚本日志统一落到/var/log/mysql_deploy.log,谁的服务器、什么时候装了什么实例,翻日志一目了然。2.3 参数自动计算与 my.cnf 模板既然是“生产可用”,参数就绝对不能是默认值裸奔。脚本里做了两件重要的事:第一,根据物理内存自动计算innodb_buffer_pool_size;第二,按 MySQL 8 的推荐实践生成一份基线 my.cnf。内存计算逻辑参考下面的公式:读取/proc/meminfo里的MemTotal,换算成 GB。innodb_buffer_pool_size = 物理内存 * 60%。如果计算结果超过 120G,封顶 120G,避免把内存全吃光。如果--memory参数显式指定,则以指定值为准。max_connections我建议单独传参覆盖,脚本默认按内存规模给基线值:内存小于 32G 给 200,32G 到 64G 给 300,超过 64G 给 500。这个值不是越大越好,连接数越大,线程栈、buffer pool 消耗都会上去,而且很多业务侧的坑反而是连接池把连接数拉满导致的。my.cnf 模板核心部分长这样:[mysqld] server-id = 23306 port = 3306 user = mysql basedir = /usr/local/mysql datadir = /data/mysql/mysql3306/data socket = /data/mysql/mysql3306/mysql.sock pid-file = /data/mysql/mysql3306/mysql.pid log-error = /data/mysql/mysql3306/error.log character-set-server = utf8mb4 collation-server = utf8mb4_0900_ai_ci # 复制与事务 server-id = 23306 binlog_format = ROW gtid_mode = ON enforce_gtid_consistency = ON log_bin = /data/mysql/mysql3306/binlog/mysql-bin binlog_expire_logs_seconds = 604800 max_binlog_size = 256M innodb_flush_log_at_trx_commit = 1 sync_binlog = 1 # InnoDB innodb_buffer_pool_size = 18432M innodb_buffer_pool_instances = 8 innodb_flush_method = O_DIRECT innodb_io_capacity = 1000 innodb_io_capacity_max = 4000 # 慢查询 slow_query_log = 1 slow_query_log_file = /data/mysql/mysql3306/slow.log long_query_time = 1 # 连接 max_connections = 300 max_connect_errors = 100000几个关键参数的选择逻辑:server-id我按端口 + 20000生成,保证同一套集群内不会碰撞。binlog_format=ROW是 MySQL 8 主从标配,虽然日志量比 STATEMENT 大,但数据一致性最好。gtid_mode=ON配上enforce_gtid_consistency=ON,后面做从库重搭、主从切换都会方便很多。binlog_expire_logs_seconds按秒设置,MySQL 8 已经废弃了expire_logs_days,这里用 604800 秒就是保留 7 天。innodb_flush_log_at_trx_commit=1和sync_binlog=1是双 1 配置,每次提交都刷盘,性能有一定代价,但生产环境的可靠性优先。long_query_time=1意味着超过 1 秒的 SQL 都会被记录下来。很多人觉得 1 秒太严格,但生产环境慢查询日志宁可多记不可漏记,后面可以按实际业务调整。2.4 安全基线:密码、权限与 systemd安装脚本最后一部分是安全初始化,这也是生产环境最关键的部分。默认安装完 MySQL 会有一个空密码 root、匿名用户、test 库,这些必须全部处理掉。我用 socket 连接 root 执行一段 SQL:ALTER USER 'root'@'localhost' IDENTIFIED BY '<生成的随机密码>'; DELETE FROM mysql.user WHERE User=''; DROP DATABASE IF EXISTS test; DELETE FROM mysql.db WHERE Db='test' OR Db='test\_%'; FLUSH PRIVILEGES;如果指定了业务账号,也会在这一步创建:CREATE USER 'app_user'@'%' IDENTIFIED BY '<独立密码>'; GRANT SELECT, INSERT, UPDATE, DELETE ON `appdb`.* TO 'app_user'@'%';这里特别提醒一句:root 账号只允许从 localhost 连接,业务账号权限按最小化原则给,绝不把整个实例的超级权限交给应用。随机生成的 root 密码会写入/etc/mysql/.mysql-secret,文件权限设为 600,只有 root 可读。systemd 服务配置也是标准化的一部分。生产环境必须让 MySQL 开机自启、异常退出自动拉起:[Unit] Description=MySQL 8.0 instance 3306 After=network.target [Service] Type=notify User=mysql Group=mysql ExecStart=/usr/local/mysql/bin/mysqld --defaults-file=/etc/mysql/mysql3306/my.cnf LimitNOFILE=65535 TimeoutStartSec=600 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.targetLimitNOFILE必须调大,MySQL 默认文件描述符限制在生产环境根本不够用。Type=notify是 MySQL 8 官方推荐的 systemd 模式,mysqld 会主动通知 systemd “我已经准备好接受连接了”,避免服务启动脚本误判启动失败。2.5 跑完一套安装的日志解读脚本正常跑完的输出大概是这个样子:[2025-01-12 23:10:01] [INFO] 检查系统环境:通过 [2025-01-12 23:10:01] [INFO] 检查端口 3306:未被占用 [2025-01-12 23:10:02] [INFO] 物理内存 32G,计算 innodb_buffer_pool_size=18432M [2025-01-12 23:10:05] [INFO] 下载 MySQL-8.0.36:通过 [2025-01-12 23:10:08] [INFO] 校验 SHA256:一致 [2025-01-12 23:10:20] [INFO] 解压二进制包:完成 [2025-01-12 23:10:21] [INFO] 创建 mysql 用户:完成 [2025-01-12 23:10:22] [INFO] 生成 my.cnf:完成 [2025-01-12 23:11:01] [INFO] 初始化数据目录:完成 [2025-01-12 23:11:02] [INFO] 注册 systemd 服务:完成 [2025-01-12 23:11:03] [INFO] 启动实例:完成 [2025-01-12 23:11:03] [INFO] 安全初始化:完成 [2025-01-12 23:11:04] [INFO] 实例 3306 安装成功看到最后一行“安装成功”不代表万事大吉,还要自己连上去确认一下变量和状态。我习惯在安装脚本跑完后补一句连接测试:mysql -uroot -p"${ROOT_PW}" -h127.0.0.1 -P3306 -e "SELECT VERSION();"能打出版本号,这台才算真正交付。3. 一键巡检脚本:不能只看进程还活着3.1 巡检项到底选哪些很多运维同学做巡检就是mysqladmin ping一下,能通就完事。但数据库这种东西,进程活着可比不上故意偷偷折磨人的。经常出现的情况是:实例 status 是 UP,但线程已经堆积、复制已经断了几小时、磁盘空间只剩 2%。所以巡检脚本必须覆盖到“实例还能不能承担业务”的层面。我把巡检项分成 6 大类:分类具体指标数据来源存活与基本状态连接数、uptime、版本、线程运行数SHOW GLOBAL STATUS性能关键指标InnoDB buffer pool 命中率、QPS、脏页比例SHOW GLOBAL STATUS复制健康io 线程、sql 线程、主从延迟、复制错误SHOW REPLICA STATUS事务与锁长事务、行锁等待、当前阻塞information_schema.innodb_trx容量风险磁盘使用率、inode 使用率、binlog 积压df、SHOW BINARY LOGS慢查询与错误慢查询数量、错误日志最近级别、Top 大表SLOW LOG、error log、information_schema.tables巡检项不能贪多,重点盯那些真正会让系统出问题的指标。比如Threads_connected高不代表危险,Threads_running高才是真正的危险信号;Innodb_row_lock_waits是累计值,连续两次差值大才说明锁冲突严重。3.2 登录凭据的安全处理巡检脚本需要直连 MySQL 拿状态,但不能用 root,更不能把 root 密码裸写在脚本里。我的做法是单独创建只读巡检账号:CREATE USER 'mysql_monitor'@'localhost' IDENTIFIED BY '<随机密码>'; GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'mysql_monitor'@'localhost'; GRANT SELECT ON performance_schema.* TO 'mysql_monitor'@'localhost'; GRANT SELECT ON sys.* TO 'mysql_monitor'@'localhost';PROCESS权限用来查SHOW PROCESSLIST和大部分状态变量,REPLICATION CLIENT用来查复制状态,performance_schema和sys的只读权限用来查锁等待和大表。这套权限足够巡检脚本用,但做不了任何破坏性操作。MySQL 客户端读取认证信息时,可以用--defaults-extra-file指定一个 600 权限的配置文件,避免密码出现在命令行进程列表中:[client] user=mysql_monitor password=xxx host=127.0.0.1 port=3306chmod 600 这个文件,设置属主为运行巡检脚本的账号,其他人都没有读权限。3.3 核心采集逻辑与关键命令巡检脚本用 MySQL 客户端连上实例后,会先取一次全量状态变量存到临时文件,后续指标从临时文件里解析,减少重复连接数据库的开销:mysql --defaults-file="${CNF_FILE}" -N -e "SHOW GLOBAL STATUS;" > "${TMP_STATUS}"然后从状态文件里取出关键值:threads_connected=$(grep -w 'Threads_connected' "${TMP_STATUS}" | awk '{print $2}') threads_running=$(grep -w 'Threads_running' "${TMP_STATUS}" | awk '{print $2}') max_connections=$(mysql --defaults-file="${CNF_FILE}" -N -e "SHOW VARIABLES LIKE 'max_connections';" | awk '{print $2}')复制状态这块需要特别注意 MySQL 版本差异。8.0.22 之前是SHOW SLAVE STATUS,之后改成了SHOW REPLICA STATUS。脚本里做兼容,先试新命令,失败再回退旧命令:set +e REPLICA_STATUS=$(mysql --defaults-file="${CNF_FILE}" -N -e "SHOW REPLICA STATUS\G" 2>/dev/null) if [ -z "${REPLICA_STATUS}" ]; then REPLICA_STATUS=$(mysql --defaults-file="${CNF_FILE}" -N -e "SHOW SLAVE STATUS\G" 2>/dev/null) fi set -e拿到输出后解析关键字段:IO_RUNNING=$(echo "${REPLICA_STATUS}" | awk -F': ' '/^(Replica_IO_Running|Slave_IO_Running):/{print $2}' | head -1) SQL_RUNNING=$(echo "${REPLICA_STATUS}" | awk -F': ' '/^(Replica_SQL_Running|Slave_SQL_Running):/{print $2}' | head -1) SECONDS_BEHIND=$(echo "${REPLICA_STATUS}" | awk -F': ' '/^(Seconds_Behind_Source|Seconds_Behind_Master):/{print $2}' | head -1)长事务检测用information_schema.innodb_trx直接扫,超过 30 秒的事务都值得关注:SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS running_seconds FROM information_schema.innodb_trx WHERE trx_started < NOW() - INTERVAL 30 SECOND ORDER BY running_seconds DESC LIMIT 20;锁等待我习惯看performance_schema.data_lock_waits,MySQL 8 里这个视图比老版本的INNODB_LOCK_WAITS更直接:SELECT COUNT(*) AS lock_wait_count FROM performance_schema.data_lock_waits;大表清单也放进巡检里,方便容量规划:SELECT table_schema, table_name, ROUND((data_length + index_length) / 1024 / 1024 / 1024, 2) AS size_gb, table_rows FROM information_schema.tables WHERE table_schema NOT IN ('mysql', 'sys', 'performance_schema', 'information_schema') ORDER BY (data_length + index_length) DESC LIMIT 10;3.4 报告输出、评分与监控集成巡检结果不能是一堆散落的数字,要能一眼看出问题。脚本会把采集到的所有指标整理成一份报告,并对每一项按危险程度打分。基本分级规则是:不满足阈值但是有风险,记WARN,扣 5 分。已经影响可用或者马上要出故障,记CRIT,扣 15 分。正常项记INFO,不扣分。总分 100 分,90 分以上算健康,70 到 90 算需要关注,70 以下就要拉响警报了。部分阈值参考:指标WARN 阈值CRIT 阈值连接数使用率大于 80%大于 95%磁盘使用率大于 70%大于 85%主从延迟大于 30 秒大于 300 秒InnoDB buffer pool 命中率小于 99%小于 95%长事务1 个以上超过 30 秒持续超过 10 分钟输出报告长这样:============================================================ MySQL 巡检报告 实例: 192.168.1.10:3306 巡检时间: 2025-01-13 02:00:00 ============================================================ 总体评分: 86 / 100,状态:需要关注 [WARN] 连接数使用率 93.5% (281/300) [INFO] InnoDB buffer pool 命中率 99.98% [INFO] 复制状态正常,延迟 0 秒 [CRIT] /data/mysql 磁盘使用率 87.2% [WARN] 存在 3 个超过 30 秒的长事务 [INFO] 慢查询数量较基线增加 18 次 ============================================================脚本退出码也按级别区分:0 表示正常,1 表示有 WARN,2 表示有 CRIT。这样接入监控平台时不用解析文本,直接看退出码即可。我这边每天晚上 3 点跑完巡检,如果退出码非 0,监控平台就会拉起一条告警,值班同学直接看报告定位问题。3.5 定时任务与告警迭代定时任务我放在 crontab 里,错开业务高峰和管理员休息时间:2 3 * * * /usr/local/bin/mysql_check.sh --port=3306 --report-dir=/var/log/mysql_reports --json >> /var/log/mysql_check_cron.log 2>&1巡检脚本同时支持多实例。生产环境往往是多端口多实例,可以为每个实例配一行 cron,也可以在脚本外做一个实例列表循环。我更喜欢后者:实例清单维护在一个/etc/mysql/instances.txt文件里,巡检脚本逐行读取并巡检,新增实例时只改清单,不用改 cron。关于告警有一条血泪经验:新脚本上线后的第一周,千万不要直接开邮件和短信告警。先把输出报告落盘,每天上班看一眼阈值有没有误报。等跑满一两周、阈值都验证过再接入告警渠道,否则你会被各种假告警骚扰到把告警当作噪音关掉。告警这种机制,宁可刚开始少一点,也不要让团队失去对它的信任。4. 真实环境里的使用记录4.1 一次安装日志实录我在一套生产用的订单库环境上重新跑了一次安装脚本,完整走一遍流程。服务器配置是 32G 内存、8 核 CPU,系统盘和数据盘分离,数据盘挂在/data。执行命令:bash install_mysql8.sh --port=3306 --data-dir=/data/mysql/mysql3306/data --memory=32G脚本先检查了端口、目录和磁盘空间,然后开始下载官方二进制包。这里要提醒一下,下载阶段非常依赖网络,如果机器不能直接访问官方 CDN,就得在脚本里预留一个镜像下载地址的变量,或者干脆预先把 tar 包放在本地目录。生产环境最好一次性先把安装包下载好,放到内网软件仓库,后续交付都走内网。初始化阶段值得关注的是数据目录权限。脚本里先mkdir -p并chown -R mysql:mysql,再切换到 mysql 用户执行初始化,很多手动初始化失败的案例都是因为忘了这一步,mysqld 报错Permission denied,错误日志里根本看不到实际情况。跑完后我特意验证了 root 密码文件/etc/mysql/.mysql-secret,权限确实是 600,只有 root 能读。然后我在业务侧建了一个只读账号和一个普通业务账号,分别录入权限最小化配置,确认应用可以通过 TCP 连接。整个安装到交付,脚本耗时不到 5 分钟,这还没算等待下载的时间。如果是手工装,光敲命令和等初始化就够喝一壶的。4.2 巡检发现了什么问题安装完成之后,我让巡检脚本每天凌晨跑,第三天就开始发现问题。第一眼看到的是磁盘使用率 WARN。数据目录所在分区用到了 78%,再细看巡检报告里的 binlog 信息,发现高峰期业务写入量很大,7 天的 binlog 积压下来占了将近 200G。原参数binlog_expire_logs_seconds=604800保留 7 天并没有错,但在这个业务模型下显然没有必要保留这么久,和业务确认恢复窗口后,我把保留时间调成了 3 天,也就是172800秒,磁盘使用率立刻降回安全水位。第二个问题是长事务。巡检脚本连续两天都报有超过 30 秒的事务,进去查information_schema.innodb_trx,发现是一个报表任务发起的事务,因为一条关联查询没有走索引,全表扫了 200 多万行,事务迟迟不提交。虽然还没造成锁阻塞,但已经让undo膨胀了不少。这个问题的价值在于,如果没有巡检,可能要等到它跟后续业务事务形成锁等待之后才会被发现。第三个问题是在慢查询 Top 榜单里看到的:某张订单流水表的COUNT(*)查询稳定排在慢日志前列。查了执行计划,发现这张表有 2.3 亿行,统计信息严重过时,导致优化器选择了错误索引。虽然跟巡检脚本本身没有直接关系,但它逼着我在巡检报告里增加了一项“Top 大表”信息,每次巡检都能盯着这些大表的容量变化。4.3 针对巡检结果做的参数调整巡检不是只看问题,还要驱动调优。针对那台订单库,我根据巡检报告做了三处调整:第一,innodb_buffer_pool_size从默认的 18G 调到了 40G。判断依据是巡检报告里 InnoDB buffer pool 命中率只有 97.8%,明显低于我常用的 99% 警戒线,说明热数据已经放不下了。机器有 128G 物理内存,调大 buffer pool 到 40G 不影响其他进程,命中率很快就回升到 99.9% 以上。第二,把long_query_time从 1 秒适当放宽到 1.5 秒。为什么放宽?因为巡检报告发现某些业务场景下 1 秒以上的查询虽然多,但执行计划正常、返回行数合理,属于业务本身复杂。如果不放宽,慢查询日志会越来越大,反而掩盖真实需要优化的慢查询。这里要强调的是,放宽阈值必须配合巡检的“慢查询数量偏离基线”提醒机制,不然退回盲区。第三,调了连接数。之前max_connections是 300,巡检发现峰值连接使用率稳定在 60% 左右,但连接池最大连接数设置存在放大效应,一有升级发布就会短时间冲到 85%。我不建议直接拉大max_connections,而是跟应用团队沟通把连接池上限压下来。这种做法比单纯调大数据库参数健康得多。参数调整要记住一个原则:临时调参和持久化必须同时做。比如动态改SET GLOBAL long_query_time=1.5之后,必须同步更新 my.cnf 里的对应值,否则 MySQL 一重启又回到旧参数,等于白调。脚本环境里我习惯于把特殊实例的覆盖参数放在独立的conf.d片段里,主模板保持统一,升级基线时不至于把特化配置冲掉。5. 常见问题与避坑经验5.1 问题速查表这两套脚本用下来,我整理了一张高频问题速查表,基本覆盖了我自己踩过或者同事踩过的大部分坑:现象常见原因处理方法mysqld: error while loading shared libraries: libaio.so.1系统缺 libaio 和 numactl 库Debian/Ubuntu 装libaio1 numactl,CentOS/RHEL 装libaio numactl-libs初始化报Permission denied数据目录属主不是 mysqlchown -R mysql:mysql $DATA_DIR安装后本地 root 登录不了root 默认 localhost,且密码文件没读到确认/etc/mysql/.mysql-secret内容和权限mysqld 启动后被 systemd 反复拉起数据目录不完整或 my.cnf 路径错看error.log,确认datadir里有mysql目录远程连接报Authentication plugin 'caching_sha2_password'客户端/驱动版本太老升级驱动到支持 MySQL 8;临时方案是建账号时指定mysql_native_password,但只建议过渡用巡检脚本连不上实例巡检账号权限不足或 .my.cnf 权限过大核对GRANT,确认配置文件 chmod 600复制状态一直显示 No主从复制账号没有起检查主库 binlog、从库START REPLICA,再看Last_IO_Errno磁盘使用率报警不停binlog 保留时间太长或 Xtrabackup 备份文件堆积确认binlog_expire_logs_seconds,清理过期备份慢查询日志里看不到预期 SQLlong_query_time只对普通语句生效,管理语句默认不记需要时开log_slow_admin_statements=ON单机多实例时端口冲突复制了旧 my.cnf 没改端口/socket每个实例独立配置目录,确认 port、socket、pid 不共用5.2 脚本迭代中的几条心得这套脚本不是一次写成的,是不断加需求、踩坑、改出来的。有几条心得写在这里,希望对正在做类似事情的人有帮助。第一,安装脚本写完之后,一定要在全新干净的系统上验证,最好用虚拟机或者容器起一个最小化系统跑一遍。最开始我在一个有历史 MySQL 环境的机器上测,很多问题被掩盖了,等换到干净机器立刻暴露了缺依赖、目录冲突的问题。干净环境能跑通,才能叫“一键”。第二,巡检脚本的阈值不能一次定死,要观察至少两个业务周期。不同业务的连接数、慢查询、主从延迟表现差异很大。比如一个 3D 渲染平台,凌晨任务一大堆,连接数常常很高,但这是正常规律;另一个偏 OLTP 的订单系统,凌晨要是连接数高,那就是异常。所以初版巡检脚本我会把阈值调得宽松一点,先收集一段时间的基线数据,再逐步收紧。第三,任何脚本改动都要走变更流程。这个听起来很重,但在生产环境里真的很重要。安装脚本一旦更新,可能影响之后所有新交付实例的初始化行为;巡检脚本一旦更新,可能改变告警判定逻辑。没有版本管理和变更记录,出了问题你都不知道是哪一版脚本干的。第四,巡检报告一定要留历史。我现在的脚本会把每次巡检的 JSON 报告按日期归档,然后每天跟昨天的报告做 diff。很多问题不是单看某一天的某一个指标就能发现的,比如Threads_running连续一周缓慢上升,这种趋势比单日告警更有预兆性。我看过太多“昨天还好好的,今天突然挂了”的故障,其实早就有迹可循,只是没人把前后的巡检数据拉出来对比。最后分享一个小技巧,也是我现在日常最依赖的一个动作:巡检脚本跑完后,把评分和各项关键指标写到一个固定的.txt文件里,每天早上用diff对比昨天的文件。新增出来的 WARN 往往比仓储里的汇总告警更容易激发人去查一次。数据库巡检这件事,怕的不是告警太多,而是告警太多之后没人看;让每一次变化都变得显眼,大家才会真的把巡检当回事。