linux系统二进制安装MySQL 8.4、8.0版本数据库,配置crontab和xtrabackup数据库热备份脚本

Rockylinux系统基础信息

我这一台服务器是Rockylinux10.1系统,最小化安装,二进制安装MySQL8.4.7版本。

1、下载MySQL数据库资源包

提供两种下载方式
1.1
下载不会保留压缩包,此命令是直接将压缩包中的内容直接解压到**/usr/local/**目录

bash 复制代码
mkdir -p /usr/local/src/mysql && \
curl -Ls https://dev.mysql.com/get/Downloads/MySQL-8.4/mysql-8.4.7-linux-glibc2.28-x86_64.tar.xz | \
tar -Jxf - -C /usr/local/
bash 复制代码
mv /usr/local/mysql-8.4.7-linux-glibc2.28-x86_64 /usr/local/mysql

关键参数解释
curl -Ls:

-L:自动跟随重定向(MySQL 下载链接可能有跳转);

-s:静默模式,不输出下载进度等冗余信息(可选,去掉 -s 可看到下载进度)。
tar -Jxf -:

-J:对应 .xz 格式的解压(如果是 .gz 用 -z);

-f -:表示从标准输入(curl 输出的数据流)读取压缩包内容,而不是从文件读取;

-C /usr/local/src/mysql:指定解压的目标目录,替代 cd 操作。

1.2
下载会保留压缩包

bash 复制代码
mkdir -p /usr/local/src/mysql && \
curl -Ls -o /usr/local/src/mysql/mysql-8.4.7-linux-glibc2.28-x86_64.tar.xz https://dev.mysql.com/get/Downloads/MySQL-8.4/mysql-8.4.7-linux-glibc2.28-x86_64.tar.xz && \
tar -Jxf /usr/local/src/mysql/mysql-8.4.7-linux-glibc2.28-x86_64.tar.xz -C /usr/local/src/mysql
bash 复制代码
mv /usr/local/src/mysql/mysql-8.4.7-linux-glibc2.28-x86_64 /usr/local/mysql

1.1如果是在容器可能缺失的依赖包,建议安装

bash 复制代码
dnf -y install libaio numactl-libs numactl perl perl-Data-Dumper ncurses-compat-libs openssl-libs procps-ng bc mailx

mail工具是用来备份邮件通知的。

2、MySQL数据库配置文件

bash 复制代码
cat >/etc/my.ini << EOF
[mysqld]

port = 3306
basedir = /usr/local/mysql
datadir = /usr/local/mysql/data
socket = /usr/local/mysql/mysqld.sock
pid-file = /usr/local/mysql/mysqld.pid

# Authentication
mysql_native_password = ON
#启用 MySQL 原生的密码认证插件,允许使用旧版密码认证

# Character Set
character_set_server = utf8mb4
#设置 MySQL 服务器的默认字符集为 utf8mb4,utf8mb4 是目前最推荐的编码方式
collation_server = utf8mb4_unicode_ci
#设置服务器的默认排序规则
init_connect = 'SET NAMES utf8mb4'
#每次客户端连接时,自动执行这条 SQL 语句(当用户连接到 MySQL 时,自动设置连接使用的字符集为 utf8mb4)

# Connection
bind-address = 0.0.0.0
#MySQL 监听所有网络接口,告诉 MySQL 在哪些 IP 地址上接受连接请求
max_connections = 1000
#MySQL 允许的最大客户端连接数,同时最多能有 1000 个客户端连接到 MySQL 服务器
max_connect_errors = 10000
#允许的最大连接错误次数,当一个客户端连续连接失败达到这个次数后,MySQL 会禁止该客户端继续连接(防止暴力破解)
connect_timeout = 60
#MySQL 服务器等待客户端完成握手的最大秒数,客户端发起 TCP 连接后,必须在 60 秒内完成 MySQL 认证握手,否则连接会被断开
interactive_timeout = 28800
#交互式连接的空闲超时时间
wait_timeout = 3600
#非交互式连接的空闲超时时间

# InnoDB
innodb_buffer_pool_size = 4G
innodb_buffer_pool_instances = 8
innodb_redo_log_capacity = 2G
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_file_per_table = ON
innodb_io_capacity = 2000
innodb_read_io_threads = 8
innodb_write_io_threads = 8

# Logs - Simple structure
log-error = /usr/local/mysql/logs/mysql_error.log

slow_query_log = ON
slow_query_log_file = /usr/local/mysql/logs/mysql_slow.log
long_query_time = 10

# Binary logs - separate directory (generates many files)
log-bin = /usr/local/mysql/binlogs/mysql-bin
binlog_expire_logs_seconds = 15552000
max_binlog_size = 512M
sync_binlog = 100

general_log = OFF
#关闭通用查询日志,general_log 是 MySQL 的通用查询日志,记录所有发往 MySQL 服务器的 SQL 语句,OFF 是正确的选择。除非特殊调试需求,否则不要在生产环境开启 general_log
general_log_file = /usr/local/mysql/logs/mysql_general.log

# Performance
tmp_table_size = 64M
#内存临时表的最大大小,当 MySQL 执行查询需要创建临时表时,优先使用内存,64M 就是内存临时表允许的最大值
max_heap_table_size = 64M
#内存表(MEMORY 引擎表)的最大大小,限制使用 ENGINE=MEMORY 创建的内存表最大能有多大,超过 64M 会报错
max_allowed_packet = 64M
#MySQL 服务器允许接收的最大数据包大小,客户端与 MySQL 服务器通信时,单个数据包(SQL语句、传输的数据)的最大限制是 64MB
thread_cache_size = 100
#缓存线程的数量,MySQL 处理完客户端连接后,不立即销毁线程,而是缓存起来,下次有新连接时复用
sort_buffer_size = 4M
#每个会话排序操作分配的缓冲区大小,当 MySQL 执行 ORDER BY 或 GROUP BY 需要排序时,为每个连接分配 4MB 内存用于排序操作
read_buffer_size = 2M
#每个会话的 MyISAM 表顺序读缓冲区大小,这个参数主要影响 MyISAM 引擎,对 InnoDB 影响很小。您使用的是 InnoDB,所以这个参数的影响有限。
join_buffer_size = 4M
#每个会话用于连接操作(JOIN)的缓冲区大小,当 MySQL 执行连接查询(多表关联)且无法使用索引时,会分配这个缓冲区来存储中间结果
table_open_cache = 4096
#MySQL 能够同时打开的表文件缓存数量,MySQL 每打开一个表,就会把表文件描述符缓存起来,避免重复打开。这个值决定了能缓存多少个表

server-id = 1
#MySQL 实例分配唯一的 ID 号
gtid_mode = ON
#开启 GTID(全局事务标识符)模式,GTID 为每个提交的事务分配一个全局唯一的 ID,格式为 server_uuid:transaction_id
enforce_gtid_consistency = ON
#强制 MySQL 只允许 GTID 安全的事务,这个参数是 GTID 模式的安全开关,禁止那些会导致 GTID 不一致的操作
lower_case_table_names = 1
#表名存储为小写,比较时不区分大小写
open_files_limit = 65535
#MySQL 进程允许打开的最大文件句柄数,MySQL 每打开一个表文件、日志文件、临时文件都会占用一个文件句柄。这个参数限制了 MySQL 能同时打开多少个文件
performance_schema = ON
#开启 MySQL 性能监控数据库,Performance Schema 是 MySQL 内置的性能监控工具,用于收集数据库运行时的各种性能指标
mysqlx = OFF
#关闭 MySQL X Plugin 插件,

[mysql]
prompt = "\\u@\\h : \\d \\r:\\m:> "

[client]
#user = 
#password = 
port = 3306
socket = /usr/local/mysql/mysqld.sock
EOF

3、设置硬链接

bash 复制代码
ln /etc/my.ini /etc/my.cnf

4、创建MySQL运行用户

bash 复制代码
groupadd -r mysql && useradd -g mysql -d /home/mysql -m -s /bin/bash mysql

5、创建MySQL数据库目录

bash 复制代码
mkdir -p /usr/local/mysql/data \
         /usr/local/mysql/logs \
         /usr/local/mysql/binlogs
bash 复制代码
chown -R mysql:mysql /usr/local/mysql

6、初始化MySQL数据库

bash 复制代码
cd /usr/local/mysql
bash 复制代码
/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.ini --initialize --user=mysql

7、启动MySQL数据库

bash 复制代码
/usr/local/mysql/support-files/mysql.server start

7.1创建远程连接root用户并设置密码

临时密码登录
bash 复制代码
/usr/local/mysql/bin/mysql -uroot -p$(grep 'temporary password' /usr/local/mysql/logs/mysql_error.log | awk -F'root@localhost: ' '{print $2}' | tail -1)
设置root用户密码,配置远程连接
sql 复制代码
  ALTER USER 'root'@'localhost' IDENTIFIED BY 'jingyu@2026';
  CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'jingyu@2026';
  GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
sql 复制代码
CREATE USER 'backup'@'%' IDENTIFIED BY 'jingyu@2026';
GRANT RELOAD, LOCK TABLES, BACKUP_ADMIN, PROCESS, SELECT, 
      SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT 
ON *.* TO 'backup'@'%';
FLUSH PRIVILEGES;
exit
7.2 IP/网段登录

如果想严格限制IP/网段,禁止@'%',示例:仅允许172.168.6.%网段登录)

sql 复制代码
CREATE USER 'backup'@'172.168.6.%' IDENTIFIED BY 'jingyu@1626';
GRANT 
  RELOAD, LOCK TABLES, BACKUP_ADMIN, PROCESS, SELECT, SHOW DATABASES,
  REPLICATION SLAVE, REPLICATION CLIENT
ON *.* TO 'backup'@'172.168.6.%';

示例:
创建备份用户,用户名:percona,授权远程连接

sql 复制代码
  CREATE USER IF NOT EXISTS 'percona'@'%' IDENTIFIED BY 'besp@2026'; \
  GRANT ALL PRIVILEGES ON *.* TO 'percona'@'%' WITH GRANT OPTION; \
  FLUSH PRIVILEGES;
8、防火墙放行3306端口
bash 复制代码
firewall-cmd --add-port=3306/tcp --permanent && \
firewall-cmd --reload
9、添加systemctl启动脚本
bash 复制代码
vim /etc/systemd/system/mysqld.service
bash 复制代码
[Unit]
Description=MySQL Server https://blog.csdn.net/2301_77161927
Documentation=man:mysqld(8)
Documentation=https://blog.csdn.net/2301_77161927    ;    http://dev.mysql.com/doc/refman/en/using-systemd.html
After=network.target
After=syslog.target

[Install]
WantedBy=multi-user.target

[Service]
User=root
Group=root
Type=forking
TimeoutSec=0
PermissionsStartOnly=true
#ExecStartPre=/usr/bin/mkdir -p /usr/local/mysql/logs/out /usr/local/mysql/logs/slow /usr/local/mysql/logs/general /usr/local/mysql/log_bin
#可选,启动之前创建MySQL数据库所需目录
#ExecStartPre=/usr/bin/chmod 755 /usr/local/mysql/logs/out /usr/local/mysql/logs/slow /usr/local/mysql/logs/general /usr/local/mysql/log_bin
#可选,启动之前授权MySQL数据库目录
ExecStart=/usr/local/mysql/support-files/mysql.server start
ExecStop=/usr/local/mysql/support-files/mysql.server stop
ExecReload=/usr/local/mysql/support-files/mysql.server reload
Restart=on-failure
RestartPreventExitStatus=1
PrivateTmp=false
LimitNOFILE=65535
LimitNPROC=65535
LimitMEMLOCK=infinity

# 端口监控配置
ExecStartPost=/bin/sh -c 'COUNT=0; while [ $COUNT -lt 30 ]; do sleep 1; COUNT=$((COUNT + 1)); if ss -tlnp | grep -q ":3306"; then echo "MySQL port 3306 is ready"; break; fi; done; if [ $COUNT -eq 30 ]; then echo "Timeout waiting for MySQL port 3306" >&2; exit 1; fi'

# PID文件监控
PIDFile=/usr/local/mysql/mysqld.pid

# 启动前检查PID文件是否存在
ExecStartPre=/bin/sh -c 'if [ -f /usr/local/mysql/mysqld.pid ]; then /bin/rm -f /usr/local/mysql/mysqld.pid; fi'

# 启动后验证PID文件
ExecStartPost=/bin/sh -c 'sleep 2; if [ ! -f /usr/local/mysql/mysqld.pid ]; then echo "PID file not created" >&2; exit 1; fi'

# 停止前检查
ExecStopPost=/bin/sh -c 'COUNT=0; while [ $COUNT -lt 30 ]; do sleep 1; COUNT=$((COUNT + 1)); if ! ss -tlnp | grep -q ":3306"; then echo "MySQL port 3306 is closed"; break; fi; done'

# 环境变量
Environment="PATH=/usr/local/mysql/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin"
Environment="LD_LIBRARY_PATH=/usr/local/mysql/lib"

# 日志配置
StandardOutput=journal
StandardError=journal
10、配置profile永久环境变量

echo命令,重复执行会重复追加,导致文件中有多份相同配置

bash 复制代码
echo "
# MySQL databases environment variables
export MYSQL_HOME=/usr/local/mysql
export PATH=\${MYSQL_HOME}/bin:\${MYSQL_HOME}/support-files:\${PATH}" >> /etc/profile

sed命令,重复执行也不会重复添加,始终保持只有一份配置

bash 复制代码
sed -i '/# MySQL databases environment variables/,+2d' /etc/profile
bash -c 'cat >> /etc/profile << EOF

# MySQL databases environment variables
export MYSQL_HOME=/usr/local/mysql
export PATH=\${MYSQL_HOME}/bin:\${MYSQL_HOME}/support-files:\${PATH}
EOF'
bash 复制代码
source /etc/profile
11、配置xtrabackup工具

percona-xtrabackup官网下载地址链接🔗

查看 GNU C 库(glibc)版本

bash 复制代码
ldd --version

输出信息解读

ldd (GNU libc) 2.39:表示当前系统的glibc版本是2.39

Copyright:版权信息

Free Software Foundation:自由软件基金会

为什么重要(特别是在MySQL安装时)

  1. MySQL二进制版本兼容性
    MySQL官方提供的预编译二进制包都是针对特定glibc版本编译的:
bash 复制代码
# MySQL下载页面常见的版本标识
mysql-8.0.36-linux-glibc2.12-x86_64.tar.xz  # 需要glibc 2.12或更高
mysql-8.0.36-linux-glibc2.17-x86_64.tar.xz  # 需要glibc 2.17或更高
mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz  # 需要glibc 2.12或更高

如果系统glibc版本低于MySQL要求的版本,启动时会报错:

/lib64/libc.so.6: version `GLIBC_2.18' not found

其他查看glibc版本的方法
bash 复制代码
# 方法1:使用getconf
getconf GNU_LIBC_VERSION
# 方法2:直接查看libc库
/lib64/libc.so.6 --version
# 方法3:查看rpm包信息(CentOS/RHEL)
rpm -q glibc
# 方法4:查看动态链接器版本
/lib64/ld-linux-x86-64.so.2 --version

常见glibc版本对应关系

glibc版本 主要Linux发行版

2.12 CentOS/RHEL 6.x

2.17 CentOS/RHEL 7.x

2.28 CentOS/RHEL 8.x, Rocky Linux 8.x

2.34 Rocky Linux 9.x, RHEL 9.x

当遇到"GLIBC_X.X not found"错误时,用于确认系统版本

11.1、安装xtrabackup backup8.4二进制版本

创建并安装到 /usr/local/xtrabackup

bash 复制代码
mkdir -p /usr/local/src/xtrabackup && \
curl -Ls -o /usr/local/src/xtrabackup/percona-xtrabackup-8.4.0-5-Linux-x86_64.glibc2.39.tar.gz https://downloads.percona.com/downloads/Percona-XtraBackup-8.4/Percona-XtraBackup-8.4.0-5/binary/tarball/percona-xtrabackup-8.4.0-5-Linux-x86_64.glibc2.39.tar.gz && \
tar -zxf percona-xtrabackup-8.4.0-5-Linux-x86_64.glibc2.39.tar.gz -C /usr/local/src/xtrabackup && \
mv /usr/local/src/xtrabackup/percona-xtrabackup-8.4.0-5-Linux-x86_64.glibc2.39 /usr/local/xtrabackup
11.2、配置xtrabackup永久环境变量
bash 复制代码
cat >> /etc/profile << EOF

# XtraBackup environment variables
export PERCONA_XTRABACKUP_HOME=/usr/local/xtrabackup
export PATH=\${PERCONA_XTRABACKUP_HOME}/bin:\${PATH}
EOF
bash 复制代码
source /etc/profile
bash 复制代码
crontab -e
bash 复制代码
0 21 * * 0 /opt/scripts/xtrabackup_MySQL_all.sh >> /var/log/xtrabackup_full.log 2>&1
0 21 * * 1-6 /opt/scripts/xtrabackup_MySQL_inc.sh >> /var/log/xtrabackup_inc.log 2>&1
13.配置Linux系统发送邮件

如果是公司自建的企业邮件,可以测试系统发送邮件是否能收到,如果能收到则不需要配置,使用第三方邮件的话,一般可能由于信誉等级低第三方拒绝收导致不能收到系统发送的邮件。

建议使用我的github仓库中的docker容器,修改enterpoint.sh脚本中三个值,docker compose up -d即可发送邮件。文档有新浪sina和QQ邮箱授权码获取方式。

bash 复制代码
git pull git@github.com:jingyu1610/mail.git
bash 复制代码
git pull https://github.com/jingyu1610/mail.git

测试命令

bash 复制代码
docker exec jingyu-mail-sender bash -c 'echo -e "Subject: Test\n\n$(date)" | sendmail -t 收件邮箱'

jingyu-mail-sender是默认容器名称,如果修改了,记得修改测试命令中的容器名称。

、配置xtrabackup热备份脚本(全备)
bash 复制代码
dnf -y install bc mailx procps-ng net-tools
bash 复制代码
vim /opt/scripts/xtrabackup_MySQL_all.sh
bash 复制代码
#!/bin/bash

# ========================================================================================
# 脚本免责声明 | SCRIPT DISCLAIMER    author: jingyu飞鸟 https://blog.csdn.net/2301_77161927
# ========================================================================================
#
# 【中文】
# 本脚本为内部运维工具,用于自动化MySQL数据库的热备份全备。
# 使用前请注意:
#   - 请在测试环境完整测试脚本功能
#   - 备份所有重要数据和配置文件
#   - 根据实际系统环境调整参数
#
# 免责声明: 本脚本按原样提供,不提供任何明示或暗示的保证。
# 尽管本脚本经过充分测试,但由于系统环境差异、网络状况、配置变更等不可控因素,
# 无法保证在所有环境下都能完美运行。使用本脚本所产生的任何后果,
# 包括但不限于数据丢失、服务中断等,均由使用者自行承担。
#
# 【English】
# This script is an internal operation tool for automated MySQL
# database installation and configuration. Please note:
#   - Test thoroughly in a staging environment first
#   - Backup all important data and configuration files
#   - Adjust parameters according to your system environment
#
# Disclaimer: This script is provided "AS IS", without any express or
# implied warranties. Although this script has been thoroughly tested,
# due to uncontrollable factors such as system environment differences,
# network conditions, and configuration variations, perfect operation
# cannot be guaranteed in all environments. Any consequences arising
# from the use of this script, including but not limited to data loss
# or service interruptions, shall be borne by the user.
# ============================================================================
# Author: jingyu飞鸟
# Blog: https://blog.csdn.net/2301_77161927?spm=1000.2115.3001.5343
# Feature: MySQL Full Hot Backup with Percona XtraBackup (Binary Installation)
# Usage: Run this script directly, test first in non-production environment
# ============================================================================

# ------------------------ Environment Setup ------------------------
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export MYSQL_HOME="/usr/local/mysql"
export PERCONA_XTRABACKUP_HOME="/usr/local/xtrabackup"
export PATH="${MYSQL_HOME}/bin:${PERCONA_XTRABACKUP_HOME}/bin:${PATH}"

# ------------------------ MySQL Configuration ------------------------
MYSQL_CNF="/etc/my.cnf"				# MySQL configuration file location
MYSQL_USER="root"					# MySQL username for backup operations
MYSQL_PASSWORD="jingyu@2026"			# MySQL user password
MYSQL_PORT="3306"					# MySQL server port number
MYSQL_SOCKET_FILE="/usr/local/mysql/mysqld.sock"        # MySQL socket file path
MYSQL_DATA_DIR="/usr/local/mysql/data"		# MySQL data directory location

# ------------------------ Backup Directory Configuration ------------------------
BACKUP_BASE="/opt/percona/xtrabackup/mysql"		# Root directory for all backups
FULL_BACKUP_DIR="${BACKUP_BASE}/all_ready"		# Storage location for full backups
INC_BACKUP_DIR="${BACKUP_BASE}/insert_backup"           # Storage location for incremental backups
LOG_DIR="${BACKUP_BASE}/logs"			# Directory for backup log files

# ------------------------ Retention Policy ------------------------
RETENTION_DAYS=30					# Number of days to keep backups (advisory only)

# ------------------------ Email Notification Configuration ------------------------
EMAIL_TO="sed123@gmail.com awk456@sina.com"	# Recipient email addresses for notifications
SEND_EMAIL_NAME="grep123@gmail.com"			# Sender email address
PROJECT_NAME="MySQL8.4.7_xtrabackup"			# Project identifier for email subjects

# Create required directories if they don't exist
mkdir -p "${FULL_BACKUP_DIR}" "${LOG_DIR}"

# ------------------------ Global Variables ------------------------
BACKUP_DATE=$(date +"%Y%m%d_%H%M%S")		# Unique timestamp for this backup
SCRIPT_START_TIME=$(date '+%Y-%m-%d %H:%M:%S') 	# Human-readable start time
SCRIPT_START_SECONDS=$(date +%s)			# Start time in seconds for duration calc
HOSTNAME=$(hostname)				# Server hostname

# Detect server IP address (prefer non-loopback)
IP_ADDR=$(ip -o addr show | awk '/inet / && !/127\.0\.0\.1/ {print $4}' | cut -d'/' -f1 | head -1)
[ -z "$IP_ADDR" ] && IP_ADDR=$(hostname -I | awk '{print $1}')

EXPIRY_DATE=$(date -d "+${RETENTION_DAYS} days" '+%Y-%m-%d')	# When this backup expires
LOG_FILE="${LOG_DIR}/xtrabackup_full_${BACKUP_DATE}.log"		# Full path to log file
BACKUP_NAME="full_backup_${BACKUP_DATE}"				# Name of backup directory
BACKUP_PATH="${FULL_BACKUP_DIR}/${BACKUP_NAME}"			# Full path to backup directory
STATE_FILE="${BACKUP_BASE}/.backup_state"				# File tracking backup state for incremental

# ------------------------ Secure Password Handling ------------------------
# Create temporary config file to avoid password exposure in process lists
TEMP_CNF=$(mktemp /tmp/.MySQL_XXXXXX.cnf)
chmod 600 "$TEMP_CNF"
cat > "$TEMP_CNF" <<EOF
[client]
user=${MYSQL_USER}
password=${MYSQL_PASSWORD}
socket=${MYSQL_SOCKET_FILE}
EOF

# Ensure temporary file is removed on script exit
trap 'rm -f "${TEMP_CNF}"' EXIT SIGQUIT ERR

# ========================================================================================
# Core Functions
# ========================================================================================

# Logging function - writes to both console and log file
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
}

# Calculate human-readable duration between two timestamps
# Args: start_seconds, end_seconds
calculate_duration() {
    local start_sec=$1
    local end_sec=$2
    local duration=$((end_sec - start_sec))
    
    if [ $duration -lt 60 ]; then
        echo "${duration} seconds"
    elif [ $duration -lt 3600 ]; then
        echo "$((duration/60)) minutes $((duration%60)) seconds"
    elif [ $duration -lt 86400 ]; then
        echo "$((duration/3600)) hours $(((duration%3600)/60)) minutes $((duration%60)) seconds"
    else
        echo "$((duration/86400)) days $(((duration%86400)/3600)) hours $(((duration%3600)/60)) minutes $((duration%60)) seconds"
    fi
}

# Convert bytes to human-readable format (B, KB, MB, GB)
# Args: size_in_bytes
format_size() {
    local size=$1
    if [ -z "$size" ] || [ "$size" -eq 0 ]; then
        echo "0 B"
        return
    fi
    if [ $size -ge 1073741824 ]; then
        echo "$(echo "scale=2; $size/1073741824" | bc) GB"
    elif [ $size -ge 1048576 ]; then
        echo "$(echo "scale=2; $size/1048576" | bc) MB"
    elif [ $size -ge 1024 ]; then
        echo "$(echo "scale=2; $size/1024" | bc) KB"
    else
        echo "${size} B"
    fi
}

# Get file permissions in numeric and symbolic format
# Args: file_path
get_permission() {
    file=$1; [ ! -e "$file" ] && echo "000 (---)" && return
    perm=$(stat -c "%a" "$file" 2>/dev/null | grep -o '^[0-7][0-7][0-7]$'); [ -z "$perm" ] && perm="000"
    rwx=""
    case "$(echo $perm | cut -c1)" in 0) rwx="${rwx}---";; 4) rwx="${rwx}r--";; 5) rwx="${rwx}r-x";; 6) rwx="${rwx}rw-";; 7) rwx="${rwx}rwx";; *) rwx="${rwx}---";; esac
    case "$(echo $perm | cut -c2)" in 0) rwx="${rwx}---";; 4) rwx="${rwx}r--";; 5) rwx="${rwx}r-x";; 6) rwx="${rwx}rw-";; 7) rwx="${rwx}rwx";; *) rwx="${rwx}---";; esac
    case "$(echo $perm | cut -c3)" in 0) rwx="${rwx}---";; 4) rwx="${rwx}r--";; 5) rwx="${rwx}r-x";; 6) rwx="${rwx}rw-";; 7) rwx="${rwx}rwx";; *) rwx="${rwx}---";; esac
    echo "$perm ($rwx)"
}

# ========================================================================================
# Pre-flight Checks
# ========================================================================================

# Verify Percona XtraBackup is installed and accessible
check_xtrabackup_installed() {
    log "Checking Percona XtraBackup installation..."
    if ! command -v xtrabackup >/dev/null 2>&1; then
        log "ERROR: xtrabackup command not found"
        log "Please verify: ls -la /usr/local/xtrabackup/bin/xtrabackup"
        return 1
    fi
    
    local version=$(xtrabackup --version 2>&1 | head -1)
    log "XtraBackup version: ${version}"
    return 0
}

# Test MySQL connection using credentials
check_mysql_connection() {
    log "Testing MySQL connection..."
    if mysql --defaults-extra-file="$TEMP_CNF" -e "SELECT 1" >/dev/null 2>&1; then
        log "MySQL connection successful"
        return 0
    else
        log "MySQL connection failed"
        return 1
    fi
}

# Display MySQL version information
check_mysql_version() {
    log "Checking MySQL version..."
    local version=$(mysql --defaults-extra-file="$TEMP_CNF" -e "SELECT VERSION();" -N 2>/dev/null)
    if [ -n "$version" ]; then
        log "MySQL version: ${version}"
    else
        log "Unable to retrieve MySQL version"
    fi
}

# Verify MySQL port is listening
check_port() {
    log "Checking MySQL port ${MYSQL_PORT} listening status..."
    if ss -tlnp 2>/dev/null | grep -q ":${MYSQL_PORT}"; then
        log "MySQL port ${MYSQL_PORT} is listening"
        return 0
    else
        log "MySQL port ${MYSQL_PORT} is not listening"
        return 1
    fi
}

# Verify MySQL process is running
check_process_status() {
    log "Checking MySQL process status..."
    if pgrep -f "mysqld" >/dev/null 2>&1; then
        log "MySQL process is running"
        return 0
    else
        log "MySQL process is not running"
        return 1
    fi
}

# Verify backup user has required privileges
check_backup_user_privileges() {
    log "Checking backup user privileges..."
    
    local priv_check=$(mysql --defaults-extra-file="$TEMP_CNF" -N -e "
        SELECT COUNT(*) FROM information_schema.USER_PRIVILEGES
        WHERE GRANTEE LIKE '%${MYSQL_USER}%'
        AND PRIVILEGE_TYPE IN ('BACKUP_ADMIN', 'RELOAD', 'LOCK TABLES', 'PROCESS', 'REPLICATION_CLIENT');" 2>/dev/null)
    
    if [ -n "$priv_check" ] && [ "$priv_check" -ge 3 ]; then
        log "Backup user privileges sufficient (matched ${priv_check} items)"
        return 0
    else
        log "WARNING: Backup user privileges may be insufficient"
        log "Recommended SQL to grant privileges:"
        log "  GRANT BACKUP_ADMIN, RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON *.* TO '${MYSQL_USER}'@'%';"
        log "  FLUSH PRIVILEGES;"
        return 0
    fi
}

# Get MySQL runtime status (uptime and connection count)
get_mysql_status() {
    log "Retrieving MySQL runtime status..."
    local uptime=$(mysql --defaults-extra-file="$TEMP_CNF" -e "SHOW GLOBAL STATUS LIKE 'Uptime';" -N 2>/dev/null | awk '{print $2}')
    local connections=$(mysql --defaults-extra-file="$TEMP_CNF" -e "SHOW GLOBAL STATUS LIKE 'Threads_connected';" -N 2>/dev/null | awk '{print $2}')
    
    if [ -n "$uptime" ]; then
        local uptime_days=$((uptime / 86400))
        local uptime_hours=$(((uptime % 86400) / 3600))
        log "  MySQL uptime: ${uptime_days} days ${uptime_hours} hours"
        log "  Current connections: ${connections}"
        echo "${uptime}|${connections}"
    else
        log "  Unable to retrieve MySQL status"
        echo "0|0"
    fi
}

# ========================================================================================
# Space Check Functions (Warning Only - No Enforcement)
# ========================================================================================

# Calculate total database size in GB
get_database_size() {
    local size_gb=0
    
    # Try to get size from information_schema first
    local size_info=$(mysql --defaults-extra-file="$TEMP_CNF" -N -e "
        SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2)
        FROM information_schema.tables
        WHERE table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys');" 2>/dev/null)
    
    if [ -n "$size_info" ] && [ "$size_info" != "NULL" ]; then
        size_gb=$size_info
    else
        # Fallback to filesystem calculation
        if [ -d "$MYSQL_DATA_DIR" ]; then
            local total_bytes=$(du -sb "$MYSQL_DATA_DIR" 2>/dev/null | awk '{print $1}')
            if [ -n "$total_bytes" ] && [ "$total_bytes" -gt 0 ]; then
                size_gb=$(echo "scale=2; $total_bytes/1073741824" | bc 2>/dev/null)
            fi
        fi
    fi
    
    # Ensure we have a valid number (default to 2GB if unknown)
    if [ -z "$size_gb" ] || [ "$(echo "$size_gb <= 0" | bc 2>/dev/null)" -eq 1 ]; then
        size_gb="2.00"
    fi
    
    echo "$size_gb"
}

# Check disk space availability (warning only, does not block backup)
# Args: database_size_gb
check_disk_space() {
    local db_size=$1
    local required_gb=$(echo "scale=2; $db_size * 2 + 5" | bc 2>/dev/null)
    
    log "========== Disk Space Check (Advisory Only) =========="
    log "Database size: ${db_size} GB"
    log "Estimated space required: ${required_gb} GB"
    
    if ! command -v bc >/dev/null 2>&1; then
        log "bc command not installed, skipping space check"
        log "Install with: dnf install -y bc"
        return 0
    fi
    
    local disk_info=$(df -BG "$BACKUP_BASE" 2>/dev/null | awk 'NR==2')
    if [ -z "$disk_info" ]; then
        log "Unable to retrieve disk information, skipping space check"
        return 0
    fi
    
    local available_gb=$(echo "$disk_info" | awk '{print $4}' | sed 's/G//')
    local used_percent=$(echo "$disk_info" | awk '{print $5}' | sed 's/%//')
    local total_gb=$(echo "$disk_info" | awk '{print $2}' | sed 's/G//')
    
    log "Partition total space: ${total_gb} GB"
    log "Available space: ${available_gb} GB"
    log "Used percentage: ${used_percent}%"
    
    if [ -n "$available_gb" ] && [ -n "$required_gb" ] && [ "$(echo "$available_gb < $required_gb" | bc 2>/dev/null)" -eq 1 ]; then
        log "WARNING: Available space ${available_gb}GB is less than estimated requirement ${required_gb%.*}GB"
        log "Backup will continue, please monitor disk usage"
    else
        log "Space appears sufficient"
    fi
    
    return 0
}

# ========================================================================================
# Email Notification Function
# ========================================================================================

# Send email notification about backup status
# Args: status, reason, backup_file, start_time, end_time, uptime_info, thread_info
send_email_notification() {
    local status=$1
    local reason=$2
    local backup_file=$3
    local start_time=$4
    local end_time=$5
    local uptime_info=${6:-"Unknown"}
    local thread_query_info=${7:-"Unknown"}
    
    local start_seconds=$(date -d "$start_time" +%s 2>/dev/null)
    local end_seconds=$(date -d "$end_time" +%s 2>/dev/null)
    [ -z "$start_seconds" ] && start_seconds=$SCRIPT_START_SECONDS
    [ -z "$end_seconds" ] && end_seconds=$(date +%s)
    
    local DURATION=$(calculate_duration $start_seconds $end_seconds)
    local DATE_TAG=$(date -d "$start_time" "+%Y-%m-%d" 2>/dev/null)
    [ -z "$DATE_TAG" ] && DATE_TAG=$(date "+%Y-%m-%d")
    
    local FSIZE="0 B"
    local FPERM="000 (---)"
    if [ "$status" = "success" ] && [ -e "$backup_file" ]; then
        local raw_size=$(du -sb "$backup_file" 2>/dev/null | awk '{print $1}')
        [ -n "$raw_size" ] && FSIZE=$(format_size "$raw_size")
        FPERM=$(get_permission "$backup_file")
    fi
    
    # Skip email if mail command is not available
    if ! command -v mail >/dev/null 2>&1; then
        log "mail command not installed, skipping email notification"
        log "Install with: dnf install -y mailx"
        return 0
    fi
    
    if [ "$status" = "success" ]; then
        SUBJECT="[SUCCESS] ${PROJECT_NAME}_Full_Backup_${DATE_TAG}"
        EMAIL_CONTENT="MySQL Full Backup Completed Successfully
================================
Server IP: ${IP_ADDR}
Hostname: ${HOSTNAME}
Project: ${PROJECT_NAME}
Backup Type: Full Backup
Start Time: ${start_time}
End Time: ${end_time}
Duration: ${DURATION}
Uptime: ${uptime_info}
Threads: ${thread_query_info}
Backup Path: ${backup_file}
Log File: ${LOG_FILE}
Backup Size: ${FSIZE}
Backup Permissions: ${FPERM}
Retention Days: ${RETENTION_DAYS}
Expiry Date: ${EXPIRY_DATE}
================================
For assistance contact: jingyu飞鸟
https://blog.csdn.net/2301_77161927
"
        echo "$EMAIL_CONTENT" | tee -a "${LOG_FILE}"
        echo "$EMAIL_CONTENT" | mail -r "${SEND_EMAIL_NAME}" -s "${SUBJECT}" ${EMAIL_TO} 2>/dev/null
        log "Email notification sent"
    else
        SUBJECT="[ERROR] ${PROJECT_NAME}_Full_Backup_${DATE_TAG}"
        EMAIL_CONTENT="MySQL Full Backup Failed
================================
Server IP: ${IP_ADDR}
Hostname: ${HOSTNAME}
Project: ${PROJECT_NAME}
Backup Type: Full Backup
Start Time: ${start_time}
End Time: ${end_time}
Duration: ${DURATION}
Uptime: ${uptime_info}
Threads: ${thread_query_info}
Backup Path: ${backup_file:-None}
Log File: ${LOG_FILE}
Backup Size: 0 B
Backup Permissions: 000 (---)
Retention Days: ${RETENTION_DAYS}
Expiry Date: ${EXPIRY_DATE}
Failure Reason: ${reason}
================================
Please check log file for detailed error information
For assistance contact: jingyu飞鸟
https://blog.csdn.net/2301_77161927
"
        echo "$EMAIL_CONTENT" | tee -a "${LOG_FILE}"
        echo "$EMAIL_CONTENT" | mail -r "${SEND_EMAIL_NAME}" -s "${SUBJECT}" ${EMAIL_TO} 2>/dev/null
        log "Email notification sent"
    fi
}

# ========================================================================================
# Metadata Management (Critical for Incremental Backup Compatibility)
# ========================================================================================

# Save backup metadata for incremental backup reference
# Args: backup_path, backup_time
save_backup_metadata() {
    local backup_path=$1
    local backup_time=$2
    
    log "Saving backup metadata..."
    
    # Extract LSN (Log Sequence Number) from backup
    local lsn=""
    local checkpoints_file="${backup_path}/xtrabackup_checkpoints"
    if [ -f "$checkpoints_file" ]; then
        lsn=$(grep "last_lsn" "$checkpoints_file" | awk '{print $3}')
        [ -z "$lsn" ] && lsn=$(grep "to_lsn" "$checkpoints_file" | awk '{print $3}')
    fi
    
    # Save state file for incremental backup script
    cat > "${STATE_FILE}" <<EOF
BACKUP_TYPE=full
FULL_BACKUP_PATH=${backup_path}
BACKUP_PATH=${backup_path}
BACKUP_TIME=${backup_time}
BACKUP_LSN=${lsn}
LAST_BACKUP_PATH=${backup_path}
EOF
    
    # Create symlink pointing to latest full backup (used by incremental script)
    ln -snf "${backup_path}" "${FULL_BACKUP_DIR}/latest_full"
    
    log "Metadata saved successfully"
    log "  - LSN: ${lsn}"
    log "  - Symlink: ${FULL_BACKUP_DIR}/latest_full -> ${backup_path}"
}

# ========================================================================================
# Main Execution
# ========================================================================================

# Log script start
{
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ========================================"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Percona XtraBackup Full Backup Started"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Server: ${HOSTNAME} (${IP_ADDR})"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ========================================"
} | tee -a "${LOG_FILE}"

# 1. Pre-flight Checks
log "========== Pre-flight Checks =========="
check_xtrabackup_installed || exit 1
check_port || { send_email_notification "failure" "MySQL port not listening" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"; exit 1; }
check_process_status || { send_email_notification "failure" "MySQL process not running" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"; exit 1; }
check_mysql_connection || { send_email_notification "failure" "MySQL connection failed" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"; exit 1; }
check_mysql_version
get_mysql_status > /dev/null
check_backup_user_privileges

# 2. Space Check (Advisory Only)
log "========== Space Check =========="
DB_SIZE=$(get_database_size)
check_disk_space "$DB_SIZE"

# 3. Execute Backup
log "========== Starting Backup =========="
log "Backup directory: ${BACKUP_PATH}"
mkdir -p "$BACKUP_PATH" || { send_email_notification "failure" "Unable to create backup directory" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"; exit 1; }

BACKUP_START_TIME=$(date '+%Y-%m-%d %H:%M:%S')
BACKUP_START_SECONDS=$(date +%s)

# Perform full backup
if xtrabackup \
    --defaults-file="$MYSQL_CNF" \
    --host="localhost" \
    --port="$MYSQL_PORT" \
    --socket="$MYSQL_SOCKET_FILE" \
    --user="$MYSQL_USER" \
    --password="$MYSQL_PASSWORD" \
    --backup \
    --target-dir="$BACKUP_PATH" \
    2>> "${LOG_FILE}"; then
    
    BACKUP_END_TIME=$(date '+%Y-%m-%d %H:%M:%S')
    BACKUP_END_SECONDS=$(date +%s)
    log "Full backup completed"
    
    # 4. Prepare Backup for Incremental Compatibility
    # CRITICAL: --apply-log-only must be used here to allow future incremental backups
    log "========== Preparing Backup (--apply-log-only) =========="
    if xtrabackup --prepare --apply-log-only --target-dir="$BACKUP_PATH" 2>> "${LOG_FILE}"; then
        PREPARE_END_TIME=$(date '+%Y-%m-%d %H:%M:%S')
        PREPARE_END_SECONDS=$(date +%s)
        log "Prepare completed successfully - backup is now ready for incremental backups"
        
        # Save metadata for incremental backup script
        save_backup_metadata "$BACKUP_PATH" "$BACKUP_START_TIME"
        
        # Calculate backup size
        raw_size=$(du -sb "$BACKUP_PATH" 2>/dev/null | awk '{print $1}')
        formatted_size=$(format_size "$raw_size")
        log "Backup size: $formatted_size"
        
        # Get MySQL status for email
        MYSQL_STATUS=$(get_mysql_status)
        MYSQL_UPTIME_INFO=$(echo "$MYSQL_STATUS" | cut -d'|' -f1)
        MYSQL_THREAD_QUERY_INFO=$(echo "$MYSQL_STATUS" | cut -d'|' -f2)
        
        # Send success notification
        send_email_notification "success" "" "$BACKUP_PATH" "$BACKUP_START_TIME" "$PREPARE_END_TIME" "$MYSQL_UPTIME_INFO" "$MYSQL_THREAD_QUERY_INFO"
        
        # Final summary
        TOTAL_DURATION=$(calculate_duration $SCRIPT_START_SECONDS $PREPARE_END_SECONDS)
        log "========== Backup Completed =========="
        log "Total duration: ${TOTAL_DURATION}"
        log "Backup path: ${BACKUP_PATH}"
        log "Log file: ${LOG_FILE}"
        
        exit 0
    else
        error_msg="Prepare failed - backup cannot be used for incremental backups"
        log "ERROR: $error_msg"
        send_email_notification "failure" "$error_msg" "$BACKUP_PATH" "$BACKUP_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"
        exit 1
    fi
else
    error_msg="Full backup execution failed"
    log "ERROR: $error_msg"
    send_email_notification "failure" "$error_msg" "" "$BACKUP_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"
    exit 1
fi
11.5、配置xtrabackup热备份脚本(增备)
bash 复制代码
vim /opt/scripts/xtrabackup_MySQL_inc.sh
bash 复制代码
#!/bin/bash

# ========================================================================================
# 脚本免责声明 | SCRIPT DISCLAIMER    author: jingyu飞鸟 https://blog.csdn.net/2301_77161927
# ========================================================================================
#
# 【中文】
# 本脚本为内部运维工具,用于自动化MySQL数据库的热备份增备。
# 使用前请注意:
#   - 请在测试环境完整测试脚本功能
#   - 备份所有重要数据和配置文件
#   - 根据实际系统环境调整参数
#
# 免责声明: 本脚本按原样提供,不提供任何明示或暗示的保证。
# 尽管本脚本经过充分测试,但由于系统环境差异、网络状况、配置变更等不可控因素,
# 无法保证在所有环境下都能完美运行。使用本脚本所产生的任何后果,
# 包括但不限于数据丢失、服务中断等,均由使用者自行承担。
#
# 【English】
# This script is an internal operation tool for automated MySQL
# database incremental backup. Please note:
#   - Test thoroughly in a staging environment first
#   - Backup all important data and configuration files
#   - Adjust parameters according to your system environment
#
# Disclaimer: This script is provided "AS IS", without any express or
# implied warranties. Although this script has been thoroughly tested,
# due to uncontrollable factors such as system environment differences,
# network conditions, and configuration variations, perfect operation
# cannot be guaranteed in all environments. Any consequences arising
# from the use of this script, including but not limited to data loss
# or service interruptions, shall be borne by the user.
# ============================================================================
# Author: jingyu飞鸟
# Blog: https://blog.csdn.net/2301_77161927?spm=1000.2115.3001.5343
# Feature: MySQL Incremental Hot Backup with Percona XtraBackup
# Usage: Run after full backup, automatically applies increment to full backup
# Note: Recovery only needs the latest full backup directory
# ============================================================================

# ------------------------ Environment Setup ------------------------
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export MYSQL_HOME="/usr/local/mysql"
export PERCONA_XTRABACKUP_HOME="/usr/local/xtrabackup"
export PATH="${MYSQL_HOME}/bin:${PERCONA_XTRABACKUP_HOME}/bin:${PATH}"

# ------------------------ MySQL Configuration ------------------------
MYSQL_CNF="/etc/my.cnf"                     # MySQL configuration file location
MYSQL_USER="root"                           # MySQL username for backup operations
MYSQL_PASSWORD="jingyu@2026"                  # MySQL user password
MYSQL_PORT="3306"                           # MySQL server port number
MYSQL_SOCKET_FILE="/usr/local/mysql/mysqld.sock"  # MySQL socket file path
MYSQL_DATA_DIR="/usr/local/mysql/data"      # MySQL data directory location

# ------------------------ Backup Directory Configuration ------------------------
BACKUP_BASE="/opt/percona/xtrabackup/mysql" # Root directory for all backups
FULL_BACKUP_DIR="${BACKUP_BASE}/all_ready"  # Storage location for full backups
INC_BACKUP_DIR="${BACKUP_BASE}/insert_backup"  # Storage location for incremental backups
LOG_DIR="${BACKUP_BASE}/logs"               # Directory for backup log files

# ------------------------ Retention Policy ------------------------
RETENTION_DAYS=30                           # Number of days to keep backups (advisory only)

# ------------------------ Email Notification Configuration ------------------------
EMAIL_TO="sed123@gmail.com awk456@sina.com" # Recipient email addresses for notifications
SEND_EMAIL_NAME="grep123@gmail.com"         # Sender email address
PROJECT_NAME="MySQL8.4.7_xtrabackup"        # Project identifier for email subjects

# Create required directories if they don't exist
mkdir -p "${INC_BACKUP_DIR}" "${LOG_DIR}"

# ------------------------ Global Variables ------------------------
BACKUP_DATE=$(date +"%Y%m%d_%H%M%S")         		# Unique timestamp for this backup
SCRIPT_START_TIME=$(date '+%Y-%m-%d %H:%M:%S')  	# Human-readable start time
SCRIPT_START_SECONDS=$(date +%s)            			# Start time in seconds for duration calc
HOSTNAME=$(hostname)                        			# Server hostname

# Detect server IP address (prefer non-loopback)
IP_ADDR=$(ip -o addr show | awk '/inet / && !/127\.0\.0\.1/ {print $4}' | cut -d'/' -f1 | head -1)
[ -z "$IP_ADDR" ] && IP_ADDR=$(hostname -I | awk '{print $1}')

EXPIRY_DATE=$(date -d "+${RETENTION_DAYS} days" '+%Y-%m-%d')			# When this backup expires
LOG_FILE="${LOG_DIR}/xtrabackup_inc_${BACKUP_DATE}.log"					# Full path to log file
BACKUP_NAME="inc_backup_${BACKUP_DATE}"									# Name of incremental backup directory
BACKUP_PATH="${INC_BACKUP_DIR}/${BACKUP_NAME}"							# Full path to incremental backup directory
STATE_FILE="${BACKUP_BASE}/.backup_state"								# File tracking backup state (created by full backup)

# ------------------------ Secure Password Handling ------------------------
# Create temporary config file to avoid password exposure in process lists
TEMP_CNF=$(mktemp /tmp/.MySQL_XXXXXX.cnf)
chmod 600 "$TEMP_CNF"
cat > "$TEMP_CNF" <<EOF
[client]
user=${MYSQL_USER}
password=${MYSQL_PASSWORD}
socket=${MYSQL_SOCKET_FILE}
EOF

# Ensure temporary file is removed on script exit
trap 'rm -f "${TEMP_CNF}"' EXIT SIGQUIT ERR

# ========================================================================================
# Core Functions
# ========================================================================================

# Logging function - writes to both console and log file
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
}

# Calculate human-readable duration between two timestamps
# Args: start_seconds, end_seconds
calculate_duration() {
    local start_sec=$1
    local end_sec=$2
    local duration=$((end_sec - start_sec))
    
    if [ $duration -lt 60 ]; then
        echo "${duration} seconds"
    elif [ $duration -lt 3600 ]; then
        echo "$((duration/60)) minutes $((duration%60)) seconds"
    elif [ $duration -lt 86400 ]; then
        echo "$((duration/3600)) hours $(((duration%3600)/60)) minutes $((duration%60)) seconds"
    else
        echo "$((duration/86400)) days $(((duration%86400)/3600)) hours $(((duration%3600)/60)) minutes $((duration%60)) seconds"
    fi
}

# Convert bytes to human-readable format (B, KB, MB, GB)
# Args: size_in_bytes
format_size() {
    local size=$1
    if [ -z "$size" ] || [ "$size" -eq 0 ]; then
        echo "0 B"
        return
    fi
    if [ $size -ge 1073741824 ]; then
        echo "$(echo "scale=2; $size/1073741824" | bc) GB"
    elif [ $size -ge 1048576 ]; then
        echo "$(echo "scale=2; $size/1048576" | bc) MB"
    elif [ $size -ge 1024 ]; then
        echo "$(echo "scale=2; $size/1024" | bc) KB"
    else
        echo "${size} B"
    fi
}

# Get file permissions in numeric and symbolic format
# Args: file_path
get_permission() {
    file=$1; [ ! -e "$file" ] && echo "000 (---)" && return
    perm=$(stat -c "%a" "$file" 2>/dev/null | grep -o '^[0-7][0-7][0-7]$'); [ -z "$perm" ] && perm="000"
    rwx=""
    case "$(echo $perm | cut -c1)" in 0) rwx="${rwx}---";; 4) rwx="${rwx}r--";; 5) rwx="${rwx}r-x";; 6) rwx="${rwx}rw-";; 7) rwx="${rwx}rwx";; *) rwx="${rwx}---";; esac
    case "$(echo $perm | cut -c2)" in 0) rwx="${rwx}---";; 4) rwx="${rwx}r--";; 5) rwx="${rwx}r-x";; 6) rwx="${rwx}rw-";; 7) rwx="${rwx}rwx";; *) rwx="${rwx}---";; esac
    case "$(echo $perm | cut -c3)" in 0) rwx="${rwx}---";; 4) rwx="${rwx}r--";; 5) rwx="${rwx}r-x";; 6) rwx="${rwx}rw-";; 7) rwx="${rwx}rwx";; *) rwx="${rwx}---";; esac
    echo "$perm ($rwx)"
}

# ========================================================================================
# Pre-flight Checks
# ========================================================================================

# Verify Percona XtraBackup is installed and accessible
check_xtrabackup_installed() {
    log "Checking Percona XtraBackup installation..."
    if ! command -v xtrabackup >/dev/null 2>&1; then
        log "ERROR: xtrabackup command not found"
        log "Please verify: ls -la /usr/local/xtrabackup/bin/xtrabackup"
        return 1
    fi
    
    local version=$(xtrabackup --version 2>&1 | head -1)
    log "XtraBackup version: ${version}"
    return 0
}

# Test MySQL connection using credentials
check_mysql_connection() {
    log "Testing MySQL connection..."
    if mysql --defaults-extra-file="$TEMP_CNF" -e "SELECT 1" >/dev/null 2>&1; then
        log "MySQL connection successful"
        return 0
    else
        log "MySQL connection failed"
        return 1
    fi
}

# Display MySQL version information
check_mysql_version() {
    log "Checking MySQL version..."
    local version=$(mysql --defaults-extra-file="$TEMP_CNF" -e "SELECT VERSION();" -N 2>/dev/null)
    if [ -n "$version" ]; then
        log "MySQL version: ${version}"
    else
        log "Unable to retrieve MySQL version"
    fi
}

# Verify MySQL port is listening
check_port() {
    log "Checking MySQL port ${MYSQL_PORT} listening status..."
    if ss -tlnp 2>/dev/null | grep -q ":${MYSQL_PORT}"; then
        log "MySQL port ${MYSQL_PORT} is listening"
        return 0
    else
        log "MySQL port ${MYSQL_PORT} is not listening"
        return 1
    fi
}

# Verify MySQL process is running
check_process_status() {
    log "Checking MySQL process status..."
    if pgrep -f "mysqld" >/dev/null 2>&1; then
        log "MySQL process is running"
        return 0
    else
        log "MySQL process is not running"
        return 1
    fi
}

# Get MySQL runtime status (uptime and connection count)
get_mysql_status() {
    log "Retrieving MySQL runtime status..."
    local uptime=$(mysql --defaults-extra-file="$TEMP_CNF" -e "SHOW GLOBAL STATUS LIKE 'Uptime';" -N 2>/dev/null | awk '{print $2}')
    local connections=$(mysql --defaults-extra-file="$TEMP_CNF" -e "SHOW GLOBAL STATUS LIKE 'Threads_connected';" -N 2>/dev/null | awk '{print $2}')
    
    if [ -n "$uptime" ]; then
        local uptime_days=$((uptime / 86400))
        local uptime_hours=$(((uptime % 86400) / 3600))
        log "  MySQL uptime: ${uptime_days} days ${uptime_hours} hours"
        log "  Current connections: ${connections}"
        echo "${uptime}|${connections}"
    else
        log "  Unable to retrieve MySQL status"
        echo "0|0"
    fi
}

# ========================================================================================
# Backup Discovery Functions
# ========================================================================================

# Get the latest full backup path (created by full backup script)
get_latest_full_backup() {
    # Priority 1: Use symlink created by full backup script
    if [ -L "${FULL_BACKUP_DIR}/latest_full" ] && [ -d "${FULL_BACKUP_DIR}/latest_full" ]; then
        readlink -f "${FULL_BACKUP_DIR}/latest_full"
    else
        # Priority 2: Find the most recent full backup directory
        find "${FULL_BACKUP_DIR}" -maxdepth 1 -type d -name "full_backup_*" 2>/dev/null | sort | tail -1
    fi
}

# Get the last backup path (for incremental base)
# This could be either a previous incremental or the full backup
get_last_backup_path() {
    local last_backup=""
    
    # Read from state file if it exists (created by full or previous incremental)
    if [ -f "$STATE_FILE" ]; then
        # Safely read BACKUP_PATH from state file without sourcing
        while IFS='=' read -r key value; do
            if [ "$key" = "BACKUP_PATH" ]; then
                last_backup="$value"
                break
            fi
        done < "$STATE_FILE"
    fi
    
    # Validate the path exists
    if [ -n "$last_backup" ] && [ -d "$last_backup" ]; then
        echo "$last_backup"
    else
        # Fallback to latest full backup
        get_latest_full_backup
    fi
}

# ========================================================================================
# Space Check Functions (Warning Only - No Enforcement)
# ========================================================================================

# Calculate total database size in GB
get_database_size() {
    local size_gb=0
    
    # Try to get size from information_schema first
    local size_info=$(mysql --defaults-extra-file="$TEMP_CNF" -N -e "
        SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2)
        FROM information_schema.tables
        WHERE table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys');" 2>/dev/null)
    
    if [ -n "$size_info" ] && [ "$size_info" != "NULL" ]; then
        size_gb=$size_info
    else
        # Fallback to filesystem calculation
        if [ -d "$MYSQL_DATA_DIR" ]; then
            local total_bytes=$(du -sb "$MYSQL_DATA_DIR" 2>/dev/null | awk '{print $1}')
            if [ -n "$total_bytes" ] && [ "$total_bytes" -gt 0 ]; then
                size_gb=$(echo "scale=2; $total_bytes/1073741824" | bc 2>/dev/null)
            fi
        fi
    fi
    
    # Ensure we have a valid number (default to 2GB if unknown)
    if [ -z "$size_gb" ] || [ "$(echo "$size_gb <= 0" | bc 2>/dev/null)" -eq 1 ]; then
        size_gb="2.00"
    fi
    
    echo "$size_gb"
}

# Check disk space availability for incremental backup (warning only)
# Args: database_size_gb
check_disk_space() {
    local db_size=$1
    # Incremental backup typically needs ~20% of database size + 2GB buffer
    local required_gb=$(echo "scale=2; $db_size * 0.2 + 2" | bc 2>/dev/null)
    
    log "========== Disk Space Check (Advisory Only) =========="
    log "Database size: ${db_size} GB"
    log "Estimated incremental backup space required: ${required_gb} GB"
    
    if ! command -v bc >/dev/null 2>&1; then
        log "bc command not installed, skipping space check"
        log "Install with: dnf install -y bc"
        return 0
    fi
    
    local disk_info=$(df -BG "$BACKUP_BASE" 2>/dev/null | awk 'NR==2')
    if [ -z "$disk_info" ]; then
        log "Unable to retrieve disk information, skipping space check"
        return 0
    fi
    
    local available_gb=$(echo "$disk_info" | awk '{print $4}' | sed 's/G//')
    local used_percent=$(echo "$disk_info" | awk '{print $5}' | sed 's/%//')
    local total_gb=$(echo "$disk_info" | awk '{print $2}' | sed 's/G//')
    
    log "Partition total space: ${total_gb} GB"
    log "Available space: ${available_gb} GB"
    log "Used percentage: ${used_percent}%"
    
    if [ -n "$available_gb" ] && [ -n "$required_gb" ] && [ "$(echo "$available_gb < $required_gb" | bc 2>/dev/null)" -eq 1 ]; then
        log "WARNING: Available space ${available_gb}GB is less than estimated requirement ${required_gb%.*}GB"
        log "Backup will continue, please monitor disk usage"
    else
        log "Space appears sufficient"
    fi
    
    return 0
}

# ========================================================================================
# Email Notification Function
# ========================================================================================

# Send email notification about backup status
# Args: status, reason, backup_file, start_time, end_time, uptime_info, thread_info
send_email_notification() {
    local status=$1
    local reason=$2
    local backup_file=$3
    local start_time=$4
    local end_time=$5
    local uptime_info=${6:-"Unknown"}
    local thread_query_info=${7:-"Unknown"}
    
    local start_seconds=$(date -d "$start_time" +%s 2>/dev/null)
    local end_seconds=$(date -d "$end_time" +%s 2>/dev/null)
    [ -z "$start_seconds" ] && start_seconds=$SCRIPT_START_SECONDS
    [ -z "$end_seconds" ] && end_seconds=$(date +%s)
    
    local DURATION=$(calculate_duration $start_seconds $end_seconds)
    local DATE_TAG=$(date -d "$start_time" "+%Y-%m-%d" 2>/dev/null)
    [ -z "$DATE_TAG" ] && DATE_TAG=$(date "+%Y-%m-%d")
    
    local FSIZE="0 B"
    local FPERM="000 (---)"
    if [ "$status" = "success" ] && [ -e "$backup_file" ]; then
        local raw_size=$(du -sb "$backup_file" 2>/dev/null | awk '{print $1}')
        [ -n "$raw_size" ] && FSIZE=$(format_size "$raw_size")
        FPERM=$(get_permission "$backup_file")
    fi
    
    # Skip email if mail command is not available
    if ! command -v mail >/dev/null 2>&1; then
        log "mail command not installed, skipping email notification"
        log "Install with: dnf install -y mailx"
        return 0
    fi
    
    if [ "$status" = "success" ]; then
        SUBJECT="[SUCCESS] ${PROJECT_NAME}_Incremental_Backup_${DATE_TAG}"
        EMAIL_CONTENT="MySQL Incremental Backup Completed Successfully
================================
Server IP: ${IP_ADDR}
Hostname: ${HOSTNAME}
Project: ${PROJECT_NAME}
Backup Type: Incremental Backup
Start Time: ${start_time}
End Time: ${end_time}
Duration: ${DURATION}
Uptime: ${uptime_info}
Threads: ${thread_query_info}
Backup Path: ${backup_file}
Log File: ${LOG_FILE}
Backup Size: ${FSIZE}
Backup Permissions: ${FPERM}
Retention Days: ${RETENTION_DAYS}
Expiry Date: ${EXPIRY_DATE}
================================
Note: Increment has been applied to full backup directory
Recovery only requires the latest full backup
For assistance contact: jingyu飞鸟
https://blog.csdn.net/2301_77161927
"
        echo "$EMAIL_CONTENT" | tee -a "${LOG_FILE}"
        echo "$EMAIL_CONTENT" | mail -r "${SEND_EMAIL_NAME}" -s "${SUBJECT}" ${EMAIL_TO} 2>/dev/null
        log "Email notification sent"
    else
        SUBJECT="[ERROR] ${PROJECT_NAME}_Incremental_Backup_${DATE_TAG}"
        EMAIL_CONTENT="MySQL Incremental Backup Failed
================================
Server IP: ${IP_ADDR}
Hostname: ${HOSTNAME}
Project: ${PROJECT_NAME}
Backup Type: Incremental Backup
Start Time: ${start_time}
End Time: ${end_time}
Duration: ${DURATION}
Uptime: ${uptime_info}
Threads: ${thread_query_info}
Backup Path: ${backup_file:-None}
Log File: ${LOG_FILE}
Backup Size: 0 B
Backup Permissions: 000 (---)
Retention Days: ${RETENTION_DAYS}
Expiry Date: ${EXPIRY_DATE}
Failure Reason: ${reason}
================================
Please check log file for detailed error information
For assistance contact: jingyu飞鸟
https://blog.csdn.net/2301_77161927
"
        echo "$EMAIL_CONTENT" | tee -a "${LOG_FILE}"
        echo "$EMAIL_CONTENT" | mail -r "${SEND_EMAIL_NAME}" -s "${SUBJECT}" ${EMAIL_TO} 2>/dev/null
        log "Email notification sent"
    fi
}

# ========================================================================================
# Metadata Management (Update State File for Next Incremental)
# ========================================================================================

# Update state file after successful incremental backup
# Args: full_backup_path, inc_backup_path, backup_time
update_backup_state() {
    local full_backup_path=$1
    local inc_backup_path=$2
    local backup_time=$3
    
    log "Updating backup state for next incremental..."
    
    # Extract LSN from the incremental backup
    local lsn=""
    local checkpoints_file="${inc_backup_path}/xtrabackup_checkpoints"
    if [ -f "$checkpoints_file" ]; then
        lsn=$(grep "last_lsn" "$checkpoints_file" | awk '{print $3}')
        [ -z "$lsn" ] && lsn=$(grep "to_lsn" "$checkpoints_file" | awk '{print $3}')
    fi
    
    # Update state file so next incremental knows where to start from
    cat > "${STATE_FILE}" <<EOF
BACKUP_TYPE=inc
BACKUP_PATH=${inc_backup_path}
BACKUP_TIME=${backup_time}
BACKUP_LSN=${lsn}
FULL_BACKUP_PATH=${full_backup_path}
LAST_BACKUP_PATH=${inc_backup_path}
EOF
    
    log "State file updated successfully"
    log "  - Last backup path: ${inc_backup_path}"
    log "  - LSN: ${lsn}"
}

# ========================================================================================
# Main Execution
# ========================================================================================

# Log script start
{
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ========================================"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Percona XtraBackup Incremental Backup Started"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Server: ${HOSTNAME} (${IP_ADDR})"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ========================================"
} | tee -a "${LOG_FILE}"

# 1. Pre-flight Checks
log "========== Pre-flight Checks =========="
check_xtrabackup_installed || exit 1
check_port || { send_email_notification "failure" "MySQL port not listening" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"; exit 1; }
check_process_status || { send_email_notification "failure" "MySQL process not running" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"; exit 1; }
check_mysql_connection || { send_email_notification "failure" "MySQL connection failed" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"; exit 1; }
check_mysql_version
get_mysql_status > /dev/null

# 2. Space Check (Advisory Only)
log "========== Space Check =========="
DB_SIZE=$(get_database_size)
check_disk_space "$DB_SIZE"

# 3. Discover Backup Paths
log "========== Discovering Backup Paths =========="

# Get the full backup directory (where incrementals will be applied)
FULL_BACKUP=$(get_latest_full_backup)
if [ -z "$FULL_BACKUP" ] || [ ! -d "$FULL_BACKUP" ]; then
    log "ERROR: No full backup found. Please run full backup first."
    send_email_notification "failure" "No full backup found - please run full backup first" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"
    exit 1
fi
log "Full backup path: ${FULL_BACKUP}"

# Get the last backup (base for this incremental)
LAST_BACKUP=$(get_last_backup_path)
if [ -z "$LAST_BACKUP" ] || [ ! -d "$LAST_BACKUP" ]; then
    log "ERROR: Cannot determine last backup path"
    send_email_notification "failure" "Cannot determine last backup path" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"
    exit 1
fi
log "Base backup for this incremental: ${LAST_BACKUP}"

# 4. Execute Incremental Backup
log "========== Starting Incremental Backup =========="
log "Incremental backup directory: ${BACKUP_PATH}"
mkdir -p "$BACKUP_PATH" || { send_email_notification "failure" "Unable to create backup directory" "" "$SCRIPT_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"; exit 1; }

BACKUP_START_TIME=$(date '+%Y-%m-%d %H:%M:%S')
BACKUP_START_SECONDS=$(date +%s)

# Perform incremental backup using --incremental-basedir
if xtrabackup \
    --defaults-file="$MYSQL_CNF" \
    --host="localhost" \
    --port="$MYSQL_PORT" \
    --socket="$MYSQL_SOCKET_FILE" \
    --user="$MYSQL_USER" \
    --password="$MYSQL_PASSWORD" \
    --backup \
    --target-dir="$BACKUP_PATH" \
    --incremental-basedir="$LAST_BACKUP" \
    2>> "${LOG_FILE}"; then
    
    BACKUP_END_TIME=$(date '+%Y-%m-%d %H:%M:%S')
    BACKUP_END_SECONDS=$(date +%s)
    
    # Calculate incremental backup size
    raw_size=$(du -sb "$BACKUP_PATH" 2>/dev/null | awk '{print $1}')
    formatted_size=$(format_size "$raw_size")
    log "Incremental backup completed successfully"
    log "Incremental backup size: ${formatted_size}"
    
    # 5. Apply Incremental to Full Backup
    # CRITICAL: This step merges the incremental changes into the full backup
    # After this, the full backup contains all changes up to this point
    log "========== Applying Incremental to Full Backup =========="
    log "Running: xtrabackup --prepare --apply-log-only --target-dir=${FULL_BACKUP} --incremental-dir=${BACKUP_PATH}"
    
    if xtrabackup --prepare --apply-log-only --target-dir="$FULL_BACKUP" --incremental-dir="$BACKUP_PATH" 2>> "${LOG_FILE}"; then
        log "Incremental successfully applied to full backup directory"
        log "Full backup is now up-to-date (LSN advanced)"
        
        # Update state file for next incremental backup
        update_backup_state "$FULL_BACKUP" "$BACKUP_PATH" "$BACKUP_START_TIME"
        
        # Get MySQL status for email
        MYSQL_STATUS=$(get_mysql_status)
        MYSQL_UPTIME_INFO=$(echo "$MYSQL_STATUS" | cut -d'|' -f1)
        MYSQL_THREAD_QUERY_INFO=$(echo "$MYSQL_STATUS" | cut -d'|' -f2)
        
        # Send success notification
        send_email_notification "success" "" "$BACKUP_PATH" "$BACKUP_START_TIME" "$BACKUP_END_TIME" "$MYSQL_UPTIME_INFO" "$MYSQL_THREAD_QUERY_INFO"
        
        # Final summary
        TOTAL_DURATION=$(calculate_duration $SCRIPT_START_SECONDS $BACKUP_END_SECONDS)
        log "========== Incremental Backup Completed =========="
        log "Incremental backup: ${BACKUP_PATH}"
        log "Backup size: ${formatted_size}"
        log "Full backup updated: ${FULL_BACKUP}"
        log "Total duration: ${TOTAL_DURATION}"
        log "Log file: ${LOG_FILE}"
        
        exit 0
    else
        error_msg="Failed to apply incremental to full backup directory"
        log "ERROR: $error_msg"
        send_email_notification "failure" "$error_msg" "$BACKUP_PATH" "$BACKUP_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"
        exit 1
    fi
else
    error_msg="Incremental backup execution failed"
    log "ERROR: $error_msg"
    send_email_notification "failure" "$error_msg" "" "$BACKUP_START_TIME" "$(date '+%Y-%m-%d %H:%M:%S')"
    exit 1
fi
相关推荐
无限进步_1 小时前
从Multics到Linux:操作系统的自由之路
linux·运维·服务器
China_Yanhy1 小时前
【云原生实战】从零构建无节点 EKS:Karpenter 极简注入与全自动算力接管指南
linux·运维·云原生
小江的记录本1 小时前
【MySQL】《MySQL日志面试背诵版+思维导图》(核心考点 + MySQL 8.0最新优化)
java·数据库·后端·python·sql·mysql·面试
北山有鸟1 小时前
常用的快捷键
linux·前端·chrome·单片机·学习
BD_Marathon1 小时前
SQL学习指南——创建和填充数据库
数据库·sql
TDengine (老段)1 小时前
TDengine RPC 通信层深度解析 — 协议格式、连接管理与重试机制
大数据·数据库·rpc·架构·时序数据库·tdengine·涛思数据
KaMeidebaby1 小时前
卡梅德生物技术快报|噬菌体筛选全流程技术方案:弧菌抑菌菌株筛选、特性鉴定与效果测试
前端·数据库·其他·百度·新浪微博
蜀道山老天师1 小时前
从零搭建 Prometheus 监控 MySQL:含二进制安装、授权、exporter 配置全流程
运维·数据库·mysql·adb·云原生·prometheus
yubin12855709231 小时前
mysql正则函数REGEXP
android·数据库·mysql