我们平时登录 Linux 服务器,默认就是黑底白字,光标一闪,提示符孤零零地等在那儿。稍微讲究一点的,可能配个 neofetch 看个 logo,仅此而已。但登录欢迎信息(motd)这个事儿,往小了说是图个好看、方便认机器,往大了说它直接关系到运维效率——你登录的到底是哪台机器?昨天部署的定时任务有没有动静?磁盘快满了有没有人提醒?这些信息如果能在登录的第一秒就甩到你脸上,比进系统之后再敲一串命令去找要舒服得多。
而自动备份,则是另一件“平时不觉得、出事才后悔”的事。我刚工作那会儿,接过一台跑着业务数据库的服务器,前任运维离职前给 crontab 里留了一行注释掉的备份命令,注释上写着“磁盘空间不够,暂停了”。结果某天数据库文件损坏,三天数据直接蒸发,全公司手动补录。从那以后我养成一个习惯:凡是经手的服务器,第一件事就是看备份策略、跑一次恢复演练。没有备份的数据,等于不存在。
这篇博文不打算讲高深的内核调优,就围绕两个非常接地气的需求展开:
- 定制 Linux 登录欢迎界面,让你(和你的同事)一登录就能看到关键信息,还能区分不同环境、不同机器。
- 搭建一套自动化备份方案,用 shell 脚本 + crontab 实现定时打包、保留版本、异地副本,并附上我踩过的各种坑。
这两块内容都不需要额外安装复杂的软件包,纯系统自带工具就能搞定,适合刚接触 Linux 的运维新手,也适合想把手头服务器整理利索的“野路子”管理员。
1. 登录欢迎信息:从一行 motd 到一套“操作台”
1.1 登录的瞬间,系统到底给你展示了什么
先拆解一下“登录”这个动作。当你 SSH 连上一台 Linux 服务器,在输完密码、看到 [root@host ~]# 提示符之前,屏幕可能已经滚过了两层信息。
第一层是 /etc/issue 的内容,它负责的是“本机终端登录前”的提示,比如在机房接显示器、用串口登录时看到的那几行系统版本信息。SSH 登录默认不显示这个文件,但某些发行版的 SSH 配置里如果启用了 Banner,就会显示另一个文件(通常是 /etc/ssh/sshd_config 里指定的 Banner 路径)。
第二层是 /etc/motd,全称 message of the day。只要启用了 PAM 的 pam_motd 模块(大多数发行版默认开启),你通过 SSH 登录成功后、Shell 提示符出现之前,系统就会把这个文件的内容打印出来。
code复制Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-91-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
这就是很多云服务器默认展示的 motd。它告诉了你发行版、内核版本、帮助文档地址,但其实有一个更重要的信息没显示:你现在到底登的是哪台机器。如果公司有十几台服务器,IP 长得又差不多,登录后第一眼看不到主机名,很容易在错误的机器上执行命令。我曾经在压测环境上跑过一条 rm -rf 级别的命令,幸好只是清了缓存目录,但那种后背发凉的感觉至今记得。
所以,定制欢迎信息的第一步,就是让机器自己“报名”。
1.2 定制 motd:静态文本 + 动态命令
/etc/motd 本身是一个静态文本文件,你可以直接编辑它:
code复制cat > /etc/motd <<'EOF'
========================================
[警告] 生产环境服务器 | 禁止随意重启服务
主机名: node01
IP: 192.168.1.10
----------------------------------------
请先阅读/root/README.md后再进行操作
========================================
EOF
但静态文本有个问题:IP 变了、内核升级了、磁盘空间变了,你不可能每次都手动改。更好的做法是利用 pam_motd 的一个特性——如果 /etc/update-motd.d/ 目录存在且包含可执行脚本,系统会在登录时执行这些脚本,把脚本的 stdout 拼起来作为欢迎信息。
这个机制在 Ubuntu 上用得最顺,具体规则是:
- 脚本按文件名数字顺序执行,比如
00-header、10-sysinfo、90-footer。 - 脚本必须是可执行文件。
- 脚本输出的内容就是 motd 的一部分。
- 如果脚本执行超时或出错,不会阻塞登录。
CentOS/RHEL 7+ 和 Rocky Linux 也支持 /etc/update-motd.d/ 目录,只不过默认没启用,需要手动创建。下面这些脚本,我通常在 Ubuntu 和 Rocky 上都会放一份。
比如 10-sysinfo,负责输出动态信息:
bash复制#!/bin/bash
# /etc/update-motd.d/10-sysinfo
# 取主机名和IP(只取第一个非回环IP)
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
# 系统负载,1/5/15分钟
LOAD=$(uptime | awk -F'load average:' '{print $2}')
# 内存使用
MEM_TOTAL=$(free -m | awk '/^Mem:/{print $2}')
MEM_USED=$(free -m | awk '/^Mem:/{print $3}')
MEM_PERCENT=$((MEM_USED * 100 / MEM_TOTAL))
# 磁盘使用(根分区)
DISK_PERCENT=$(df -h / | awk 'NR==2{print $5}')
echo "主机名: $HOSTNAME"
echo "IP地址: $IP"
echo "负载: $LOAD"
echo "内存: ${MEM_USED}MB/${MEM_TOTAL}MB (${MEM_PERCENT}%)"
echo "根分区: 已用 ${DISK_PERCENT}"
再如 20-disk-warning,用于磁盘告警:
bash复制#!/bin/bash
# /etc/update-motd.d/20-disk-warning
# 遍历所有挂载点,发现使用率超过90%的输出告警
df -h | awk 'NR>1 {gsub(/%/,"",$5); if($5 > 90) print "警告: 分区 "$6" 使用率已达 "$5"%"}'
把脚本放进目录后记得加执行权限:
bash复制chmod +x /etc/update-motd.d/10-sysinfo /etc/update-motd.d/20-disk-warning
重新 SSH 登录,你就能看到效果了。如果你是 root 用户,也可以直接执行 run-parts /etc/update-motd.d/ 预览输出,不必每次退出重新登录。
1.3 进阶玩法:动态欢迎脚本 + neofetch
如果觉得纯文字太朴素,还可以让欢迎脚本调用 neoofetch 或者 fastfetch 输出系统 logo 和硬件信息。
有些发行版的包管理器直接提供了 neofetch:
bash复制# Debian/Ubuntu
apt install -y neofetch
# Rocky/RHEL,需要先启用 EPEL
dnf install -y epel-release
dnf install -y neofetch
装好之后写一个 15-neofetch 脚本:
bash复制#!/bin/bash
# /etc/update-motd.d/15-neofetch
# 只在SSH登录时显示,避免重复输出
if [ -n "$SSH_CONNECTION" ]; then
neofetch --ascii_distro ubuntu --cpu_temp C 2>/dev/null || true
fi
这里我特意加了一个条件判断。原因是要避免重复输出:如果用户通过 su - 切换用户,或者在本机打开新终端,PAM 可能还会再执行一次 motd 脚本,每次刷屏一堆 neofetch 信息反而烦人。用 SSH_CONNECTION 环境变量判断当前是不是 SSH 会话,只有真正远程登录时才展示完整的“形象工程”。
关于 neofetch 的显示效果,有一点提醒:如果你用的是 Windows 上的 MobaXterm、FinalShell 这类终端,对 Unicode 和 ANSI 颜色支持各有差异,neofetch 的 ASCII logo 在某些终端里会错位。我建议在 motd 里不依赖彩色 logo,而是把真正关键的信息(主机名、IP、磁盘告警)放在最前面,哪怕终端不支持颜色也能看到。
1.4 别把 motd 变成“假告警”:和监控系统的分工
定制 motd 时最容易犯的错误,是试图把它做成一个“实时监控面板”。其实 motd 只在登录瞬间输出一次,它是一个静态快照,不可能替代 Zabbix、Prometheus、Grafana 这类持续监控系统。合理的分工是:
- motd 负责解决“我登录的时候,快速了解这台机器的大致健康状态”。
- 监控系统负责“7x24 小时盯着指标,出问题主动告警”。
所以我在 motd 里只放了主机名、IP、负载、内存、磁盘这几个和登录操作最相关的指标。真正需要告警的,比如 CPU 持续飙高、某个服务挂了,应该交给 cron + 脚本或监控系统去发告警,而不是等用户登录才看到。
有一个小技巧:在 motd 里带上“最近一次备份时间”或者“最近一次成功执行的关键任务时间”,非常有用。比如你有一个每日备份任务,motd 里显示“昨日 02:00 备份成功/失败”,登录时一眼就能发现备份是不是连续几天失败了。这个思路后面讲自动备份的时候会用到,相当于给备份加了一道人肉巡检。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 自动备份:先想清楚备份什么、怎么存、存多久
2.1 备份的本质是“恢复”,不是“打包”
很多初学者做备份,就是写一条 tar czf backup.tar.gz /home,往 crontab 里一挂,完事儿。但等真出了事故,准备从备份里恢复时才发现:备份文件是坏的、备份目录没包含关键子目录、解压出来的权限全乱了、或者备份文件本身就存在同一块坏了的磁盘上。备份最容易给人虚假的安全感——你以为自己有备份,实际上啥都没有。
所以在动手写脚本之前,先回答三个问题:
- 备份什么:数据库数据目录、应用代码目录、配置文件(
/etc)、用户上传文件,还是全盘?不同数据要区分对待,因为备份频率和保留策略不一样。数据库可能每小时增量备份一次,而静态代码可能一天一次就够了。 - 备份存哪里:是存本机另一块磁盘,还是远程服务器?如果存本机同一块磁盘,那磁盘坏了备份也一起没了。我通常至少保留两份:一份本地磁盘副本,一份通过 rsync/scp 推到远程主机或对象存储。
- 保留多久:全量备份一天一个,占空间很大,不可能无限保留。需要制定一个“保留策略”,比如“最近7天每天保留,最近4周每周保留,最近6个月每月保留”。
这三个问题想清楚,备份脚本才有灵魂。下面我给的方案是一个通用框架,你可以根据自己的需求改。
2.2 选型:tar + cron 还是 rsync + cron
备份工具最常用的就是 tar 和 rsync,两者各有各的适用场景。
tar 适合对整个目录做“归档快照”,打包成单个文件,方便传输和存储。它的好处是保留了文件权限、属主、时间戳,而且方便做压缩,还能通过 --listed-incremental 做增量备份。缺点是恢复时得先解压到临时目录,再手工移动到目标位置,操作略重。
rsync 适合做“目录同步”,它只传输差异部分,增量效率极高。配合 --link-dest 可以做一个“伪增量”目录备份——每一次备份都生成一份完整的目录结构,但相同的文件通过硬链接共享空间。它的好处是备份目录可以直接浏览、直接复制出来用,不需要解压,恢复时跟普通文件操作一样简单。缺点是目录结构非常占 inode,文件数量极多时同步会慢。
我的习惯是:数据库用 tar 打包(配合 mysqldump/pg_dump 导出后再打包),普通文件目录用 rsync 同步。下面分别给两个方案。
2.3 数据库备份:mysqldump + tar + 保留策略
以 MySQL/MariaDB 为例,一个可以被 cron 调用的库备份脚本:
bash复制#!/bin/bash
# /usr/local/bin/backup_mysql.sh
set -euo pipefail
# ===== 配置区 =====
DB_USER="backup_user"
DB_PASS="your_password"
DB_NAME="your_database"
BACKUP_DIR="/data/backup/mysql"
REMOTE_HOST="backup@192.168.1.200"
REMOTE_DIR="/data/backup/mysql"
RETENTION_DAYS=7
MYSQLDUMP=/usr/bin/mysqldump
# =================
# 日志函数
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> /var/log/mysql_backup.log
}
log "开始备份数据库: $DB_NAME"
# 1. 创建备份目录
mkdir -p "$BACKUP_DIR"
# 2. 导出数据库到SQL文件
DUMP_FILE="${BACKUP_DIR}/${DB_NAME}_$(date +%Y%m%d_%H%M%S).sql"
$MYSQLDUMP --single-transaction --routines --triggers \
--user="$DB_USER" --password="$DB_PASS" \
"$DB_NAME" > "$DUMP_FILE" 2>> /var/log/mysql_backup.err
# 3. 为了节省磁盘空间,立即压缩
gzip -9 "$DUMP_FILE"
# 4. 同步到远程主机
if rsync -az --timeout=60 "$DUMP_FILE.gz" "${REMOTE_HOST}:${REMOTE_DIR}/" 2>> /var/log/mysql_backup.err; then
log "远程备份成功: $DUMP_FILE.gz"
else
log "远程备份失败!"
fi
# 5. 清理本地7天前的备份
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +${RETENTION_DAYS} -delete
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +${RETENTION_DAYS} -print >> /var/log/mysql_backup.log
log "备份完成"
这个脚本里的几个关键点:
--single-transaction选项非常关键。它让mysqldump在 InnoDB 表上使用事务快照,导出过程中不会锁表,不影响业务写入。如果你是 MyISAM 表,那就没办法了,最好停机或者使用--lock-tables,但会阻塞写操作。--routines --triggers是导出存储过程和触发器。很多新手备份数据库时只导出了表数据,恢复后才发现存储过程全丢了,所以一开始就把这俩选项加上。set -euo pipefail是 shell 脚本三板斧:遇到错误退出、变量未定义报错、管道中任一命令失败则整体失败。这样脚本不会“假装成功”。- 备份后立即
gzip压缩,因为 SQL 文件很大很正常,压缩后通常能缩小到原来的 1/5 到 1/10。 - 远程同步同步的是压缩后的
.sql.gz文件,不会把原始 SQL 明文留在远程磁盘上(当然你还需要考虑网络安全,见后文)。
2.4 普通文件备份:rsync + --link-dest 做时间点快照
对于 /home、/var/www、/etc 这类目录,我更推荐 rsync 的时间点快照方案。它每一份快照目录看起来都像完整备份,但实际存储成本很低。
bash复制#!/bin/bash
# /usr/local/bin/backup_files.sh
set -euo pipefail
SNAPSHOT_ROOT="/data/snapshot"
LATEST_LINK="${SNAPSHOT_ROOT}/latest"
STAMP=$(date +%Y%m%d_%H%M%S)
DEST="${SNAPSHOT_ROOT}/${STAMP}"
LOG=/var/log/file_backup.log
mkdir -p "$SNAPSHOT_ROOT"
# rsync进行同步,--link-dest 引用上次备份的目录作为"基础层"
rsync -av --delete \
--link-dest="$LATEST_LINK" \
/etc/ \
/var/www/ \
/home/ \
"${DEST}/" >> "$LOG" 2>&1
# 更新 latest 软链接指向最新备份
rm -rf "$LATEST_LINK"
ln -s "$DEST" "$LATEST_LINK"
# 只保留最近14天的快照(find 配合 -maxdepth 限制目录层级)
find "$SNAPSHOT_ROOT" -maxdepth 1 -type d -name "20*" -mtime +14 -exec rm -rf {} \;
原理说透一下:
第一次运行时,--link-dest 指向的 latest 目录不存在或者为空,rsync 就会完整拷贝所有源文件到 DEST。之后的每次运行,rsync 会把 DEST 里的文件和 LATEST_LINK(也就是上一次的 DEST)做对比:如果文件没变,就不传输、不新占空间,而是为 DEST 里的文件创建一个指向旧文件 inode 的硬链接;只有文件变了,才真正写入新数据。
这样做的效果是:你每个时间点都有一个“完整快照”目录,可以直接浏览、直接恢复,但磁盘总占用约等于“初始全量 + 每次的增量变化”。这就是硬链接的妙处。
注意几个坑:
--delete会把源端已删除的文件从备份端删掉,这一点必须想清楚。如果你希望保留“历史文件被删”这个状态,就不要加--delete;如果你希望备份目录和源目录严格一致,就加上。我一般加上,因为它保证了备份不会无限膨胀,但它也意味着源目录里的误删会被同步到备份端,所以这个方案对“防误删”的能力有限。find ... -exec rm -rf {} \;删除快照目录时,因为硬链接的存在,某个文件可能被多个快照目录共享。但只要某个文件的所有硬链接都被删除,磁盘空间才会真正释放。这个流程没问题,但删除大量小文件时比较慢,建议用-print输出日志,方便排查。- 如果要同时备份多个目录,注意 rsync 的目标路径写法。我把源路径都放在一条命令中,最终会在目标目录里拼出
/etc、/var/www、/home的同名子目录,这个行为取决于 rsync 的“合并源路径到单个目标”规则,实测下来结构清晰,不冲突。
3. crontab 调度:别让它“跑了”就完了
3.1 定时任务该怎么写
备份脚本写好之后,交给 cron 调度。编辑当前用户的 crontab:
bash复制crontab -e
添加:
cron复制# 每天凌晨2点执行mysql备份
0 2 * * * /usr/local/bin/backup_mysql.sh > /dev/null 2>&1
# 每天凌晨3点执行文件快照备份
0 3 * * * /usr/local/bin/backup_files.sh > /dev/null 2>&1
/dev/null 的作用是丢弃 cron 默认会自动邮寄给用户的输出。如果不想丢,建议改成写日志文件,比如 >> /var/log/backup_cron.log 2>&1,这样出问题时有据可查。
3.2 排查 cron 问题的固定套路
cron 最常见的坑有三个:
第一个是 执行环境问题。cron 执行脚本时的 PATH 环境变量非常精简,通常只有 /usr/bin:/bin。如果你的脚本里用了 /usr/local/bin/ 下的命令(比如 neofetch、rclone、restic),在 crontab 里直接写命令名可能找不到,直接 command not found。解决方式是在脚本开头显式 export PATH=,或在 crontab 里写命令的绝对路径:
cron复制0 2 * * * /usr/bin/nice -n 5 /usr/local/bin/backup_mysql.sh >> /var/log/mysql_backup.log 2>&1
第二个是 百分号必须转义。cron 配置文件里,% 是特殊字符,会被当作换行符或参数分隔符。如果你在命令里写了 date +%Y%m%d,必须写成 date +\%Y\%m\%d,否则 cron 会直接截断命令。更好的做法是在脚本内部处理日期变量,crontab 里只写一个干净的调用。
第三个是 备份脚本可能还没跑完,就被下一个任务踩到。比如备份任务预计要跑40分钟,但 cron 设置的是每30分钟跑一次,两个实例就会并发执行,可能导致重复备份、文件锁冲突甚至数据不一致。解决方式是给脚本加一个“单实例锁”,用的是 flock 命令:
bash复制# 在 backup_mysql.sh 开头加
exec 9>/var/run/backup_mysql.lock
flock -n 9 || exit 0
flock -n 表示“拿不到锁就立刻退出”,这样就保证了同一时间只有一个实例在跑。
3.3 给备份加一个“打卡机”:状态标记文件
前面提到 motd 里可以显示“最近一次备份时间”,做法其实很简单。备份脚本在执行成功之后,写一个状态文件:
bash复制echo "$(date '+%Y-%m-%d %H:%M:%S') mysql_backup OK" > /var/status/mysql_backup.status
然后在 motd 的 10-sysinfo 脚本里读取这个文件:
bash复制if [ -f /var/status/mysql_backup.status ]; then
cat /var/status/mysql_backup.status
else
echo "mysql_backup 从未执行过"
fi
更进一步,可以对比状态文件的修改时间和当前时间,如果超过 36 小时没更新,就输出红色告警。这个思路等于给 cron 任务加了一道人肉巡检,登录时扫一眼就能发现备份是否异常。对于没有部署专业监控系统的小团队来说,这个方法成本低、效果直接。
4. 恢复演练:备份工作的“期末考”
4.1 盲恢复的教训
很多人备份做得勤快,但从来没有实际恢复过一次。等到灾难真的发生,面对损坏的备份文件、不完整的备份集、或者恢复后权限错乱的数据目录,才意识到自己“备份了个寂寞”。我刚负责服务器那会儿,同事提醒我“每周做一次恢复测试”,我当时觉得浪费时间,后来有一次测试恢复一个小库,才发现由于备份脚本顺序问题,导出的 SQL 文件其实是空的——mysqldump 执行失败但脚本没有及时退出,照样 gzip、照样同步到远程。幸好这是测试时发现的,如果是真实事故,后果不堪设想。
从那以后,我把恢复演练当成了备份的“期末考”:备份做得再漂亮,恢复不了等于零。
4.2 数据库备份恢复演练脚本
以 MySQL 为例,恢复演练不能在生产库上瞎搞,正确做法是:在临时实例上恢复,验证数据完整性。
bash复制#!/bin/bash
# /usr/local/bin/test_restore_mysql.sh
# 从最近的备份中恢复数据到临时库
set -euo pipefail
# 配置
BACKUP_DIR="/data/backup/mysql"
TEMP_DATA_DIR="/var/lib/mysql-restore-test"
MYSQLD_BIN="/usr/sbin/mysqld"
MYSQL_BIN="/usr/bin/mysql"
# 1. 找到最新的备份文件
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/*.sql.gz | head -1)
echo "正在测试恢复: $LATEST_BACKUP"
# 2. 准备临时数据目录(清空重建)
rm -rf "$TEMP_DATA_DIR"
mkdir -p "$TEMP_DATA_DIR"
chown mysql:mysql "$TEMP_DATA_DIR"
# 3. 初始化临时数据库实例
"$MYSQLD_BIN" --initialize-insecure --datadir="$TEMP_DATA_DIR" --user=mysql
这里我简化了流程,因为不同发行版 mysqld 初始化参数略有差异。大概思路是:
- 用
--initialize-insecure初始化一个全新的数据目录; - 在临时端口(比如 3307)启动 mysqld,指定
--datadir=$TEMP_DATA_DIR; - 解压最新备份的
.sql.gz文件,用mysql客户端导入; - 执行几条
SELECT COUNT(*) FROM ...或业务查询,验证关键表的数据量是否合理; - 关闭临时实例,清理临时目录。
这个流程最好写成一个脚本,放到 crontab 里每周执行一次。恢复演练的价值不仅是验证备份文件可用,还能验证你的备份脚本里 --routines --triggers 这些参数是否真正生效,否则恢复出来的库缺存储过程,业务照样跑不起来。
4.3 文件备份恢复演练
对于 rsync 目录快照的恢复,演练比数据库简单得多。因为我采用的是目录结构型备份,恢复时直接拷贝即可:
bash复制# 假设要恢复 /home/user/document.txt
cp /data/snapshot/20250101_033000/home/user/document.txt /home/user/document.txt
但这里有个隐患:rsync 备份是“按源目录结构”存放的,如果恢复时你要回滚整个 /home,直接用 cp -a 或者 rsync 反向同步回去就行。不过在恢复前务必做好“当前状态快照”,防止恢复了错误的时间点。
文件备份的演练不需要每次真的大规模回滚,只需要定期抽几个文件检查权限、内容完整性。我一般是在每月一次的系统巡检时,对比最新快照里某几个文件的 md5sum 和源文件是否一致,确认备份过程中没有文件损坏。
4.4 远程备份的“最后一公里”:异地副本测试
如果备份同步到了远程主机,恢复演练就多了一个环节:从远程主机拉取备份文件并恢复。我遇到过一种情况:本地备份和同步脚本都显示成功,但远程磁盘其实早就满了,rsync 写入了部分文件后失败,脚本却因为管道未正确捕获错误而返回了 0。所以远程同步是否成功,不能只看脚本退出码,还要在远程端检查文件大小是否和本地一致。
一个简单的校验方法:
bash复制# 备份脚本中同步之后,用 ssh 远程执行 ls -l 检查大小
REMOTE_SIZE=$(ssh backup@192.168.1.200 "stat -c %s ${REMOTE_DIR}/$(basename $DUMP_FILE.gz)")
LOCAL_SIZE=$(stat -c %s "$DUMP_FILE.gz")
if [ "$REMOTE_SIZE" = "$LOCAL_SIZE" ]; then
echo "远程文件大小校验通过"
else
echo "远程文件大小异常"
# 可以在这里触发告警
fi
5. 备份存储与安全:别让备份成为“裸奔的数据”
5.1 备份目录要不要加密
备份文件里通常包含了数据库内容,也就是整个业务的核心数据。如果备份同步到远程主机或者对象存储,而网络传输没有加密、远程存储没有访问控制,那么备份本身就是数据泄露的窗口。
几个基础层面的建议:
- rsync 同步必须走 SSH 协议,不要用 rsyncd 的明文模式。rsync 命令中的
host:path语法默认走 SSH,只要你配置了 SSH 密钥登录,链路就是加密的。 - 远程主机的备份目录权限要收紧,建议
chmod 700,只允许备份用户读取。 - 如果备份包含高度敏感的数据,并且要上传到公有云对象存储,建议在本地先加密再上传。工具上可以用
gpg对称加密,或者用age这类更现代的工具。gpg 加密的命令:
bash复制gpg --symmetric --cipher-algo AES256 "$DUMP_FILE.gz"
# 会生成 $DUMP_FILE.gz.gpg
解密恢复时:
bash复制gpg --decrypt "$DUMP_FILE.gz.gpg" > "$DUMP_FILE.gz"
密钥管理是个大话题,对于小团队,至少做到“密钥不放在备份脚本里明文写死”。可以用环境变量或单独的密钥文件,文件权限设为 600。
5.2 备份到对象存储:rclone 的配置细节
如果你的备份需求是“异地容灾”,推荐用 rclone 把备份文件同步到对象存储。rclone 的安装很简单,但配置时注意:
bash复制rclone config
# 按向导选择 S3 兼容对象存储、阿里云 OSS、腾讯云 COS 等
配置完成之后,备份脚本里加上一行同步命令:
bash复制rclone copy "$BACKUP_DIR" remote:backup-bucket/mysql/ \
--transfers 4 --checkers 8 --log-file /var/log/rclone_backup.log
注意 --transfers 和 --checkers 可以控制并发数,避免把带宽打满影响业务。如果网络带宽有限,也可以给 rclone 加上 --bwlimit 10M 限速。
对象存储方案我个人的体会是:对于中小型团队,性价比很高,“上传成功”这个动作本身就是一种天然的异地备份,不需要自己再维护一台远程备份服务器。但同样要定期做恢复演练,下载一个文件试试能不能解压。
6. 踩坑实录:备份和 motd 相关的 10 个经典问题
最后集中分享一下实际操作中容易踩的坑,大部分都是我在维护服务器时真实遇到过的。
6.1 motd 相关
- 脚本执行权限没加。
/etc/update-motd.d/里的脚本必须有 execute 权限,否则静默跳过。排查时执行run-parts /etc/update-motd.d/看看是否有输出。 - 脚本退出码错误。
update-motd.d的脚本如果报错,可能会把错误信息输出到 motd 里,看起来一团乱。脚本里建议加上2>/dev/null或者|| true,避免非核心命令阻塞。 - 转义字符被解释。如果 motd 脚本里用了 ANSI 颜色码,一定要确认没有把转义序列直接写进单引号而丢失。用
echo -e或者printf才能正确解释\e[31m这类颜色控制符。 - 登录慢。如果 motd 脚本里有命令超时或 DNS 解析慢,会导致 SSH 登录后提示符迟迟不出现。排查方式:
timeout 5 run-parts /etc/update-motd.d/,看哪个脚本消耗时间。 - 图形/颜色在部分终端乱码。统一规范,不要在 motd 里放太花哨的符号,重点信息放前面。
6.2 备份相关
- mysqldump 没有加密码参数,交互式询问密码导致备份失败。如果不方便把密码写在命令行里(
ps能看到),可以用[client]配置区:
ini复制# /root/.my.cnf
[client]
user=backup_user
password=your_password
然后脚本里不要写 --password 参数,mysqldump 会自动读取这个文件。注意 .my.cnf 权限必须设为 600。
- tar 打包时带了绝对路径,解压时覆盖系统文件。打包时用
-C切换目录,解压前先查看文件列表。
bash复制tar czf backup.tar.gz -C /var/www/uploads .
-
rsync 的目标目录如果写错了层级,会把源目录整个复制成另一层子目录。最好先
rsync -av --dry-run预览一下。 -
备份文件时间戳不准确。如果服务器时区不对,
date +%Y%m%d出来的文件名可能是昨天。建议先timedatectl set-timezone Asia/Shanghai,统一时区,同时确保chrony或ntpd在同步时间。 -
删除旧备份的命令用了
-delete但没仔细检查模式。比如find / -name "*.tar.gz" -delete这种写法极其危险,一个空格或通配符错误就可能删掉系统文件。我一般会先-print看输出,核对无误后再加-delete。
6.3 一个小故事:cron 里 % 符号的经典事故
有次帮同事排查一个诡异的备份问题:脚本单独执行没问题,但放到 crontab 里就只生成了一个空文件。折腾半天,发现 crontab 里写的是:
cron复制0 2 * * * /usr/local/bin/backup_mysql.sh --date=$(date +\%Y\%m\%d) ...
同事把 \% 转义写成了 % 没转义,cron 看到 %,把后面的内容当成了标准输入传给命令,命令根本没拿到日期参数。这类问题排查起来很隐蔽,因为手动执行脚本时根本不会触发。现在我的习惯是:crontab 里绝不写任何带 %、$、管道符等特殊字符的命令,一律把这些逻辑封装进脚本,crontab 只保留最干净的调用。
最后说两句实在的
登录欢迎和自动备份,看起来是两个不相干的功能,但把它们放在一起做,是因为它们共同服务一个目标:让人对服务器的状态“心里有数”。
motd 解决的是登录时快速感知机器状态,备份解决的是数据出事后能快速恢复。把两者结合,用备份脚本写状态文件,再用 motd 显示备份状态,就形成了一个非常朴素但稳定的闭环:每天登录服务器的人,不用主动去查备份日志,一眼就能看出备份是否正常。
如果你是小团队里唯一的运维,或者你只是管理一两台个人的 Linux 服务器,我特别建议你抽出半天时间,把这套方案部署下去。不用买额外的软件,不用高配置,都是系统自带工具加一个几十行的脚本。
有一个我后来才后悔没早做的事:在第一次写完备份脚本后,立刻删掉一个真实文件、然后用备份恢复一次。只有真正经历过一次“删了再恢复”的完整流程,你才会对备份方案产生信任感,也才会发现脚本里那些被你忽略的逻辑漏洞。
下一步你可以做的扩展有很多:把 motd 从“快照信息”升级成“带颜色的健康评分”;备份脚本里增加对 Docker 容器的 docker inspect 元数据备份;或者用 systemd timer 替代 crontab 来调度任务(更精细、可查状态)。但不管是哪一步,都别丢掉“试恢复”这个习惯。数据这东西,平时看不见摸不着,只有出事的时候才显得比命还重要。
