🚀 运维进阶学习规划¶
更新时间:2026-06-10 目标:脚本 → Python 自动化 → 容器 → 云计算
当前状态¶
| 项目 | 状态 | 说明 |
|---|---|---|
| RHCSA | ✅ 已通过 | |
| RHCE | ✅ 已通过 | 2026-06-08 |
| systemd 服务管理 | ✅ 在用 | nginx、mkdocs、openclaw、hermes |
| Nginx 反向代理 | ✅ 已配置 | know.bbben.xyz、openclaw.bbben.xyz |
| 基本进程管理 | ✅ 会用 | ps、top、kill |
环境准备¶
服务器(主要练习环境)¶
# 系统更新
sudo apt update && sudo apt upgrade -y
# 基础工具
sudo apt install -y curl wget git vim tmux htop tree jq unzip
# 开发工具
sudo apt install -y build-essential python3 python3-pip python3-venv
# 网络工具
sudo apt install -y net-tools dnsutils tcpdump nmap netcat-openbsd
# Docker(容器阶段再装)
# curl -fsSL https://get.docker.com | sh
# sudo usermod -aG docker $USER
工具说明:
| 工具 | 功能 | 常用场景 |
|---|---|---|
curl |
命令行 HTTP 客户端,发送各种协议请求 | 测 API、下载文件、测网关连通性 |
wget |
文件下载工具,支持断点续传 | 下载大文件、批量下载 |
git |
版本控制工具 | 管理代码、团队协作、回滚版本 |
vim |
终端文本编辑器 | 服务器上改配置文件、写脚本 |
tmux |
终端复用器,一个终端分多个窗口 | SSH 断开后任务不中断、多窗口并行 |
htop |
增强版 top,交互式进程监控 | 查看 CPU/内存占用、杀进程 |
tree |
目录树可视化 | 看文件结构一目了然 |
jq |
JSON 命令行处理器 | 解析 API 返回、日志过滤 |
unzip |
解压 zip 文件 | 解压部署包 |
build-essential |
GCC/G++/make 等编译工具集 | 编译安装软件、Python 扩展 |
python3-pip |
Python 包管理器 | 装 Python 库(psutil、flask 等) |
python3-venv |
Python 虚拟环境 | 隔离项目依赖,避免污染全局 |
net-tools |
网络工具集(ifconfig、netstat 等) | 查看网卡、端口占用 |
dnsutils |
DNS 工具(dig、nslookup) | 解析域名、排查 DNS 问题 |
tcpdump |
网络抓包工具 | 分析网络流量、排查连接问题 |
nmap |
网络扫描工具 | 扫描端口、探测主机 |
netcat-openbsd |
网络瑞士军刀(nc) | 测端口连通性、简易服务器 |
Mac(开发环境)¶
# Homebrew(如果没有)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 基础工具
brew install tree htop jq wget
# Python
brew install python@3.11
# Docker Desktop(容器阶段)
# brew install --cask docker
| 工具 | 功能 | 常用场景 |
|---|---|---|
Homebrew |
macOS 包管理器(类似 apt) | 装 Mac 上的各种工具 |
Docker Desktop |
Docker 图形化管理工具 | 容器开发、镜像构建、可视化管理 |
Tailscale 组网¶
# 服务器
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Mac
brew install tailscale
# 或 App Store 安装 Tailscale
时间线总览¶
| 阶段 | 内容 | 周数 | 优先级 |
|---|---|---|---|
| 第一阶段 | Shell 脚本与自动化 | 3 周 | 🔴 当前重点 |
| 第二阶段 | Python 运维自动化 | 2 周 | 🔴 当前重点 |
| 第三阶段 | Docker 容器化 | 3 周 | 🟡 脚本熟练后 |
| 第四阶段 | Kubernetes 入门 | 2 周 | 🟢 Docker 熟练后 |
| 第五阶段 | 云计算与 DevOps | 2 周 | 🟢 后续拓展 |
总计约 12 周,可根据实际情况调整节奏。
第一阶段:Shell 脚本与自动化(3 周)¶
1.1 Shell 脚本基础 — 变量、条件判断、循环、函数¶
📚 基础知识补充¶
Bash 脚本运行方式¶
| 方式 | 示例 | 需要执行权限? | 在当前shell执行? |
|---|---|---|---|
| 直接执行 | ./script.sh |
✅ | ❌(子shell) |
| source 执行 | source script.sh 或 . script.sh |
❌ | ✅(当前shell) |
| bash 解释器 | bash script.sh |
❌ | ❌(子shell) |
| sh 解释器 | sh script.sh |
❌ | ❌(子shell) |
| 重定向输入 | bash < script.sh |
❌ | ❌(子shell) |
| 管道输入 | cat script.sh \| bash |
❌ | ❌(子shell) |
核心区别两点:
1. 执行权限:./script.sh 需要 chmod +x,其他方式不需要(用解释器读文件)
2. 执行环境:source/. 在当前 shell 执行(变量、函数、cd 会影响当前环境);其他在子 shell 执行(跑完环境销毁)
实际场景:
- 日常脚本:./script.sh 或 bash script.sh
- 改环境变量:source script.sh
- 临时跑一下:bash script.sh(不用加权限,最省事)
Shell 脚本中的换行符¶
1. 行尾换行 — 命令结束符
每行末尾隐含一个 \n,shell 靠它判断命令结束、该执行了。
2. 续行符 \ — 把多行合成一行
行末加 \,告诉 shell 下一行还是当前命令,别急着执行:
3. 多行结构 — 换行是语法的一部分
if、for、函数定义等,换行就是语法结构的分隔符。
4. \n 转义序列
在 echo、printf 里手动插入换行字符:
简单记:行尾换行 = shell 的"分号"表示命令结束;\ = 换行的转义告诉 shell 继续;\n = 字面意义上的换行字符用在字符串里。
echo 中的 \n 换行不生效问题¶
echo 默认不解释转义序列,\n 会被当作两个字面字符输出。
解决方案:
1. echo -e "hello\nworld" — 加 -e 选项(不同系统行为可能不一致)
2. echo $'hello\nworld' — 用 $'...' 语法
3. printf "hello\nworld\n" — 推荐,行为最确定
建议:写脚本优先用 printf,因为 echo -e 在 GNU 和 BSD(macOS)上行为不一致。
Shell 中 {} 花括号 vs () 圆括号¶
${} — 变量引用
$() — 命令替换 执行括号里的命令,把输出结果赋值或嵌入:
() 的其他用法:
- 子shell:(cd /tmp; ls) — 在子shell里执行,不影响当前目录
- 数组定义:arr=(a b c)
- 算术运算:$((2+3)) — 结果是5
- 进程替换:diff <(cmd1) <(cmd2) — 把命令输出当文件用
简单记:{} → 包变量名,防歧义;$() → 包命令,拿结果;$(( )) → 包算式,算数。
中断正在运行的 Shell 脚本¶
Ctrl+C — 最常用 发送 SIGINT 中断信号。如果脚本里有子进程(如 ping),Ctrl+C 只杀子进程,循环可能继续。多按几次。
Ctrl+Z — 挂起(暂停不终止)
脚本暂停,进程还在。后续操作:
- jobs — 查看挂起进程
- fg — 恢复运行
- kill %1 — 杀掉挂起的进程
kill/pkill — 最彻底
另开终端窗口:
- ps aux | grep 脚本名 找进程ID
- kill <PID> 普通杀
- kill -9 <PID> 强制杀
- pkill -f 脚本名.sh 按名字杀,最方便
练习 1:域名解析脚本
题目: 写一个脚本 resolve.sh,接收一个域名参数,自动解析出 IP 地址并输出。要求判断参数是否为空,为空时给出提示。
实际场景: 运维日常排查 DNS 问题,比如网站打不开先确认域名是否正确解析到服务器 IP;部署新服务前验证域名指向;监控脚本里定时检查 DNS 是否被篡改。
思路:
- 脚本要接收一个域名作为输入 → 需要判断用户有没有传参数
- 判断参数为空用
[ -z "$1" ],为空就提示用法并退出 - 拿到域名后,解析 IP → 用
dig命令,+short只输出 IP - 解析结果可能有多条(一个域名对应多个 IP),取第一条就行
- 如果解析失败(结果为空),要给用户报错提示
- 最后输出“域名 → IP”的格式化结果
答案:
#!/bin/bash # Shebang 行,告诉系统用 bash 来执行这个脚本
# resolve.sh — 域名解析脚本 # 脚本名称和用途说明
# 检查是否传了参数
# $1 是第一个位置参数(你执行脚本时跟在后面的第1个值)
# -z 判断字符串是否为空
if [ -z "$1" ]; then # 如果 $1 为空(没传参数)
echo "用法: $0 <域名>" # $0 是脚本自身的名字
echo "示例: $0 bbben.xyz"
exit 1 # exit 1 表示异常退出(非0都是异常)
fi
# 把第一个参数赋值给变量 domain
domain="$1" # 引号防止特殊字符出问题
# 用 dig 命令解析域名,+short 只返回 IP,head -1 取第一行
# $() 是命令替换,把命令的输出结果赋给变量
ip=$(dig +short "$domain" | head -1)
# 检查是否解析成功
if [ -z "$ip" ]; then # 如果 ip 变量为空,说明解析失败
echo "❌ 无法解析域名: $domain"
exit 1
fi
# 输出结果
echo "✅ $domain → $ip" # 双引号里的变量会被展开
运行方式:
预期效果:
实战经验:
- dig +short 的 + 是 dig 特有选项前缀,+short 必须连写,写成 + short 会报错 ; Invalid option
- dig 是 DNS 查询工具(Domain Information Groper),运维排查 DNS 问题首选,比 nslookup 和 host 信息更全
- dig 域名 正向解析,dig -x IP 反向解析,@8.8.8.8 指定 DNS 服务器
- 脚本里的 dig +short "$domain" | head -1:+short 精简输出,head -1 取第一行(一个域名可能对应多个 IP)
- 易错点: Shell 对空格敏感,[ -z "$1" ] 的空格不能丢,[ 后面和 ] 前面必须有空格
- 易错点: 复制粘贴脚本时经常丢失空格,运行报错先检查空格是否完整
练习 2:用户权限判断
题目: 写一个脚本,判断当前用户是 root 还是普通用户。root 用户执行 df -h 显示磁盘使用情况,普通用户显示提示信息并输出自己的用户 ID。
实际场景: 防止普通用户误执行需要 root 权限的命令导致报错;运维脚本里先判断权限再决定执行哪些操作,比如普通用户只允许查看,root 才能修改配置。
思路:
- 需要判断当前用户身份 → 用
id -u获取 UID,root 的 UID 永远是 0 - 用
if [ "$uid" -eq 0 ]做数值比较 - root 用户 → 直接执行
df -h - 普通用户 → 先用
whoami显示用户名,再用id -u显示 UID - 用
else分支处理两种情况
答案:
#!/bin/bash # 用 bash 执行
# check-user.sh — 用户权限判断
# id -u 输出当前用户的 UID(用户ID号)
# root 的 UID 永远是 0,普通用户从 1000 开始
# -eq 是数值比较:等于(equal)
if [ "$(id -u)" -eq 0 ]; then # $(id -u) 执行命令并获取输出
echo "✅ 当前是 root 用户"
echo "磁盘使用情况:"
df -h # df -h 以人类可读格式显示磁盘使用
else
# whoami 输出当前用户名
echo "⚠️ 当前是普通用户: $(whoami) (UID=$(id -u))"
echo "如需执行 root 操作,请使用 sudo"
fi
预期效果(root 用户):
预期效果(普通用户):
实战经验:
- Shell if 判断与退出码(exit code): 每个命令执行后都有退出码,0 = 命令成功,非0 = 命令失败
- &>/dev/null 只是丢弃屏幕输出,退出码照样存在,shell 看的是退出码
- 反直觉的 if 逻辑: 退出码 0 → 条件成立 → 执行 then;退出码 非0 → 条件不成立 → 跳过
- Shell 继承 C 语言约定:0 表示成功(没有错误),非 0 表示有错误
- 简单记:0 = 成功 = 真 = if 里走 then
练习 3:批量 Ping 探测
题目: 写一个脚本,用 for 循环 ping 192.168.1.1 到 192.168.1.20,只显示存活的主机,最后统计存活数量。
实际场景: 公司内网新到一批设备,快速扫描哪些已经开机联网;机房巡检时批量检查服务器是否在线;排除网络故障时确认目标主机是否可达。
思路:
- 需要 ping 多个 IP → 写一个循环,从 1 到 20 遍历
- 拼接 IP 地址:
192.168.1.$i - 用
ping -c 1 -W 1只发 1 个包,超时 1 秒,避免卡住 - 判断 ping 是否成功 → 看
$?(上一条命令的返回值),0 表示成功 - 成功的打印 ✅,失败的不打印(只显示存活的)
- 最后统计成功数量
答案:
#!/bin/bash # 用 bash 执行
# ping-scan.sh — 批量 Ping 探测
network="192.168.1" # 定义网段前缀
count=0 # 计数器,初始为 0
echo "🔍 扫描 ${network}.1-20 ..." # ${network} 和 $network 等价,花括号更明确
echo "---------------------------"
# for 循环:seq 1 20 生成 1 到 20 的数字序列
for i in $(seq 1 20); do # 每次循环 i 从 1 变到 20
ip="${network}.${i}" # 拼接完整 IP,如 192.168.1.1
# ping -c 1:只发 1 个包 -W 1:超时 1 秒
# &>/dev/null:把标准输出和错误输出都丢弃(不显示在终端)
# 这里只关心命令的返回值:成功=0(存活),失败=非0(不存活)
if ping -c 1 -W 1 "$ip" &>/dev/null; then
echo "✅ $ip 存活"
((count++)) # (( )) 是数学运算,count 自增 1
fi
done
echo "---------------------------"
echo "📊 共发现 $count 台存活主机"
预期效果:
🔍 扫描 192.168.1.1-20 ...
---------------------------
✅ 192.168.1.1 存活
✅ 192.168.1.5 存活
---------------------------
📊 共发现 2 台存活主机
练习 4:端口检测函数
题目: 写一个脚本,定义一个 check_port 函数,接收 IP 和端口号两个参数,检测端口是否开放。支持一次检测多个端口。
实际场景: 郤署新服务后验证端口是否正常监听;排查防火墙问题时确认端口是否被拦截;安全审计时检查服务器开放了哪些端口。
思路:
- 要检测端口是否开放 → 用 bash 内置的
/dev/tcp机制 - 把检测逻辑封装成函数
check_port,接收主机和端口两个参数 - 用
timeout 2限制超时,避免卡死 - 端口开放 → 返回 0,关闭 → 返回非 0
- 主程序用
for循环遍历多个端口,逐个调用函数检测
答案:
#!/bin/bash # 用 bash 执行
# check-port.sh — 端口检测函数
# 定义函数 check_port
# 函数用 关键字() { } 包裹,调用时直接写函数名
cHECK_PORT() {
local host="$1" # local 声明局部变量,只在函数内有效
local port="$2" # $1 是函数第一个参数,$2 是第二个
# timeout 2:2秒超时
# bash -c "echo >/dev/tcp/$host/$port":
# /dev/tcp 是 bash 内置的网络测试机制
# 如果端口能连上,echo 写入成功(返回0)
# 如果连不上,会报错(返回非0)
# 2>/dev/null:丢弃错误信息
if timeout 2 bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null; then
echo "✅ $host:$port 开放"
else
echo "❌ $host:$port 关闭"
fi
}
# 检测本机常用端口
host="127.0.0.1" # 127.0.0.1 是本机回环地址
echo "🔍 检测 $host 的端口状态"
echo "---------------------------"
# for 循环遍历每个端口号
for port in 22 80 443 3306 8000 18789; do
check_port "$host" "$port" # 调用函数,传入主机和端口
done
预期效果:
🔍 检测 127.0.0.1 的端口状态
---------------------------
✅ 127.0.0.1:22 开放
✅ 127.0.0.1:80 开放
❌ 127.0.0.1:443 关闭
❌ 127.0.0.1:3306 关闭
✅ 127.0.0.1:8000 开放
✅ 127.0.0.1:18789 开放
练习 5:服务自动重启监控
题目: 写一个脚本,用 while 循环每 5 秒检查一次 nginx 服务是否存活,如果挂了就自动重启,并记录日志。
实际场景: 生产环境关键服务(Nginx、数据库)的守护进程,防止服务意外崩溃导致网站宕机;比 systemd 的 Restart 更灵活,可以加自定义逻辑(比如发告警通知)。
思路:
- 用
while true无限循环,每隔几秒检查一次 - 检查 nginx 是否在运行 →
systemctl is-active nginx - 正常运行 → 记录日志,继续循环
- 挂了 → 自动重启
systemctl restart nginx - 重启后再验证一次是否成功
- 所有操作记录到日志文件,方便事后排查
答案:
#!/bin/bash # 用 bash 执行
# watch-nginx.sh — Nginx 自动重启监控
log="/var/log/nginx-watch.log" # 日志文件路径
echo "👁️ 开始监控 nginx 服务 (每5秒检查一次)"
echo "按 Ctrl+C 停止"
# while true 无限循环,脚本会一直运行直到按 Ctrl+C
while true; do
# systemctl is-active nginx:检查 nginx 是否在运行
# &>/dev/null:丢弃所有输出,只看返回值(0=运行中,非0=已停止)
if systemctl is-active nginx &>/dev/null; then
# 正常运行时,追加写入日志(>> 是追加,> 是覆盖)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ nginx 正常运行" >> "$log"
else
# nginx 挂了,tee -a 同时输出到终端 AND 追加到日志文件
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ❌ nginx 已停止,正在重启..." | tee -a "$log"
systemctl restart nginx # 重启 nginx
sleep 2 # 等 2 秒让服务启动
# 再次检查是否重启成功
if systemctl is-active nginx &>/dev/null; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ nginx 重启成功" | tee -a "$log"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ❌ nginx 重启失败!" | tee -a "$log"
fi
fi
sleep 5 # 每次检查间隔 5 秒
done
预期效果:
👁️ 开始监控 nginx 服务 (每5秒检查一次)
按 Ctrl+C 停止
[2026-06-08 20:30:01] ❌ nginx 已停止,正在重启...
[2026-06-08 20:30:03] ✅ nginx 重启成功
1.2 正则表达式、sed、awk¶
练习 1:提取 Nginx 500 错误
题目: 从 Nginx 访问日志中提取所有 500 错误的请求,显示时间、URL 和客户端 IP。
实际场景: 网站报 500 错误时快速定位是哪个接口出了问题;排查用户投诉“页面打不开”时确认是服务端错误还是客户端问题;分析攻击来源(某个 IP 频繁触发 500 可能在扫描漏洞)。
思路:
- Nginx 日志每行有很多字段(IP、时间、URL、状态码等)
- 用
awk按空格分隔字段,第 $9 个字段是 HTTP 状态码 - 筛选
$9 == 500的行 - 输出需要的字段:时间(\(4)、URL(\)7)、客户端IP($1)
- 用
head -20限制只看前 20 条
答案:
或写成脚本:
#!/bin/bash # 用 bash 执行
# extract-500.sh — 提取 Nginx 500 错误
log="/var/log/nginx/access.log" # Nginx 访问日志路径
# -f 判断文件是否存在,! 取反
# [ ! -f "$log" ] = 如果日志文件不存在
if [ ! -f "$log" ]; then
echo "❌ 日志文件不存在: $log"
exit 1
fi
echo "🔍 提取 500 错误记录"
echo "---------------------------"
# awk 处理每一行日志:
# $9 = 第9个字段(HTTP状态码),$4=时间,$1=客户端IP,$7=请求的URL
# gsub 是全局替换,去掉时间字段里的 [ ] 方括号
awk '$9 == 500 { # 条件:第9个字段等于500
gsub(/\[/, "", $4); # 去掉 $4 里的左方括号 [
gsub(/\]/, "", $4); # 去掉 $4 里的右方括号 ]
printf "时间: %-20s IP: %-16s URL: %s\n", $4, $1, $7 # 格式化输出
}' "$log"
# 统计500错误总数
# awk '$9 == 500' 过滤出500的行,wc -l 统计行数
count=$(awk '$9 == 500' "$log" | wc -l)
echo "---------------------------"
echo "📊 共 $count 条 500 错误"
预期效果:
🔍 提取 500 错误记录
---------------------------
时间: 08/Jun/2026:14:23:01 IP: 192.168.1.100 URL: /api/data
时间: 08/Jun/2026:15:10:22 IP: 10.0.0.5 URL: /submit
---------------------------
📊 共 2 条 500 错误
练习 2:批量替换配置文件 IP
题目: 用 sed 把配置文件中所有 192.168.1.100 替换成 10.0.0.50,先预览变更,确认后再执行。
实际场景: 服务器迁移 IP 变更后批量修改 Nginx/数据库/应用配置;批量更新配置文件中的端口号、路径等参数;先预览再执行避免改错配置导致服务挂掉。
思路:
- 用
sed做字符串替换,语法:sed 's/旧/新/g' - 直接改文件会不可逆 → 先用
grep -n预览哪些行会被改 - 确认无误后,用
sed -i原地修改文件 - 改完后再
grep验证,确认旧 IP 已经不存在 - 用
-i.bak可以自动备份原文件,方便回滚
答案:
#!/bin/bash # 用 bash 执行
# replace-ip.sh — 批量替换配置文件 IP
old_ip="192.168.1.100" # 要被替换的旧 IP
new_ip="10.0.0.50" # 替换后的新 IP
config="/etc/nginx/sites-available/default" # 要修改的配置文件
echo "📋 预览变更:"
echo "---------------------------"
# grep -n 显示匹配行的行号,方便定位
# while read line 逐行读取 grep 的输出
while read line; do
echo " 找到: $line"
done < <(grep -n "$old_ip" "$config") # <(...) 是进程替换
echo ""
# read -p 显示提示并等待用户输入
read -p "确认替换?(y/n): " confirm
# = 是字符串比较(数值比较用 -eq)
if [ "$confirm" = "y" ]; then
# sed -i 直接修改文件(不加 -i 只输出到终端)
# s/旧/新/g = 全局替换(g=global,替换所有匹配)
sed -i "s/$old_ip/$new_ip/g" "$config"
echo "✅ 替换完成"
echo "验证:"
grep -n "$new_ip" "$config" # 确认替换结果
else
echo "❌ 已取消"
fi
预期效果:
📋 预览变更:
---------------------------
找到: 5: server_name 192.168.1.100;
找到: 8: proxy_pass http://192.168.1.100:8000;
确认替换?(y/n): y
✅ 替换完成
验证:
5: server_name 10.0.0.50;
8: proxy_pass http://10.0.0.50:8000;
练习 3:统计访问量 Top 10 IP
题目: 用 awk + sort + uniq 统计 Nginx 日志中访问量最高的前 10 个 IP。
实际场景: 分析网站流量来源,找出主要用户群体;发现 DDoS 攻击(某个 IP 访问量异常高);统计爬虫 IP 进行封禁;为 CDN 或防火墙提供黑白名单依据。
思路:
- Nginx 日志每行第 1 个字段是客户端 IP
- 用
awk '{print $1}'提取所有 IP sort排序(让相同 IP 相邻)uniq -c去重并计数sort -rn按数量逆序排序head -10取前 10 名
答案:
#!/bin/bash # 用 bash 执行
# top-ips.sh — 统计访问量 Top 10 IP
log="/var/log/nginx/access.log" # Nginx 访问日志路径
echo "🏆 访问量 Top 10 IP"
echo "---------------------------"
# 管道处理流程:
# 1. awk '{print $1}' → 提取每行第1个字段(客户端IP)
# 2. sort → 排序(把相同的IP排在一起)
# 3. uniq -c → 去重并统计每个IP出现的次数
# 4. sort -rn → 按数字逆序排序(最多的排最前面)
# 5. head -10 → 取前10行
# 6. while read → 逐行读取,格式化输出
awk '{print $1}' "$log" | sort | uniq -c | sort -rn | head -10 | \
while read count ip; do
# printf 格式化输出,%-6s 左对齐占6个字符宽度
printf "%-6s 次 %s\n" "$count" "$ip"
done
预期效果:
🏆 访问量 Top 10 IP
---------------------------
3521 次 154.12.60.122
1205 次 192.168.1.1
847 次 10.0.0.5
...
练习 4:清理配置文件注释和空行
题目: 用 sed 删除配置文件中的所有注释行(# 开头)和空行,输出清理后的内容。
实际场景: 审计配置文件时快速查看有效配置项;配置文件导入其他系统时去掉注释避免解析错误;对比两个配置文件差异前先清理格式。
思路:
- 注释行以
#开头 → 用 sed 的正则/^#/d删除 - 空行 → 用 sed 的
/^$/d删除 - 两个条件可以合并:
sed '/^#/d; /^$/d' - 不改原文件,只输出到终端(不加
-i) - 如果需要保存结果,用重定向写入新文件
答案:
#!/bin/bash # 用 bash 执行
# clean-config.sh — 清理配置文件注释和空行
config="$1" # $1 是脚本的第一个参数(配置文件路径)
# 检查参数是否为空,或者文件是否存在
if [ -z "$config" ] || [ ! -f "$config" ]; then
echo "用法: $0 <配置文件>"
exit 1
fi
echo "📋 原始行数: $(wc -l < "$config")" # wc -l 统计行数,< 避免显示文件名
# sed '/^#/d; /^$/d' 详解:
# /^#/d → 删除以 # 开头的行(注释行)
# ^# → 以 # 开头
# d → delete 删除
# /^$/d → 删除空行
# ^$ → 从行首到行尾之间什么都没有(空行)
echo "📋 清理后行数: $(sed '/^#/d; /^$/d' "$config" | wc -l)"
echo "---------------------------"
echo "清理后内容:"
echo "---------------------------"
sed '/^#/d; /^$/d' "$config" # 输出清理后的内容(不加 -i 不改原文件)
运行方式:
预期效果:
📋 原始行数: 35
📋 清理后行数: 12
---------------------------
清理后内容:
---------------------------
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
sendfile on;
...
}
练习 5:分析 SSH 登录失败记录
题目: 用 awk 分析 /var/log/auth.log,找出所有登录失败的来源 IP,按次数排序。
实际场景: 安全运维日常巡检,发现暴力破解 SSH 的攻击 IP;统计哪些 IP 在尝试爆破,为 fail2ban 或防火墙封禁提供依据;生成安全报告汇报给领导。
思路:
- SSH 登录失败会在
/var/log/auth.log记录 "Failed password" - 先用
grep过滤出这些行 - 用
awk提取攻击者 IP(在日志行的倒数第 4 个字段) - 后面的流程和统计 Top IP 一样:
sort → uniq -c → sort -rn → head - 最后统计总失败次数
答案:
#!/bin/bash # 用 bash 执行
# ssh-failures.sh — 分析 SSH 登录失败记录
log="/var/log/auth.log" # 认证日志路径(Ubuntu/Debian)
# CentOS/RHEL 是 /var/log/secure
if [ ! -f "$log" ]; then # 如果日志文件不存在
echo "❌ 日志文件不存在: $log"
exit 1
fi
echo "🔒 SSH 登录失败 IP 排行"
echo "---------------------------"
# 管道处理流程:
# 1. grep "Failed password" → 过滤出包含 "Failed password" 的行(登录失败记录)
# 2. awk '{print $(NF-3)}' → 提取倒数第4个字段(就是攻击者IP)
# NF 是当前行的字段总数,NF-3 就是倒数第4个
# 3. sort → 排序(把相同IP放一起)
# 4. uniq -c → 去重并计数
grep "Failed password" "$log" | \
awk '{print $(NF-3)}' | \ # 提取IP地址
sort | uniq -c | sort -rn | head -10 | \
while read count ip; do
printf "%-6s 次 %s\n" "$count" "$ip"
done
# -c 只统计匹配行的数量(不输出内容)
total=$(grep -c "Failed password" "$log")
echo "---------------------------"
echo "📊 共 $total 次登录失败尝试"
预期效果:
🔒 SSH 登录失败 IP 排行
---------------------------
45 次 185.220.101.1
23 次 45.155.205.1
12 次 192.168.1.50
---------------------------
📊 共 80 次登录失败尝试
1.3 管道与重定向¶
练习 1:分别输出 stdout 和 stderr
题目: 写一个脚本,把标准输出写入一个文件,错误输出写入另一个文件。
实际场景: 定时任务执行脚本时,正常日志和错误日志分开记录方便排查;批量处理文件时统计成功数和失败数;CI/CD 流水线里分离构建日志和错误信息。
思路:
- Linux 有三个标准文件描述符:0=stdin、1=stdout、2=stderr
>重定向 stdout(默认是 1),2>重定向 stderr- 脚本里用
>&2把输出发送到 stderr - 运行时
./script > out.log 2> err.log分别捕获 - 这样正常信息和错误信息各归各的文件
答案:
#!/bin/bash # 用 bash 执行
# redirect.sh — 分别输出 stdout 和 stderr
# echo 默认输出到标准输出(stdout,文件描述符1)
echo "这是标准输出"
# >&2 把输出重定向到标准错误(stderr,文件描述符2)
# 文件描述符:0=stdin(输入), 1=stdout(正常输出), 2=stderr(错误输出)
echo "这是错误输出" >&2
# 运行方式:
# ./redirect.sh > /tmp/stdout.log 2> /tmp/stderr.log
# > 重定向 stdout 到文件
# 2> 重定向 stderr 到文件
预期效果:
$ ./redirect.sh > /tmp/stdout.log 2> /tmp/stderr.log
$ cat /tmp/stdout.log
这是标准输出
$ cat /tmp/stderr.log
这是错误输出
练习 2:tee 同时输出和记录
题目: 用 tee 命令把 nginx 状态信息同时显示在终端和写入日志文件。
实际场景: 运维执行巡检命令时既要实时看到结果又要留痕;部署脚本执行过程同时输出到终端和日志文件;排查问题时边操作边记录操作日志。
思路:
>只写文件不显示,| tee既写文件又显示在终端- 语法:
command | tee file.log - 默认覆盖文件,加
-a变成追加模式 - 可以同时 tee 多个文件:
command | tee file1 file2 - 适合巡检脚本:实时看到输出 + 留日志记录
答案:
#!/bin/bash # 用 bash 执行
# tee-log.sh — tee 同时输出和记录
# $(date +%Y%m%d) 生成日期字符串如 20260610,用在日志文件名里
log="/tmp/nginx-status-$(date +%Y%m%d).log"
# tee -a 的作用:
# tee 本身是 "三通管",从管道读入,同时输出到屏幕和文件
# -a 表示 append(追加),不加 -a 会覆盖文件
echo "=== Nginx 状态报告 ===" | tee -a "$log"
echo "时间: $(date)" | tee -a "$log"
echo "---------------------------" | tee -a "$log"
systemctl status nginx --no-pager | tee -a "$log"
# --no-pager 不分页,直接输出所有内容
echo "---------------------------" | tee -a "$log"
echo "✅ 报告已保存到: $log"
预期效果:
=== Nginx 状态报告 ===
时间: Mon 08 Jun 2026 08:30:00 PM CST
---------------------------
● nginx.service - A high performance web server
Loaded: loaded
Active: active (running) since Mon 2026-06-08
---------------------------
✅ 报告已保存到: /tmp/nginx-status-20260608.log
练习 3:管道组合查进程
题目: 用管道组合命令,查看当前内存使用最多的 5 个进程。
实际场景: 服务器变慢时快速找出内存占用最高的进程;判断是否被挖矿程序感染(异常进程占满内存);决定是否需要重启某个内存泄漏的服务。
思路:
ps aux显示所有进程的详细信息--sort=-%mem按内存使用率降序排序head -6取前 6 行(第 1 行是标题 + 前 5 个进程)awk格式化输出,只显示 USER、PID、%MEM、COMMAND- 这是一条管道链:
ps → sort → head → awk
答案:
ps aux --sort=-%mem | head -6 | awk 'NR==1{print $0} NR>1{printf "%-10s %-8s %-6s %s\n", $1, $2, $4, $11}'
预期效果:
练习 4:追加模式写日志
题目: 写一个脚本,把所有输出追加写入日志文件(不覆盖),同时在终端显示。
实际场景: 长时间运行的运维脚本需要记录完整执行历史;巡检报告生成后追加到月度汇总文件;多次执行部署脚本时保留每次的执行记录。
思路:
>覆盖写入,>>追加写入 → 用>>保留历史command >> file.log每次执行都追加到同一文件- 用
tee -a可以同时追加到文件 + 显示在终端 - 日志文件名带日期可以按天归档
- 给日志加时间戳前缀,方便排查
答案:
#!/bin/bash # 用 bash 执行
# append-log.sh — 追加模式写日志
log="/var/log/my-script.log" # 日志文件路径
# >> 是追加写入(不会覆盖已有内容)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 脚本开始执行" >> "$log"
# tee -a 同时做两件事:
# 1. 输出到终端(让你看到)
# 2. 追加写入日志文件(-a = append)
echo "磁盘使用情况:" | tee -a "$log"
df -h / | tee -a "$log" # df -h / 只看根分区
echo "内存使用情况:" | tee -a "$log"
free -h | tee -a "$log" # free -h 人类可读的内存信息
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 脚本执行完毕" >> "$log"
预期效果:
磁盘使用情况:
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 40G 12G 26G 32% /
内存使用情况:
total used free shared buff/cache available
Mem: 3.8Gi 1.2Gi 1.5Gi 8.0Mi 1.1Gi 2.4Gi
练习 5:错误输出合并过滤
题目: 把标准输出和错误输出合并,然后通过管道过滤出包含 "error" 的行。
实际场景: 批量执行命令后从混合输出中快速提取错误信息;日志分析时过滤出所有报错行;排查脚本执行失败原因时只看错误输出。
思路:
- stdout 和 stderr 默认分开显示,合并用
2>&1(把 stderr 合并到 stdout) - 合并后用管道
|传给grep grep -i "error"过滤包含 error 的行(-i 忽略大小写)- 这样混合输出里只留下错误信息
- 也可以用
|&简写(bash 4.0+)
答案:
#!/bin/bash # 用 bash 执行
# merge-filter.sh — 合并输出并过滤
# { } 把多条命令包成一组
# >&2 输出到 stderr(错误输出)
# 2>&1 把 stderr(2) 合并到 stdout(1)
# 最终效果:stdout 和 stderr 的内容都通过管道传给 grep
{
echo "info: 系统正常" # 这个输出到 stdout
echo "error: 连接超时" >&2 # 这个输出到 stderr
echo "info: 重试中"
echo "error: 认证失败" >&2 # 这个也输出到 stderr
echo "info: 任务完成"
} 2>&1 | grep -i "error" # 2>&1 合并后 grep 能同时过滤两种输出
# -i 忽略大小写(error 和 ERROR 都匹配)
预期效果:
1.4 Crontab 定时任务¶
练习 1:每天凌晨 3 点备份数据库
题目: 设置一个 cron 任务,每天凌晨 3 点执行 MySQL 备份脚本,日志追加写入文件。
实际场景: 生产数据库必须定期备份,防止数据丢失;凌晨 3 点是访问低峰期,备份不影响业务;自动清理 30 天前的备份避免磁盘占满。
思路:
- cron 时间格式:
分 时 日 月 周,凌晨 3 点 =0 3 * * * - 用
crontab -e编辑定时任务 - 备份脚本的输出用
>>追加到日志文件 - 脚本里加
find ... -mtime +30 -delete自动清理旧备份 - crontab 里写绝对路径,cron 环境变量和登录不同
答案:
# 先写备份脚本
cat > /opt/scripts/backup-db.sh << 'EOF' # cat > 文件 << 'EOF' 是创建文件的快捷方式
#!/bin/bash
backup_dir="/backup/mysql" # 备份存放目录
date=$(date +%Y%m%d) # 日期如 20260610,用在文件名里
mkdir -p "$backup_dir" # -p 递归创建目录,已存在不报错
# mysqldump 导出所有数据库 | gzip 压缩 > 重定向到文件
mysqldump --all-databases | gzip > "$backup_dir/all-db-$date.sql.gz"
# find 查找 30 天前的备份文件并删除
# -mtime +30 表示修改时间超过 30 天
find "$backup_dir" -name "*.sql.gz" -mtime +30 -delete
echo "[$(date)] 备份完成: all-db-$date.sql.gz"
EOF
chmod +x /opt/scripts/backup-db.sh # 加执行权限
# 添加 cron 任务:
# crontab -l 2>/dev/null 列出现有任务(报错丢弃)
# 分钟 小时 日 月 周 = 0 3 * * * = 每天凌晨3点
# 2>&1 把错误输出也写入日志
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/scripts/backup-db.sh >> /var/log/db-backup.log 2>&1") | crontab -
# | crontab - 把前面的输出(原有任务+新任务)设置为新的 crontab
预期效果: 每天凌晨 3 点自动备份,日志写入 /var/log/db-backup.log
练习 2:每 10 分钟检查 Nginx
题目: 设置每 10 分钟检查一次 Nginx 是否存活,挂了自动重启。
实际场景: 比 systemd 的 Restart 更灵活的守护方案;可以加自定义逻辑(比如重启失败时发告警邮件);适合没有 systemd 的老系统或容器环境。
思路:
- 每 10 分钟 =
*/10 * * * * - 检查服务状态:
systemctl is-active nginx - 不正常则重启:
systemctl restart nginx - 把检查逻辑写成脚本,cron 只调用脚本
- 脚本里加日志记录,方便排查
答案:
cat > /opt/scripts/check-nginx.sh << 'EOF'
#!/bin/bash
# ! 取反:如果 nginx 不在运行则执行
# systemctl is-active 检查服务状态,返回 0 表示运行中
if ! systemctl is-active nginx &>/dev/null; then
echo "[$(date)] ❌ nginx 挂了,正在重启..." >> /var/log/nginx-watch.log
systemctl restart nginx
echo "[$(date)] ✅ nginx 已重启" >> /var/log/nginx-watch.log
fi
EOF
chmod +x /opt/scripts/check-nginx.sh
# */10 * * * * = 每 10 分钟执行一次
# */10 表示 "每10"(分钟字段的步长写法)
(crontab -l 2>/dev/null; echo "*/10 * * * * /opt/scripts/check-nginx.sh") | crontab -
验证:
预期效果: 每 10 分钟自动检查,Nginx 挂了会在 10 分钟内自动重启
练习 3:每周日清理日志
题目: 设置每周日凌晨 2 点清理 7 天前的 Nginx 日志文件。
实际场景: 日志文件会持续增长,不清理会撑爆磁盘;生产环境通常保留 7-30 天日志;自动清理比手动删除更可靠,避免运维遗忘。
思路:
- 每周日凌晨 2 点 =
0 2 * * 0(周日=0) - 清理命令:
find /path -mtime +7 -delete -mtime +7表示 7 天前修改的文件- 把命令写进脚本,cron 调用脚本
- 清理前可以先
ls预览,确认无误再删
答案:
cat > /opt/scripts/clean-logs.sh << 'EOF'
#!/bin/bash
log_dir="/var/log/nginx"
# 先统计有多少个过期文件(-mtime +7 = 修改时间超过7天)
count=$(find "$log_dir" -name "*.log" -mtime +7 | wc -l)
# 再删除这些文件
find "$log_dir" -name "*.log" -mtime +7 -delete
echo "[$(date)] 清理完成,删除 $count 个过期日志文件" >> /var/log/clean-logs.log
EOF
chmod +x /opt/scripts/clean-logs.sh
# 0 2 * * 0 = 每周日凌晨 2 点
# 最后一个字段是星期几:0=周日, 1=周一, ..., 6=周六
(crontab -l 2>/dev/null; echo "0 2 * * 0 /opt/scripts/clean-logs.sh") | crontab -
验证:
预期效果: 每周日凌晨 2 点自动清理超过 7 天的日志
练习 4:排查 cron 不执行
题目: 一个 cron 任务设置了但不执行,写出排查步骤。
实际场景: 生产环境 cron 任务静默失败是常见问题;排查步骤是运维必备技能(权限、环境变量、路径、语法);避免备份/清理任务失效导致数据丢失或磁盘撑爆。
思路:
- 先检查 cron 服务本身是否在运行:
systemctl status cron - 检查任务是否真的写进去了:
crontab -l - 检查脚本权限是否可执行:
chmod +x - 检查脚本里用的是绝对路径(cron 环境变量很少)
- 手动执行脚本看是否报错
- 查看 cron 日志:
grep CRON /var/log/syslog
答案:
# 步骤 1:确认 cron 任务是否存在
crontab -l | grep "my-script"
# 步骤 2:检查 cron 服务是否运行
systemctl status cron
# 步骤 3:查看 cron 日志
grep "CRON" /var/log/syslog | tail -20
# 步骤 4:检查脚本权限
ls -la /opt/scripts/my-script.sh
# 确认有 x(执行)权限
# 步骤 5:检查脚本是否有语法错误
bash -n /opt/scripts/my-script.sh
# 步骤 6:手动执行脚本测试
/opt/scripts/my-script.sh
# 步骤 7:检查环境变量(cron 环境变量与 shell 不同)
# 在脚本开头加:
# PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
预期效果: 按步骤排查后定位问题(常见原因:权限不足、环境变量缺失、路径错误)
练习 5:导出 cron 任务报告
题目: 写一个脚本,把当前用户的所有 cron 任务导出成可读的 Markdown 表格。
实际场景: 运维交接时导出所有定时任务清单;审计时查看服务器有哪些自动化任务;生成运维文档记录服务器的定时任务配置。
思路:
crontab -l列出当前用户的所有 cron 任务- 用
grep -v过滤掉注释行和空行 - 把每行 cron 按空格拆分,提取时间和命令
- 格式化成 Markdown 表格输出
- 可以用
awk或while read逐行处理
答案:
#!/bin/bash
# cron-report.sh — 导出 cron 任务报告
echo "# Crontab 任务报告"
echo ""
echo "| 时间 | 命令 | 说明 |"
echo "|------|------|------|"
crontab -l 2>/dev/null | grep -v "^#" | grep -v "^$" | while read line; do
time=$(echo "$line" | awk '{print $1, $2, $3, $4, $5}')
cmd=$(echo "$line" | awk '{for(i=6;i<=NF;i++) printf $i" "; print ""}')
echo "| $time | \`$cmd\` | - |"
done
预期效果:
# Crontab 任务报告
| 时间 | 命令 | 说明 |
|------|------|------|
| 0 3 * * * | `/opt/scripts/backup-db.sh >> /var/log/db-backup.log 2>&1` | - |
| */10 * * * * | `/opt/scripts/check-nginx.sh` | - |
| 0 2 * * 0 | `/opt/scripts/clean-logs.sh` | - |
1.5 日志管理¶
练习 1:查看服务最近 1 小时日志
题目: 用 journalctl 查看 nginx 服务过去 1 小时的日志。
实际场景: 服务异常时快速查看最近日志定位问题;排查 Nginx 502/504 错误时查看后端连接情况;巡检时确认服务运行正常。
思路:
journalctl是 systemd 的日志查看工具-u nginx指定服务名--since "1 hour ago"过滤最近 1 小时- 也可以用
-f实时跟踪(类似 tail -f) --no-pager避免分页,直接输出到终端
答案:
进阶版脚本:
#!/bin/bash
# service-log.sh — 查看服务最近日志
service="$1"
period="${2:-1 hour ago}"
if [ -z "$service" ]; then
echo "用法: $0 <服务名> [时间段]"
echo "示例: $0 nginx '30 minutes ago'"
exit 1
fi
echo "📋 $service 最近日志 ($period)"
echo "---------------------------"
journalctl -u "$service" --since "$period" --no-pager | tail -30
count=$(journalctl -u "$service" --since "$period" --no-pager | wc -l)
echo "---------------------------"
echo "📊 共 $count 条日志"
预期效果:
📋 nginx 最近日志 (1 hour ago)
---------------------------
Jun 08 20:00:01 server nginx[12345]: 200 GET /api/data
Jun 08 20:05:12 server nginx[12345]: 404 GET /missing
Jun 08 20:10:30 server nginx[12345]: 200 GET /
---------------------------
📊 共 156 条日志
练习 2:配置 logrotate
题目: 配置 logrotate,让 Nginx 日志每天轮转,保留 30 天,超过 100MB 立即轮转。
实际场景: 生产环境日志必须轮转,否则会撑爆磁盘;保留 30 天是为了事后追溯问题;压缩旧日志节省磁盘空间。
思路:
- logrotate 配置文件在
/etc/logrotate.d/ - 关键参数:
daily(每天轮转)、rotate 30(保留 30 份)、size 100M(超 100M 立即轮转) compress压缩旧日志,delaycompress延迟一轮再压缩missingok文件不存在不报错,notifempty空文件不轮转- 用
logrotate -d预览配置效果,logrotate -f强制执行一次
答案:
cat > /etc/logrotate.d/nginx-custom << 'EOF'
/var/log/nginx/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
EOF
# 测试配置
logrotate -d /etc/logrotate.d/nginx-custom
# 手动执行一次轮转测试
logrotate -f /etc/logrotate.d/nginx-custom
预期效果:
练习 3:实时跟踪服务日志
题目: 用 journalctl 实时跟踪 openclaw-gateway 服务的日志输出。
实际场景: 部署新版本后实时观察日志确认是否正常启动;排查间歇性错误时实时监控等待问题复现;开发调试时观察应用运行状态。
思路:
journalctl -u 服务名 -f实时跟踪日志(类似 tail -f)-f是 follow 模式,新日志会自动显示--no-pager不分页- 按 Ctrl+C 停止跟踪
- 可以配合
grep过滤:journalctl -u nginx -f | grep error
答案:
# 实时跟踪
journalctl -u openclaw-gateway -f
# 实时跟踪并显示时间戳
journalctl -u openclaw-gateway -f --no-pager -o short-iso
# 只显示最近 50 行然后继续跟踪
journalctl -u openclaw-gateway -n 50 -f
预期效果:
2026-06-08T20:30:01+08:00 server openclaw[1128542]: Gateway started on port 18789
2026-06-08T20:30:05+08:00 server openclaw[1128542]: Agent connected
# (光标停留,持续输出新日志)
练习 4:配置远程日志
题目: 配置 rsyslog,把本机的认证日志发送到远程服务器 10.0.0.100。
实际场景: 多台服务器的日志集中管理,方便统一查看和分析;攻击者入侵后可能删除本地日志,远程日志保留证据;合规要求(等保)日志必须集中存储。
思路:
- rsyslog 是 Linux 默认的日志服务
- 配置文件
/etc/rsyslog.conf里加规则 authpriv.* @@10.0.0.100表示把认证日志发到远程@@是 TCP,@是 UDP- 远程服务器需要开启 rsyslog 的接收端口(514)
- 改完配置后
systemctl restart rsyslog
答案:
# 编辑 rsyslog 配置
cat > /etc/rsyslog.d/remote-auth.conf << 'EOF'
# 把认证日志发送到远程服务器
auth,authpriv.* @@10.0.0.100:514
EOF
# 重启 rsyslog
systemctl restart rsyslog
# 验证(在远程服务器上查看)
# 远程服务器需要配置 UDP/TCP 514 端口监听
预期效果: 本机的 /var/log/auth.log 内容会同步发送到 10.0.0.100
练习 5:自动分析崩溃服务
题目: 写一个脚本,自动分析 systemd 服务的崩溃日志,输出最近 24 小时内重启超过 3 次的服务。
实际场景: 服务频繁重启说明有稳定性问题(内存泄漏、依赖故障);运维巡检时自动发现潜在风险;生成报告汇报给开发团队修复。
思路:
- 用
systemctl list-units --type=service列出所有服务 - 对每个服务用
journalctl -u 服务名 --since "24 hours ago"查日志 - 从日志里统计 "Started" 或 "Stopped" 出现次数 = 重启次数
- 超过 3 次的列入报告
- 用
awk或grep -c做计数
答案:
#!/bin/bash
# crash-analyzer.sh — 分析崩溃服务
echo "🔍 最近 24 小时重启超过 3 次的服务"
echo "---------------------------"
systemctl list-units --type=service --state=running --no-pager | awk 'NR>1{print $1}' | while read service; do
count=$(journalctl -u "$service" --since "24 hours ago" --no-pager 2>/dev/null | grep -c "Started\|Stopped\|Failed")
if [ "$count" -gt 3 ]; then
echo "⚠️ $service — 重启 $count 次"
fi
done
echo "---------------------------"
echo "✅ 分析完成"
预期效果:
🔍 最近 24 小时重启超过 3 次的服务
---------------------------
⚠️ nginx — 重启 5 次
⚠️ openclaw-gateway — 重启 4 次
---------------------------
✅ 分析完成
1.6 网络排查¶
练习 1:curl 测试反向代理性能
题目: 用 curl 测试反向代理是否正常,输出 DNS 解析、连接、首字节、总耗时。
实际场景: 网站变慢时定位瓶颈在哪一段(DNS慢?服务器处理慢?网络慢?);部署 CDN 后验证加速效果;性能测试基准数据采集。
思路:
curl -o /dev/null不下载内容,只测速度-s静默模式,-w自定义输出格式- 格式里用
%{time_dns}、%{time_connect}、%{time_starttransfer}等变量 - 对比直连和反向代理的耗时差异
- 重复多次取平均值更准确
答案:
#!/bin/bash
# proxy-test.sh — 测试反向代理性能
url="$1"
if [ -z "$url" ]; then
echo "用法: $0 <URL>"
echo "示例: $0 http://know.bbben.xyz"
exit 1
fi
echo "🔍 测试: $url"
echo "---------------------------"
curl -o /dev/null -s -w "
DNS 解析: %{time_namelookup}s
TCP 连接: %{time_connect}s
SSL 握手: %{time_appconnect}s
首字节: %{time_starttransfer}s
总耗时: %{time_total}s
HTTP 状态: %{http_code}
" "$url"
运行方式:
预期效果:
🔍 测试: http://know.bbben.xyz
---------------------------
DNS 解析: 0.012s
TCP 连接: 0.045s
SSL 握手: 0.000s
首字节: 0.089s
总耗时: 0.095s
HTTP 状态: 200
练习 2:dig 对比 DNS 解析
题目: 用 dig 对比不同 DNS 服务器的解析结果,排查 DNS 问题。
实际场景: 网站部分地区打不开时排查是否 DNS 解析不一致;验证 DNS 切换是否生效;排查 CDN 回源是否正确。
思路:
dig查询 DNS 解析结果- 用
@指定DNS服务器对比不同 DNS 的结果 - 比如对比 8.8.8.8(Google)和 114.114.114.114(国内)
+short只输出 IP,方便对比- 如果结果不一致,说明 DNS 传播有问题
答案:
#!/bin/bash # 用 bash 执行
# dns-check.sh — 对比 DNS 解析结果
domain="$1" # 第一个参数:要解析的域名
if [ -z "$domain" ]; then # 如果没传域名
echo "用法: $0 <域名>"
exit 1
fi
echo "🔍 DNS 解析对比: $domain"
echo "---------------------------"
# 遍历 4 个不同的公共 DNS 服务器
# 8.8.8.8 = Google DNS
# 114.114.114.114 = 国内通用 DNS
# 223.5.5.5 = 阿里 DNS
# 1.1.1.1 = Cloudflare DNS
for dns in 8.8.8.8 114.114.114.114 223.5.5.5 1.1.1.1; do
# dig +short 只返回 IP 地址
# @ 指定使用哪个 DNS 服务器来解析
result=$(dig +short "$domain" @"$dns" | head -1)
# printf 格式化输出,%-16s 左对齐占 16 个字符宽度
printf "DNS %-16s → %s\n" "$dns" "$result"
done
预期效果:
🔍 DNS 解析对比: know.bbben.xyz
---------------------------
DNS 8.8.8.8 → 154.12.60.122
DNS 114.114.114.114 → 154.12.60.122
DNS 223.5.5.5 → 154.12.60.122
DNS 1.1.1.1 → 154.12.60.122
练习 3:查看监听端口
题目: 用 ss 查看当前所有 TCP 监听端口和对应的进程名。
实际场景: 新服务部署后确认端口是否正常监听;安全审计时检查服务器开放了哪些端口;排查端口冲突问题。
思路:
ss -tlnp查看所有 TCP 监听端口-tTCP、-l监听、-n数字显示(不解析域名)、-p显示进程netstat -tlnp也能做到,但 ss 更现代更快- 看
Local Address列确认端口是否在预期地址上监听 0.0.0.0:80表示所有 IP 都能访问,127.0.0.1:3306表示只监听本地
答案:
进阶版脚本:
#!/bin/bash # 用 bash 执行
# port-list.sh — 查看监听端口
echo "🔌 当前监听端口"
echo "---------------------------"
# ss 参数详解:
# -t 只看 TCP 协议
# -l 只看 LISTEN(监听)状态的连接
# -n 不解析服务名,直接显示端口号(如 80 而不是 http)
# -p 显示占用端口的进程名
ss -tlnp | awk 'NR==1{printf "%-20s %-15s %s\n", $1, $4, $6} NR>1{
# NR==1 匹配第一行(表头),直接格式化输出
# NR>1 匹配数据行,做字段提取处理
split($4, a, ":"); # 按冒号分割 "0.0.0.0:80" → a[1]="0.0.0.0" a[2]="80"
port=a[length(a)]; # 取最后一个元素(端口号)
gsub(/.*users:\(\("/, "", $6); # 去掉进程信息前面的杂项,只留进程名
gsub(/".*/, "", $6); # 去掉进程名后面的引号和括号内容
printf "%-20s %-15s %s\n", $1, port, $6
}'
预期效果:
🔌 当前监听端口
---------------------------
State Local Address Process
LISTEN 80 nginx
LISTEN 22 sshd
LISTEN 8000 mkdocs
LISTEN 18789 node
LISTEN 24566 1panel-core
练习 4:tcpdump 抓包
题目: 用 tcpdump 抓取 80 端口的 HTTP 请求,只显示前 20 条。
实际场景: 排查网络连接问题时抓包分析;调试 HTTP 接口时查看实际请求内容;安全分析时检查是否有异常流量。
思路:
tcpdump是命令行抓包工具tcpdump -i eth0 port 80指定网卡和端口-c 20只抓 20 条,避免输出太多-n不解析域名,加快输出速度- 抓到的包可以用
-w file.pcap保存,再用 Wireshark 分析
答案:
# 基础抓包
tcpdump -i eth0 port 80 -c 20 -nn
# 只显示目标 IP 和端口
tcpdump -i eth0 port 80 -c 20 -nn | awk '{print $3, $5}'
# 保存到文件用 Wireshark 分析
tcpdump -i eth0 port 80 -c 100 -w /tmp/http-capture.pcap
预期效果:
20:30:01.123 IP 192.168.1.100.45678 > 154.12.60.122.80: Flags [S], seq 1234
20:30:01.125 IP 154.12.60.122.80 > 192.168.1.100.45678: Flags [S.], seq 5678
20:30:01.130 IP 192.168.1.100.45678 > 154.12.60.122.80: Flags [P.], seq 1, ack 1
练习 5:iptables 封禁 IP
题目: 用 iptables 禁止 185.220.101.1 访问本机 22 端口,并验证规则生效。
实际场景: 封禁暴力破解 SSH 的攻击 IP;临时封禁异常流量来源;紧急情况下快速阻断恶意访问。
思路:
iptables -A INPUT -s 攻击IP -p tcp --dport 22 -j DROP添加封禁规则-A INPUT追加到 INPUT 链,-s指定源 IP-j DROP直接丢弃数据包iptables -L INPUT -n查看规则列表iptables -D删除规则(解封)- 用
iptables-save保存规则,重启后生效
答案:
# 添加封禁规则
iptables -A INPUT -s 185.220.101.1 -p tcp --dport 22 -j DROP
# 查看规则
iptables -L INPUT -n --line-numbers
# 验证(从被封禁的 IP 测试)
# 从另一台机器 ssh 到本机,应该超时
# 删除规则(解封)
# iptables -D INPUT -s 185.220.101.1 -p tcp --dport 22 -j DROP
# 保存规则(重启后生效)
iptables-save > /etc/iptables/rules.v4
预期效果:
Chain INPUT (policy ACCEPT)
num TARGET PROTO SOURCE DESTINATION
1 DROP tcp 185.220.101.1 0.0.0.0/0 tcp dpt:22
第二阶段:Python 运维自动化(2 周)¶
2.1 Python 基础与环境¶
练习 1:系统信息采集脚本
题目: 写一个 Python 脚本,采集系统信息(CPU、内存、磁盘、IP)并格式化输出。
实际场景: 监控系统的基础数据采集模块;巡检报告自动生成;异常检测(CPU/内存突然飙高时触发告警)。
思路:
- 用
psutil库采集 CPU、内存、磁盘信息 - 用
socket获取本机 IP - 用
platform获取操作系统信息 - 把采集到的数据格式化输出(也可以写入 JSON 文件)
- 可以加判断逻辑,CPU 超过 80% 时标红告警
答案:
#!/usr/bin/env python3 # 用 python3 执行
"""system-info.py — 系统信息采集"""
import psutil # 第三方库,获取系统信息(需 pip install psutil)
import platform # 内置库,获取系统平台信息
import socket # 内置库,网络相关
from datetime import datetime # 内置库,时间处理
def get_system_info():
"""采集系统信息"""
info = {
"hostname": socket.gethostname(), # 获取主机名
"platform": platform.platform(), # 获取系统信息如 Linux-6.1.0-...
"cpu_count": psutil.cpu_count(), # CPU 核心数
"cpu_percent": psutil.cpu_percent(interval=1), # CPU 使用率,interval=1 采样1秒
"memory": psutil.virtual_memory(), # 内存信息对象
"disk": psutil.disk_usage("/"), # 根分区磁盘使用情况
}
# 获取本机 IP 地址的技巧:
# 创建一个 UDP socket 连接外部地址(不会真的发数据)
# 通过 getsockname() 获取本机出口 IP
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK_DGRAM = UDP
s.connect(("8.8.8.8", 80)) # 连接 Google DNS(只是形式上的)
info["ip"] = s.getsockname()[0] # getsockname()[0] = 本机 IP
s.close() # 关闭 socket
except:
info["ip"] = "N/A" # 获取失败则显示 N/A
return info
def print_report(info):
"""格式化输出报告"""
mem = info["memory"]
disk = info["disk"]
print("=" * 50)
print(f"📊 系统状态报告 — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 50)
print(f"主机名: {info['hostname']}")
print(f"系统: {info['platform']}")
print(f"IP 地址: {info['ip']}")
print(f"CPU 核心: {info['cpu_count']}")
print(f"CPU 使用: {info['cpu_percent']}%")
print(f"内存使用: {mem.percent}% ({mem.used // (1024**3)}G / {mem.total // (1024**3)}G)")
print(f"磁盘使用: {disk.percent}% ({disk.used // (1024**3)}G / {disk.total // (1024**3)}G)")
print("=" * 50)
if __name__ == "__main__":
info = get_system_info()
print_report(info)
运行方式:
预期效果:
==================================================
📊 系统状态报告 — 2026-06-10 15:00:00
==================================================
主机名: my-server
系统: Linux-6.1.0-10-amd64
IP 地址: 100.79.187.12
CPU 核心: 2
CPU 使用: 12.5%
内存使用: 45% (2G / 4G)
磁盘使用: 32% (12G / 40G)
==================================================
练习 2:批量 SSH 执行命令
题目: 写一个 Python 脚本,批量 SSH 到多台服务器执行命令。
实际场景: 批量更新服务器配置(改密码、装软件);集群巡检时一次查完所有服务器状态;紧急修复时同时在所有服务器上执行修复命令。
思路:
- 用
paramiko库做 SSH 连接 - 循环遍历服务器列表,每台建立 SSH 连接
- 执行命令并获取输出
- 收集所有服务器的结果,格式化输出
- 加错误处理:某台连不上不影响其他服务器
答案:
#!/usr/bin/env python3 # 用 python3 执行
"""batch-ssh.py — 批量 SSH 执行命令"""
import subprocess # 内置库,执行系统命令
import sys # 内置库,获取命令行参数
from concurrent.futures import ThreadPoolExecutor # 线程池,实现并发执行
# 服务器列表,每台有 host(IP)和 user(用户名)
SERVERS = [
{"host": "192.168.1.10", "user": "root"},
{"host": "192.168.1.11", "user": "root"},
{"host": "192.168.1.12", "user": "root"},
]
def run_ssh(server, command):
"""SSH 到一台服务器执行命令"""
host = server["host"]
user = server["user"]
try:
# subprocess.run 执行系统命令
# ["ssh", f"{user}@{host}", command] = ssh root@192.168.1.10 uptime
# capture_output=True 捕获输出,text=True 返回字符串(不是字节)
# timeout=10 超时 10 秒
result = subprocess.run(
["ssh", f"{user}@{host}", command],
capture_output=True, text=True, timeout=10
)
return host, result.stdout.strip(), result.returncode # 返回值:主机、输出、退出码
except subprocess.TimeoutExpired:
return host, "❌ 连接超时", 1
except Exception as e:
return host, f"❌ 错误: {e}", 1
def main():
# sys.argv 是命令行参数列表,sys.argv[0] 是脚本名,sys.argv[1] 是第一个参数
command = sys.argv[1] if len(sys.argv) > 1 else "uptime" # 默认执行 uptime
print(f"🚀 批量执行: {command}")
print("=" * 50)
# ThreadPoolExecutor 创建线程池,max_workers=5 最多同时 5 个线程
with ThreadPoolExecutor(max_workers=5) as executor:
# executor.submit 提交任务到线程池
futures = [executor.submit(run_ssh, s, command) for s in SERVERS]
for future in futures:
host, output, code = future.result() # .result() 获取返回值
status = "✅" if code == 0 else "❌" # 退出码 0 = 成功
print(f"\n{status} {host}:")
print(f" {output}")
if __name__ == "__main__": # Python 入口:只有直接运行时才执行 main()
main()
运行方式:
练习 3:日志分析器
题目: 写一个 Python 脚本,分析 Nginx 日志,统计状态码分布、最频繁 IP、响应时间。
实际场景: 网站运维日报自动生成;发现异常流量(大量 404/500)时快速定位;为容量规划提供数据支撑。
思路:
- 逐行读取日志文件
- 每行按空格分割,提取状态码、IP、URL、响应时间
- 用字典统计各状态码出现次数
- 用字典统计各 IP 的访问次数
- 计算平均响应时间
- 格式化输出统计报告
答案:
#!/usr/bin/env python3 # 用 python3 执行
"""log-analyzer.py — Nginx 日志分析"""
import re # 正则表达式库
from collections import Counter # Counter 计数器,自动统计每个元素出现次数
def parse_nginx_log(log_file):
"""解析 Nginx 日志"""
# Nginx combined 格式日志的正则:
# IP - - [时间] "方法 URL 协议" 状态码 响应大小
pattern = r'(\S+) - - \[(.+?)\] "(\S+) (\S+) \S+" (\d{3}) (\d+|-)'
# \S+ 匹配非空白字符(IP、方法、URL)
# (.+?) 匹配时间(非贪婪,遇到 ] 就停)
# \d{3} 匹配三位数状态码
status_codes = Counter() # 状态码计数器
ip_counter = Counter() # IP 计数器
total = 0 # 总请求数
with open(log_file) as f: # 打开日志文件
for line in f: # 逐行读取
match = re.match(pattern, line) # 正则匹配
if match: # 匹配成功
# match.groups() 返回所有捕获组
ip, _, method, url, status, size = match.groups()
status_codes[status] += 1 # 该状态码计数+1
ip_counter[ip] += 1 # 该 IP 计数+1
total += 1
return total, status_codes, ip_counter
def print_report(total, status_codes, ip_counter):
"""输出分析报告"""
print(f"📊 日志分析报告")
print(f"{'=' * 40}")
print(f"总请求数: {total}")
print()
print("📈 状态码分布:")
# most_common() 按出现次数从多到少排序
for code, count in status_codes.most_common():
pct = count / total * 100 # 计算百分比
print(f" {code}: {count} ({pct:.1f}%)") # .1f 保留一位小数
print()
print("🏆 Top 10 IP:")
for ip, count in ip_counter.most_common(10): # 取前 10
print(f" {ip}: {count} 次")
if __name__ == "__main__":
total, status_codes, ip_counter = parse_nginx_log("/var/log/nginx/access.log")
print_report(total, status_codes, ip_counter)
预期效果:
📊 日志分析报告
========================================
总请求数: 15234
📈 状态码分布:
200: 14500 (95.2%)
404: 523 (3.4%)
500: 211 (1.4%)
🏆 Top 10 IP:
154.12.60.122: 3521 次
192.168.1.1: 1205 次
练习 4:端口扫描器
题目: 写一个 Python 脚本,扫描指定 IP 的常用端口。
实际场景: 新服务器上线前确认只开放了必要端口;安全审计检查是否有未授权的端口开放;网络故障排查确认目标端口是否可达。
思路:
- 定义常用端口列表(22、80、443、3306 等)
- 用
socket.socket()创建 TCP 连接 connect_ex()尝试连接,返回 0 表示开放- 设置超时时间(如 1 秒),避免卡住
- 并发扫描可以用
threading加速
答案:
#!/usr/bin/env python3 # 用 python3 执行
"""port-scanner.py — 端口扫描器"""
import socket # 内置库,网络连接
from concurrent.futures import ThreadPoolExecutor # 线程池并发扫描
def check_port(host, port):
"""检测单个端口是否开放"""
try:
# AF_INET = IPv4,SOCK_STREAM = TCP 协议
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1) # 超时 1 秒
# connect_ex 尝试连接,返回 0=成功(端口开放),非0=失败(端口关闭)
result = sock.connect_ex((host, port))
sock.close() # 关闭 socket 释放资源
return port, result == 0 # 返回 (端口号, 是否开放)
except:
return port, False # 异常也视为关闭
def scan_ports(host, ports):
"""批量扫描多个端口"""
open_ports = [] # 存放开放的端口
# 20 个线程同时扫描,大大加快速度
with ThreadPoolExecutor(max_workers=20) as executor:
# 为每个端口提交一个检测任务
futures = [executor.submit(check_port, host, p) for p in ports]
for future in futures:
port, is_open = future.result()
if is_open:
open_ports.append(port)
return sorted(open_ports) # 排序后返回
if __name__ == "__main__":
import sys
host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" # 默认扫描本机
# 常用端口列表
common_ports = [21, 22, 25, 53, 80, 110, 143, 443, 993, 995,
3306, 5432, 6379, 8000, 8080, 8443, 18789]
print(f"🔍 扫描 {host} ...")
open_ports = scan_ports(host, common_ports)
print(f"✅ 开放端口: {open_ports}")
for port in open_ports:
print(f" 🟢 {port}")
预期效果:
练习 5:配置文件解析器
题目: 写一个 Python 脚本,解析 INI 格式的配置文件并生成报告。
实际场景: 批量读取多台服务器的配置文件进行对比;配置审计时快速查看所有配置项;自动发现配置中的敏感信息(密码明文)。
思路:
- 用
configparser库解析 INI 格式配置文件 config.read()读取文件config.sections()获取所有配置段config.items(section)获取段内所有键值对- 可以加安全检查:发现 password/key 等敏感字段时标红告警
答案:
#!/usr/bin/env python3 # 用 python3 执行
"""config-parser.py — INI 配置文件解析"""
import configparser # 内置库,解析 INI 格式配置文件
import os
def parse_ini_config(config_file):
"""解析 INI 配置文件"""
config = configparser.ConfigParser() # 创建解析器对象
config.read(config_file) # 读取配置文件
print(f"📄 配置文件分析: {config_file}")
print("=" * 50)
for section in config.sections(): # 遍历所有配置段(如 [database])
print(f"\n[{section}]")
for key, value in config.items(section): # 遍历段内的每个键值对
# 隐藏敏感信息:检查 key 名是否包含 password/secret/key
if any(w in key.lower() for w in ["password", "secret", "key"]):
print(f" {key} = ****") # 密码用 **** 替代
else:
print(f" {key} = {value}")
print(f"\n📊 共 {len(config.sections())} 个配置段")
# 示例配置文件
sample_config = """
[database]
host = localhost
port = 3306
user = root
password = my-secret-pw
name = mydb
[server]
host = 0.0.0.0
port = 8000
debug = true
[logging]
level = INFO
file = /var/log/app.log
"""
if __name__ == "__main__":
# 写入示例配置
with open("/tmp/test.ini", "w") as f:
f.write(sample_config)
parse_ini_config("/tmp/test.ini")
预期效果:
📄 配置文件分析: /tmp/test.ini
==================================================
[database]
host = localhost
port = 3306
user = root
password = ****
name = mydb
[server]
host = 0.0.0.0
port = 8000
debug = true
[logging]
level = INFO
file = /var/log/app.log
📊 共 3 个配置段
2.2 Python 进阶实战¶
练习 1:自动部署脚本
题目: 写一个 Python 脚本,自动在远程服务器上部署应用(创建目录、上传文件、启动服务)。
实际场景: 替代手动 SSH 一步步操作,减少人为失误;CI/CD 流水线的部署环节;多环境(开发/测试/生产)统一部署流程。
思路:
- 用
paramiko建立 SSH 连接 - 用
sftp上传文件到远程服务器 - 远程执行命令:创建目录、解压文件、启动服务
- 每步操作后检查返回值,失败时回滚
- 把部署过程记录到日志文件
答案:
#!/usr/bin/env python3 # 用 python3 执行
"""auto-deploy.py — 自动部署脚本"""
import subprocess # 内置库,执行系统命令
import sys
def run_cmd(cmd):
"""执行一条系统命令"""
# shell=True 允许 shell 语法(如 && 、管道)
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.returncode == 0, result.stdout.strip() # 返回 (是否成功, 输出内容)
def deploy(host, app_name, version):
"""部署应用到远程服务器"""
# 部署步骤列表:每步是一个 (命令, 描述) 元组
steps = [
(f"ssh {host} 'mkdir -p /opt/{app_name}'", "创建部署目录"),
# scp = secure copy,通过 SSH 传输文件
(f"scp dist/{app_name}-{version}.tar.gz {host}:/opt/{app_name}/", "上传应用包"),
(f"ssh {host} 'cd /opt/{app_name} && tar xzf {app_name}-{version}.tar.gz'", "解压应用"),
(f"ssh {host} 'systemctl restart {app_name}'", "重启服务"),
]
print(f"🚀 部署 {app_name} v{version} 到 {host}")
print("=" * 50)
for cmd, desc in steps:
print(f"\n📋 {desc}...")
ok, output = run_cmd(cmd)
if ok:
print(f" ✅ 成功")
else:
print(f" ❌ 失败: {output}")
return False # 任何一步失败就停止部署
print(f"\n🎉 部署完成!")
return True
if __name__ == "__main__":
deploy("192.168.1.10", "myapp", "1.0.0") # 部署 myapp v1.0.0 到指定服务器
练习 2:监控面板
题目: 写一个 Python 脚本,实时监控服务器状态并生成 Web 页面。
实际场景: 运维驾驶舱,打开浏览器就能看服务器状态;给领导展示服务器运行情况;比 Prometheus+Grafana 更轻量的简易监控方案。
思路:
- 用
psutil采集 CPU、内存、磁盘数据 - 用
Flask写一个简单的 Web 服务 - 前端用 HTML + JS 定时刷新数据
- 数据用 JSON 格式传给前端
- 前端用图表库(如 Chart.js)可视化展示
答案:
#!/usr/bin/env python3 # 用 python3 执行
"""monitor.py — 实时监控面板"""
import psutil # 获取系统信息
import time # 内置库,时间相关
from datetime import datetime # 内置库,日期时间
def generate_html():
"""生成监控 HTML 页面"""
# interval=1 采样1秒获取CPU使用率
cpu = psutil.cpu_percent(interval=1)
mem = psutil.virtual_memory() # 内存信息
disk = psutil.disk_usage("/") # 磁盘信息
net = psutil.net_io_counters() # 网络流量统计
# f-string 格式化字符串,可以直接在 {} 里写 Python 表达式
# {{ }} 在 f-string 里表示字面量的 { 和 }(CSS 花括号)
html = f"""<!DOCTYPE html>
<html>
<head><title>服务器监控</title>
<!-- meta refresh 每5秒自动刷新页面 -->
<meta http-equiv="refresh" content="5">
<style>
body {{ font-family: Arial; margin: 20px; background: #1a1a2e; color: #fff; }}
.card {{ background: #16213e; padding: 20px; border-radius: 10px; margin: 10px; display: inline-block; min-width: 200px; }}
.metric {{ font-size: 2em; font-weight: bold; }}
.label {{ color: #888; }}
.good {{ color: #0f0; }}
.warn {{ color: #ff0; }}
.bad {{ color: #f00; }}
</style></head>
<body>
<h1>🖥️ 服务器监控面板</h1>
<p>更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<div class="card">
<div class="label">CPU 使用率</div>
<!-- 三元表达式:条件为真返回前者,否则返回后者 -->
<div class="metric {'good' if cpu < 60 else 'warn' if cpu < 80 else 'bad'}">{cpu}%</div>
</div>
<div class="card">
<div class="label">内存使用率</div>
<div class="metric {'good' if mem.percent < 60 else 'warn' if mem.percent < 80 else 'bad'}">{mem.percent}%</div>
<!-- // 整除,1024**3 = 1024的3次方 = 1GB -->
<div>{mem.used // (1024**3)}G / {mem.total // (1024**3)}G</div>
</div>
<div class="card">
<div class="label">磁盘使用率</div>
<div class="metric {'good' if disk.percent < 60 else 'warn' if disk.percent < 80 else 'bad'}">{disk.percent}%</div>
<div>{disk.used // (1024**3)}G / {disk.total // (1024**3)}G</div>
</div>
<div class="card">
<div class="label">网络流量</div>
<div>↑ {net.bytes_sent // (1024**2)} MB</div>
<div>↓ {net.bytes_recv // (1024**2)} MB</div>
</div>
</body></html>"""
# 写入 HTML 文件,用浏览器打开就能看到监控面板
with open("/var/www/html/monitor.html", "w") as f:
f.write(html)
if __name__ == "__main__":
while True: # 无限循环,持续更新
generate_html() # 每次生成新的 HTML
time.sleep(5) # 间隔 5 秒
练习 3:日志告警系统
题目: 写一个 Python 脚本,实时监控日志文件,发现错误立即发送通知。
实际场景: 生产环境日志实时告警(钉钉/企业微信/邮件);替代 ELK 的轻量级日志监控方案;关键业务接口报错时第一时间通知运维。
思路:
- 用
tail -f的 Python 等价物:open()+seek()实时读取新行 - 用正则匹配错误关键词(error、exception、fatal)
- 匹配到后调用 Webhook 发通知(钉钉/企业微信)
- 加冷却时间,避免同一错误重复告警
- 用
watchdog库可以监控文件变化
答案:
#!/usr/bin/env python3 # 用 python3 执行
"""log-watcher.py — 日志告警系统"""
import time # 内置库,sleep 等待
import subprocess # 内置库,执行命令
from collections import deque # deque 双端队列,头部删除效率高
LOG_FILE = "/var/log/nginx/error.log" # 要监控的日志文件
ALERT_THRESHOLD = 5 # 5 分钟内超过 5 次错误则告警
CHECK_INTERVAL = 10 # 每 10 秒检查一次
def send_alert(message):
"""发送告警通知"""
print(f"🚨 告警: {message}")
# 实际生产中可以对接:
# requests.post(webhook_url, json={"text": message}) # 钉钉/企业微信
def tail_log(file_path, last_pos):
"""读取日志文件新增内容(类似 tail -f)"""
try:
with open(file_path) as f:
f.seek(last_pos) # 跳到上次读取的位置
new_lines = f.readlines() # 读取新增的行
new_pos = f.tell() # 记录当前文件位置
return new_lines, new_pos
except FileNotFoundError:
return [], last_pos # 文件不存在则返回空
def main():
last_pos = 0 # 上次读取的文件位置
error_times = deque() # 存放错误发生的时间戳
print(f"👁️ 开始监控 {LOG_FILE}")
print(f" 告警阈值: {ALERT_THRESHOLD} 次/5 分钟")
while True: # 无限循环持续监控
new_lines, last_pos = tail_log(LOG_FILE, last_pos)
for line in new_lines: # 遍历新增的每一行
if "error" in line.lower() or "crit" in line.lower():
now = time.time() # 当前时间戳(秒)
error_times.append(now) # 记录这次错误的时间
# popleft() 删除队列头部(最早的时间戳)
# 如果最早的时间超过 5 分钟(300秒),就删掉
while error_times and error_times[0] < now - 300:
error_times.popleft()
# 如果 5 分钟内的错误次数 >= 阈值,发送告警
if len(error_times) >= ALERT_THRESHOLD:
send_alert(f"5 分钟内出现 {len(error_times)} 次错误!")
error_times.clear() # 清空计数,避免重复告警
time.sleep(CHECK_INTERVAL) # 等 10 秒再检查
if __name__ == "__main__":
main()
练习 4:数据库健康检查
题目: 写一个 Python 脚本,检查 MySQL 数据库健康状态。
实际场景: 数据库巡检自动化(连接数、慢查询、磁盘空间);数据库性能下降时快速诊断;生成数据库健康报告。
思路:
- 用
pymysql连接数据库 - 执行 SQL 查询:
SHOW STATUS、SHOW PROCESSLIST - 获取关键指标:连接数、查询数、慢查询数
- 查询
information_schema获取磁盘使用情况 - 把结果格式化输出或写入报告文件
答案:
#!/usr/bin/env python3
"""db-health.py — 数据库健康检查"""
import subprocess
import json
def check_mysql():
"""检查 MySQL 状态"""
checks = {
"连接测试": "mysqladmin ping -u root -p'密码' 2>/dev/null",
"进程列表": "mysqladmin processlist -u root -p'密码' 2>/dev/null | wc -l",
"数据库大小": "mysql -u root -p'密码' -e 'SELECT table_schema, SUM(data_length + index_length) / 1024 / 1024 AS MB FROM information_schema.tables GROUP BY table_schema' 2>/dev/null",
}
print("🔍 MySQL 健康检查")
print("=" * 50)
for name, cmd in checks.items():
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
print(f"✅ {name}:")
print(f" {result.stdout.strip()[:200]}")
else:
print(f"❌ {name}: {result.stderr.strip()[:100]}")
if __name__ == "__main__":
check_mysql()
练习 5:HTML 报告生成器
题目: 写一个 Python 脚本,生成服务器状态的 HTML 报告。
实际场景: 运维周报/月报自动生成;给非技术人员(领导/客户)展示服务器状态;归档服务器历史状态数据。
思路:
- 采集系统数据(CPU、内存、磁盘、网络)
- 用 Python 字符串模板生成 HTML
- 加 CSS 样式让报告美观
- 用
datetime在文件名里加日期 - 可以用
Jinja2模板引擎生成更复杂的 HTML
答案:
#!/usr/bin/env python3
"""report-gen.py — HTML 报告生成器"""
import psutil
from datetime import datetime
def generate_report():
"""生成服务器状态报告"""
cpu = psutil.cpu_percent(interval=1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
temps = psutil.sensors_temperatures() if hasattr(psutil, 'sensors_temperatures') else {}
html = f"""<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>服务器状态报告</title>
<style>
body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ padding: 10px; border: 1px solid #ddd; text-align: left; }}
th {{ background: #f5f5f5; }}
.status-ok {{ color: green; }}
.status-warn {{ color: orange; }}
.status-bad {{ color: red; }}
</style></head>
<body>
<h1>🖥️ 服务器状态报告</h1>
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<table>
<tr><th>指标</th><th>值</th><th>状态</th></tr>
<tr><td>CPU 使用率</td><td>{cpu}%</td>
<td class="{'status-ok' if cpu < 60 else 'status-warn' if cpu < 80 else 'status-bad'}">
{'✅ 正常' if cpu < 60 else '⚠️ 警告' if cpu < 80 else '❌ 危险'}</td></tr>
<tr><td>内存使用率</td><td>{mem.percent}% ({mem.used // (1024**3)}G / {mem.total // (1024**3)}G)</td>
<td class="{'status-ok' if mem.percent < 60 else 'status-warn' if mem.percent < 80 else 'status-bad'}">
{'✅ 正常' if mem.percent < 60 else '⚠️ 警告' if mem.percent < 80 else '❌ 危险'}</td></tr>
<tr><td>磁盘使用率</td><td>{disk.percent}% ({disk.used // (1024**3)}G / {disk.total // (1024**3)}G)</td>
<td class="{'status-ok' if disk.percent < 60 else 'status-warn' if disk.percent < 80 else 'status-bad'}">
{'✅ 正常' if disk.percent < 60 else '⚠️ 警告' if disk.percent < 80 else '❌ 危险'}</td></tr>
</table>
</body></html>"""
output_path = "/tmp/server-report.html"
with open(output_path, "w") as f:
f.write(html)
print(f"📊 报告已生成: {output_path}")
if __name__ == "__main__":
generate_report()
第三阶段:Docker 容器化(3 周)¶
3.1 Docker 基础¶
练习 1:运行 Python 容器
题目: 拉取 Python 镜像,进入容器执行命令,然后退出。
实际场景: 快速测试某个 Python 版本是否兼容你的代码;在隔离环境里运行不受信任的脚本;临时用容器当沙盒环境做实验。
思路:
docker pull python:3.11拉取镜像docker run -it python:3.11 bash交互式进入容器- 在容器里执行 Python 命令
exit退出容器- 用完后
docker rm清理容器
答案:
# 拉取镜像
docker pull python:3.11
# 运行容器并进入交互模式
docker run -it --name my-python python:3.11 python3 --version
# 退出后查看容器状态
docker ps -a | grep my-python
# 清理
docker rm my-python
预期效果:
$ docker run -it --name my-python python:3.11 python3 --version
Python 3.11.9
$ docker ps -a | grep my-python
CONTAINER ID IMAGE STATUS NAMES
abc123def456 python:3.11 Exited (0) 10 seconds ago my-python
练习 2:Dockerfile 打包脚本
题目: 写一个 Dockerfile,把 Shell 脚本打包成镜像,容器启动时自动执行。
实际场景: 把运维脚本打包成容器,任何服务器 docker run 就能执行;保证脚本运行环境一致(依赖、版本);分发给团队成员使用。
思路:
FROM选择基础镜像(如ubuntu:22.04)COPY把脚本复制到容器里RUN chmod +x给脚本加执行权限CMD指定容器启动时执行的命令docker build构建镜像,docker run运行
答案:
# Dockerfile
FROM ubuntu:22.04
COPY health-check.sh /app/
RUN chmod +x /app/health-check.sh
CMD ["/app/health-check.sh"]
# 构建镜像
docker build -t health-checker .
# 运行容器
docker run -d --name health health-checker
# 查看输出
docker logs health
预期效果:
$ docker build -t health-checker .
Successfully built abc123def456
$ docker logs health
🔍 开始检查服务器状态...
✅ CPU: 12%
✅ 内存: 45%
✅ 磁盘: 32%
练习 3:docker logs 查看输出
题目: 用 docker logs 查看容器日志,支持实时跟踪和时间戳。
实际场景: 容器化应用排错时查看运行日志;部署后确认容器是否正常启动;监控容器输出发现异常信息。
思路:
docker logs 容器名查看全部日志-f实时跟踪(类似 tail -f)--timestamps显示时间戳--tail 100只看最后 100 行- 容器的日志会自动重定向到 Docker 的日志驱动
答案:
# 查看所有日志
docker logs my-container
# 实时跟踪
docker logs -f my-container
# 显示时间戳
docker logs -f --timestamps my-container
# 查看最近 50 行
docker logs --tail 50 my-container
# 查看最近 1 小时
docker logs --since 1h my-container
预期效果:
2026-06-08T20:30:01.123456789Z INFO 服务器启动成功
2026-06-08T20:30:05.987654321Z INFO 监听端口 8000
2026-06-08T20:31:01.111111111Z INFO 处理请求 GET /
练习 4:exec 进入容器
题目: 用 docker exec 进入正在运行的容器,查看文件系统结构。
实际场景: 容器内排查问题(配置文件对不对、依赖装没装);调试容器化应用时查看运行状态;学习某个镜像的目录结构。
思路:
docker exec -it 容器名 bash进入容器-it交互式终端- 在容器里用
ls、cat等命令查看文件 - 容器里没有的工具可以用
apt install临时安装 exit退出,容器继续运行
答案:
# 进入容器
docker exec -it my-container /bin/bash
# 在容器内执行命令
ls -la /
cat /etc/os-release
ps aux
# 不进入容器直接执行命令
docker exec my-container ls -la /app
docker exec my-container cat /etc/hostname
预期效果:
$ docker exec -it my-container /bin/bash
root@abc123def456:/# ls -la /
total 56
drwxr-xr-x 1 root root 4096 Jun 8 20:30 .
drwxr-xr-x 1 root root 4096 Jun 8 20:30 ..
...
练习 5:镜像层分析
题目: 对比两个镜像的大小,分析哪个层占空间最大。
实际场景: 优化 Dockerfile 时找出最大的层进行合并;清理磁盘空间时决定哪些镜像该删;选择基础镜像时对比大小。
思路:
docker images查看所有镜像的大小docker history 镜像名查看每一层的大小- 找出最大的层,思考能否合并或优化
- 多阶段构建(multi-stage)可以大幅减小镜像
- 用
docker image inspect查看更详细的信息
答案:
# 查看镜像大小
docker images | grep -E "python|nginx"
# 查看镜像每一层的大小
docker history python:3.11
docker history nginx:latest
# 导出镜像为 tar 查看大小
docker save python:3.11 | gzip > /tmp/python.tar.gz
ls -lh /tmp/python.tar.gz
预期效果:
$ docker images | grep -E "python|nginx"
REPOSITORY TAG IMAGE ID SIZE
python 3.11 abc123def456 912MB
nginx latest def456abc789 187MB
$ docker history python:3.11
IMAGE CREATED CREATED BY SIZE
abc123def456 2 weeks ago /bin/sh -c #(nop) CMD ["python3"] 0B
<missing> 2 weeks ago /bin/sh -c pip install --no-cache-dir -r /r 15.2MB
<missing> 2 weeks ago /bin/sh -c apt-get update && apt-get install 245MB
3.2 Docker 网络¶
练习 1:自定义网络通信
题目: 创建自定义网络,让两个容器通过容器名互相 ping 通。
实际场景: 多容器应用(Web + 数据库 + 缓存)需要互相通信;自定义网络比默认 bridge 更安全(自动 DNS 解析);微服务架构中服务发现的基础。
思路:
docker network create mynet创建自定义网络docker run --network mynet让容器加入网络- 同一网络内的容器可以用容器名互相访问
docker exec 容器A ping 容器B验证连通性- 默认 bridge 网络不支持容器名解析,必须自定义网络
答案:
# 创建自定义网络
docker network create mynet
# 启动两个容器
docker run -d --name web --network mynet nginx
docker run -d --name api --network mynet python:3.11 sleep 3600
# 从 api 容器 ping web 容器
docker exec api ping -c 3 web
# 从 web 容器 ping api 容器
docker exec web ping -c 3 api
预期效果:
$ docker exec api ping -c 3 web
PING web (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.089 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.065 ms
64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.071 ms
练习 2:端口映射
题目: 用 -p 把容器内 Nginx 映射到宿主机 8080 端口。
实际场景: 容器内的 Web 服务需要被外部访问;开发环境用不同端口映射多个相同服务;生产环境通过端口映射暴露服务给负载均衡器。
思路:
-p 宿主机端口:容器端口做端口映射docker run -p 8080:80 nginx把容器 80 端口映射到宿主机 8080- 访问
http://localhost:8080就能访问容器内的 Nginx - 可以多次
-p映射多个端口 -P自动映射所有暴露的端口到随机端口
答案:
# 映射到 8080 端口
docker run -d -p 8080:80 --name my-nginx nginx
# 访问测试
curl -I http://localhost:8080
# 查看端口映射
docker port my-nginx
预期效果:
$ curl -I http://localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.25.3
$ docker port my-nginx
80/tcp -> 0.0.0.0:8080
练习 3:bridge vs host 网络
题目: 对比 bridge 和 host 网络模式下容器访问外部网络的性能。
实际场景: bridge 模式有 NAT 开销,host 模式性能更好;高并发场景(如代理服务器)考虑用 host 模式;选择网络模式需要权衡隔离性和性能。
思路:
- bridge 模式:容器有独立网络栈,通过虚拟网桥通信
- host 模式:容器直接使用宿主机网络,无隔离
- 用
time ping对比两种模式下的延迟 - bridge 多一层 NAT 转换,延迟会稍高
- host 模式端口直接占用宿主机端口
答案:
# bridge 模式(默认)
time docker run --rm alpine ping -c 10 8.8.8.8
# host 模式
time docker run --rm --network host alpine ping -c 10 8.8.8.8
预期效果:
练习 4:inspect 查看网络配置
题目: 用 docker network inspect 查看容器的网络配置详情。
实际场景: 排查容器网络问题时查看 IP 地址、网关、DNS 配置;确认容器是否加入了正确的网络;网络故障时检查容器的网络配置是否正确。
思路:
docker network inspect 网络名查看网络详情- 可以看到哪些容器连接在这个网络上
- 每个容器的 IP 地址、MAC 地址
docker inspect 容器名可以看单个容器的网络配置- 配合
docker network ls查看所有网络
答案:
# 查看自定义网络详情
docker network inspect mynet
# 查看特定容器的网络
docker inspect --format='{{json .NetworkSettings}}' my-container | python3 -m json.tool
预期效果:
[
{
"Name": "mynet",
"Driver": "bridge",
"Containers": {
"abc123...": {
"Name": "web",
"IPv4Address": "172.18.0.2/16"
},
"def456...": {
"Name": "api",
"IPv4Address": "172.18.0.3/16"
}
}
}
]
练习 5:端口映射关系脚本
题目: 写一个脚本,自动检查所有运行中容器的端口映射关系,输出成表格。
实际场景: 运维管理大量容器时快速查看哪些端口被占用;排查端口冲突问题;生成容器端口清单文档。
思路:
docker ps --format获取运行中容器的信息- 提取容器名、端口映射、状态
- 用
docker inspect或--format模板获取端口详情 - 格式化成表格输出(
printf或column) - 可以加过滤条件,只看特定端口范围
答案:
#!/bin/bash
# port-map.sh — 查看容器端口映射
echo "📦 容器端口映射"
echo "---------------------------"
printf "%-15s %-10s %-20s %s\n" "容器名" "容器端口" "宿主机地址" "镜像"
echo "---------------------------"
docker ps --format '{{.Names}} {{.Ports}} {{.Image}}' | while read name ports image; do
if [ -z "$ports" ]; then
printf "%-15s %-10s %-20s %s\n" "$name" "无" "-" "$image"
else
printf "%-15s %-10s %-20s %s\n" "$name" "$ports" "已映射" "$image"
fi
done
预期效果:
📦 容器端口映射
---------------------------
容器名 容器端口 宿主机地址 镜像
---------------------------
my-nginx 80/tcp 已映射 nginx
knowledge-site 8000/tcp 已映射 squidfunk/mkdocs
openclaw 18789/tcp 已映射 node:22
3.3 数据持久化¶
练习 1:命名卷持久化
题目: 创建命名卷挂载到 Nginx,修改网页文件后删除容器,重建后数据还在。
实际场景: 数据库容器的数据必须持久化,否则容器删除数据就丢了;Web 应用的上传文件需要持久化;日志文件需要保留。
思路:
docker volume create mydata创建命名卷docker run -v mydata:/usr/share/nginx/html nginx挂载卷- 进容器修改网页文件
- 删除容器后重建,同样的卷挂载上去,数据还在
- 命名卷由 Docker 管理,删除容器不会删除卷
答案:
# 创建命名卷
docker volume create web-data
# 挂载卷到容器
docker run -d -v web-data:/usr/share/nginx/html --name web nginx
# 修改网页内容
docker exec web sh -c 'echo "<h1>Hello World</h1>" > /usr/share/nginx/html/index.html'
# 验证内容
curl http://localhost:80
# 删除容器
docker rm -f web
# 重建容器(数据还在)
docker run -d -v web-data:/usr/share/nginx/html --name web nginx
# 验证数据持久化
curl http://localhost:80
预期效果:
# 删除前
$ curl http://localhost:80
<h1>Hello World</h1>
# 删除后重建
$ curl http://localhost:80
<h1>Hello World</h1> ← 数据还在
练习 2:bind mount 挂载目录
题目: 用 bind mount 把宿主机目录挂载到容器中。
实际场景: 开发时把本地代码目录挂载到容器,改代码不用重建镜像;配置文件放在宿主机,容器启动时读取;调试时方便查看容器内的文件。
思路:
-v /宿主机路径:/容器路径做 bind mount- 宿主机目录的变化会实时反映到容器里
- 容器里的修改也会影响宿主机
- 适合开发环境,生产环境更推荐用 volume
- 注意权限问题,可能需要
--user指定运行用户
答案:
# 创建测试目录
mkdir -p /tmp/test-mount
echo "宿主机文件" > /tmp/test-mount/host-file.txt
# 挂载到容器
docker run -it --rm -v /tmp/test-mount:/data alpine ls -la /data
# 在容器中创建文件
docker run -it --rm -v /tmp/test-mount:/data alpine sh -c 'echo "容器创建的文件" > /data/container-file.txt'
# 在宿主机查看
ls -la /tmp/test-mount/
cat /tmp/test-mount/container-file.txt
预期效果:
# 宿主机查看
$ ls -la /tmp/test-mount/
-rw-r--r-- 1 root root 12 Jun 8 20:30 host-file.txt
-rw-r--r-- 1 root root 18 Jun 8 20:31 container-file.txt
$ cat /tmp/test-mount/container-file.txt
容器创建的文件
练习 3:备份 volume 数据
题目: 备份一个 volume 的数据到宿主机。
实际场景: 数据库定期备份防止数据丢失;迁移数据时需要导出 volume;灾难恢复前备份当前状态。
思路:
- 用
--volumes-from创建一个临时容器挂载同一个卷 - 临时容器里用
tar打包卷里的数据 - 把 tar 文件复制到宿主机
- 恢复时反向操作:创建临时容器 → 解压 tar → 数据回到卷里
- 也可以用
docker run -v 卷名:/data -v /宿主机/备份:/backup直接复制
答案:
# 创建 volume 并写入数据
docker volume create test-data
docker run --rm -v test-data:/data alpine sh -c 'echo "重要数据" > /data/file.txt'
# 备份 volume
docker run --rm \
-v test-data:/data:ro \
-v /backup:/backup \
alpine tar czf /backup/test-data-$(date +%Y%m%d).tar.gz -C /data .
# 查看备份
ls -lh /backup/
tar tzf /backup/test-data-*.tar.gz
预期效果:
$ ls -lh /backup/
-rw-r--r-- 1 root root 128 Jun 8 20:30 test-data-20260608.tar.gz
$ tar tzf /backup/test-data-20260608.tar.gz
file.txt
练习 4:volume vs bind mount 性能
题目: 对比 volume 和 bind mount 的写入性能。
实际场景: 选择数据持久化方案时需要权衡性能;数据库等 IO 密集型应用对性能敏感;开发环境用 bind mount 更方便,生产环境 volume 性能更好。
思路:
- 分别用 volume 和 bind mount 挂载同一个目录
- 用
dd或fio工具做写入测试 - 对比写入速度和 IOPS
- volume 是 Docker 管理的,性能通常更好
- bind mount 受宿主机文件系统影响
答案:
# volume 写入测试
docker run --rm -v test-vol:/data alpine sh -c 'dd if=/dev/zero of=/data/test bs=1M count=100 2>&1'
# bind mount 写入测试
docker run --rm -v /tmp/test-bind:/data alpine sh -c 'dd if=/dev/zero of=/data/test bs=1M count=100 2>&1'
# 清理
docker volume rm test-vol
rm -rf /tmp/test-bind/test
预期效果:
# volume 通常更快(Docker 管理的存储)
100+0 records in
100+0 records out
104857600 bytes transferred in 0.234 secs (448 MB/sec)
# bind mount 受宿主机文件系统影响
100+0 records in
100+0 records out
104857600 bytes transferred in 0.456 secs (230 MB/sec)
练习 5:自动备份所有 volume
题目: 写一个脚本,自动备份所有 Docker volume 到指定目录。
实际场景: 批量管理多个 volume 的备份;定期备份策略自动化;灾难恢复前的准备工作。
思路:
docker volume ls列出所有 volume- 循环遍历每个 volume
- 对每个 volume 用 tar 打包数据
- 备份文件名带日期,方便管理
- 可以加
find ... -mtime +7 -delete自动清理旧备份
答案:
#!/bin/bash
# backup-volumes.sh — 自动备份所有 Docker volume
backup_dir="/backup/docker-volumes"
date=$(date +%Y%m%d)
mkdir -p "$backup_dir"
echo "📦 开始备份 Docker Volumes"
echo "---------------------------"
docker volume ls -q | while read vol; do
echo "备份: $vol"
docker run --rm \
-v "$vol":/data:ro \
-v "$backup_dir":/backup \
alpine tar czf "/backup/${vol}-${date}.tar.gz" -C /data . 2>/dev/null
if [ $? -eq 0 ]; then
echo " ✅ 成功"
else
echo " ❌ 失败"
fi
done
echo "---------------------------"
echo "备份文件:"
ls -lh "$backup_dir"/*-"$date".tar.gz 2>/dev/null
预期效果:
📦 开始备份 Docker Volumes
---------------------------
备份: web-data
✅ 成功
备份: test-data
✅ 成功
---------------------------
备份文件:
-rw-r--r-- 1 root root 128K Jun 8 20:30 web-data-20260608.tar.gz
-rw-r--r-- 1 root root 32K Jun 8 20:30 test-data-20260608.tar.gz
3.4 docker-compose¶
练习 1:Nginx + PHP + MySQL 三件套
题目: 写一个 docker-compose.yml,一键启动 Nginx + PHP + MySQL。
实际场景: 快速搭建 LAMP/LEMP 开发环境;测试 PHP 应用时不需要手动装一堆依赖;团队成员 docker-compose up 就能跑起完整环境。
思路:
- 定义三个服务:nginx、php、mysql
- 用
volumes做数据持久化 - 用
networks让服务之间互通 - Nginx 配置转发 PHP 请求到 php-fpm
docker-compose up -d一键启动所有服务
答案:
# docker-compose.yml
version: '3.8'
services:
web:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./html:/var/www/html
depends_on:
- php
restart: always
php:
image: php:8.2-fpm
volumes:
- ./html:/var/www/html
restart: always
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: my-secret-pw
MYSQL_DATABASE: testdb
volumes:
- db-data:/var/lib/mysql
restart: always
volumes:
db-data:
预期效果:
$ docker-compose ps
NAME SERVICE STATUS PORTS
web-1 web running 0.0.0.0:80->80/tcp
php-1 php running
db-1 db running 3306/tcp
练习 2:开机自动启动
题目: 在 compose 中配置 restart: always,让所有容器开机自动启动。
实际场景: 服务器重启后容器自动恢复,不需要人工干预;生产环境必须配置自动重启;避免因为疏忽忘记启动服务。
思路:
restart: always无论什么原因停止都会自动重启restart: unless-stopped手动停止后不会自动重启restart: on-failure只在异常退出时重启- 在每个服务下都加上 restart 策略
docker-compose up -d后容器会随系统启动
答案:
# 在每个服务下添加 restart: always
services:
web:
image: nginx
restart: always # 开机自启 + 崩溃自动重启
app:
image: python:3.11
restart: unless-stopped # 手动停止后不自动重启
练习 3:compose 日志查看
题目: 用 docker-compose logs -f 实时查看所有服务日志。
实际场景: 部署后确认所有服务正常启动;排查服务间通信问题时同时看多个服务的日志;调试时实时观察应用运行状态。
思路:
docker-compose logs -f实时跟踪所有服务日志docker-compose logs -f nginx只看某个服务--tail 100只看最后 100 行- 不同服务的日志会用不同颜色区分
- 按 Ctrl+C 停止跟踪
答案:
# 查看所有服务日志
docker-compose logs -f
# 只看某个服务
docker-compose logs -f web
# 查看最近 100 行
docker-compose logs --tail 100
# 带时间戳
docker-compose logs -f --timestamps
预期效果:
web-1 | 2026-06-08T20:30:01.123Z 200 GET /
db-1 | 2026-06-08T20:30:02.456Z ready for connections
php-1 | 2026-06-08T20:30:03.789Z PHP 8.2.19 started
练习 4:Nginx + Flask 应用
题目: 写一个 compose 文件,配置 Nginx 反向代理到 Python Flask 应用。
实际场景: 生产环境用 Nginx 做反向代理和负载均衡;Flask 开发服务器不适合直接对外;静态文件由 Nginx 处理,动态请求转发给 Flask。
思路:
- 定义两个服务:nginx 和 flask
- Flask 应用暴露端口但不对外映射
- Nginx 配置
proxy_pass转发到 Flask - 用
depends_on确保 Flask 先启动 - 静态文件直接由 Nginx 服务,不经过 Flask
答案:
# docker-compose.yml
version: '3.8'
services:
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- flask
restart: always
flask:
build: .
expose:
- "5000"
restart: always
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY app.py .
RUN pip install flask
CMD ["python", "app.py"]
# app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello from Flask!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
预期效果:
练习 5:一键部署 MkDocs + Nginx
题目: 用 docker-compose 一键部署 MkDocs 知识库 + Nginx 反向代理。
实际场景: 团队知识库的快速部署方案;文档站点的容器化部署;比手动装 Python + MkDocs 更标准化。
思路:
- 一个服务用 MkDocs 镜像构建文档
- 另一个服务用 Nginx 做反向代理
- MkDocs 构建后的静态文件通过 volume 共享给 Nginx
- 或者 MkDocs serve 直接运行,Nginx 代理转发
docker-compose up -d一键部署整个站点
答案:
# docker-compose.yml
version: '3.8'
services:
mkdocs:
image: squidfunk/mkdocs-material
volumes:
- ./docs:/docs
- ./mkdocs.yml:/docs/mkdocs.yml
expose:
- "8000"
restart: always
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- mkdocs
restart: always
# nginx.conf
server {
listen 80;
server_name know.bbben.xyz;
location / {
proxy_pass http://mkdocs:8000;
proxy_set_header Host $host;
}
}
预期效果:
$ docker-compose ps
NAME SERVICE STATUS PORTS
mkdocs-1 mkdocs running 8000/tcp
nginx-1 nginx running 0.0.0.0:80->80/tcp
$ curl -H "Host: know.bbben.xyz" http://localhost
<!doctype html>
<html lang="zh">
...
第四阶段:Kubernetes 入门(2 周)¶
4.1 K8s 核心概念¶
关键概念速查:
| 概念 | 说明 | 类比 |
|---|---|---|
| Pod | 最小调度单元,一个或多个容器 | 一间房子 |
| Deployment | 管理 Pod 的副本数和滚动更新 | 物业公司 |
| Service | 为 Pod 提供稳定的访问入口 | 门牌号 |
| Ingress | HTTP/HTTPS 路由规则 | 前台接待 |
| ConfigMap/Secret | 配置和密钥管理 | 保险箱 |
| PV/PVC | 持久化存储 | 仓库 |
4.2 本地环境搭建¶
推荐方案:Mac 用 k3d(轻量 K3s in Docker)
# 前提:Docker Desktop 已安装运行
# 安装 k3d
brew install k3d
# 创建集群
k3d cluster create mycluster --servers 1 --agents 2
# 验证
kubectl get nodes
kubectl get pods -A
推荐方案:服务器用 k3s(轻量 K8s)
4.3 练习¶
练习 1:部署第一个应用
题目: 用 kubectl 部署一个 Nginx Pod。
实际场景: K8s 入门第一步,验证集群是否正常工作;快速启动一个临时 Pod 做测试;理解 K8s 的 Pod 概念。
思路:
kubectl run nginx --image=nginx创建 Podkubectl get pods查看 Pod 状态kubectl describe pod nginx查看详细信息kubectl port-forward暴露端口到本地访问kubectl delete pod nginx删除 Pod
答案:
# 创建 deployment
kubectl create deployment nginx --image=nginx --replicas=3
# 查看 pod 状态
kubectl get pods -o wide
# 暴露为 Service
kubectl expose deployment nginx --port=80 --type=NodePort
# 查看 Service
kubectl get svc nginx
预期效果:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-576b4c6b65-xxxx 1/1 Running 0 30s 10.42.x.x agent-0
nginx-576b4c6b65-yyyy 1/1 Running 0 30s 10.42.x.x agent-1
nginx-576b4c6b65-zzzz 1/1 Running 0 30s 10.42.x.x agent-2
练习 2:YAML 声明式部署
题目: 用 YAML 文件部署一个 Flask 应用。
实际场景: 生产环境用 YAML 声明式管理资源;YAML 文件可以版本控制,方便回滚;比命令式部署更可重复、可审计。
思路:
- 写 Deployment YAML,指定镜像、副本数、端口
- 写 Service YAML,暴露服务给外部访问
kubectl apply -f deployment.yaml创建资源kubectl get pods -w实时观察 Pod 启动过程- 修改 YAML 后再次
apply自动更新
答案:
# flask-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
spec:
replicas: 2
selector:
matchLabels:
app: flask
template:
metadata:
labels:
app: flask
spec:
containers:
- name: flask
image: python:3.11
command: ["python", "-c", "from flask import Flask; app=Flask(__name__); @app.route('/')\ndef hello(): return 'Hello K8s!'; app.run(host='0.0.0.0', port=5000)"]
ports:
- containerPort: 5000
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
---
# flask-service.yaml
apiVersion: v1
kind: Service
metadata:
name: flask-service
spec:
selector:
app: flask
ports:
- port: 80
targetPort: 5000
type: NodePort
练习 3:滚动更新与回滚
题目: 更新 Deployment 的镜像版本,观察滚动更新过程,然后回滚。
实际场景: 应用版本升级时的零停机更新;发现新版本有 bug 时快速回滚到旧版本;灰度发布时逐步更新部分 Pod。
思路:
kubectl set image deployment/xxx container=new-image:v2更新镜像kubectl rollout status观察更新进度- 如果有问题:
kubectl rollout undo deployment/xxx回滚 kubectl rollout history查看更新历史- 滚动更新确保始终有 Pod 在服务请求
答案:
# 更新镜像
kubectl set image deployment/nginx nginx=nginx:1.25
# 观察更新过程
kubectl rollout status deployment/nginx
# 查看历史版本
kubectl rollout history deployment/nginx
# 回滚到上一版本
kubectl rollout undo deployment/nginx
# 回滚到指定版本
kubectl rollout undo deployment/nginx --to-revision=1
练习 4:ConfigMap 管理配置
题目: 用 ConfigMap 注入配置到 Pod。
实际场景: 把配置文件和代码分离,不同环境用不同配置;密码、API Key 等敏感信息不写死在镜像里;配置更新不需要重建镜像。
思路:
kubectl create configmap myconfig --from-file=config.txt- 在 Pod YAML 里用
envFrom或volumes引用 ConfigMap - 环境变量方式:
envFrom: - configMapRef - 文件挂载方式:
volumes: - configMap - ConfigMap 更新后,挂载的文件会自动更新(有延迟)
答案:
# 创建 ConfigMap
kubectl create configmap nginx-config --from-literal=index.html="<h1>Hello from ConfigMap</h1>"
# 在 Pod 中使用
kubectl run nginx-cm --image=nginx --restart=Never \
--overrides='{"spec":{"volumes":[{"name":"html","configMap":{"name":"nginx-config"}}],"containers":[{"name":"nginx","image":"nginx","volumeMounts":[{"name":"html","mountPath":"/usr/share/nginx/html"}]}]}}'
# 验证
kubectl exec nginx-cm -- curl -s localhost
练习 5:查看 Pod 日志和调试
题目: 查看 Pod 日志,进入 Pod 调试。
实际场景: Pod 启动失败时查看日志定位原因;应用运行异常时进入容器排查;确认应用是否正确加载了配置。
思路:
kubectl logs Pod名查看 Pod 日志-f实时跟踪日志kubectl exec -it Pod名 -- bash进入 Podkubectl describe pod Pod名查看事件和状态kubectl get events查看集群事件
答案:
# 查看日志
kubectl logs nginx-xxxx
# 实时跟踪日志
kubectl logs -f nginx-xxxx
# 查看之前容器的日志(容器崩溃重启后)
kubectl logs --previous nginx-xxxx
# 进入 Pod
kubectl exec -it nginx-xxxx -- /bin/bash
# 执行单个命令
kubectl exec nginx-xxxx -- ls -la /usr/share/nginx/html
第五阶段:云计算与 DevOps(2 周)¶
5.1 Terraform 基础¶
安装:
# macOS
brew install terraform
# Linux
wget https://releases.hashicorp.com/terraform/1.5.0/terraform_1.5.0_linux_amd64.zip
unzip terraform_1.5.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
练习 1:创建云服务器(以阿里云为例)
题目: 用 Terraform 创建一台阿里云 ECS 服务器。
实际场景: 基础设施即代码(IaC),服务器配置可以版本控制;批量创建多台相同配置的服务器;环境可重现,开发/测试/生产用同一套 Terraform 代码。
思路:
- 写
main.tf定义资源(ECS 实例、安全组、VPC) - 配置阿里云 Provider(access_key、region)
terraform init初始化terraform plan预览变更terraform apply执行创建terraform destroy销毁资源
答案:
# main.tf
terraform {
required_providers {
alicloud = {
source = "aliyun/alicloud"
version = "~> 1.200"
}
}
}
provider "alicloud" {
region = "cn-hangzhou"
}
resource "alicloud_instance" "web" {
instance_name = "my-web-server"
image_id = "ubuntu_22_04_x64_20G_alibase_20230613.vhd"
instance_type = "ecs.t6-c1m1.large"
security_groups = [alicloud_security_group.default.id]
tags = {
Name = "web-server"
Env = "dev"
}
}
resource "alicloud_security_group" "default" {
name = "my-sg"
vpc_id = alicloud_vpc.default.id
}
resource "alicloud_vpc" "default" {
vpc_name = "my-vpc"
cidr_block = "172.16.0.0/12"
}
5.2 Ansible 基础¶
安装:
练习 1:批量执行命令
题目: 写一个 Ansible Playbook,批量在多台服务器上执行命令。
实际场景: 批量更新服务器配置(改密码、装软件);集群巡检一次查完所有服务器状态;紧急修复时同时在所有服务器上执行。
思路:
- 写
inventory.ini定义服务器列表 - 写 Playbook YAML 定义要执行的任务
ansible-playbook -i inventory.ini playbook.yml执行- 可以用
--check做 dry-run 预览 - 用
tags控制执行部分任务
答案:
# inventory.ini
[webservers]
web1 ansible_host=192.168.1.10
web2 ansible_host=192.168.1.11
[dbservers]
db1 ansible_host=192.168.1.20
# 测试连通性
ansible all -i inventory.ini -m ping
# 批量执行命令
ansible webservers -i inventory.ini -m shell -a "uptime"
# 批量安装软件
ansible all -i inventory.ini -m apt -a "name=htop state=present" --become
练习 2:Playbook 部署应用
题目: 写一个 Ansible Playbook,自动部署一个 Web 应用。
实际场景: 自动化部署标准流程(安装依赖 → 配置 → 启动服务);多台服务器同步部署,保证一致性;回滚时可以快速恢复到上一版本。
思路:
- Playbook 分阶段:安装依赖 → 拉代码 → 配置 → 启动
- 用
handlers在配置变更后自动重启服务 - 用
vars定义变量(端口、路径等) - 用
templates模板生成配置文件 - 用
roles组织复杂的 Playbook
答案:
# deploy.yml
---
- hosts: webservers
become: yes
tasks:
- name: 安装 Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: 启动 Nginx
systemd:
name: nginx
state: started
enabled: yes
- name: 复制配置文件
copy:
src: ./nginx.conf
dest: /etc/nginx/nginx.conf
notify: Restart Nginx
handlers:
- name: Restart Nginx
systemd:
name: nginx
state: restarted
5.3 监控体系¶
核心组件:
| 组件 | 作用 |
|---|---|
| Prometheus | 时序数据库,采集和存储指标 |
| Grafana | 可视化面板 |
| Alertmanager | 告警管理 |
| Node Exporter | 采集服务器指标 |
| cAdvisor | 采集容器指标 |
一键部署(Docker Compose):
# docker-compose.monitoring.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
restart: always
grafana:
image: grafana/grafana
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: admin123
restart: always
node-exporter:
image: prom/node-exporter
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
restart: always
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
访问 Grafana:http://localhost:3000,添加 Prometheus 数据源,导入 Node Exporter Dashboard(ID: 1860)。
概念速查¶
systemd 服务 vs Docker 容器¶
| 维度 | systemd 服务 | Docker 容器 |
|---|---|---|
| 管理工具 | systemctl | docker / podman |
| 隔离性 | 共享宿主机环境 | 独立文件系统/网络/进程 |
| 启动方式 | systemctl start xxx |
docker run xxx |
| 资源占用 | 轻 | 稍重(但比虚拟机轻) |
| 部署方式 | 手动装依赖、写 service 文件 | Dockerfile 一键构建 |
| 可移植性 | 换机器要重装环境 | 镜像在哪都能跑 |
| 适合场景 | 单机、简单服务 | 微服务、多环境、团队协作 |
脚本 → 服务 → 容器的演进¶
Python vs Shell 的选择¶
| 场景 | 推荐 | 原因 |
|---|---|---|
| 简单系统管理 | Shell | 一行命令搞定 |
| 复杂逻辑/数据处理 | Python | 可读性、错误处理、库支持 |
| 系统调用/管道组合 | Shell | 天然适合 |
| API 调用/解析 JSON | Python | requests/json 库强大 |
| 日志分析(简单过滤) | Shell (awk/sed) | 快速 |
| 日志分析(复杂统计) | Python | 灵活 |
学习原则¶
- 先实操,再理论 — 每学一个知识点就写个脚本验证
- 先单机,再集群 — 把单机运维搞扎实再碰 K8s
- 先会用,再原理 — 先把 Docker 跑起来,再深入原理
- 考试与实战并行 — 学的东西直接用在服务器上