生产可用的 MySQL8 一键安装脚本和一键巡检脚本

目录

脚本介绍

目前本脚本用于在 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 等残留,交互确认后执行清理。

脚本执行步骤

  1. 环境检查:解析参数、检查依赖命令、识别操作系统信息。
  2. (可选)仅基线模式:--baseline-only 只执行系统基线,不安装MySQL。
  3. 残留清理:发现 MySQL 残留则提示确认,确认后停止服务/杀进程/删除目录与 unit。
  4. 安装前校验:检查 mysqld进程、端口占用、安装目录/数据目录是否仍存在残留。
  5. 系统基线优化:按发行版执行防火墙/SELinux/SWAP/limits/时区/GRUB 等配置。
  6. 安装部署:创建 mysql 用户/组、解压二进制包、写入 my.cnf。
  7. 初始化与启动:初始化数据目录、生成 systemd 服务、启动并等待就绪。
  8. 账号与免密:重置 root 密码并写 /root/.my.cnf。
  9. 备份与定时任务:生成备份脚本并安装 crontab 定时任务。
  10. 总结输出:打印关键路径、端口、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 实例:

  1. ✅ 实例基础信息:版本、运行时间、字符集、时区
  2. ✅ 连接与线程:当前连接数、最大连接使用率、线程缓存命中率
  3. ✅ 性能瓶颈:QPS/TPS、全表扫描次数、磁盘临时表比例、排序溢出
  4. ✅ InnoDB 健康度:缓冲池命中率、脏页比例、死锁次数
  5. ✅ 安全风险:空密码账户、匿名用户、root 远程登录、SSL 是否启用
  6. ✅ 主从复制状态:IO/SQL 线程、延迟、错误信息(自动识别)
  7. ✅ 表结构隐患:无主键表、超大表(>5GB)、高碎片表 Top20
  8. ✅ 日志与备份建议:慢查询、binlog 是否开启、保留策略是否合理
  9. ✅ 配置合规性检查:innodb_flush_log_at_trx_commit、sync_binlog 等关键参数是否安全
  10. 🎯 所有结果以可视化 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()
相关推荐
星梦清河2 小时前
MySQL—分组函数
数据库·mysql
霖霖总总2 小时前
[小技巧33]MySQL 事务持久化的一致性保障:binlog 与 redo log 的两阶段提交机制解析
数据库·mysql
消失的旧时光-19435 小时前
第五课:数据库不是存数据那么简单 —— MySQL 与索引的后端视角
数据库·mysql
nice_lcj5205 小时前
MySQL中GROUP_CONCAT函数详解 | 按日期分组拼接销售产品经典案例
数据库·mysql
·云扬·6 小时前
MySQL索引实战指南:添加场景、联合索引要点与失效场景解析
数据库·mysql
小白考证进阶中6 小时前
MySQL OCP认证可以考中文?备考难度怎么样?
数据库·mysql·dba·数据库管理·开闭原则·数据库管理员·mysql认证
小冷coding7 小时前
【Java】以 Java + Redis + MySQL 为技术栈,模拟电商商品详情的读写场景,Cache Aside+ 延迟双删 方案
java·redis·mysql
韩立学长8 小时前
【开题答辩实录分享】以《足球球员数据分析系统开题报告》为例进行选题答辩实录分享
java·数据库·mysql
小-黯8 小时前
QT编译MySQL驱动教程(Windows/Linux)
windows·qt·mysql