综合练习
这一课你将能做到
- 独立写出「MySQL 定时备份脚本」:打包、时间戳命名、保留 7 天、日志记录、crontab 调度全流程
- 独立写出「多节点巡检脚本」:循环远程主机,采集磁盘/内存/负载/进程状态并汇总成报告
- 独立写出「日志分析流水线」:从 Nginx 访问日志统计 Top IP、错误率,从应用日志统计异常数
为什么要学
前面 11 课你学了单词,这一课开始写文章。真实工作里没有「用一下 grep」这种需求,只有「给我做一个数据库每天自动备份」「发版前帮我巡检一遍所有节点」这种完整任务。把它们拆解成前面学过的命令组合,正是 Shell 工程师的日常。面试手写题也几乎都出在这个粒度:「写个备份脚本」「写个巡检脚本」——本课三个脚本就是你的答案模板,背熟结构、理解每行,面试直接默写。
人话:这就像 Java 里学完语法开始写第一个完整的 CRUD 项目。三个脚本对应三类最高频运维任务:备份(保数据)、巡检(保稳定)、分析(找问题),它们会一直陪着你到每一份后端工作里。
先建立图像
三个脚本的技能映射图:
┌───────────────┬──────────────────────────────────────┐
│ 脚本 │ 用到的课程技能 │
├───────────────┼──────────────────────────────────────┤
│ 备份脚本 │ 变量(5) + 条件(6) + 函数(7) + tar(3) │
│ │ + find 清理(2) + crontab(9) │
├───────────────┼──────────────────────────────────────┤
│ 巡检脚本 │ 函数(7) + 循环(6) + ssh/scp(10) │
│ │ + df/free/uptime(11) + awk(4) │
├───────────────┼──────────────────────────────────────┤
│ 日志分析 │ 管道(4) + 正则(8) + sort/uniq(4) │
│ │ + awk/sed(4) + grep -E(8) │
└───────────────┴──────────────────────────────────────┘
生产脚本的五要素(每个脚本都必须有):
1. 安全带 set -euo pipefail
2. 可配置 变量集中在文件头部
3. 防御 目录/参数检查,失败即退出
4. 留痕 每步写日志,时间戳开头
5. 自清理 旧产物定期删除,不无限膨胀
跟着做
练习 1:MySQL 定时备份脚本(完整可运行)
mkdir -p ~/practice/bin ~/practice/backup ~/practice/logs
cat > ~/practice/bin/backup-mysql.sh << 'EOF'
#!/bin/bash
set -euo pipefail
# ========== 可配置区 ==========
DB_USER="root"
DB_PASS="your_password" # 生产建议用 ~/.my.cnf 免密方式
DB_NAME="mydb"
BACKUP_DIR="$HOME/practice/backup"
KEEP_DAYS=7
# ==============================
log() { echo "[$(date '+%F %T')] $*" >> "$HOME/practice/logs/backup.log"; }
die() { log "ERROR: $*"; exit 1; }
mkdir -p "$BACKUP_DIR"
command -v mysqldump > /dev/null || die "mysqldump 不存在,请安装 mysql-client"
FILE="$BACKUP_DIR/${DB_NAME}-$(date +%F-%H%M).sql.gz"
log "开始备份 $DB_NAME -> $FILE"
# --single-transaction: InnoDB 不锁表;管道失败整体失败(pipefail)
mysqldump -u"$DB_USER" -p"$DB_PASS" --single-transaction "$DB_NAME" \
| gzip > "$FILE"
[[ -s "$FILE" ]] || die "备份文件为空:$FILE" # -s 判断文件存在且非空
log "备份完成,大小:$(ls -lh "$FILE" | awk '{print $5}')"
# 清理 N 天前的旧备份
find "$BACKUP_DIR" -name "${DB_NAME}-*.sql.gz" -mtime +$KEEP_DAYS -delete
log "已清理 ${KEEP_DAYS} 天前的旧备份"
log "===== 本次任务结束 ====="
EOF
chmod 700 ~/practice/bin/backup-mysql.sh
# 没装 MySQL 也先跑一次,观察 die() 如何拦截并记录日志
bash ~/practice/bin/backup-mysql.sh; echo "退出码:$?"
tail -5 ~/practice/logs/backup.log
装入 MySQL 的机器上去掉注释实测;定时调度(crontab -e):
# 每天 03:00 执行,输出并入日志
0 3 * * * /home/你的用户名/practice/bin/backup-mysql.sh >> /home/你的用户名/practice/logs/cron.log 2>&1
验证技巧:先写 下一分钟 的时间(date +%M 加一),观察 cron.log 是否准时出现记录,再改回 0 3 * * *。
练习 2:多节点巡检脚本
cat > ~/practice/bin/inspect.sh << 'EOF'
#!/bin/bash
set -uo pipefail
# 巡检目标:本机演示。有多台机器时改为 IP 列表 + ssh 远程执行
REPORT="$HOME/practice/logs/inspect-$(date +%F).txt"
get_disk() { df -h / | awk 'NR==2 {print $5}'; } # 根分区使用率
get_mem() { free | awk '/Mem:/ {printf "%.0f%%", $3/$2*100}'; }
get_load() { uptime | awk -F'load average:' '{print $2}'; }
check_app() { pgrep -f "java.*myapp" > /dev/null && echo "RUNNING" || echo "DOWN"; }
main() {
{
echo "===== 巡检报告 $(date '+%F %T') ====="
echo "磁盘使用率 : $(get_disk) (阈值 80%)"
echo "内存使用率 : $(get_mem) (阈值 85%)"
echo "负载 :$(get_load)"
echo "myapp 进程 : $(check_app)"
echo "最近登录 : $(who | head -3)"
} >> "$REPORT"
echo "报告已生成:$REPORT"
}
main
EOF
bash ~/practice/bin/inspect.sh
cat ~/practice/logs/inspect-*.txt
多机版只需两处升级:节点列表 NODES=("10.0.0.5" "10.0.0.6"),取数改走远程 ssh -o ConnectTimeout=3 deploy@$node 'df -h /' (前提:第 10 课的 ssh 免密已配好)。把 get_disk 里的命令换成 ssh ... "df -h /" 即可,函数结构完全不变——这就是函数化的价值。
练习 3:日志分析流水线
# 造一份仿真 Nginx 日志
cat > ~/practice/access.log << 'EOF'
10.0.0.1 - - [06/Oct/2026:10:00:01] "GET /api/user HTTP/1.1" 200 1024
10.0.0.2 - - [06/Oct/2026:10:00:02] "GET /api/user HTTP/1.1" 502 512
10.0.0.1 - - [06/Oct/2026:10:00:03] "POST /api/order HTTP/1.1" 500 2048
10.0.0.3 - - [06/Oct/2026:10:00:04] "GET /api/user HTTP/1.1" 200 512
10.0.0.2 - - [06/Oct/2026:10:00:05] "GET /api/order HTTP/1.1" 502 512
10.0.0.1 - - [06/Oct/2026:10:00:06] "GET /api/user HTTP/1.1" 404 256
10.0.0.4 - - [06/Oct/2026:10:00:07] "GET /api/user HTTP/1.1" 200 512
EOF
LOG=~/practice/access.log
echo "== 总请求数:$(wc -l < $LOG)"
echo "== 状态码分布 =="
awk '{print $9}' $LOG | sort | uniq -c | sort -rn
echo "== 5xx 错误率 =="
total=$(wc -l < $LOG); err=$(awk '$9 >= 500' $LOG | wc -l)
echo "$err / $total = $(awk "BEGIN{printf \"%.1f%%\", $err/$total*100}")"
echo "== 请求量 Top3 IP =="
awk '{print $1}' $LOG | sort | uniq -c | sort -rn | head -3
echo "== 502 都来自谁 =="
awk '$9 == 502 {print $1}' $LOG | sort -u
echo "== 应用日志 ERROR 计数(有真实日志时)=="
grep -c "ERROR" ~/net-lab/server.log 2>/dev/null || echo "无日志或无 ERROR"
每一条都能拆开单独跑——这正是第 4 课教的「逐段验证法」。把这段存成 log-analyze.sh,参数化 LOG=${1:?用法:$0 <日志路径>},就是一个可复用的分析工具。
练习 4:三脚本统一 crontab
crontab -e
# 巡检:每 30 分钟一次,报告自动按天落盘
*/30 * * * * /home/你的用户名/practice/bin/inspect.sh >> /home/你的用户名/practice/logs/cron.log 2>&1
至此你拥有一套「自动备份 + 定时巡检 + 随手分析」的迷你运维平台——全是用前 11 课的零件手工组装的。
讲清楚要点
- 备份脚本的核心三防线:
--single-transaction 不锁表、[[ -s $FILE ]] 验证产物非空、find -mtime +N -delete 防磁盘慢性死亡。缺一条都算不上合格备份。真正可靠的方案还要异机/异地存储,脚本内 rsync 推到备份机是进阶方向。
- 巡检脚本要「输出结构化」:每行一个指标、带阈值标注,人看省脑、机器(脚本/告警系统解析)也省力。把超阈值项额外 echo 到 stderr,接告警通道就是半自动化监控。
- 日志分析永远是「先过滤再统计」:先 grep/awk 缩小数据面,再 sort/uniq 聚合。直接对原始日志 uniq 是新手常见低效做法;百万行日志先按日期截断(
grep "^2026-10-06")能把处理时间降一个量级。
- 所有脚本共享同一个日志规范:
[时间] 消息 追加写入、报错走 die() 统一格式。排查定时任务时,规范日志能让你 30 秒定位问题,散装 echo 则要翻半天。
- 调度验证用「下一分钟法」:先用
date 算出下一分钟写入 crontab,确认触发和产物都正常,再改回正式时间。直接上正式表达式然后等第二天发现没跑,是最浪费时间的做法。
- 脚本要能被别人看懂:可配置区在头部、函数有名字、关键步骤有日志。你写的运维脚本大概率会被同事接手,可读性就是可靠性的一部分。
你可能会懵的地方
-
密码明文写在脚本里安全吗?
不安全,chmod 700 只是防同机其他用户,看不到密钥泄漏风险。生产做法:MySQL 用 ~/.my.cnf(600 权限)存凭据实现免密,或接入密钥管理。至少做到「密码不进 Git 仓库」。
-
巡检脚本里 ssh 失败会让整个循环中断吗?
不加 set -e 会继续(所以练习里只用 -uo pipefail);更稳的做法是每次 ssh 加 -o ConnectTimeout=3 和 || echo "$node 不可达",把失败记录为数据而不是让脚本崩溃——巡检脚本的第一原则是「自己别挂」。
-
错误率统计里的 awk "BEGIN{...}" 是什么?
awk 的 BEGIN 块在读输入前执行,常用于纯计算(不需要输入数据)。这里借它做浮点除法——bash 原生 $(( )) 只支持整数,遇到百分比这种小数就交给 awk 或 bc。
-
备份文件的命名为什么带 %H%M(时分)?
一天内可能多次备份(每次发版前备份是常见策略),同名会互相覆盖。文件名带越精确的时间戳,恢复时选择余地越大。命名即元数据,这是运维习惯里性价比最高的一条。
-
这些脚本可以直接用于生产吗?
结构和思路可以直接迁移,但生产化还差几件事:凭据管理、异机备份、失败告警(接企业微信/钉钉 webhook)、以及最重要的——先用测试库验证再上生产。本课目标是「结构和每行都懂」,生产化是水到渠成的下一步。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 备份后不验证产物 | 空文件也当成功,出事才知道 | `[[ -s $FILE ]] |
| crontab 里直接写多行逻辑 | % 转义、PATH、引号全是坑 | 封装脚本,crontab 只调路径 |
| 旧备份不清理 | 磁盘慢性死亡,最终把系统盘写满 | find -mtime +N -delete |
| 巡检脚本 set -e 全开 | 一台机器 ssh 失败,全场中断 | 每次检查独立容错,失败记为数据 |
| 日志分析一次管道从头堆到尾 | 中间出错无法定位 | 逐段验证,确认后再串全 |
卡点急救
- crontab 到点没执行:按第 9 课清单逐层查——配置在不在(crontab -l)、cron 服务活着吗(systemctl status cron)、执行日志有没有记录(/var/log/cron)、脚本手动跑有没有报错、输出重定向了吗。绝大多数答案在「cron 环境和交互环境不同」里。
- 备份出来文件是空的:mysqldump 阶段失败但管道吞了错误。检查 pipefail 是否开启、手动跑 mysqldump 看具体报错(密码错、库不存在、磁盘满最常见)。
- 巡检报告重复内容堆积:脚本被多次调度写入同一文件。报告按天命名(inspect-$(date +%F).txt)并在头部加时间戳,或调度前先判断是否已有当次运行标记。
面试常问
-
现场写一个 Shell 脚本:每天凌晨备份 MySQL 并保留 7 天。
答题骨架:变量区(库名、目录、保留天数)→ log/die 函数 → mysqldump --single-transaction | gzip → [[ -s ]] 验证 → find -mtime +7 -delete → crontab 0 3 * * * 脚本路径 >> 日志 2>&1。说出「不锁表、验证产物、自动清理」三个关键词就是高分答案。
-
你有写过的自动化运维脚本吗?讲讲设计思路。
参考思路:三个脚本(备份/巡检/日志分析)共同遵循「set -euo pipefail + 配置头部集中 + 函数封装 + 统一日志 + 失败即退出」五要素;巡检脚本预留 ssh 远程执行接口实现多机扩展;所有脚本可被 crontab 调度且输出重定向留痕。重点展示「结构化思维」而不是罗列命令。
-
如何统计昨天 Nginx 日志中每个接口的平均响应时间 Top5?
前提:日志里有 $request_time 字段。思路:awk '$4 ~ "06/Oct/2026" {split($7,a,"?"); sum[a[1]]+=行内耗时; cnt[a[1]]++} END {for (u in sum) printf "%.3f %d %s\n", sum[u]/cnt[u], cnt[u], u}' access.log | sort -rn | head -5。考点:awk 关联数组、split 去掉查询参数、END 块汇总输出、sort -rn 排序。
自测三问
- 备份脚本里
[[ -s "$FILE" ]] 防的是什么事故?
- 巡检脚本为什么用
set -uo pipefail 而不开 -e?
- 把本机巡检改成多机巡检,最少需要改哪几处?
小结一句话
备份保数据、巡检保稳定、分析找问题——三个脚本五要素(安全带、可配置、防御、留痕、自清理)跑通,你就完成了从「会敲命令」到「会写工具」的跨越。
延伸
- ShellCheck:脚本静态检查工具
- 下一步建议:把本课脚本用 ShellCheck 扫一遍并修正告警;进阶学习 systemd 定时器与 Ansible 批量运维,向基础设施自动化方向深入。