目录
脚本介绍
目前本脚本用于在 Linux 主机上通过 MySQL 二进制 tar 包快速完成 MySQL 8.0 的安装部署,并配套基础环境优化、残留清理、日志记录和自动备份能力。脚本运行过程中会将输出同时打印到屏幕并写入日志文件,便于排查问题与留痕。
bash
[root@idss ~]# ./install_mysql8.sh -h
==============================================================================
[2026-01-21 10:12:55] [INFO] 安装日志文件:/var/log/mysql_install_idss_20260121_101255.log
==============================================================================
==============================================================================
[2026-01-21 10:12:55] [阶段] 环境检查
------------------------------------------------------------------------------
用法:
bash install_mysql8.sh -p /path/mysql-8.0.xx-linux-x86_64.tar.gz [选项]
必选参数:
-p, --pkg MySQL 二进制安装包路径(.tar.gz/.tar/.tar.xz)
可选参数:
-b, --basedir 安装目录(默认:/usr/local/mysql)
-D, --base-data 数据根目录(默认:/data/mysql)
-P, --port 端口(默认:3306)
-S, --server-id server-id(默认:6666)
-s, --socket socket(默认:/tmp/mysql.sock)
-B, --bind-address bind-address(默认:0.0.0.0)
-u, --mysql-user OS 用户(默认:mysql)
-g, --mysql-group OS 组(默认:mysql)
-r, --root-pass MySQL root 密码(默认:mysql)
my.cnf 参数:
--buffer-pool innodb_buffer_pool_size(如 4G);默认自动(内存 70%,上限 64G)
--max-connections 默认:3000
--lower-case-table-names 默认:1
--charset 默认:utf8mb4
--collation 默认:utf8mb4_0900_ai_ci
安装可选项:
--baseline-only 仅执行系统基线(不安装 MySQL;不要求 -p)
重要说明:
1) 若检测到 MySQL 残留(systemd unit / mysqld 进程 / 安装目录 / 数据目录 / socket / /run/mysql),将提示确认后清理
2) 安装完成后默认写入 /root/.my.cnf(免密登录)
3) 本脚本会执行系统基线变更:禁用防火墙/SELinux/SWAP,配置时区,写入 THP/NUMA/IO 调度器内核参数(通常需重启生效)
4) 仅支持 systemd 启动(非 systemd 直接退出)
5) 安装完成后自动创建 mysqldump 每日全备(排除系统库),压缩,保留 7 天(按批次目录清理)
示例:
bash install_mysql8.sh -p /root/mysql-8.0.35-linux-glibc2.12-x86_64.tar.gz
bash install_mysql8.sh --baseline-only
脚本主要能力
系统基线优化(默认执行):处理防火墙、SELinux、SWAP、时区、limits/file-max、GRUB 内核参数等。MySQL 安装与配置:解压安装包到指定目录,生成目录结构与 my.cnf,自动计算 innodb_buffer_pool_size(默认取物理内存 70%,上限 64G)。初始化与启动:执行初始化(--initialize)、生成 systemd unit、启动服务并做健康检查。root 密码处理:从错误日志解析临时密码并重置为指定密码,同时生成 /root/.my.cnf 方便免密登录。自动备份:生成 mysqldump 每日全备脚本(排除系统库、按库拆分、压缩、校验、保留 7 天)并写入 root crontab 定时执行。MySQL 残留检测与清理:检测旧 mysqld 服务/进程/目录/socket 等残留,交互确认后执行清理。
脚本执行步骤
环境检查:解析参数、检查依赖命令、识别操作系统信息。(可选)仅基线模式:--baseline-only 只执行系统基线,不安装MySQL。残留清理:发现 MySQL 残留则提示确认,确认后停止服务/杀进程/删除目录与 unit。安装前校验:检查 mysqld进程、端口占用、安装目录/数据目录是否仍存在残留。系统基线优化:按发行版执行防火墙/SELinux/SWAP/limits/时区/GRUB 等配置。安装部署:创建 mysql 用户/组、解压二进制包、写入 my.cnf。初始化与启动:初始化数据目录、生成 systemd 服务、启动并等待就绪。账号与免密:重置 root 密码并写 /root/.my.cnf。备份与定时任务:生成备份脚本并安装 crontab 定时任务。总结输出:打印关键路径、端口、socket、日志文件位置等信息。
常用用法
正常安装:
bash
bash install_mysql8.sh -p /path/mysql-8.0.xx-linux-x86_64.tar.gz
仅执行系统基线(不安装 MySQL):
bash
bash install_mysql8.sh --baseline-only
安装演示
脚本的执行过程:
bash
[root@idss ~]# bash install_mysql8.sh -p mysql-8.0.43-linux-glibc2.17-x86_64.tar.xz
==============================================================================
[2026-01-21 10:27:12] [INFO] 安装日志文件:/var/log/mysql_install_idss_20260121_102712.log
==============================================================================
==============================================================================
[2026-01-21 10:27:12] [阶段] 环境检查
------------------------------------------------------------------------------
[2026-01-21 10:27:12] [环境检查] 操作系统信息:
[2026-01-21 10:27:12] [环境检查] 系统名称 : CentOS Linux 7 (Core)
[2026-01-21 10:27:12] [环境检查] 系统ID : centos
[2026-01-21 10:27:12] [环境检查] 系统代号 : 7
[2026-01-21 10:27:12] [环境检查] CPU 架构 : x86_64
[2026-01-21 10:27:12] [环境检查] glibc minor : 17
[2026-01-21 10:27:12] [环境检查] 安装规划:
[2026-01-21 10:27:12] [环境检查] 安装目录 : /usr/local/mysql
[2026-01-21 10:27:12] [环境检查] 数据根目录 : /data/mysql
[2026-01-21 10:27:12] [环境检查] 端口 : 3306
[2026-01-21 10:27:12] [环境检查] server_id : 6666
[2026-01-21 10:27:12] [环境检查] bind-address : 0.0.0.0
[2026-01-21 10:27:12] [环境检查] socket : /tmp/mysql.sock
[2026-01-21 10:27:12] [环境检查] buffer_pool : 680M
[2026-01-21 10:27:12] [环境检查] 字符集 : utf8mb4
[2026-01-21 10:27:12] [环境检查] 排序规则 : utf8mb4_0900_ai_ci
------------------------------------------------------------------------------
[2026-01-21 10:27:12] [阶段] 环境检查(结束,耗时 0s)
==============================================================================
==============================================================================
[2026-01-21 10:27:12] [阶段] 残留清理
------------------------------------------------------------------------------
[2026-01-21 10:27:12] [残留清理] [警告] 检测到可能存在 MySQL 残留,即将进入交互确认清理。
------------------------------------------------------------------------------
[警告] 检测到可能存在 MySQL 残留(服务/进程/目录/运行文件)。
该操作将执行 rm -rf 删除以下路径:
- 安装目录(BASEDIR):/usr/local/mysql
- 数据目录(BASE_DATA):/data/mysql
并删除 systemd unit:mysqld.service / mysql.service(如存在)
同时删除 /root/.my.cnf(若存在)。
------------------------------------------------------------------------------
输入 'YES' 确认清理并继续: YES
[2026-01-21 10:28:12] [残留清理] 清理:检测信息汇总
[2026-01-21 10:28:12] [残留清理] unit_mysqld=是
[2026-01-21 10:28:13] [残留清理] unit_mysql=否
[2026-01-21 10:28:13] [残留清理] 进程=否
[2026-01-21 10:28:13] [残留清理] basedir=是
[2026-01-21 10:28:13] [残留清理] base_data=是
[2026-01-21 10:28:13] [残留清理] socket=否
[2026-01-21 10:28:13] [残留清理] /run/mysql=否
[2026-01-21 10:28:13] [残留清理] 清理:停止/禁用 mysqld.service
[2026-01-21 10:28:13] [残留清理] systemctl stop mysqld.service
[2026-01-21 10:28:13] [残留清理] systemctl disable mysqld.service
| Removed symlink /etc/systemd/system/multi-user.target.wants/mysqld.service.
[2026-01-21 10:28:13] [残留清理] systemctl reset-failed mysqld.service
| Failed to reset failed state of unit mysqld.service: Unit mysqld.service is not loaded.
[2026-01-21 10:28:13] [残留清理] systemctl daemon-reload
[2026-01-21 10:28:13] [残留清理] 清理:rm -rf /usr/local/mysql
[2026-01-21 10:28:13] [残留清理] 清理:rm -rf /data/mysql
[2026-01-21 10:28:14] [残留清理] 清理完成。
------------------------------------------------------------------------------
[2026-01-21 10:28:14] [阶段] 残留清理(结束,耗时 62s)
==============================================================================
==============================================================================
[2026-01-21 10:28:14] [阶段] 安装前校验
------------------------------------------------------------------------------
[2026-01-21 10:28:14] [安装前校验] 校验项:mysqld 进程 / 端口占用 / 目录残留
[2026-01-21 10:28:14] [安装前校验] 安装前校验:通过
------------------------------------------------------------------------------
[2026-01-21 10:28:14] [阶段] 安装前校验(结束,耗时 0s)
==============================================================================
==============================================================================
[2026-01-21 10:28:14] [阶段] 系统基线优化
------------------------------------------------------------------------------
[2026-01-21 10:28:14] [系统基线优化] 基线:禁用防火墙
[2026-01-21 10:28:14] [系统基线优化] 停止 firewalld
[2026-01-21 10:28:20] [系统基线优化] 禁用 firewalld
| Removed symlink /etc/systemd/system/multi-user.target.wants/firewalld.service.
| Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service.
[2026-01-21 10:28:20] [系统基线优化] firewalld 状态
| ● firewalld.service - firewalld - dynamic firewall daemon
| Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled)
| Active: inactive (dead)
| Docs: man:firewalld(1)
|
| Jan 16 22:43:57 idss systemd[1]: Starting firewalld - dynamic firewall daemon...
| Jan 16 22:43:58 idss systemd[1]: Started firewalld - dynamic firewall daemon.
| Jan 21 10:28:14 idss systemd[1]: Stopping firewalld - dynamic firewall daemon...
| Jan 21 10:28:20 idss systemd[1]: Stopped firewalld - dynamic firewall daemon.
[2026-01-21 10:28:20] [系统基线优化] 基线:禁用 SELinux(如存在)
[2026-01-21 10:28:20] [系统基线优化] 临时关闭 SELinux(setenforce 0)
[2026-01-21 10:28:20] [系统基线优化] SELinux 状态(sestatus)
| SELinux status: enabled
| SELinuxfs mount: /sys/fs/selinux
| SELinux root directory: /etc/selinux
| Loaded policy name: targeted
| Current mode: permissive
| Mode from config file: disabled
| Policy MLS status: enabled
| Policy deny_unknown status: allowed
| Max kernel policy version: 31
[2026-01-21 10:28:20] [系统基线优化] 基线:禁用 SWAP(会修改 /etc/fstab)
[2026-01-21 10:28:20] [系统基线优化] swapoff -a
[2026-01-21 10:28:23] [系统基线优化] 注释 /etc/fstab 中 swap 行
[2026-01-21 10:28:23] [系统基线优化] 内存与 SWAP 状态(free -h)
| total used free shared buff/cache available
| Mem: 972M 209M 102M 31M 660M 586M
| Swap: 0B 0B 0B
[2026-01-21 10:28:23] [系统基线优化] 基线:优化 nproc/nofile & fs.file-max
[2026-01-21 10:28:23] [系统基线优化] 基线:limits/file-max 写入完成
[2026-01-21 10:28:23] [系统基线优化] 基线:配置时区:Asia/Shanghai
[2026-01-21 10:28:23] [系统基线优化] 设置时区
[2026-01-21 10:28:23] [系统基线优化] 时区状态(timedatectl)
| Local time: Wed 2026-01-21 10:28:23 CST
| Universal time: Wed 2026-01-21 02:28:23 UTC
| RTC time: Wed 2026-01-21 02:28:21
| Time zone: Asia/Shanghai (CST, +0800)
| NTP enabled: n/a
| NTP synchronized: no
| RTC in local TZ: no
| DST active: n/a
[2026-01-21 10:28:23] [系统基线优化] 基线:写入 THP/NUMA/IO 调度器内核参数(通常需重启生效)
[2026-01-21 10:28:23] [系统基线优化] 目标参数:transparent_hugepage=never numa=off elevator=deadline
[2026-01-21 10:28:23] [系统基线优化] 写入内核参数(grubby):transparent_hugepage=never
[2026-01-21 10:28:23] [系统基线优化] 写入内核参数(grubby):numa=off
[2026-01-21 10:28:23] [系统基线优化] 写入内核参数(grubby):elevator=deadline
[2026-01-21 10:28:23] [系统基线优化] 当前内核参数(grubby info 摘要)
| args="ro crashkernel=auto spectre_v2=retpoline rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet LANG=en_US.UTF-8 transparent_hugepage=never numa=off elevator=deadline"
| args="ro crashkernel=auto spectre_v2=retpoline rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet transparent_hugepage=never numa=off elevator=deadline"
[2026-01-21 10:28:23] [系统基线优化] 系统基线优化:完成
------------------------------------------------------------------------------
[2026-01-21 10:28:23] [阶段] 系统基线优化(结束,耗时 9s)
==============================================================================
==============================================================================
[2026-01-21 10:28:23] [阶段] MySQL 安装部署
------------------------------------------------------------------------------
[2026-01-21 10:28:23] [MySQL 安装部署] 解压 MySQL 安装包 -> /usr/local/mysql
[2026-01-21 10:29:15] [MySQL 安装部署] 写入 my.cnf:/data/mysql/conf/my.cnf
[2026-01-21 10:29:15] [MySQL 安装部署] MySQL 安装部署:完成
------------------------------------------------------------------------------
[2026-01-21 10:29:15] [阶段] MySQL 安装部署(结束,耗时 52s)
==============================================================================
==============================================================================
[2026-01-21 10:29:15] [阶段] 初始化与启动
------------------------------------------------------------------------------
[2026-01-21 10:29:15] [初始化与启动] 初始化 MySQL(--initialize,会生成临时密码写入错误日志)
[2026-01-21 10:29:23] [初始化与启动] 写入 systemd unit:/etc/systemd/system/mysqld.service
[2026-01-21 10:29:23] [初始化与启动] systemctl daemon-reload
[2026-01-21 10:29:25] [初始化与启动] systemctl enable mysqld.service
| Created symlink from /etc/systemd/system/multi-user.target.wants/mysqld.service to /etc/systemd/system/mysqld.service.
[2026-01-21 10:29:25] [初始化与启动] 使用 systemd 启动 MySQL:mysqld.service
[2026-01-21 10:29:25] [初始化与启动] systemctl restart mysqld.service
[2026-01-21 10:29:27] [初始化与启动] 健康检查:等待 MySQL 就绪(最多 60 秒)
[2026-01-21 10:29:27] [初始化与启动] 重置 root 密码(使用 --connect-expired-password)
[2026-01-21 10:29:27] [初始化与启动] root 密码设置完成。
[2026-01-21 10:29:27] [初始化与启动] 写入 /root/.my.cnf(权限 600,root 免密登录)
[2026-01-21 10:29:27] [初始化与启动] 初始化与启动:完成
------------------------------------------------------------------------------
[2026-01-21 10:29:27] [阶段] 初始化与启动(结束,耗时 12s)
==============================================================================
==============================================================================
[2026-01-21 10:29:27] [阶段] 备份与定时任务
------------------------------------------------------------------------------
[2026-01-21 10:29:27] [备份与定时任务] 创建 mysqldump 全备脚本:/usr/local/bin/mysql_full_backup.sh
[2026-01-21 10:29:27] [备份与定时任务] 安装备份定时任务到 root crontab
[2026-01-21 10:29:27] [备份与定时任务] 备份任务已写入 root crontab:每天 02:00 执行全备,保留 7 天。
[2026-01-21 10:29:27] [备份与定时任务] 备份目录:/data/mysql/backup
[2026-01-21 10:29:27] [备份与定时任务] 备份脚本:/usr/local/bin/mysql_full_backup.sh
[2026-01-21 10:29:27] [备份与定时任务] 备份日志:/var/log/mysql_full_backup.log
[2026-01-21 10:29:27] [备份与定时任务] 备份与定时任务:完成
------------------------------------------------------------------------------
[2026-01-21 10:29:27] [阶段] 备份与定时任务(结束,耗时 0s)
==============================================================================
==============================================================================
[2026-01-21 10:29:27] [阶段] 总结输出
------------------------------------------------------------------------------
[2026-01-21 10:29:27] [总结输出] 安装总结:
[2026-01-21 10:29:27] [总结输出] 服务状态: systemctl status mysqld.service
[2026-01-21 10:29:27] [总结输出] 安装目录: /usr/local/mysql
[2026-01-21 10:29:27] [总结输出] 配置文件: /data/mysql/conf/my.cnf
[2026-01-21 10:29:27] [总结输出] 数据目录: /data/mysql/data
[2026-01-21 10:29:27] [总结输出] binlog目录: /data/mysql/binlog
[2026-01-21 10:29:27] [总结输出] 日志目录: /data/mysql/log
[2026-01-21 10:29:27] [总结输出] socket: /tmp/mysql.sock
[2026-01-21 10:29:27] [总结输出] 端口: 3306
[2026-01-21 10:29:27] [总结输出] root 密码: 1
[2026-01-21 10:29:27] [总结输出] 免密文件: /root/.my.cnf(已生成,权限 600)
[2026-01-21 10:29:27] [总结输出] 备份任务: 已写入 root crontab(每天 02:00,保留 7 天)
[2026-01-21 10:29:27] [总结输出] 备份目录: /data/mysql/backup
[2026-01-21 10:29:27] [总结输出] 本次安装日志文件:/var/log/mysql_install_idss_20260121_102712.log
提示:安装完成后建议重启主机,确保系统配置生效。
------------------------------------------------------------------------------
[2026-01-21 10:29:27] [阶段] 总结输出(结束,耗时 0s)
==============================================================================
安装完成后,测试免密登陆以及 mysqldump 备份:
bash
[root@idss ~]# mysql
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.43 MySQL Community Server - GPL
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
root@db 10:36: [(none)]> exit
Bye
[root@idss ~]# mysql_full_backup.sh
[2026-01-21 10:36:15] 开始备份批次:/data/mysql/backup/batch_idss_2026-01-21_103615
[2026-01-21 10:36:15] [警告] 未发现需要备份的用户库,退出。
[root@idss ~]#
脚本还自动配置了一个备份任务:
bash
[root@idss ~]# crontab -l
# >>> MySQL 每日全备任务(自动生成)开始 >>>
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
00 02 * * * /usr/local/bin/mysql_full_backup.sh >> /var/log/mysql_full_backup.log 2>&1
# <<< MySQL 每日全备任务(自动生成)结束 <<<
[root@idss ~]#
脚本支持重复多次执行,大概功能就是这样了,大家可以自行测试!
脚本内容
bash
#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# MySQL 8.0 一键安装脚本(Binary tar + systemd)
# 说明:
# - 默认执行系统基线:防火墙/SELinux/SWAP/时区/GRUB 内核参数(部分需重启生效)
# - 若检测到 MySQL 残留(服务/进程/目录/运行文件),会提示确认后清理(rm -rf)
# - 安装完成后:自动创建 mysqldump 每日全备任务(排除系统库),压缩、保留 7 天
###############################################################################
#==============================================================================
# 日志:同时输出到屏幕与文件(stdout/stderr 都记录)
#==============================================================================
LOG_DIR="/var/log"
LOG_FILE=""
# 统一宽度(尽量适配常见终端宽度)
LINE_WIDTH=78
ts() { date +'%F %T'; }
line() { printf '%*s\n' "${LINE_WIDTH}" '' | tr ' ' '='; }
subline() { printf '%*s\n' "${LINE_WIDTH}" '' | tr ' ' '-'; }
init_log() {
mkdir -p "${LOG_DIR}" >/dev/null 2>&1 || true
local host ts_now
host="$(hostname -s 2>/dev/null || hostname || echo unknown)"
ts_now="$(date +'%Y%m%d_%H%M%S')"
LOG_FILE="${LOG_DIR}/mysql_install_${host}_${ts_now}.log"
# stdout/stderr -> tee:屏幕+文件
exec > >(tee -ai "${LOG_FILE}") 2>&1
line
printf "[%s] [INFO] 安装日志文件:%s\n" "$(ts)" "${LOG_FILE}"
line
}
STAGE="INIT"
log() { echo "[$(ts)] [${STAGE}] $*"; }
warn() { echo "[$(ts)] [${STAGE}] [警告] $*" >&2; }
err() { echo "[$(ts)] [${STAGE}] [错误] $*" >&2; }
die() {
err "$*"
exit 1
}
# 执行命令并把 stdout/stderr 缩进展示(更美观)
run_cmd() {
# 用法:run_cmd "说明文字" command args...
local title="$1"
shift || true
log "${title}"
("$@" 2>&1 | sed 's/^/| /')
}
# 执行命令,但不因失败中断(用于 status/探测类)
run_cmd_soft() {
local title="$1"
shift || true
log "${title}"
("$@" 2>&1 | sed 's/^/| /') || true
}
_stage_start_ts=0
stage_begin() {
STAGE="$1"
_stage_start_ts="$(date +%s)"
echo
line
printf "[%s] [阶段] %s\n" "$(ts)" "${STAGE}"
subline
}
stage_end() {
local end_ts cost
end_ts="$(date +%s)"
cost=$((end_ts - _stage_start_ts))
subline
printf "[%s] [阶段] %s(结束,耗时 %ss)\n" "$(ts)" "${STAGE}" "${cost}"
line
echo
STAGE="MAIN"
}
# 统一键值输出:左侧固定宽度对齐,观感更像"报告"
kv() {
# 用法:kv "键" "值"
# 示例:kv "系统名称" "${pretty_name}"
printf "[%s] [%s] %-12s : %s\n" "$(ts)" "${STAGE}" "$1" "$2"
}
on_exit() {
local rc=$?
if ((rc != 0)); then
echo
line
printf "[%s] [FAIL] 脚本异常退出(exit code=%s)\n" "$(ts)" "${rc}"
printf "[%s] [FAIL] 请优先查看日志文件:%s\n" "$(ts)" "${LOG_FILE:-未初始化}"
if [[ -n "${LOG_FILE:-}" && -f "${LOG_FILE}" ]]; then
subline
printf "[%s] [FAIL] 最近 120 行日志(快速定位)\n" "$(ts)"
tail -n 120 "${LOG_FILE}" 2>/dev/null | sed 's/^/| /' || true
fi
line
fi
}
trap on_exit EXIT
#==============================================================================
# 全局默认参数
#==============================================================================
PKG=""
BASEDIR="/usr/local/mysql"
BASE_DATA="/data/mysql"
PORT="3306"
SERVER_ID="6666"
SOCKET="/tmp/mysql.sock"
BIND_ADDR="0.0.0.0"
MYSQL_USER="mysql"
MYSQL_GROUP="mysql"
ROOT_PASS="mysql"
BUFFER_POOL="" # 自动计算(物理内存 70%,上限 64G)
MAX_CONNECTIONS="3000"
LOWER_CASE_TABLE_NAMES="1"
CHARSET="utf8mb4"
COLLATION="utf8mb4_0900_ai_ci"
TIMEZONE_DEFAULT="Asia/Shanghai"
# 备份默认配置(mysqldump 每日全备)
BACKUP_DIR="/data/mysql/backup" # 备份根目录
BACKUP_SCRIPT="/usr/local/bin/mysql_full_backup.sh" # 备份执行脚本
BACKUP_LOG="/var/log/mysql_full_backup.log" # 备份日志
CRON_FILE="/etc/cron.d/mysql_full_backup" # 预留(本脚本使用 root crontab)
BACKUP_HOUR="02" # 每日备份小时
BACKUP_MIN="00" # 每日备份分钟
RETENTION_DAYS="7" # 备份保留天数(按批次目录清理)
BACKUP_CONCURRENCY_MAX="4" # 并行备份最大并发(按 CPU 自动取值,上限此值)
BACKUP_SPACE_MIN_GB="10" # 备份目录最小可用空间(GB),不足则失败退出
# 操作系统信息(detect_os 填充)
os_id="unknown"
os_major="0" # 使用 glibc minor 映射(沿用旧逻辑)
cpu_arch="$(uname -m)"
glibc_minor="0"
pretty_name="Unknown OS"
# 运行时变量
TMP_EXTRACT_DIR=""
BASELINE_ONLY=0
#==============================================================================
# 颜色输出(仅用于提示,不强依赖)
#==============================================================================
color() {
# 用法:color red|green|yellow|blue "文本"
local c="${1:-}"
shift || true
case "$c" in
red) echo -e "\033[31m$*\033[0m" ;;
green) echo -e "\033[32m$*\033[0m" ;;
yellow) echo -e "\033[33m$*\033[0m" ;;
blue) echo -e "\033[34m$*\033[0m" ;;
*) echo "$*" ;;
esac
}
#==============================================================================
# 用法 / 参数解析
#==============================================================================
usage() {
cat <<'EOF'
用法:
bash install_mysql8.sh -p /path/mysql-8.0.xx-linux-x86_64.tar.gz [选项]
必选参数:
-p, --pkg MySQL 二进制安装包路径(.tar.gz/.tar/.tar.xz)
可选参数:
-b, --basedir 安装目录(默认:/usr/local/mysql)
-D, --base-data 数据根目录(默认:/data/mysql)
-P, --port 端口(默认:3306)
-S, --server-id server-id(默认:6666)
-s, --socket socket(默认:/tmp/mysql.sock)
-B, --bind-address bind-address(默认:0.0.0.0)
-u, --mysql-user OS 用户(默认:mysql)
-g, --mysql-group OS 组(默认:mysql)
-r, --root-pass MySQL root 密码(默认:mysql)
my.cnf 参数:
--buffer-pool innodb_buffer_pool_size(如 4G);默认自动(内存 70%,上限 64G)
--max-connections 默认:3000
--lower-case-table-names 默认:1
--charset 默认:utf8mb4
--collation 默认:utf8mb4_0900_ai_ci
安装可选项:
--baseline-only 仅执行系统基线(不安装 MySQL;不要求 -p)
重要说明:
1) 若检测到 MySQL 残留(systemd unit / mysqld 进程 / 安装目录 / 数据目录 / socket / /run/mysql),将提示确认后清理
2) 安装完成后默认写入 /root/.my.cnf(免密登录)
3) 本脚本会执行系统基线变更:禁用防火墙/SELinux/SWAP,配置时区,写入 THP/NUMA/IO 调度器内核参数(通常需重启生效)
4) 仅支持 systemd 启动(非 systemd 直接退出)
5) 安装完成后自动创建 mysqldump 每日全备(排除系统库),压缩,保留 7 天(按批次目录清理)
示例:
bash install_mysql8.sh -p /root/mysql-8.0.35-linux-glibc2.12-x86_64.tar.gz
bash install_mysql8.sh --baseline-only
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-p | --pkg)
PKG="${2:-}"
shift 2
;;
-b | --basedir)
BASEDIR="${2:-}"
shift 2
;;
-D | --base-data)
BASE_DATA="${2:-}"
shift 2
;;
-P | --port)
PORT="${2:-}"
shift 2
;;
-S | --server-id)
SERVER_ID="${2:-}"
shift 2
;;
-s | --socket)
SOCKET="${2:-}"
shift 2
;;
-B | --bind-address)
BIND_ADDR="${2:-}"
shift 2
;;
-u | --mysql-user)
MYSQL_USER="${2:-}"
shift 2
;;
-g | --mysql-group)
MYSQL_GROUP="${2:-}"
shift 2
;;
-r | --root-pass)
ROOT_PASS="${2:-}"
shift 2
;;
--buffer-pool)
BUFFER_POOL="${2:-}"
shift 2
;;
--max-connections)
MAX_CONNECTIONS="${2:-}"
shift 2
;;
--lower-case-table-names)
LOWER_CASE_TABLE_NAMES="${2:-}"
shift 2
;;
--charset)
CHARSET="${2:-}"
shift 2
;;
--collation)
COLLATION="${2:-}"
shift 2
;;
--baseline-only)
BASELINE_ONLY=1
shift 1
;;
-h | --help)
usage
exit 0
;;
*)
color red "[错误] 未知参数:$1"
usage
exit 1
;;
esac
done
}
#==============================================================================
# 前置检查 / 通用工具函数
#==============================================================================
require_root() { [[ "$(id -u)" == "0" ]] || die "必须使用 root 执行"; }
# 判断是否为 systemd 系统(要求 systemctl 且存在 /run/systemd/system)
is_systemd() { command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; }
# 检查必需命令
require_cmds() {
local must=(tar awk grep sed cut tr head tail ps id getent hostname sysctl find mktemp wc rm mkdir chmod chown dirname uname ldd)
for c in "${must[@]}"; do
command -v "$c" >/dev/null 2>&1 || die "缺少命令:$c"
done
if ! command -v ss >/dev/null 2>&1 && ! command -v netstat >/dev/null 2>&1; then
warn "未检测到 ss/netstat,将无法进行端口占用检查。建议安装 iproute2 或 net-tools。"
fi
}
# 校验安装包
check_pkg() {
[[ -n "${PKG}" ]] || die "-p/--pkg 为必选参数"
[[ -f "${PKG}" ]] || die "安装包不存在:${PKG}"
case "${PKG}" in
*.tar | *.tar.gz | *.tar.xz) : ;;
*) die "不支持的安装包格式:${PKG}(需要 .tar/.tar.gz/.tar.xz)" ;;
esac
}
# 端口占用检测
port_in_use() {
local p="$1"
if command -v ss >/dev/null 2>&1; then
ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${p}$"
elif command -v netstat >/dev/null 2>&1; then
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${p}$"
else
return 1
fi
}
# 获取物理内存(字节)
detect_total_mem_bytes() { awk '/MemTotal/ {print $2*1024}' /proc/meminfo; }
# 自动计算 innodb_buffer_pool_size:内存 70%,上限 64G
auto_innodb_buffer_pool() {
local mem_bytes target cap mib
mem_bytes="$(detect_total_mem_bytes)"
target=$((mem_bytes * 70 / 100))
cap=$((64 * 1024 * 1024 * 1024))
((target > cap)) && target=$cap
mib=$((target / 1024 / 1024))
echo "${mib}M"
}
# 防误删保护(避免 rm -rf / 或空路径)
safe_path_guard() {
local p="$1"
[[ -n "$p" ]] || die "路径为空,拒绝操作"
[[ "$p" != "/" ]] || die "路径为 /,拒绝操作"
}
#==============================================================================
# 操作系统识别
#==============================================================================
detect_os() {
# 从 ldd --version 获取 glibc minor
glibc_minor="$(ldd --version 2>/dev/null | head -n 1 | awk '{print $NF}' | awk -F. '{print $2}' || true)"
[[ -n "${glibc_minor}" ]] || glibc_minor="0"
[[ "${glibc_minor}" =~ ^[0-9]+$ ]] || glibc_minor="0"
if [[ -f /etc/os-release ]]; then
os_id="$(grep -E '^ID=' /etc/os-release | head -n 1 | cut -d= -f2 | tr -d '"' || true)"
pretty_name="$(grep -E '^PRETTY_NAME=' /etc/os-release | head -n 1 | cut -d= -f2 | tr -d '"' || true)"
else
os_id="unknown"
pretty_name="$(cat /etc/system-release 2>/dev/null || cat /etc/redhat-release 2>/dev/null || echo "Unknown OS")"
fi
[[ -n "$os_id" ]] || os_id="unknown"
[[ -n "$pretty_name" ]] || pretty_name="Unknown OS"
# glibc minor -> os_major(旧逻辑)
if ((glibc_minor >= 12 && glibc_minor <= 16)); then
os_major="6"
elif ((glibc_minor >= 17 && glibc_minor <= 27)); then
os_major="7"
elif ((glibc_minor >= 28 && glibc_minor <= 33)); then
os_major="8"
elif ((glibc_minor >= 34 && glibc_minor <= 38)); then
os_major="9"
elif ((glibc_minor >= 39)); then
os_major="10"
else
os_major="0"
warn "当前系统 [${pretty_name}] 的 glibc minor=${glibc_minor} 不在脚本支持范围内。"
fi
log "操作系统信息:"
kv "系统名称" "${pretty_name}"
kv "系统ID" "${os_id}"
kv "系统代号" "${os_major}"
kv "CPU 架构" "${cpu_arch}"
kv "glibc minor" "${glibc_minor}"
}
#==============================================================================
# 基线:limits & fs.file-max(使用标记块,避免重复写入)
#==============================================================================
ensure_marked_block() {
# 参数:文件、开始标记、结束标记、内容(不包含标记)
local file="$1" begin="$2" end="$3" content="$4"
[[ -e "$file" ]] || touch "$file"
# 若标记存在:替换标记块内容;否则追加标记块
if grep -Fq "$begin" "$file" && grep -Fq "$end" "$file"; then
awk -v begin="$begin" -v end="$end" -v content="$content" '
BEGIN {inblk=0}
$0==begin {
print begin
print content
print end
inblk=1
next
}
$0==end && inblk==1 { inblk=0; next }
inblk==0 { print }
' "$file" >"${file}.tmp" && mv -f "${file}.tmp" "$file"
else
{
echo
echo "$begin"
echo "$content"
echo "$end"
} >>"$file"
fi
}
baseline_limits_and_filemax() {
log "基线:优化 nproc/nofile & fs.file-max"
# /etc/security/limits.conf
local limits_file="/etc/security/limits.conf"
local limits_begin="# >>> MySQL 基线(nproc/nofile)开始 >>>"
local limits_end="# <<< MySQL 基线(nproc/nofile)结束 <<<"
local limits_body
limits_body="$(
cat <<'EOF'
* soft nproc 65536
* hard nproc 65536
* soft nofile 65536
* hard nofile 65536
mysql soft nproc 65536
mysql hard nproc 65536
mysql soft nofile 65536
mysql hard nofile 65536
EOF
)"
ensure_marked_block "$limits_file" "$limits_begin" "$limits_end" "$limits_body"
# /etc/security/limits.d/* nproc
local nproc_body
nproc_body="$(
cat <<'EOF'
mysql soft nproc unlimited
EOF
)"
for f in /etc/security/limits.d/20-nproc.conf /etc/security/limits.d/90-nproc.conf; do
[[ -e "$f" ]] || continue
ensure_marked_block "$f" \
"# >>> MySQL 基线(nproc unlimited)开始 >>>" \
"# <<< MySQL 基线(nproc unlimited)结束 <<<" \
"$nproc_body"
done
# fs.file-max
local target=65535 current
if [[ -r /proc/sys/fs/file-max ]]; then
current="$(cat /proc/sys/fs/file-max 2>/dev/null || echo 0)"
current="${current:-0}"
if [[ "$current" =~ ^[0-9]+$ ]] && ((current < target)); then
if [[ -f /etc/sysctl.conf ]]; then
if grep -Eq '^\s*fs\.file-max\s*=' /etc/sysctl.conf; then
sed -ri "s/^\s*fs\.file-max\s*=.*/fs.file-max = ${target}/" /etc/sysctl.conf || true
else
echo "fs.file-max = ${target}" >>/etc/sysctl.conf
fi
/usr/sbin/sysctl -p >/dev/null 2>&1 || true
fi
fi
fi
log "基线:limits/file-max 写入完成"
}
#==============================================================================
# 基线:防火墙 / SELinux / SWAP / 时区 / GRUB 内核参数
#==============================================================================
baseline_disable_firewall() {
log "基线:禁用防火墙"
case "${os_id}" in
ubuntu | debian | deepin)
if command -v ufw >/dev/null 2>&1; then
run_cmd_soft "停止 ufw" ufw stop
run_cmd_soft "禁用 ufw" ufw disable
run_cmd_soft "ufw 状态" ufw status
else
log "未安装 ufw,跳过。"
fi
;;
sles | sled | opensuse* | suse)
# 旧 SUSE 可能使用 SuSEfirewall2
if ((os_major == 7)); then
if command -v service >/dev/null 2>&1; then
run_cmd_soft "停止 SuSEfirewall2" service SuSEfirewall2 stop
run_cmd_soft "关闭 SuSEfirewall2" SuSEfirewall2 off
run_cmd_soft "SuSEfirewall2 状态" service SuSEfirewall2 status
else
warn "未找到 service,跳过 SuSEfirewall2。"
fi
else
if is_systemd; then
run_cmd_soft "停止 firewalld" systemctl stop firewalld.service
run_cmd_soft "禁用 firewalld" systemctl disable firewalld.service
run_cmd_soft "firewalld 状态" systemctl status firewalld --no-pager -l
fi
fi
;;
*)
# RHEL6/7/8/9 等:分别处理 iptables 与 firewalld
if ((os_major == 6)); then
if command -v chkconfig >/dev/null 2>&1 && command -v service >/dev/null 2>&1; then
run_cmd_soft "关闭 iptables 开机自启" chkconfig iptables off
run_cmd_soft "停止 iptables" service iptables stop
run_cmd_soft "iptables 状态" service iptables status
else
warn "未找到 chkconfig/service,跳过 iptables。"
fi
else
if is_systemd; then
run_cmd_soft "停止 firewalld" systemctl stop firewalld.service
run_cmd_soft "禁用 firewalld" systemctl disable firewalld.service
run_cmd_soft "firewalld 状态" systemctl status firewalld --no-pager -l
else
warn "非 systemd,跳过 firewalld。"
fi
fi
;;
esac
}
baseline_disable_selinux() {
log "基线:禁用 SELinux(如存在)"
if ! command -v getenforce >/dev/null 2>&1; then
log "未检测到 getenforce,跳过。"
return 0
fi
if [[ "$(getenforce 2>/dev/null || true)" != "Disabled" ]]; then
run_cmd_soft "临时关闭 SELinux(setenforce 0)" setenforce 0
fi
if [[ -f /etc/selinux/config ]]; then
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config || true
sed -i 's/^SELINUX=permissive/SELINUX=disabled/g' /etc/selinux/config || true
fi
command -v sestatus >/dev/null 2>&1 && run_cmd_soft "SELinux 状态(sestatus)" sestatus
}
baseline_disable_swap() {
log "基线:禁用 SWAP(会修改 /etc/fstab)"
run_cmd_soft "swapoff -a" swapoff -a
if [[ -f /etc/fstab ]] && grep -Eq '^[^#].*\s+swap\s' /etc/fstab; then
run_cmd_soft "注释 /etc/fstab 中 swap 行" bash -c "sed -ri 's@^([^#].*\\s+swap\\s+.*)\$@#\\1@g' /etc/fstab"
fi
run_cmd_soft "内存与 SWAP 状态(free -h)" free -h
}
baseline_set_timezone() {
log "基线:配置时区:${TIMEZONE_DEFAULT}"
if command -v timedatectl >/dev/null 2>&1; then
run_cmd_soft "设置时区" timedatectl set-timezone "${TIMEZONE_DEFAULT}"
run_cmd_soft "时区状态(timedatectl)" bash -c 'timedatectl status | sed -n "1,12p"'
else
if [[ -f "/usr/share/zoneinfo/${TIMEZONE_DEFAULT}" ]]; then
run_cmd_soft "写入 /etc/localtime" ln -sf "/usr/share/zoneinfo/${TIMEZONE_DEFAULT}" /etc/localtime
fi
run_cmd_soft "当前时间(date)" date
fi
}
baseline_grub_tuning() {
log "基线:写入 THP/NUMA/IO 调度器内核参数(通常需重启生效)"
log "目标参数:transparent_hugepage=never numa=off elevator=deadline"
local args="transparent_hugepage=never numa=off elevator=deadline"
# RHEL 系:优先使用 grubby
if command -v grubby >/dev/null 2>&1; then
local opt
for opt in transparent_hugepage=never numa=off elevator=deadline; do
grubby --info=ALL 2>/dev/null | grep -q "$opt" && continue
run_cmd_soft "写入内核参数(grubby):${opt}" grubby --update-kernel=ALL --args="$opt"
done
run_cmd_soft "当前内核参数(grubby info 摘要)" bash -c 'grubby --info=ALL 2>/dev/null | awk "/args=|numa=off|transparent_hugepage=never|elevator=deadline/{print}"'
return 0
fi
# Debian/Ubuntu/SUSE:写 /etc/default/grub 并尝试生成 grub.cfg
if [[ -f /etc/default/grub ]]; then
if grep -q '^GRUB_CMDLINE_LINUX=' /etc/default/grub; then
if ! grep -q 'transparent_hugepage=never' /etc/default/grub; then
run_cmd_soft "写入 /etc/default/grub(追加内核参数)" bash -c "sed -ri 's@^(GRUB_CMDLINE_LINUX=\")([^\"]*)\"@\\1\\2 ${args}\"@' /etc/default/grub"
else
log "/etc/default/grub 已包含 transparent_hugepage=never,跳过追加。"
fi
else
echo "GRUB_CMDLINE_LINUX=\"${args}\"" >>/etc/default/grub
log "已追加 GRUB_CMDLINE_LINUX 到 /etc/default/grub"
fi
if command -v grub2-mkconfig >/dev/null 2>&1; then
run_cmd_soft "生成 grub.cfg(grub2-mkconfig)" grub2-mkconfig -o /boot/grub2/grub.cfg
elif command -v grub-mkconfig >/dev/null 2>&1; then
run_cmd_soft "生成 grub.cfg(grub-mkconfig)" grub-mkconfig -o /boot/grub/grub.cfg
else
warn "未找到 grub-mkconfig/grub2-mkconfig:已写 /etc/default/grub,但未生成 grub.cfg。"
fi
else
warn "未找到 grubby 或 /etc/default/grub,跳过 GRUB 调整。"
fi
}
apply_baseline() {
baseline_disable_firewall
baseline_disable_selinux
baseline_disable_swap
baseline_limits_and_filemax
baseline_set_timezone
baseline_grub_tuning
}
#==============================================================================
# MySQL 残留检测与清理(危险操作:rm -rf,强制二次确认)
#==============================================================================
systemd_unit_exists() {
local u="$1"
[[ -f "/etc/systemd/system/${u}" ]] && return 0
is_systemd || return 1
systemctl list-unit-files --type=service 2>/dev/null | awk '{print $1}' | grep -qx "${u}"
}
mysql_dirs_exist() { [[ -d "${BASEDIR}" || -d "${BASE_DATA}" ]]; }
mysql_runtime_exist() { [[ -S "${SOCKET}" || -f "${SOCKET}.lock" || -d /run/mysql ]]; }
mysql_residue_detected() {
systemd_unit_exists "mysqld.service" || systemd_unit_exists "mysql.service" ||
pgrep -x mysqld >/dev/null 2>&1 ||
mysql_dirs_exist ||
mysql_runtime_exist
}
confirm_cleanup() {
echo
subline
color yellow "[警告] 检测到可能存在 MySQL 残留(服务/进程/目录/运行文件)。"
echo "该操作将执行 rm -rf 删除以下路径:"
echo " - 安装目录(BASEDIR):${BASEDIR}"
echo " - 数据目录(BASE_DATA):${BASE_DATA}"
echo "并删除 systemd unit:mysqld.service / mysql.service(如存在)"
echo "同时删除 /root/.my.cnf(若存在)。"
subline
echo
read -r -p "输入 'YES' 确认清理并继续: " ans
[[ "${ans}" == "YES" ]]
}
stop_disable_unit() {
local u="$1"
is_systemd || return 0
if systemd_unit_exists "$u"; then
log "清理:停止/禁用 ${u}"
run_cmd_soft "systemctl stop ${u}" systemctl stop "$u"
run_cmd_soft "systemctl disable ${u}" systemctl disable "$u"
run_cmd_soft "systemctl reset-failed ${u}" systemctl reset-failed "$u"
fi
}
remove_unit_files() {
local u="$1"
local unit="/etc/systemd/system/${u}"
local dropin="/etc/systemd/system/${u}.d"
[[ -f "$unit" ]] && {
log "清理:删除 unit:$unit"
rm -f "$unit" || true
}
[[ -d "$dropin" ]] && {
log "清理:删除 drop-in:$dropin"
rm -rf "$dropin" || true
}
}
cleanup_mysql_residue_if_needed() {
mysql_residue_detected || {
log "未检测到 MySQL 残留,跳过清理。"
return 0
}
warn "检测到可能存在 MySQL 残留,即将进入交互确认清理。"
confirm_cleanup || die "用户取消清理"
log "清理:检测信息汇总"
log " unit_mysqld=$(systemd_unit_exists mysqld.service && echo 是 || echo 否)"
log " unit_mysql=$(systemd_unit_exists mysql.service && echo 是 || echo 否)"
log " 进程=$(pgrep -x mysqld >/dev/null 2>&1 && echo 是 || echo 否)"
log " basedir=$([[ -d "${BASEDIR}" ]] && echo 是 || echo 否)"
log " base_data=$([[ -d "${BASE_DATA}" ]] && echo 是 || echo 否)"
log " socket=$([[ -S "${SOCKET}" ]] && echo 是 || echo 否)"
log " /run/mysql=$([[ -d /run/mysql ]] && echo 是 || echo 否)"
set +e
stop_disable_unit "mysqld.service"
stop_disable_unit "mysql.service"
if pgrep -x mysqld >/dev/null 2>&1; then
log "清理:终止 mysqld 进程"
pkill -TERM mysqld >/dev/null 2>&1 || true
sleep 2
pgrep -x mysqld >/dev/null 2>&1 && pkill -KILL mysqld >/dev/null 2>&1 || true
fi
if is_systemd; then
remove_unit_files "mysqld.service"
remove_unit_files "mysql.service"
run_cmd_soft "systemctl daemon-reload" systemctl daemon-reload
fi
rm -f "${SOCKET}" "${SOCKET}.lock" >/dev/null 2>&1 || true
rm -rf /run/mysql >/dev/null 2>&1 || true
if [[ -f /root/.my.cnf ]]; then
log "清理:删除 /root/.my.cnf"
rm -f /root/.my.cnf || true
fi
safe_path_guard "${BASEDIR}"
safe_path_guard "${BASE_DATA}"
[[ -d "${BASEDIR}" ]] && {
log "清理:rm -rf ${BASEDIR}"
rm -rf "${BASEDIR}" || true
}
[[ -d "${BASE_DATA}" ]] && {
log "清理:rm -rf ${BASE_DATA}"
rm -rf "${BASE_DATA}" || true
}
set -e
if mysql_dirs_exist || mysql_runtime_exist; then
warn "清理结束,但仍存在残留(可能被挂载/占用/只读)。请手工检查。"
else
log "清理完成。"
fi
}
#==============================================================================
# MySQL 安装步骤
#==============================================================================
create_mysql_user_group() {
if ! getent group "${MYSQL_GROUP}" >/dev/null 2>&1; then
log "创建用户组:${MYSQL_GROUP}"
groupadd "${MYSQL_GROUP}"
fi
if ! id "${MYSQL_USER}" >/dev/null 2>&1; then
log "创建用户:${MYSQL_USER}"
useradd -r -g "${MYSQL_GROUP}" -s /bin/false "${MYSQL_USER}"
fi
}
ensure_socket_dir() {
local parent
parent="$(dirname "$SOCKET")"
mkdir -p "$parent"
[[ "$parent" == "/tmp" ]] && chmod 1777 /tmp || true
}
extract_mysql_binary() {
log "解压 MySQL 安装包 -> ${BASEDIR}"
TMP_EXTRACT_DIR="$(mktemp -d)"
trap '[[ -n "${TMP_EXTRACT_DIR}" && -d "${TMP_EXTRACT_DIR}" ]] && rm -rf "${TMP_EXTRACT_DIR}"' EXIT
if [[ "${PKG}" == *.tar.xz ]]; then
tar -xJf "${PKG}" -C "${TMP_EXTRACT_DIR}"
else
tar -xf "${PKG}" -C "${TMP_EXTRACT_DIR}"
fi
local extracted
extracted="$(find "${TMP_EXTRACT_DIR}" -maxdepth 1 -mindepth 1 -type d | head -n 1)"
[[ -n "${extracted}" ]] || die "未能定位解压目录(解压失败?)"
mv "${extracted}" "${BASEDIR}"
chown -R "${MYSQL_USER}:${MYSQL_GROUP}" "${BASEDIR}"
}
write_my_cnf() {
local conf_dir="${BASE_DATA}/conf"
local data_dir="${BASE_DATA}/data"
local binlog_dir="${BASE_DATA}/binlog"
local log_dir="${BASE_DATA}/log"
local tmp_dir="${BASE_DATA}/tmpdir"
mkdir -p "${conf_dir}" "${data_dir}" "${binlog_dir}" "${log_dir}" "${tmp_dir}"
chown -R "${MYSQL_USER}:${MYSQL_GROUP}" "${BASE_DATA}"
local mycnf="${conf_dir}/my.cnf"
local errlog="${log_dir}/mysql.err"
touch "${errlog}"
chown "${MYSQL_USER}:${MYSQL_GROUP}" "${errlog}"
log "写入 my.cnf:${mycnf}"
cat >"${mycnf}" <<EOF
[client]
port = ${PORT}
socket = ${SOCKET}
default-character-set = ${CHARSET}
[mysqld]
user = ${MYSQL_USER}
port = ${PORT}
bind-address = ${BIND_ADDR}
socket = ${SOCKET}
basedir = ${BASEDIR}
datadir = ${data_dir}
tmpdir = ${tmp_dir}
log-error = ${errlog}
log-bin = ${binlog_dir}/mysql-bin
log_bin_index = ${binlog_dir}/mysql-bin.index
relay-log = ${binlog_dir}/mysql-relay-bin
slow_query_log = 1
slow_query_log_file = ${log_dir}/mysql-slow.log
long_query_time = 1
log_slow_admin_statements = 1
log_queries_not_using_indexes = 0
server-id = ${SERVER_ID}
binlog_format = ROW
log-slave-updates = 1
sync_binlog = 1
expire_logs_days = 30
skip-name-resolve = ON
local-infile = OFF
character-set-server = ${CHARSET}
collation-server = ${COLLATION}
explicit_defaults_for_timestamp = ON
default_password_lifetime = 0
sql_mode = STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
max_connections = ${MAX_CONNECTIONS}
max_allowed_packet = 32M
max_connect_errors = 1000
wait_timeout = 3600
interactive_timeout = 3600
table_open_cache = 4096
table_open_cache_instances = 16
thread_cache_size = 64
default-storage-engine = InnoDB
innodb_buffer_pool_size = ${BUFFER_POOL}
innodb_buffer_pool_instances = 1
innodb_flush_log_at_trx_commit = 2
innodb_log_files_in_group = 3
innodb_log_file_size = 2G
innodb_log_buffer_size = 16M
innodb_file_per_table = 1
innodb_flush_method = O_DIRECT
innodb_io_capacity = 1000
innodb_io_capacity_max = 2000
innodb_read_io_threads = 8
innodb_write_io_threads = 4
innodb_lock_wait_timeout = 30
innodb_print_all_deadlocks = 1
gtid_mode = ON
enforce_gtid_consistency = ON
lower_case_table_names = ${LOWER_CASE_TABLE_NAMES}
mysqlx = 0
[mysqldump]
quick
max_allowed_packet = 32M
[mysql]
default-character-set = utf8mb4
prompt="\\u@db \\R:\\m:\\s [\\d]> "
auto-rehash
auto-vertical-output
disable-tee
[mysqld_safe]
open-files-limit = 1048576
EOF
chown "${MYSQL_USER}:${MYSQL_GROUP}" "${mycnf}"
}
write_systemd_unit() {
local mycnf="${BASE_DATA}/conf/my.cnf"
local unit="/etc/systemd/system/mysqld.service"
local runtime_dir="/run/mysql"
mkdir -p "${runtime_dir}"
chown -R "${MYSQL_USER}:${MYSQL_GROUP}" "${runtime_dir}"
log "写入 systemd unit:${unit}"
cat >"${unit}" <<EOF
[Unit]
Description=MySQL Server
After=network.target
After=syslog.target
[Install]
WantedBy=multi-user.target
[Service]
User=${MYSQL_USER}
Group=${MYSQL_GROUP}
Type=notify
TimeoutSec=0
PermissionsStartOnly=true
ExecStart=${BASEDIR}/bin/mysqld --defaults-file=${mycnf}
ExecStop=${BASEDIR}/bin/mysqladmin --defaults-file=${mycnf} shutdown
LimitNOFILE=65535
Restart=on-failure
RestartPreventExitStatus=1
PrivateTmp=false
EOF
run_cmd_soft "systemctl daemon-reload" systemctl daemon-reload
run_cmd_soft "systemctl enable mysqld.service" systemctl enable mysqld.service
}
wait_mysql_ready() {
local mycnf="${BASE_DATA}/conf/my.cnf"
local admin="${BASEDIR}/bin/mysqladmin"
local i
for i in {1..60}; do
if [[ -S "${SOCKET}" ]] && "${admin}" --defaults-file="${mycnf}" ping >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
initialize_mysql() {
local mycnf="${BASE_DATA}/conf/my.cnf"
log "初始化 MySQL(--initialize,会生成临时密码写入错误日志)"
"${BASEDIR}/bin/mysqld" --defaults-file="${mycnf}" --user="${MYSQL_USER}" --initialize
}
start_mysql_systemd() {
log "使用 systemd 启动 MySQL:mysqld.service"
run_cmd_soft "systemctl restart mysqld.service" systemctl restart mysqld.service
}
reset_root_password_if_possible() {
local mycnf="${BASE_DATA}/conf/my.cnf"
local errlog="${BASE_DATA}/log/mysql.err"
if ! grep -qi "temporary password" "${errlog}"; then
warn "未在 ${errlog} 发现临时密码记录。可能数据目录复用或初始化方式不同;请手工确认 root 密码。"
return 0
fi
local temp_pwd
temp_pwd="$(grep -i 'temporary password' "${errlog}" | tail -n 1 | awk '{print $NF}')"
[[ -n "${temp_pwd}" ]] || {
warn "解析临时密码失败。"
return 0
}
log "重置 root 密码(使用 --connect-expired-password)"
MYSQL_PWD="${temp_pwd}" \
"${BASEDIR}/bin/mysql" --defaults-file="${mycnf}" -uroot --connect-expired-password \
-e "ALTER USER USER() IDENTIFIED BY '${ROOT_PASS}';"
unset MYSQL_PWD
log "root 密码设置完成。"
}
write_root_my_cnf() {
local f="/root/.my.cnf"
log "写入 /root/.my.cnf(权限 600,root 免密登录)"
umask 077
cat >"${f}" <<EOF
[client]
user=root
password=${ROOT_PASS}
socket=${SOCKET}
default-character-set=${CHARSET}
[mysql]
default-character-set = utf8mb4
prompt="\\u@db \\R:\\m:\\s [\\d]> "
auto-rehash
auto-vertical-output
disable-tee
EOF
chmod 600 "${f}"
}
#==============================================================================
# 备份:生成备份脚本 + 安装定时任务(升级版:按库拆分、并行、元数据清单、校验值)
#==============================================================================
write_backup_script() {
log "创建 mysqldump 全备脚本:${BACKUP_SCRIPT}"
mkdir -p "${BACKUP_DIR}"
chmod 750 "${BACKUP_DIR}" || true
touch "${BACKUP_LOG}" || true
chmod 640 "${BACKUP_LOG}" || true
# 第一段:写入"安装脚本注入的常量"(允许变量展开)
cat >"${BACKUP_SCRIPT}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
###############################################################################
# MySQL 每日全备脚本(mysqldump)
# 功能:
# - 备份除系统库之外的所有数据库(排除:mysql/sys/performance_schema/information_schema)
# - 按库拆分文件:每个库一个 .sql.gz,便于恢复与定位问题
# - 并行备份:根据 CPU 核心数自动选择并发(上限 ${BACKUP_CONCURRENCY_MAX})
# - 生成元数据:记录 binlog 文件/位置、GTID、版本、server_uuid、校验值等
# - 保留策略:按"批次目录"保留 ${RETENTION_DAYS} 天,超期整批删除
# 依赖:
# - /root/.my.cnf(由安装脚本生成,免密连接 MySQL)
###############################################################################
BACKUP_DIR="${BACKUP_DIR}"
BACKUP_LOG="${BACKUP_LOG}"
RETENTION_DAYS="${RETENTION_DAYS}"
CONCURRENCY_MAX="${BACKUP_CONCURRENCY_MAX}"
SPACE_MIN_GB="${BACKUP_SPACE_MIN_GB}"
EXCLUDE_DBS_REGEX='^(information_schema|performance_schema|mysql|sys)$'
EOF
# 第二段:主体逻辑(禁止变量展开)
cat >>"${BACKUP_SCRIPT}" <<'EOF'
ts() { date +'%F %T'; }
log() { echo "[$(ts)] $*" | tee -a "${BACKUP_LOG}"; }
df_avail_gb() { df -Pk "$1" | awk 'NR==2{printf "%.0f", $4/1024/1024}'; }
json_escape() { sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
main() {
local cmds=(mysql mysqldump gzip find awk sed sha256sum xargs hostname date df getconf)
for c in "${cmds[@]}"; do
command -v "$c" >/dev/null 2>&1 || { log "[错误] 缺少命令:$c"; exit 1; }
done
umask 077
mkdir -p "${BACKUP_DIR}"
touch "${BACKUP_LOG}" || true
if [[ ! -f /root/.my.cnf ]]; then
log "[错误] 未找到 /root/.my.cnf,无法免密连接 MySQL。"
exit 1
fi
local avail_gb
avail_gb="$(df_avail_gb "${BACKUP_DIR}")"
if [[ "${avail_gb}" =~ ^[0-9]+$ ]] && (( avail_gb < SPACE_MIN_GB )); then
log "[错误] 备份目录空间不足:可用 ${avail_gb}GB < 要求 ${SPACE_MIN_GB}GB"
exit 1
fi
local host short ts_now batch_dir
host="$(hostname -s 2>/dev/null || hostname)"
short="${host:-localhost}"
ts_now="$(date +'%F_%H%M%S')"
batch_dir="${BACKUP_DIR}/batch_${short}_${ts_now}"
mkdir -p "${batch_dir}"
log "开始备份批次:${batch_dir}"
mapfile -t DBS < <(mysql -Nse "SHOW DATABASES;" | awk 'NF' | grep -Ev "${EXCLUDE_DBS_REGEX}" || true)
if (( ${#DBS[@]} == 0 )); then
log "[警告] 未发现需要备份的用户库,退出。"
exit 0
fi
local mysql_version server_uuid server_id gtid_executed master_status_file master_status_pos
mysql_version="$(mysql -Nse "SELECT VERSION();" 2>/dev/null || true)"
server_uuid="$(mysql -Nse "SELECT @@GLOBAL.server_uuid;" 2>/dev/null || true)"
server_id="$(mysql -Nse "SELECT @@GLOBAL.server_id;" 2>/dev/null || true)"
gtid_executed="$(mysql -Nse "SELECT @@GLOBAL.gtid_executed;" 2>/dev/null || true)"
read -r master_status_file master_status_pos < <(mysql -Nse "SHOW MASTER STATUS;" 2>/dev/null | awk 'NR==1{print $1, $2}' || true)
{
echo "时间戳=${ts_now}"
echo "主机=${short}"
echo "MySQL版本=${mysql_version}"
echo "server_uuid=${server_uuid}"
echo "server_id=${server_id}"
echo "binlog文件=${master_status_file:-}"
echo "binlog位置=${master_status_pos:-}"
echo "gtid_executed=${gtid_executed}"
echo "数据库列表=${DBS[*]}"
} > "${batch_dir}/snapshot.txt"
local cpu jobs
cpu="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)"
jobs="${cpu}"
(( jobs > CONCURRENCY_MAX )) && jobs="${CONCURRENCY_MAX}"
(( jobs < 1 )) && jobs=1
log "开始导出:库数量=${#DBS[@]},并行度=${jobs}"
printf "%s\n" "${DBS[@]}" > "${batch_dir}/db.list"
cat "${batch_dir}/db.list" | xargs -r -I{} -P "${jobs}" bash -c '
set -euo pipefail
db="$1"
outdir="$2"
host_short="$3"
ts_now="$4"
logf="$5"
out="${outdir}/db_${db}_${host_short}_${ts_now}.sql.gz"
tmp="${out}.tmp"
mysqldump --single-transaction --quick \
--routines --events --triggers --hex-blob \
--default-character-set=utf8mb4 \
--set-gtid-purged=OFF \
--databases "${db}" 2>>"${logf}" | gzip -1 > "${tmp}"
mv -f "${tmp}" "${out}"
sha256sum "${out}" >> "${outdir}/SHA256SUMS"
' _ "{}" "${batch_dir}" "${short}" "${ts_now}" "${BACKUP_LOG}"
{
echo "{"
echo " \"时间戳\": \"${ts_now}\","
echo " \"主机\": \"${short}\","
echo " \"MySQL版本\": \"$(printf "%s" "${mysql_version}" | json_escape)\","
echo " \"server_uuid\": \"$(printf "%s" "${server_uuid}" | json_escape)\","
echo " \"server_id\": \"$(printf "%s" "${server_id}" | json_escape)\","
echo " \"binlog位点\": {"
echo " \"文件\": \"$(printf "%s" "${master_status_file:-}" | json_escape)\","
echo " \"位置\": \"$(printf "%s" "${master_status_pos:-}" | json_escape)\""
echo " },"
echo " \"gtid_executed\": \"$(printf "%s" "${gtid_executed}" | json_escape)\","
echo " \"数据库\": ["
for i in "${!DBS[@]}"; do
if (( i == ${#DBS[@]} - 1 )); then
echo " \"$(printf "%s" "${DBS[$i]}" | json_escape)\""
else
echo " \"$(printf "%s" "${DBS[$i]}" | json_escape)\","
fi
done
echo " ]"
echo "}"
} > "${batch_dir}/manifest.json"
log "导出完成:${batch_dir}"
log "校验文件:${batch_dir}/SHA256SUMS"
log "元数据: ${batch_dir}/snapshot.txt 与 ${batch_dir}/manifest.json"
find "${BACKUP_DIR}" -maxdepth 1 -type d -name 'batch_*' -mtime +"${RETENTION_DAYS}" -print -exec rm -rf {} \; >>"${BACKUP_LOG}" 2>&1 || true
log "保留策略完成:保留最近 ${RETENTION_DAYS} 天"
}
main "$@"
EOF
chmod 750 "${BACKUP_SCRIPT}"
}
install_backup_cron() {
log "安装备份定时任务到 root crontab"
local begin="# >>> MySQL 每日全备任务(自动生成)开始 >>>"
local end="# <<< MySQL 每日全备任务(自动生成)结束 <<<"
local body
body="$(
cat <<EOF
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
${BACKUP_MIN} ${BACKUP_HOUR} * * * ${BACKUP_SCRIPT} >> ${BACKUP_LOG} 2>&1
EOF
)"
local current tmp
current="$(crontab -l 2>/dev/null || true)"
tmp="$(mktemp)"
if printf "%s\n" "${current}" | grep -Fq "${begin}" && printf "%s\n" "${current}" | grep -Fq "${end}"; then
awk -v begin="${begin}" -v end="${end}" -v body="${body}" '
BEGIN{inblk=0}
$0==begin {print begin; print body; print end; inblk=1; next}
$0==end && inblk==1 {inblk=0; next}
inblk==0 {print}
' <<<"${current}" >"${tmp}"
else
{
printf "%s\n" "${current}"
echo
echo "${begin}"
echo "${body}"
echo "${end}"
} >"${tmp}"
fi
crontab "${tmp}"
rm -f "${tmp}"
log "备份任务已写入 root crontab:每天 ${BACKUP_HOUR}:${BACKUP_MIN} 执行全备,保留 ${RETENTION_DAYS} 天。"
log "备份目录:${BACKUP_DIR}"
log "备份脚本:${BACKUP_SCRIPT}"
log "备份日志:${BACKUP_LOG}"
}
#==============================================================================
# 安装结果输出
#==============================================================================
print_summary() {
local mycnf="${BASE_DATA}/conf/my.cnf"
local data_dir="${BASE_DATA}/data"
log "安装总结:"
log " 服务状态: systemctl status mysqld.service"
log " 安装目录: ${BASEDIR}"
log " 配置文件: ${mycnf}"
log " 数据目录: ${data_dir}"
log " binlog目录: ${BASE_DATA}/binlog"
log " 日志目录: ${BASE_DATA}/log"
log " socket: ${SOCKET}"
log " 端口: ${PORT}"
log " root 密码: ${ROOT_PASS}"
log " 免密文件: /root/.my.cnf(已生成,权限 600)"
log " 备份任务: 已写入 root crontab(每天 ${BACKUP_HOUR}:${BACKUP_MIN},保留 ${RETENTION_DAYS} 天)"
log " 备份目录: ${BACKUP_DIR}"
log "本次安装日志文件:${LOG_FILE}"
echo
color yellow "提示:安装完成后建议重启主机,确保系统配置生效。"
echo
}
#==============================================================================
# 主流程(按阶段分段输出)
#==============================================================================
main() {
require_root
init_log
stage_begin "环境检查"
parse_args "$@"
require_cmds
detect_os
if ((BASELINE_ONLY == 1)); then
log "模式:--baseline-only(仅执行系统基线,不安装 MySQL)"
stage_end
stage_begin "系统基线优化"
apply_baseline
log "系统基线执行完成(baseline-only 模式)"
stage_end
exit 0
fi
check_pkg
is_systemd || die "未检测到 systemd:该脚本仅支持 systemd 管理 mysqld。"
[[ "${BUFFER_POOL}" != "" ]] || BUFFER_POOL="$(auto_innodb_buffer_pool)"
log "安装规划:"
kv "安装目录" "${BASEDIR}"
kv "数据根目录" "${BASE_DATA}"
kv "端口" "${PORT}"
kv "server_id" "${SERVER_ID}"
kv "bind-address" "${BIND_ADDR}"
kv "socket" "${SOCKET}"
kv "buffer_pool" "${BUFFER_POOL}"
kv "字符集" "${CHARSET}"
kv "排序规则" "${COLLATION}"
stage_end
stage_begin "残留清理"
cleanup_mysql_residue_if_needed
stage_end
stage_begin "安装前校验"
log "校验项:mysqld 进程 / 端口占用 / 目录残留"
pgrep -x mysqld >/dev/null 2>&1 && die "检测到 mysqld 仍在运行,请先停止(清理未完全生效或进程残留)。"
port_in_use "${PORT}" && die "端口 ${PORT} 已被占用。"
[[ -d "${BASEDIR}" || -d "${BASE_DATA}" ]] && die "检测到目录残留:${BASEDIR} 或 ${BASE_DATA}。请确认是否挂载点/只读/权限问题。"
log "安装前校验:通过"
stage_end
stage_begin "系统基线优化"
apply_baseline
log "系统基线优化:完成"
stage_end
stage_begin "MySQL 安装部署"
create_mysql_user_group
ensure_socket_dir
mkdir -p "${BASE_DATA}"
chown -R "${MYSQL_USER}:${MYSQL_GROUP}" "${BASE_DATA}"
extract_mysql_binary
write_my_cnf
log "MySQL 安装部署:完成"
stage_end
stage_begin "初始化与启动"
initialize_mysql
write_systemd_unit
start_mysql_systemd
log "健康检查:等待 MySQL 就绪(最多 60 秒)"
if ! wait_mysql_ready; then
err "等待超时,MySQL 仍未就绪。诊断信息如下:"
run_cmd_soft "mysqld.service 状态" systemctl status mysqld.service -l --no-pager
run_cmd_soft "错误日志(最后 200 行)" bash -c "tail -n 200 '${BASE_DATA}/log/mysql.err'"
die "MySQL 未就绪,终止。"
fi
reset_root_password_if_possible
write_root_my_cnf
log "初始化与启动:完成"
stage_end
stage_begin "备份与定时任务"
write_backup_script
install_backup_cron
log "备份与定时任务:完成"
stage_end
stage_begin "总结输出"
print_summary
stage_end
}
main "$@"
巡检脚本
巡检报告介绍
这份报告能告诉你什么?我们的脚本不是简单跑几个 SHOW STATUS,而是真正站在 DBA + 运维 + 安全 的角度,深度扫描你的 MySQL 实例:
- ✅ 实例基础信息:版本、运行时间、字符集、时区
- ✅ 连接与线程:当前连接数、最大连接使用率、线程缓存命中率
- ✅ 性能瓶颈:QPS/TPS、全表扫描次数、磁盘临时表比例、排序溢出
- ✅ InnoDB 健康度:缓冲池命中率、脏页比例、死锁次数
- ✅ 安全风险:空密码账户、匿名用户、root 远程登录、SSL 是否启用
- ✅ 主从复制状态:IO/SQL 线程、延迟、错误信息(自动识别)
- ✅ 表结构隐患:无主键表、超大表(>5GB)、高碎片表 Top20
- ✅ 日志与备份建议:慢查询、binlog 是否开启、保留策略是否合理
- ✅ 配置合规性检查:innodb_flush_log_at_trx_commit、sync_binlog 等关键参数是否安全
- 🎯 所有结果以可视化 HTML 报告呈现,红绿灯式预警,一目了然!
如何使用?3 步搞定!
第1步:安装依赖(仅需一次)pip install pymysql
第2步:保存脚本为 mysql_inspect.py
第3步:运行脚本python3 mysql_inspect.py
然后按提示输入:
💻 请输入主机资源配置(用于参数合理性分析):
CPU 核数 (默认: 8): 4
内存大小 (GB, 默认: 16): 8
数据盘总容量 (TB, 默认: 2): 1
📝 请输入 MySQL 连接信息:
Host (默认: 127.0.0.1): 192.168.1.101
Port (默认: 3306): 3306
User: root
Password:
巡检脚本内容
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MySQL 全面巡检脚本 v4.3(含巡检时间)
✅ 显示巡检发起时间
✅ 增强参数检查(30+ 判断逻辑)
✅ 完整 InnoDB Status 节选(6 段落)
✅ 智能推荐基于主机资源配置
✅ 兼容 MySQL 5.6 / 5.7 / 8.0 / 8.4
"""
import pymysql
import os
import sys
import getpass
import datetime
from collections import OrderedDict
REPORT_DIR = 'report'
os.makedirs(REPORT_DIR, exist_ok=True)
def prompt_host_resources():
print("\n💻 请输入主机资源配置(用于参数合理性分析):")
cpu_cores_str = input("CPU 核数 (默认: 8): ").strip() or "8"
memory_gb_str = input("内存大小 (GB, 默认: 16): ").strip() or "16"
disk_tb_str = input("数据盘总容量 (TB, 默认: 2): ").strip() or "2"
try:
cpu_cores = int(cpu_cores_str)
memory_gb = float(memory_gb_str)
disk_tb = float(disk_tb_str)
except ValueError:
print("❌ 输入格式错误,使用默认值:8核 / 16GB / 2TB")
cpu_cores, memory_gb, disk_tb = 8, 16.0, 2.0
return {
'cpu_cores': cpu_cores,
'memory_gb': memory_gb,
'disk_tb': disk_tb
}
def prompt_db_info():
print("\n📝 请输入 MySQL 连接信息:")
host = input("Host (默认: 127.0.0.1): ").strip() or "127.0.0.1"
port_str = input("Port (默认: 3306): ").strip() or "3306"
port = int(port_str) if port_str.isdigit() else 3306
user = input("User: ").strip()
if not user:
print("❌ 用户名不能为空")
sys.exit(1)
password = getpass.getpass("Password: ")
return {'host': host, 'port': port, 'user': user, 'password': password}
def run_query(conn, sql):
cursor = conn.cursor()
try:
cursor.execute(sql)
return cursor.fetchall()
except Exception as e:
return []
finally:
cursor.close()
def extract_innodb_status_sections(innodb_status_text):
"""从 SHOW ENGINE INNODB STATUS 提取关键段落(增强版)"""
sections = {}
current_section = None
lines = innodb_status_text.split('\n') if innodb_status_text else []
for line in lines:
stripped_line = line.strip()
if not stripped_line:
continue
# 段落分隔线:如 "====================================="
if stripped_line.startswith('==='):
clean_title = stripped_line.replace('=', '').strip()
if clean_title:
current_section = clean_title.split()[0]
else:
current_section = 'unknown'
sections[current_section] = []
continue
# 显式段落标识(增强覆盖)
if 'SEMAPHORES' in stripped_line:
current_section = 'SEMAPHORES'
sections[current_section] = [stripped_line]
elif 'LATEST DETECTED DEADLOCK' in stripped_line:
current_section = 'LATEST_DEADLOCK'
sections[current_section] = []
elif 'BUFFER POOL AND MEMORY' in stripped_line:
current_section = 'BUFFER_POOL'
sections[current_section] = []
elif 'LOG' in stripped_line and '---' in stripped_line:
current_section = 'LOG'
sections[current_section] = []
elif 'PENDING' in stripped_line and ('normal aio' in stripped_line or 'flushes' in stripped_line):
current_section = 'PENDING_IO'
sections[current_section] = [stripped_line]
elif 'ROW OPERATIONS' in stripped_line:
current_section = 'ROW_OPERATIONS'
sections[current_section] = []
elif current_section is not None and current_section in sections:
sections[current_section].append(stripped_line)
return sections
def analyze_config_parameters(vars_dict, status_dict, version, host_res):
"""
全面分析 MySQL 配置参数合理性(v2.0)
支持 5.6 ~ 8.4,结合主机资源配置智能判断
"""
issues = []
mem_gb = host_res['memory_gb']
cpu_cores = host_res['cpu_cores']
disk_tb = host_res['disk_tb']
# --- 辅助函数 ---
def get_val(key, default=0):
val = vars_dict.get(key, default)
try:
return int(val) if isinstance(val, str) and val.isdigit() else float(val) if '.' in str(val) else int(val)
except:
return default
def add_issue(level, param, current, recommend, desc):
issues.append((level, param, str(current), str(recommend), desc))
# --- 1. 数据安全(高危)---
if vars_dict.get('innodb_flush_log_at_trx_commit', '1') != '1':
add_issue('high', 'innodb_flush_log_at_trx_commit', vars_dict['innodb_flush_log_at_trx_commit'], '1', '事务提交时可能丢失数据(除非你明确接受风险)')
if vars_dict.get('sync_binlog', '0') != '1':
add_issue('high', 'sync_binlog', vars_dict['sync_binlog'], '1', '主库崩溃可能导致 binlog 丢失,主从不一致')
if vars_dict.get('log_bin', 'OFF').upper() == 'OFF':
add_issue('high', 'log_bin', 'OFF', 'ON', '未开启 binlog,无法做 PITR 恢复或主从复制')
# --- 2. InnoDB 核心配置 ---
bp_size = get_val('innodb_buffer_pool_size')
bp_gb = round(bp_size / (1024**3), 2)
recommended_bp = min(mem_gb * 0.75, 128) # 最多128GB
if bp_gb < mem_gb * 0.3:
add_issue('medium', 'innodb_buffer_pool_size', f"{bp_gb} GB", f"≈{recommended_bp:.0f} GB", "缓冲池过小,命中率可能偏低")
elif bp_gb > mem_gb * 0.85:
add_issue('medium', 'innodb_buffer_pool_size', f"{bp_gb} GB", f"≤{mem_gb*0.8:.0f} GB", "缓冲池过大,可能引发 OOM")
log_file_size = get_val('innodb_log_file_size')
log_gb = round(log_file_size / (1024**3), 2)
if log_gb < 0.5:
add_issue('medium', 'innodb_log_file_size', f"{log_gb} GB", "1~4 GB", "日志文件太小,频繁 checkpoint 影响性能")
io_cap = get_val('innodb_io_capacity', 200)
if io_cap < 1000 and 'ssd' in str(vars_dict.get('datadir', '')).lower():
add_issue('low', 'innodb_io_capacity', io_cap, '2000+', 'SSD 磁盘建议提高 I/O 容量')
# --- 3. 连接与线程 ---
max_conn = get_val('max_connections', 151)
thread_cache = get_val('thread_cache_size', 0)
if max_conn < 500 and host_res['memory_gb'] > 16:
add_issue('low', 'max_connections', max_conn, '500~2000', '连接数上限偏低,高并发场景易打满')
if thread_cache < min(100, max_conn // 10):
add_issue('low', 'thread_cache_size', thread_cache, f"≈{min(100, max_conn//10)}", '线程缓存不足,频繁创建线程开销大')
# --- 4. 临时表与排序 ---
tmp_table = get_val('tmp_table_size', 16777216) # 默认16M
max_heap = get_val('max_heap_table_size', 16777216)
tmp_mb = tmp_table / (1024**2)
if tmp_mb < 256:
add_issue('medium', 'tmp_table_size / max_heap_table_size', f"{tmp_mb:.0f} MB", "256~1024 MB", '内存临时表易转磁盘,拖慢复杂查询')
sort_buffer = get_val('sort_buffer_size', 262144)
if sort_buffer < 2 * 1024 * 1024: # <2MB
add_issue('low', 'sort_buffer_size', f"{sort_buffer/(1024**2):.1f} MB", "2~4 MB", '排序内存不足,可能使用磁盘排序')
# --- 5. 表缓存与打开文件 ---
table_open_cache = get_val('table_open_cache', 2000)
open_files_limit = get_val('open_files_limit', 5000)
if table_open_cache < 4000 and disk_tb > 1:
add_issue('low', 'table_open_cache', table_open_cache, '4000~8000', '大库建议增大表缓存,避免频繁 open/close')
# --- 6. MySQL 8.0+ 特有优化 ---
ver_major = int(version.split('.')[0])
if ver_major >= 8:
replica_parallel_type = vars_dict.get('slave_parallel_type', vars_dict.get('replica_parallel_type', 'DATABASE'))
if replica_parallel_type.upper() != 'LOGICAL_CLOCK':
add_issue('medium', 'replica_parallel_type', replica_parallel_type, 'LOGICAL_CLOCK', '建议开启基于事务组的并行复制')
dep_track = vars_dict.get('binlog_transaction_dependency_tracking', 'COMMIT_ORDER')
if dep_track.upper() != 'WRITESET':
add_issue('low', 'binlog_transaction_dependency_tracking', dep_track, 'WRITESET', '提升主从并行回放效率')
# 检查是否关闭了默认的 ONLY_FULL_GROUP_BY(常见开发需求,但需谨慎)
sql_mode = vars_dict.get('sql_mode', '')
if 'ONLY_FULL_GROUP_BY' not in sql_mode and 'production' in str(vars_dict.get('hostname', '')).lower():
add_issue('low', 'sql_mode', '缺少 ONLY_FULL_GROUP_BY', '保留默认', '可能隐藏 SQL 逻辑错误')
# --- 7. 其他常见隐患 ---
if vars_dict.get('query_cache_type', 'OFF') != 'OFF' or get_val('query_cache_size') > 0:
add_issue('medium', 'query_cache', 'ENABLED', 'DISABLED', 'Query Cache 在高并发下易成瓶颈,MySQL 8.0 已移除')
return issues
def main():
inspection_start_time = datetime.datetime.now() # ⭐ 记录巡检开始时间
host_res = prompt_host_resources()
db_info = prompt_db_info()
try:
conn = pymysql.connect(
host=db_info['host'],
port=db_info['port'],
user=db_info['user'],
password=db_info['password'],
database='information_schema',
charset='utf8mb4',
connect_timeout=10,
autocommit=True,
ssl_disabled=True
)
print("✅ 成功连接 MySQL")
except Exception as e:
print(f"❌ 连接失败: {e}")
print("💡 请运行: pip install --upgrade pymysql>=1.0.2")
sys.exit(1)
status_rows = run_query(conn, "SHOW GLOBAL STATUS")
vars_rows = run_query(conn, "SHOW GLOBAL VARIABLES")
status_dict = {row[0]: row[1] for row in status_rows if len(row) >= 2}
vars_dict = {row[0]: row[1] for row in vars_rows if len(row) >= 2}
version = vars_dict.get('version', 'Unknown')
ver_major = int(version.split('.')[0])
uptime_seconds = int(status_dict.get('Uptime', 0))
data = OrderedDict()
# === 基础信息 ===
data['host'] = host_res
data['basic'] = {
'version': version,
'hostname': vars_dict.get('hostname', 'Unknown'),
'port': db_info['port'],
'uptime': str(datetime.timedelta(seconds=uptime_seconds)),
'character_set_server': vars_dict.get('character_set_server', 'N/A'),
}
# === 安全检查 ===
security_issues = []
anon_users = run_query(conn, """
SELECT CONCAT(user, '@', host) AS account
FROM mysql.user
WHERE (authentication_string = '' OR authentication_string IS NULL)
OR user = ''
""")
if anon_users:
security_issues.append(f"存在空密码或匿名账户: {[r[0] for r in anon_users]}")
remote_root = run_query(conn, """
SELECT host FROM mysql.user
WHERE user = 'root' AND host NOT IN ('localhost', '127.0.0.1', '::1', '%')
""")
if remote_root:
security_issues.append("root 用户允许远程登录")
have_ssl = vars_dict.get('have_ssl', 'DISABLED')
if have_ssl.upper() != 'YES':
security_issues.append("SSL 未启用")
data['security_issues'] = security_issues
# === 性能指标 ===
qps = round(int(status_dict.get('Questions', 0)) / max(uptime_seconds, 1), 2)
tps = round((int(status_dict.get('Com_commit', 0)) + int(status_dict.get('Com_rollback', 0))) / max(uptime_seconds, 1), 2)
full_scans = status_dict.get('Handler_read_rnd_next', '0')
created_tmp_disk = int(status_dict.get('Created_tmp_disk_tables', 0))
created_tmp = max(int(status_dict.get('Created_tmp_tables', 1)), 1)
tmp_disk_pct = round(created_tmp_disk / created_tmp * 100, 2)
sort_merge_passes = int(status_dict.get('Sort_merge_passes', 0))
sorts = int(status_dict.get('Sort_rows', 0))
sort_disk_pct = round(sort_merge_passes / max(sorts, 1) * 100, 2) if sorts > 0 else 0
threads_created = int(status_dict.get('Threads_created', 0))
connections = int(status_dict.get('Connections', 1))
thread_cache_hit = (1 - threads_created / connections) * 100
data['performance'] = {
'qps': qps,
'tps': tps,
'full_table_scans': full_scans,
'tmp_disk_table_pct': f"{tmp_disk_pct}%",
'sort_disk_pct': f"{sort_disk_pct:.2f}%",
'thread_cache_hit_rate': f"{thread_cache_hit:.2f}%"
}
# === InnoDB 基础 ===
bp_reads = int(status_dict.get('Innodb_buffer_pool_reads', 0))
bp_read_requests = max(int(status_dict.get('Innodb_buffer_pool_read_requests', 1)), 1)
bp_hit = (1 - bp_reads / bp_read_requests) * 100
dirty_pages = int(status_dict.get('Innodb_buffer_pool_pages_dirty', 0))
total_pages = max(int(status_dict.get('Innodb_buffer_pool_pages_total', 1)), 1)
dirty_pct = round(dirty_pages / total_pages * 100, 2)
data['innodb_basic'] = {
'buffer_pool_size_gb': round(int(vars_dict.get('innodb_buffer_pool_size', 0)) / 1024**3, 2),
'buffer_pool_hit_rate': round(bp_hit, 2),
'dirty_page_pct': dirty_pct,
'deadlocks': status_dict.get('Innodb_deadlocks', '0'),
}
# === ⭐⭐⭐ 核心:完整 InnoDB Status 节选 ⭐⭐⭐ ===
innodb_status_rows = run_query(conn, "SHOW ENGINE INNODB STATUS")
innodb_status_text = innodb_status_rows[0][2] if innodb_status_rows and len(innodb_status_rows[0]) >= 3 else ""
innodb_sections = extract_innodb_status_sections(innodb_status_text)
def truncate_lines(lines, max_lines=30):
content = '\n'.join(lines[:max_lines])
return content + ("..." if len(lines) > max_lines else "")
data['innodb_full_sections'] = {
'SEMAPHORES': truncate_lines(innodb_sections.get('SEMAPHORES', []), 20),
'LATEST_DEADLOCK': truncate_lines(innodb_sections.get('LATEST_DEADLOCK', []), 50),
'BUFFER_POOL': truncate_lines(innodb_sections.get('BUFFER_POOL', []), 30),
'LOG': truncate_lines(innodb_sections.get('LOG', []), 20),
'PENDING_IO': truncate_lines(innodb_sections.get('PENDING_IO', []), 15),
'ROW_OPERATIONS': truncate_lines(innodb_sections.get('ROW_OPERATIONS', []), 20),
}
# === 长事务、锁等待 ===
long_trx = []
if ver_major >= 5:
trx_query = """
SELECT trx_id, trx_started, trx_state, trx_operation_state,
TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) AS duration_sec,
trx_mysql_thread_id
FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60
ORDER BY duration_sec DESC
"""
long_trx = run_query(conn, trx_query)
data['long_transactions'] = long_trx
lock_waits = []
if ver_major == 5 and int(version.split('.')[1]) < 7:
lock_waits = run_query(conn, """
SELECT r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
w.requested_lock_id,
w.blocking_lock_id
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
""")
elif ver_major >= 5:
lock_waits = run_query(conn, """
SELECT waiting_thread_id, blocking_thread_id,
wait_age, LEFT(s.sql_text, 100) AS sql_text
FROM performance_schema.data_lock_waits w
JOIN performance_schema.events_statements_current s ON s.thread_id = w.waiting_thread_id
""")
data['lock_waits'] = lock_waits
# === 连接健康 ===
threads_connected = int(status_dict.get('Threads_connected', 0))
max_connections = int(vars_dict.get('max_connections', 151))
aborted_connects = int(status_dict.get('Aborted_connects', 0))
aborted_clients = int(status_dict.get('Aborted_clients', 0))
data['connection_health'] = {
'current_connections': threads_connected,
'max_connections': max_connections,
'usage_pct': round(threads_connected / max_connections * 100, 2),
'aborted_connects': aborted_connects,
'aborted_clients': aborted_clients
}
# === 慢查询 ===
slow_query_log = vars_dict.get('slow_query_log', 'OFF')
long_query_time = vars_dict.get('long_query_time', '10')
slow_queries = status_dict.get('Slow_queries', '0')
data['slow_query'] = {
'enabled': slow_query_log.upper() == 'ON',
'threshold_sec': long_query_time,
'total_slow': slow_queries
}
# === 复制状态 ===
slave_status = None
replica_rows = run_query(conn, "SHOW REPLICA STATUS")
if not replica_rows:
replica_rows = run_query(conn, "SHOW SLAVE STATUS")
if replica_rows:
cursor = conn.cursor()
cursor.execute("SHOW SLAVE STATUS")
if cursor.description:
cols = [d[0] for d in cursor.description]
slave_status = dict(zip(cols, replica_rows[0]))
cursor.close()
if slave_status:
io_running = slave_status.get('Slave_IO_Running', 'No')
sql_running = slave_status.get('Slave_SQL_Running', 'No')
seconds_behind = slave_status.get('Seconds_Behind_Master', None)
relay_space = slave_status.get('Relay_Log_Space', 0)
data['replication'] = {
'io_running': io_running,
'sql_running': sql_running,
'seconds_behind': seconds_behind,
'relay_log_space_mb': round(int(relay_space) / 1024 / 1024, 2),
'master_host': slave_status.get('Master_Host', 'N/A')
}
else:
data['replication'] = None
# === 表结构隐患 ===
data['tables_without_pk'] = run_query(conn, """
SELECT t.table_schema, t.table_name
FROM information_schema.tables t
LEFT JOIN information_schema.key_column_usage k
ON t.table_schema = k.table_schema AND t.table_name = k.table_name AND k.constraint_name = 'PRIMARY'
WHERE t.table_type = 'BASE TABLE'
AND t.table_schema NOT IN ('mysql','information_schema','performance_schema','sys')
AND k.table_name IS NULL
ORDER BY t.table_schema, t.table_name
""")
data['large_tables'] = run_query(conn, """
SELECT table_schema, table_name,
ROUND((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')
AND table_type = 'BASE TABLE'
AND (data_length + index_length) > 5 * 1024 * 1024 * 1024
ORDER BY (data_length + index_length) DESC LIMIT 20
""")
data['autoinc_near_max'] = run_query(conn, """
SELECT table_schema, table_name, column_name,
auto_increment,
CASE data_type
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 65535
WHEN 'mediumint' THEN 16777215
WHEN 'int' THEN 4294967295
WHEN 'bigint' THEN 18446744073709551615
END AS max_value,
ROUND(auto_increment /
CASE data_type
WHEN 'tinyint' THEN 255
WHEN 'smallint' THEN 65535
WHEN 'mediumint' THEN 16777215
WHEN 'int' THEN 4294967295
WHEN 'bigint' THEN 18446744073709551615
END * 100, 2) AS used_pct
FROM information_schema.columns c
JOIN information_schema.tables t USING (table_schema, table_name)
WHERE c.extra = 'auto_increment'
AND t.table_schema NOT IN ('mysql','information_schema','performance_schema','sys')
AND c.data_type IN ('tinyint','smallint','mediumint','int','bigint')
AND auto_increment IS NOT NULL
AND (
CASE data_type
WHEN 'tinyint' THEN auto_increment > 0.8 * 255
WHEN 'smallint' THEN auto_increment > 0.8 * 65535
WHEN 'mediumint' THEN auto_increment > 0.8 * 16777215
WHEN 'int' THEN auto_increment > 0.8 * 4294967295
WHEN 'bigint' THEN auto_increment > 0.8 * 18446744073709551615
END
)
ORDER BY used_pct DESC
""")
unused_indexes = run_query(conn, """
SELECT object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND object_schema NOT IN ('mysql','information_schema','performance_schema','sys')
ORDER BY object_schema, object_name
LIMIT 50
""")
data['unused_indexes'] = unused_indexes
# === 存储使用 ===
total_data_bytes = sum(
int(r[1]) + int(r[2]) for r in run_query(conn, """
SELECT table_schema,
COALESCE(SUM(data_length), 0),
COALESCE(SUM(index_length), 0)
FROM information_schema.tables
GROUP BY table_schema
""") if len(r) >= 3
)
disk_used_tb = total_data_bytes / (1024**4)
disk_used_pct = round(disk_used_tb / host_res['disk_tb'] * 100, 2)
binlog_size = 0
if vars_dict.get('log_bin', 'OFF').upper() == 'ON':
binlogs = run_query(conn, "SHOW BINARY LOGS")
if binlogs:
binlog_size = sum(int(row[1]) for row in binlogs if len(row) >= 2)
data['storage'] = {
'disk_used_tb': round(disk_used_tb, 2),
'disk_total_tb': host_res['disk_tb'],
'disk_used_pct': disk_used_pct,
'binlog_size_gb': round(binlog_size / (1024**3), 2)
}
undo_info = []
if ver_major >= 8:
undo_info = run_query(conn, """
SELECT tablespace_name, file_name, ROUND(bytes/1024/1024/1024, 2) AS size_gb
FROM information_schema.files
WHERE file_type = 'UNDO LOG'
""")
data['undo_tablespaces'] = undo_info
# === ⭐ 关键:调用新版参数分析函数 ⭐ ===
data['config_analysis'] = analyze_config_parameters(vars_dict, status_dict, version, host_res)
conn.close()
# === 生成 HTML 报告 ===
inspection_time_str = inspection_start_time.strftime('%Y-%m-%d %H:%M:%S')
report_file = os.path.join(REPORT_DIR, f"mysql_inspection_v4.3_{inspection_start_time.strftime('%Y%m%d_%H%M%S')}.html")
def fmt_table(rows, headers):
if not rows:
return '<p>✅ 无异常</p>'
html = '<table><thead><tr>' + ''.join(f'<th>{h}</th>' for h in headers) + '</tr></thead><tbody>'
for r in rows:
html += '<tr>' + ''.join(f'<td>{cell}</td>' for cell in r) + '</tr>'
html += '</tbody></table>'
return html
def fmt_config_table(rows):
if not rows:
return '<p>✅ 所有参数配置合理</p>'
html = '<table><thead><tr><th>风险</th><th>参数</th><th>当前值</th><th>建议值</th><th>说明</th></tr></thead><tbody>'
for level, param, current, recommend, desc in rows:
color = {'high': '#e74c3c', 'medium': '#f39c12', 'low': '#3498db'}[level]
html += f'<tr><td style="color:{color}; font-weight:bold;">{level.upper()}</td><td>{param}</td><td>{current}</td><td>{recommend}</td><td>{desc}</td></tr>'
html += '</tbody></table>'
return html
def fmt_text_block(text):
if not text.strip():
return '<p>✅ 无内容</p>'
return f'<pre style="background:#f8f9fa; padding:10px; border-radius:5px; white-space: pre-wrap; word-break: break-word;">{text}</pre>'
html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>MySQL 全面巡检报告 v4.3</title>
<style>
body {{ font-family: 'Segoe UI', Tahoma, sans-serif; margin: 20px; background: #f9fbfd; }}
h1, h2, h3 {{ color: #2c3e50; }}
.section {{ background: white; padding: 20px; margin: 25px 0; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }}
table {{ width: 100%; border-collapse: collapse; margin: 15px 0; }}
th, td {{ border: 1px solid #ddd; padding: 10px; text-align: left; }}
th {{ background-color: #f8f9fa; }}
.warning {{ color: #e74c3c; }}
.good {{ color: #27ae60; }}
pre {{ white-space: pre-wrap; word-break: break-word; }}
</style>
</head>
<body>
<h1>🔍 MySQL 全面巡检报告 v4.3</h1>
<p style="text-align:center; color:#7f8c8d; font-size:16px;">
🕒 巡检时间:{inspection_time_str}
| 主机配置:{host_res['cpu_cores']}核 / {host_res['memory_gb']}GB / {host_res['disk_tb']}TB
</p>
<!-- 基础信息 -->
<div class="section">
<h2>1. 实例基础信息</h2>
<table>
<tr><td>MySQL 版本</td><td>{data['basic']['version']}</td></tr>
<tr><td>运行时间</td><td>{data['basic']['uptime']}</td></tr>
</table>
</div>
<!-- 安全 -->
<div class="section">
<h2>2. 安全风险</h2>
{('<div class="warning">' + '<br>'.join(f"⚠️ {i}" for i in data['security_issues']) + '</div>') if data['security_issues'] else '<p>✅ 无安全风险</p>'}
</div>
<!-- 性能 -->
<div class="section">
<h2>3. 性能概览</h2>
<table>
<tr><td>QPS</td><td>{data['performance']['qps']}</td></tr>
<tr><td>磁盘临时表占比</td><td>{data['performance']['tmp_disk_table_pct']}</td></tr>
<tr><td>线程缓存命中率</td><td class="{'good' if float(data['performance']['thread_cache_hit_rate'].rstrip('%')) > 90 else 'warning'}">{data['performance']['thread_cache_hit_rate']}</td></tr>
</table>
</div>
<!-- InnoDB 基础 -->
<div class="section">
<h2>4. InnoDB 基础健康度</h2>
<table>
<tr><td>缓冲池命中率</td><td class="{'good' if data['innodb_basic']['buffer_pool_hit_rate'] > 95 else 'warning'}">{data['innodb_basic']['buffer_pool_hit_rate']}%</td></tr>
<tr><td>脏页比例</td><td>{data['innodb_basic']['dirty_page_pct']}%</td></tr>
</table>
</div>
<!-- ⭐⭐⭐ 新增:完整 InnoDB Status 节选 ⭐⭐⭐ -->
<div class="section">
<h2>5. InnoDB Status 完整节选</h2>
<p style="color:#7f8c8d;">以下内容来自 <code>SHOW ENGINE INNODB STATUS</code>,反映 InnoDB 引擎内部实时状态。</p>
<h3>5.1 SEMAPHORES(信号量)</h3>
{fmt_text_block(data['innodb_full_sections']['SEMAPHORES'])}
<h3>5.2 LATEST DETECTED DEADLOCK(最新死锁)</h3>
{fmt_text_block(data['innodb_full_sections']['LATEST_DEADLOCK'])}
<h3>5.3 BUFFER POOL AND MEMORY(缓冲池与内存)</h3>
{fmt_text_block(data['innodb_full_sections']['BUFFER_POOL'])}
<h3>5.4 LOG(日志)</h3>
{fmt_text_block(data['innodb_full_sections']['LOG'])}
<h3>5.5 PENDING I/O(挂起的 I/O)</h3>
{fmt_text_block(data['innodb_full_sections']['PENDING_IO'])}
<h3>5.6 ROW OPERATIONS(行操作)</h3>
{fmt_text_block(data['innodb_full_sections']['ROW_OPERATIONS'])}
</div>
<!-- 事务与锁 -->
<div class="section">
<h2>6. 事务与锁分析</h2>
<h3>长事务(>60秒)</h3>
{fmt_table(data['long_transactions'], ['Trx ID', 'Started', 'State', 'Op State', 'Duration(s)', 'Thread ID'])}
<h3>锁等待</h3>
{fmt_table(data['lock_waits'], ['Waiting Thread', 'Blocking Thread', 'Wait Age', 'SQL']) if data['lock_waits'] and len(data['lock_waits'][0]) >= 4 else
fmt_table(data['lock_waits'], ['Waiting Trx', 'Waiting Thread', 'Blocking Trx', 'Blocking Thread', 'Req Lock', 'Block Lock'])}
</div>
<!-- 连接健康 -->
<div class="section">
<h2>7. 连接健康度</h2>
<table>
<tr><td>当前连接数</td><td>{data['connection_health']['current_connections']}</td></tr>
<tr><td>连接使用率</td><td class="{'warning' if data['connection_health']['usage_pct'] > 80 else 'good'}">{data['connection_health']['usage_pct']}%</td></tr>
<tr><td>Aborted Connects</td><td>{data['connection_health']['aborted_connects']}</td></tr>
</table>
</div>
<!-- 复制 -->
<div class="section">
<h2>8. 主从复制状态</h2>
{f'''
<table>
<tr><td>IO 线程</td><td>{data['replication']['io_running']}</td></tr>
<tr><td>SQL 线程</td><td>{data['replication']['sql_running']}</td></tr>
<tr><td>延迟 (秒)</td><td class="{'warning' if data['replication']['seconds_behind'] not in (None, '0') and int(data['replication']['seconds_behind'] or 0) > 30 else 'good'}">{data['replication']['seconds_behind'] or 'N/A'}</td></tr>
<tr><td>中继日志大小</td><td>{data['replication']['relay_log_space_mb']} MB</td></tr>
</table>
''' if data['replication'] else '<p>✅ 未配置复制</p>'}
</div>
<!-- 参数分析 -->
<div class="section">
<h2>9. 关键参数配置分析</h2>
{fmt_config_table(data['config_analysis'])}
</div>
<!-- 存储 -->
<div class="section">
<h2>10. 存储使用</h2>
<table>
<tr><td>数据+索引</td><td>{data['storage']['disk_used_tb']} TB</td></tr>
<tr><td>磁盘使用率</td><td class="{'warning' if data['storage']['disk_used_pct'] > 80 else 'good'}">{data['storage']['disk_used_pct']}%</td></tr>
<tr><td>Binlog 大小</td><td>{data['storage']['binlog_size_gb']} GB</td></tr>
</table>
{f"<h3>Undo 表空间 (MySQL 8.0+)</h3>{fmt_table(data['undo_tablespaces'], ['Tablespace', 'File', 'Size (GB)'])}" if data['undo_tablespaces'] else ""}
</div>
<!-- 表结构 -->
<div class="section">
<h2>11. 表结构隐患</h2>
<h3>无主键表</h3>
{fmt_table(data['tables_without_pk'], ['Schema', 'Table'])}
<h3>自增主键即将耗尽</h3>
{fmt_table(data['autoinc_near_max'], ['Schema', 'Table', 'Column', 'Current', 'Max', 'Used %'])}
</div>
<footer style="text-align: center; margin-top: 50px; color: #95a5a6;">
MySQL 全面巡检 v4.3 | 覆盖 15+ 维度 | InnoDB Status 全景 + 智能参数分析 | 🕒 巡检时间:{inspection_time_str}
</footer>
</body>
</html>
"""
with open(report_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"\n✅ 全面巡检报告已生成: {report_file}")
print("💡 温馨提示:请同时检查 MySQL 错误日志中的 [ERROR] 和 [Warning] 条目!")
if __name__ == '__main__':
main()