跳转至

🚀 运维进阶学习规划

更新时间: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.shbash script.sh - 改环境变量:source script.sh - 临时跑一下:bash script.sh(不用加权限,最省事)

Shell 脚本中的换行符

1. 行尾换行 — 命令结束符 每行末尾隐含一个 \n,shell 靠它判断命令结束、该执行了。

2. 续行符 \ — 把多行合成一行 行末加 \,告诉 shell 下一行还是当前命令,别急着执行:

echo "hello" \
"world"          # 等价于 echo "hello world"

3. 多行结构 — 换行是语法的一部分 iffor、函数定义等,换行就是语法结构的分隔符。

4. \n 转义序列echoprintf 里手动插入换行字符:

echo -e "hello\nworld"

简单记:行尾换行 = 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 () 圆括号

${} — 变量引用

echo ${name}_world   # 拼接时必须加花括号,否则歧义
echo $name_world     # 错!shell会找 $name_world 这个变量

$() — 命令替换 执行括号里的命令,把输出结果赋值或嵌入:

today=$(date +%Y-%m-%d)    # 把date命令的输出赋给today
echo "当前有$(ls | wc -l)个文件"  # 嵌入到字符串里

() 的其他用法: - 子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 是否被篡改。

思路:

  1. 脚本要接收一个域名作为输入 → 需要判断用户有没有传参数
  2. 判断参数为空用 [ -z "$1" ],为空就提示用法并退出
  3. 拿到域名后,解析 IP → 用 dig 命令,+short 只输出 IP
  4. 解析结果可能有多条(一个域名对应多个 IP),取第一条就行
  5. 如果解析失败(结果为空),要给用户报错提示
  6. 最后输出“域名 → 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"        # 双引号里的变量会被展开

运行方式:

chmod +x resolve.sh      # chmod +x 给文件加执行权限
./resolve.sh bbben.xyz   # ./ 表示当前目录,执行脚本并传入域名参数

预期效果:

✅ bbben.xyz → 154.12.60.122

实战经验: - dig +short+ 是 dig 特有选项前缀,+short 必须连写,写成 + short 会报错 ; Invalid option - dig 是 DNS 查询工具(Domain Information Groper),运维排查 DNS 问题首选,比 nslookuphost 信息更全 - 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 才能修改配置。

思路:

  1. 需要判断当前用户身份 → 用 id -u 获取 UID,root 的 UID 永远是 0
  2. if [ "$uid" -eq 0 ] 做数值比较
  3. root 用户 → 直接执行 df -h
  4. 普通用户 → 先用 whoami 显示用户名,再用 id -u 显示 UID
  5. 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 用户):

✅ 当前是 root 用户
磁盘使用情况:
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   12G   26G  32% /

预期效果(普通用户):

⚠️  当前是普通用户: testuser (UID=1000)
如需执行 root 操作,请使用 sudo

实战经验: - 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,只显示存活的主机,最后统计存活数量。

实际场景: 公司内网新到一批设备,快速扫描哪些已经开机联网;机房巡检时批量检查服务器是否在线;排除网络故障时确认目标主机是否可达。

思路:

  1. 需要 ping 多个 IP → 写一个循环,从 1 到 20 遍历
  2. 拼接 IP 地址:192.168.1.$i
  3. ping -c 1 -W 1 只发 1 个包,超时 1 秒,避免卡住
  4. 判断 ping 是否成功 → 看 $?(上一条命令的返回值),0 表示成功
  5. 成功的打印 ✅,失败的不打印(只显示存活的)
  6. 最后统计成功数量

答案:

#!/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 和端口号两个参数,检测端口是否开放。支持一次检测多个端口。

实际场景: 郤署新服务后验证端口是否正常监听;排查防火墙问题时确认端口是否被拦截;安全审计时检查服务器开放了哪些端口。

思路:

  1. 要检测端口是否开放 → 用 bash 内置的 /dev/tcp 机制
  2. 把检测逻辑封装成函数 check_port,接收主机和端口两个参数
  3. timeout 2 限制超时,避免卡死
  4. 端口开放 → 返回 0,关闭 → 返回非 0
  5. 主程序用 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 更灵活,可以加自定义逻辑(比如发告警通知)。

思路:

  1. while true 无限循环,每隔几秒检查一次
  2. 检查 nginx 是否在运行 → systemctl is-active nginx
  3. 正常运行 → 记录日志,继续循环
  4. 挂了 → 自动重启 systemctl restart nginx
  5. 重启后再验证一次是否成功
  6. 所有操作记录到日志文件,方便事后排查

答案:

#!/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 可能在扫描漏洞)。

思路:

  1. Nginx 日志每行有很多字段(IP、时间、URL、状态码等)
  2. awk 按空格分隔字段,第 $9 个字段是 HTTP 状态码
  3. 筛选 $9 == 500 的行
  4. 输出需要的字段:时间(\(4)、URL(\)7)、客户端IP($1)
  5. head -20 限制只看前 20 条

答案:

awk '$9 == 500 {print $4, $7, $1}' /var/log/nginx/access.log | head -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/数据库/应用配置;批量更新配置文件中的端口号、路径等参数;先预览再执行避免改错配置导致服务挂掉。

思路:

  1. sed 做字符串替换,语法:sed 's/旧/新/g'
  2. 直接改文件会不可逆 → 先用 grep -n 预览哪些行会被改
  3. 确认无误后,用 sed -i 原地修改文件
  4. 改完后再 grep 验证,确认旧 IP 已经不存在
  5. -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 或防火墙提供黑白名单依据。

思路:

  1. Nginx 日志每行第 1 个字段是客户端 IP
  2. awk '{print $1}' 提取所有 IP
  3. sort 排序(让相同 IP 相邻)
  4. uniq -c 去重并计数
  5. sort -rn 按数量逆序排序
  6. 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 删除配置文件中的所有注释行(# 开头)和空行,输出清理后的内容。

实际场景: 审计配置文件时快速查看有效配置项;配置文件导入其他系统时去掉注释避免解析错误;对比两个配置文件差异前先清理格式。

思路:

  1. 注释行以 # 开头 → 用 sed 的正则 /^#/d 删除
  2. 空行 → 用 sed 的 /^$/d 删除
  3. 两个条件可以合并:sed '/^#/d; /^$/d'
  4. 不改原文件,只输出到终端(不加 -i
  5. 如果需要保存结果,用重定向写入新文件

答案:

#!/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 不改原文件)

运行方式:

./clean-config.sh /etc/nginx/nginx.conf

预期效果:

📋 原始行数: 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 或防火墙封禁提供依据;生成安全报告汇报给领导。

思路:

  1. SSH 登录失败会在 /var/log/auth.log 记录 "Failed password"
  2. 先用 grep 过滤出这些行
  3. awk 提取攻击者 IP(在日志行的倒数第 4 个字段)
  4. 后面的流程和统计 Top IP 一样:sort → uniq -c → sort -rn → head
  5. 最后统计总失败次数

答案:

#!/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 流水线里分离构建日志和错误信息。

思路:

  1. Linux 有三个标准文件描述符:0=stdin、1=stdout、2=stderr
  2. > 重定向 stdout(默认是 1),2> 重定向 stderr
  3. 脚本里用 >&2 把输出发送到 stderr
  4. 运行时 ./script > out.log 2> err.log 分别捕获
  5. 这样正常信息和错误信息各归各的文件

答案:

#!/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 状态信息同时显示在终端和写入日志文件。

实际场景: 运维执行巡检命令时既要实时看到结果又要留痕;部署脚本执行过程同时输出到终端和日志文件;排查问题时边操作边记录操作日志。

思路:

  1. > 只写文件不显示,| tee 既写文件又显示在终端
  2. 语法:command | tee file.log
  3. 默认覆盖文件,加 -a 变成追加模式
  4. 可以同时 tee 多个文件:command | tee file1 file2
  5. 适合巡检脚本:实时看到输出 + 留日志记录

答案:

#!/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 个进程。

实际场景: 服务器变慢时快速找出内存占用最高的进程;判断是否被挖矿程序感染(异常进程占满内存);决定是否需要重启某个内存泄漏的服务。

思路:

  1. ps aux 显示所有进程的详细信息
  2. --sort=-%mem 按内存使用率降序排序
  3. head -6 取前 6 行(第 1 行是标题 + 前 5 个进程)
  4. awk 格式化输出,只显示 USER、PID、%MEM、COMMAND
  5. 这是一条管道链: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}'

预期效果:

USER       PID      %MEM   COMMAND
root       1128542  13.7   node
root       1111091  2.9    python3
root       1124670  1.2    nginx


练习 4:追加模式写日志

题目: 写一个脚本,把所有输出追加写入日志文件(不覆盖),同时在终端显示。

实际场景: 长时间运行的运维脚本需要记录完整执行历史;巡检报告生成后追加到月度汇总文件;多次执行部署脚本时保留每次的执行记录。

思路:

  1. > 覆盖写入,>> 追加写入 → 用 >> 保留历史
  2. command >> file.log 每次执行都追加到同一文件
  3. tee -a 可以同时追加到文件 + 显示在终端
  4. 日志文件名带日期可以按天归档
  5. 给日志加时间戳前缀,方便排查

答案:

#!/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" 的行。

实际场景: 批量执行命令后从混合输出中快速提取错误信息;日志分析时过滤出所有报错行;排查脚本执行失败原因时只看错误输出。

思路:

  1. stdout 和 stderr 默认分开显示,合并用 2>&1(把 stderr 合并到 stdout)
  2. 合并后用管道 | 传给 grep
  3. grep -i "error" 过滤包含 error 的行(-i 忽略大小写)
  4. 这样混合输出里只留下错误信息
  5. 也可以用 |& 简写(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 都匹配)

预期效果:

error: 连接超时
error: 认证失败


1.4 Crontab 定时任务


练习 1:每天凌晨 3 点备份数据库

题目: 设置一个 cron 任务,每天凌晨 3 点执行 MySQL 备份脚本,日志追加写入文件。

实际场景: 生产数据库必须定期备份,防止数据丢失;凌晨 3 点是访问低峰期,备份不影响业务;自动清理 30 天前的备份避免磁盘占满。

思路:

  1. cron 时间格式:分 时 日 月 周,凌晨 3 点 = 0 3 * * *
  2. crontab -e 编辑定时任务
  3. 备份脚本的输出用 >> 追加到日志文件
  4. 脚本里加 find ... -mtime +30 -delete 自动清理旧备份
  5. 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
**验证:**
```bash
crontab -l
# 输出: 0 3 * * * /opt/scripts/backup-db.sh >> /var/log/db-backup.log 2>&1

预期效果: 每天凌晨 3 点自动备份,日志写入 /var/log/db-backup.log


练习 2:每 10 分钟检查 Nginx

题目: 设置每 10 分钟检查一次 Nginx 是否存活,挂了自动重启。

实际场景: 比 systemd 的 Restart 更灵活的守护方案;可以加自定义逻辑(比如重启失败时发告警邮件);适合没有 systemd 的老系统或容器环境。

思路:

  1. 每 10 分钟 = */10 * * * *
  2. 检查服务状态:systemctl is-active nginx
  3. 不正常则重启:systemctl restart nginx
  4. 把检查逻辑写成脚本,cron 只调用脚本
  5. 脚本里加日志记录,方便排查

答案:

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 -

验证:

crontab -l | grep check-nginx
# 输出: */10 * * * * /opt/scripts/check-nginx.sh

预期效果: 每 10 分钟自动检查,Nginx 挂了会在 10 分钟内自动重启


练习 3:每周日清理日志

题目: 设置每周日凌晨 2 点清理 7 天前的 Nginx 日志文件。

实际场景: 日志文件会持续增长,不清理会撑爆磁盘;生产环境通常保留 7-30 天日志;自动清理比手动删除更可靠,避免运维遗忘。

思路:

  1. 每周日凌晨 2 点 = 0 2 * * 0(周日=0)
  2. 清理命令:find /path -mtime +7 -delete
  3. -mtime +7 表示 7 天前修改的文件
  4. 把命令写进脚本,cron 调用脚本
  5. 清理前可以先 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 -

验证:

crontab -l | grep clean-logs
# 输出: 0 2 * * 0 /opt/scripts/clean-logs.sh

预期效果: 每周日凌晨 2 点自动清理超过 7 天的日志


练习 4:排查 cron 不执行

题目: 一个 cron 任务设置了但不执行,写出排查步骤。

实际场景: 生产环境 cron 任务静默失败是常见问题;排查步骤是运维必备技能(权限、环境变量、路径、语法);避免备份/清理任务失效导致数据丢失或磁盘撑爆。

思路:

  1. 先检查 cron 服务本身是否在运行:systemctl status cron
  2. 检查任务是否真的写进去了:crontab -l
  3. 检查脚本权限是否可执行:chmod +x
  4. 检查脚本里用的是绝对路径(cron 环境变量很少)
  5. 手动执行脚本看是否报错
  6. 查看 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 表格。

实际场景: 运维交接时导出所有定时任务清单;审计时查看服务器有哪些自动化任务;生成运维文档记录服务器的定时任务配置。

思路:

  1. crontab -l 列出当前用户的所有 cron 任务
  2. grep -v 过滤掉注释行和空行
  3. 把每行 cron 按空格拆分,提取时间和命令
  4. 格式化成 Markdown 表格输出
  5. 可以用 awkwhile 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 错误时查看后端连接情况;巡检时确认服务运行正常。

思路:

  1. journalctl 是 systemd 的日志查看工具
  2. -u nginx 指定服务名
  3. --since "1 hour ago" 过滤最近 1 小时
  4. 也可以用 -f 实时跟踪(类似 tail -f)
  5. --no-pager 避免分页,直接输出到终端

答案:

journalctl -u nginx --since "1 hour ago" --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 天是为了事后追溯问题;压缩旧日志节省磁盘空间。

思路:

  1. logrotate 配置文件在 /etc/logrotate.d/
  2. 关键参数:daily(每天轮转)、rotate 30(保留 30 份)、size 100M(超 100M 立即轮转)
  3. compress 压缩旧日志,delaycompress 延迟一轮再压缩
  4. missingok 文件不存在不报错,notifempty 空文件不轮转
  5. 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

预期效果:

rotation logrotate /var/log/nginx/access.log
rotation logrotate /var/log/nginx/error.log


练习 3:实时跟踪服务日志

题目:journalctl 实时跟踪 openclaw-gateway 服务的日志输出。

实际场景: 部署新版本后实时观察日志确认是否正常启动;排查间歇性错误时实时监控等待问题复现;开发调试时观察应用运行状态。

思路:

  1. journalctl -u 服务名 -f 实时跟踪日志(类似 tail -f)
  2. -f 是 follow 模式,新日志会自动显示
  3. --no-pager 不分页
  4. 按 Ctrl+C 停止跟踪
  5. 可以配合 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

实际场景: 多台服务器的日志集中管理,方便统一查看和分析;攻击者入侵后可能删除本地日志,远程日志保留证据;合规要求(等保)日志必须集中存储。

思路:

  1. rsyslog 是 Linux 默认的日志服务
  2. 配置文件 /etc/rsyslog.conf 里加规则
  3. authpriv.* @@10.0.0.100 表示把认证日志发到远程
  4. @@ 是 TCP,@ 是 UDP
  5. 远程服务器需要开启 rsyslog 的接收端口(514)
  6. 改完配置后 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 次的服务。

实际场景: 服务频繁重启说明有稳定性问题(内存泄漏、依赖故障);运维巡检时自动发现潜在风险;生成报告汇报给开发团队修复。

思路:

  1. systemctl list-units --type=service 列出所有服务
  2. 对每个服务用 journalctl -u 服务名 --since "24 hours ago" 查日志
  3. 从日志里统计 "Started" 或 "Stopped" 出现次数 = 重启次数
  4. 超过 3 次的列入报告
  5. awkgrep -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 后验证加速效果;性能测试基准数据采集。

思路:

  1. curl -o /dev/null 不下载内容,只测速度
  2. -s 静默模式,-w 自定义输出格式
  3. 格式里用 %{time_dns}%{time_connect}%{time_starttransfer} 等变量
  4. 对比直连和反向代理的耗时差异
  5. 重复多次取平均值更准确

答案:

#!/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"

运行方式:

./proxy-test.sh http://know.bbben.xyz

预期效果:

🔍 测试: 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 回源是否正确。

思路:

  1. dig 查询 DNS 解析结果
  2. @指定DNS服务器 对比不同 DNS 的结果
  3. 比如对比 8.8.8.8(Google)和 114.114.114.114(国内)
  4. +short 只输出 IP,方便对比
  5. 如果结果不一致,说明 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 监听端口和对应的进程名。

实际场景: 新服务部署后确认端口是否正常监听;安全审计时检查服务器开放了哪些端口;排查端口冲突问题。

思路:

  1. ss -tlnp 查看所有 TCP 监听端口
  2. -t TCP、-l 监听、-n 数字显示(不解析域名)、-p 显示进程
  3. netstat -tlnp 也能做到,但 ss 更现代更快
  4. Local Address 列确认端口是否在预期地址上监听
  5. 0.0.0.0:80 表示所有 IP 都能访问,127.0.0.1:3306 表示只监听本地

答案:

ss -tlnp | awk 'NR==1{print $0} NR>1{print $0}' | column -t

进阶版脚本:

#!/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 接口时查看实际请求内容;安全分析时检查是否有异常流量。

思路:

  1. tcpdump 是命令行抓包工具
  2. tcpdump -i eth0 port 80 指定网卡和端口
  3. -c 20 只抓 20 条,避免输出太多
  4. -n 不解析域名,加快输出速度
  5. 抓到的包可以用 -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;临时封禁异常流量来源;紧急情况下快速阻断恶意访问。

思路:

  1. iptables -A INPUT -s 攻击IP -p tcp --dport 22 -j DROP 添加封禁规则
  2. -A INPUT 追加到 INPUT 链,-s 指定源 IP
  3. -j DROP 直接丢弃数据包
  4. iptables -L INPUT -n 查看规则列表
  5. iptables -D 删除规则(解封)
  6. 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/内存突然飙高时触发告警)。

思路:

  1. psutil 库采集 CPU、内存、磁盘信息
  2. socket 获取本机 IP
  3. platform 获取操作系统信息
  4. 把采集到的数据格式化输出(也可以写入 JSON 文件)
  5. 可以加判断逻辑,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)

运行方式:

pip install psutil
python3 system-info.py

预期效果:

==================================================
📊 系统状态报告 — 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 到多台服务器执行命令。

实际场景: 批量更新服务器配置(改密码、装软件);集群巡检时一次查完所有服务器状态;紧急修复时同时在所有服务器上执行修复命令。

思路:

  1. paramiko 库做 SSH 连接
  2. 循环遍历服务器列表,每台建立 SSH 连接
  3. 执行命令并获取输出
  4. 收集所有服务器的结果,格式化输出
  5. 加错误处理:某台连不上不影响其他服务器

答案:

#!/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()

运行方式:

python3 batch-ssh.py "uptime"    # 默认执行 uptime
python3 batch-ssh.py "df -h"     # 执行 df -h 查看磁盘


练习 3:日志分析器

题目: 写一个 Python 脚本,分析 Nginx 日志,统计状态码分布、最频繁 IP、响应时间。

实际场景: 网站运维日报自动生成;发现异常流量(大量 404/500)时快速定位;为容量规划提供数据支撑。

思路:

  1. 逐行读取日志文件
  2. 每行按空格分割,提取状态码、IP、URL、响应时间
  3. 用字典统计各状态码出现次数
  4. 用字典统计各 IP 的访问次数
  5. 计算平均响应时间
  6. 格式化输出统计报告

答案:

#!/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 的常用端口。

实际场景: 新服务器上线前确认只开放了必要端口;安全审计检查是否有未授权的端口开放;网络故障排查确认目标端口是否可达。

思路:

  1. 定义常用端口列表(22、80、443、3306 等)
  2. socket.socket() 创建 TCP 连接
  3. connect_ex() 尝试连接,返回 0 表示开放
  4. 设置超时时间(如 1 秒),避免卡住
  5. 并发扫描可以用 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}")

预期效果:

🔍 扫描 127.0.0.1 ...
✅ 开放端口: [22, 80, 8000, 18789]
  🟢 22
  🟢 80
  🟢 8000
  🟢 18789


练习 5:配置文件解析器

题目: 写一个 Python 脚本,解析 INI 格式的配置文件并生成报告。

实际场景: 批量读取多台服务器的配置文件进行对比;配置审计时快速查看所有配置项;自动发现配置中的敏感信息(密码明文)。

思路:

  1. configparser 库解析 INI 格式配置文件
  2. config.read() 读取文件
  3. config.sections() 获取所有配置段
  4. config.items(section) 获取段内所有键值对
  5. 可以加安全检查:发现 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 流水线的部署环节;多环境(开发/测试/生产)统一部署流程。

思路:

  1. paramiko 建立 SSH 连接
  2. sftp 上传文件到远程服务器
  3. 远程执行命令:创建目录、解压文件、启动服务
  4. 每步操作后检查返回值,失败时回滚
  5. 把部署过程记录到日志文件

答案:

#!/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 更轻量的简易监控方案。

思路:

  1. psutil 采集 CPU、内存、磁盘数据
  2. Flask 写一个简单的 Web 服务
  3. 前端用 HTML + JS 定时刷新数据
  4. 数据用 JSON 格式传给前端
  5. 前端用图表库(如 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 的轻量级日志监控方案;关键业务接口报错时第一时间通知运维。

思路:

  1. tail -f 的 Python 等价物:open() + seek() 实时读取新行
  2. 用正则匹配错误关键词(error、exception、fatal)
  3. 匹配到后调用 Webhook 发通知(钉钉/企业微信)
  4. 加冷却时间,避免同一错误重复告警
  5. 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 数据库健康状态。

实际场景: 数据库巡检自动化(连接数、慢查询、磁盘空间);数据库性能下降时快速诊断;生成数据库健康报告。

思路:

  1. pymysql 连接数据库
  2. 执行 SQL 查询:SHOW STATUSSHOW PROCESSLIST
  3. 获取关键指标:连接数、查询数、慢查询数
  4. 查询 information_schema 获取磁盘使用情况
  5. 把结果格式化输出或写入报告文件

答案:

#!/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 报告。

实际场景: 运维周报/月报自动生成;给非技术人员(领导/客户)展示服务器状态;归档服务器历史状态数据。

思路:

  1. 采集系统数据(CPU、内存、磁盘、网络)
  2. 用 Python 字符串模板生成 HTML
  3. 加 CSS 样式让报告美观
  4. datetime 在文件名里加日期
  5. 可以用 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 版本是否兼容你的代码;在隔离环境里运行不受信任的脚本;临时用容器当沙盒环境做实验。

思路:

  1. docker pull python:3.11 拉取镜像
  2. docker run -it python:3.11 bash 交互式进入容器
  3. 在容器里执行 Python 命令
  4. exit 退出容器
  5. 用完后 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 就能执行;保证脚本运行环境一致(依赖、版本);分发给团队成员使用。

思路:

  1. FROM 选择基础镜像(如 ubuntu:22.04
  2. COPY 把脚本复制到容器里
  3. RUN chmod +x 给脚本加执行权限
  4. CMD 指定容器启动时执行的命令
  5. 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 查看容器日志,支持实时跟踪和时间戳。

实际场景: 容器化应用排错时查看运行日志;部署后确认容器是否正常启动;监控容器输出发现异常信息。

思路:

  1. docker logs 容器名 查看全部日志
  2. -f 实时跟踪(类似 tail -f)
  3. --timestamps 显示时间戳
  4. --tail 100 只看最后 100 行
  5. 容器的日志会自动重定向到 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 进入正在运行的容器,查看文件系统结构。

实际场景: 容器内排查问题(配置文件对不对、依赖装没装);调试容器化应用时查看运行状态;学习某个镜像的目录结构。

思路:

  1. docker exec -it 容器名 bash 进入容器
  2. -it 交互式终端
  3. 在容器里用 lscat 等命令查看文件
  4. 容器里没有的工具可以用 apt install 临时安装
  5. 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 时找出最大的层进行合并;清理磁盘空间时决定哪些镜像该删;选择基础镜像时对比大小。

思路:

  1. docker images 查看所有镜像的大小
  2. docker history 镜像名 查看每一层的大小
  3. 找出最大的层,思考能否合并或优化
  4. 多阶段构建(multi-stage)可以大幅减小镜像
  5. 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 解析);微服务架构中服务发现的基础。

思路:

  1. docker network create mynet 创建自定义网络
  2. docker run --network mynet 让容器加入网络
  3. 同一网络内的容器可以用容器名互相访问
  4. docker exec 容器A ping 容器B 验证连通性
  5. 默认 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 服务需要被外部访问;开发环境用不同端口映射多个相同服务;生产环境通过端口映射暴露服务给负载均衡器。

思路:

  1. -p 宿主机端口:容器端口 做端口映射
  2. docker run -p 8080:80 nginx 把容器 80 端口映射到宿主机 8080
  3. 访问 http://localhost:8080 就能访问容器内的 Nginx
  4. 可以多次 -p 映射多个端口
  5. -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 网络

题目: 对比 bridgehost 网络模式下容器访问外部网络的性能。

实际场景: bridge 模式有 NAT 开销,host 模式性能更好;高并发场景(如代理服务器)考虑用 host 模式;选择网络模式需要权衡隔离性和性能。

思路:

  1. bridge 模式:容器有独立网络栈,通过虚拟网桥通信
  2. host 模式:容器直接使用宿主机网络,无隔离
  3. time ping 对比两种模式下的延迟
  4. bridge 多一层 NAT 转换,延迟会稍高
  5. 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

预期效果:

# bridge 模式
real    0m0.523s

# host 模式(少了一层网络转换,更快)
real    0m0.312s


练习 4:inspect 查看网络配置

题目:docker network inspect 查看容器的网络配置详情。

实际场景: 排查容器网络问题时查看 IP 地址、网关、DNS 配置;确认容器是否加入了正确的网络;网络故障时检查容器的网络配置是否正确。

思路:

  1. docker network inspect 网络名 查看网络详情
  2. 可以看到哪些容器连接在这个网络上
  3. 每个容器的 IP 地址、MAC 地址
  4. docker inspect 容器名 可以看单个容器的网络配置
  5. 配合 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:端口映射关系脚本

题目: 写一个脚本,自动检查所有运行中容器的端口映射关系,输出成表格。

实际场景: 运维管理大量容器时快速查看哪些端口被占用;排查端口冲突问题;生成容器端口清单文档。

思路:

  1. docker ps --format 获取运行中容器的信息
  2. 提取容器名、端口映射、状态
  3. docker inspect--format 模板获取端口详情
  4. 格式化成表格输出(printfcolumn
  5. 可以加过滤条件,只看特定端口范围

答案:

#!/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 应用的上传文件需要持久化;日志文件需要保留。

思路:

  1. docker volume create mydata 创建命名卷
  2. docker run -v mydata:/usr/share/nginx/html nginx 挂载卷
  3. 进容器修改网页文件
  4. 删除容器后重建,同样的卷挂载上去,数据还在
  5. 命名卷由 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 把宿主机目录挂载到容器中。

实际场景: 开发时把本地代码目录挂载到容器,改代码不用重建镜像;配置文件放在宿主机,容器启动时读取;调试时方便查看容器内的文件。

思路:

  1. -v /宿主机路径:/容器路径 做 bind mount
  2. 宿主机目录的变化会实时反映到容器里
  3. 容器里的修改也会影响宿主机
  4. 适合开发环境,生产环境更推荐用 volume
  5. 注意权限问题,可能需要 --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;灾难恢复前备份当前状态。

思路:

  1. --volumes-from 创建一个临时容器挂载同一个卷
  2. 临时容器里用 tar 打包卷里的数据
  3. 把 tar 文件复制到宿主机
  4. 恢复时反向操作:创建临时容器 → 解压 tar → 数据回到卷里
  5. 也可以用 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 性能更好。

思路:

  1. 分别用 volume 和 bind mount 挂载同一个目录
  2. ddfio 工具做写入测试
  3. 对比写入速度和 IOPS
  4. volume 是 Docker 管理的,性能通常更好
  5. 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 的备份;定期备份策略自动化;灾难恢复前的准备工作。

思路:

  1. docker volume ls 列出所有 volume
  2. 循环遍历每个 volume
  3. 对每个 volume 用 tar 打包数据
  4. 备份文件名带日期,方便管理
  5. 可以加 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 就能跑起完整环境。

思路:

  1. 定义三个服务:nginx、php、mysql
  2. volumes 做数据持久化
  3. networks 让服务之间互通
  4. Nginx 配置转发 PHP 请求到 php-fpm
  5. 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 up -d

# 查看状态
docker-compose ps

# 停止
docker-compose down

预期效果:

$ 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,让所有容器开机自动启动。

实际场景: 服务器重启后容器自动恢复,不需要人工干预;生产环境必须配置自动重启;避免因为疏忽忘记启动服务。

思路:

  1. restart: always 无论什么原因停止都会自动重启
  2. restart: unless-stopped 手动停止后不会自动重启
  3. restart: on-failure 只在异常退出时重启
  4. 在每个服务下都加上 restart 策略
  5. docker-compose up -d 后容器会随系统启动

答案:

# 在每个服务下添加 restart: always
services:
  web:
    image: nginx
    restart: always  # 开机自启 + 崩溃自动重启

  app:
    image: python:3.11
    restart: unless-stopped  # 手动停止后不自动重启

# 验证
docker inspect --format='{{.HostConfig.RestartPolicy.Name}}' my-container
# 输出: always

练习 3:compose 日志查看

题目:docker-compose logs -f 实时查看所有服务日志。

实际场景: 部署后确认所有服务正常启动;排查服务间通信问题时同时看多个服务的日志;调试时实时观察应用运行状态。

思路:

  1. docker-compose logs -f 实时跟踪所有服务日志
  2. docker-compose logs -f nginx 只看某个服务
  3. --tail 100 只看最后 100 行
  4. 不同服务的日志会用不同颜色区分
  5. 按 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。

思路:

  1. 定义两个服务:nginx 和 flask
  2. Flask 应用暴露端口但不对外映射
  3. Nginx 配置 proxy_pass 转发到 Flask
  4. depends_on 确保 Flask 先启动
  5. 静态文件直接由 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)
# nginx.conf
server {
    listen 80;
    location / {
        proxy_pass http://flask:5000;
    }
}
docker-compose up -d
curl http://localhost

预期效果:

$ curl http://localhost
Hello from Flask!


练习 5:一键部署 MkDocs + Nginx

题目: 用 docker-compose 一键部署 MkDocs 知识库 + Nginx 反向代理。

实际场景: 团队知识库的快速部署方案;文档站点的容器化部署;比手动装 Python + MkDocs 更标准化。

思路:

  1. 一个服务用 MkDocs 镜像构建文档
  2. 另一个服务用 Nginx 做反向代理
  3. MkDocs 构建后的静态文件通过 volume 共享给 Nginx
  4. 或者 MkDocs serve 直接运行,Nginx 代理转发
  5. 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 up -d
curl -H "Host: know.bbben.xyz" http://localhost

预期效果:

$ 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)

# 一键安装 k3s
curl -sfL https://get.k3s.io | sh -

# 验证
kubectl get nodes
kubectl get pods -A

4.3 练习


练习 1:部署第一个应用

题目: 用 kubectl 部署一个 Nginx Pod。

实际场景: K8s 入门第一步,验证集群是否正常工作;快速启动一个临时 Pod 做测试;理解 K8s 的 Pod 概念。

思路:

  1. kubectl run nginx --image=nginx 创建 Pod
  2. kubectl get pods 查看 Pod 状态
  3. kubectl describe pod nginx 查看详细信息
  4. kubectl port-forward 暴露端口到本地访问
  5. 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 文件可以版本控制,方便回滚;比命令式部署更可重复、可审计。

思路:

  1. 写 Deployment YAML,指定镜像、副本数、端口
  2. 写 Service YAML,暴露服务给外部访问
  3. kubectl apply -f deployment.yaml 创建资源
  4. kubectl get pods -w 实时观察 Pod 启动过程
  5. 修改 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

kubectl apply -f flask-deployment.yaml
kubectl get pods -l app=flask
kubectl get svc flask-service

练习 3:滚动更新与回滚

题目: 更新 Deployment 的镜像版本,观察滚动更新过程,然后回滚。

实际场景: 应用版本升级时的零停机更新;发现新版本有 bug 时快速回滚到旧版本;灰度发布时逐步更新部分 Pod。

思路:

  1. kubectl set image deployment/xxx container=new-image:v2 更新镜像
  2. kubectl rollout status 观察更新进度
  3. 如果有问题:kubectl rollout undo deployment/xxx 回滚
  4. kubectl rollout history 查看更新历史
  5. 滚动更新确保始终有 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 等敏感信息不写死在镜像里;配置更新不需要重建镜像。

思路:

  1. kubectl create configmap myconfig --from-file=config.txt
  2. 在 Pod YAML 里用 envFromvolumes 引用 ConfigMap
  3. 环境变量方式:envFrom: - configMapRef
  4. 文件挂载方式:volumes: - configMap
  5. 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 启动失败时查看日志定位原因;应用运行异常时进入容器排查;确认应用是否正确加载了配置。

思路:

  1. kubectl logs Pod名 查看 Pod 日志
  2. -f 实时跟踪日志
  3. kubectl exec -it Pod名 -- bash 进入 Pod
  4. kubectl describe pod Pod名 查看事件和状态
  5. 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 代码。

思路:

  1. main.tf 定义资源(ECS 实例、安全组、VPC)
  2. 配置阿里云 Provider(access_key、region)
  3. terraform init 初始化
  4. terraform plan 预览变更
  5. terraform apply 执行创建
  6. 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"
}

terraform init
terraform plan
terraform apply

5.2 Ansible 基础

安装:

# macOS
brew install ansible

# Linux
sudo apt install ansible

练习 1:批量执行命令

题目: 写一个 Ansible Playbook,批量在多台服务器上执行命令。

实际场景: 批量更新服务器配置(改密码、装软件);集群巡检一次查完所有服务器状态;紧急修复时同时在所有服务器上执行。

思路:

  1. inventory.ini 定义服务器列表
  2. 写 Playbook YAML 定义要执行的任务
  3. ansible-playbook -i inventory.ini playbook.yml 执行
  4. 可以用 --check 做 dry-run 预览
  5. 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 应用。

实际场景: 自动化部署标准流程(安装依赖 → 配置 → 启动服务);多台服务器同步部署,保证一致性;回滚时可以快速恢复到上一版本。

思路:

  1. Playbook 分阶段:安装依赖 → 拉代码 → 配置 → 启动
  2. handlers 在配置变更后自动重启服务
  3. vars 定义变量(端口、路径等)
  4. templates 模板生成配置文件
  5. 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

ansible-playbook -i inventory.ini deploy.yml

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']
docker-compose -f docker-compose.monitoring.yml up -d

访问 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 一键构建
可移植性 换机器要重装环境 镜像在哪都能跑
适合场景 单机、简单服务 微服务、多环境、团队协作

脚本 → 服务 → 容器的演进

你写的脚本(backup.sh)
  ↓ 注册为 systemd 服务 → systemctl 管理
  ↓ 打包为 Docker 镜像 → docker 管理
  ↓ 镜像运行 → 容器(轻量隔离的"服务")

Python vs Shell 的选择

场景 推荐 原因
简单系统管理 Shell 一行命令搞定
复杂逻辑/数据处理 Python 可读性、错误处理、库支持
系统调用/管道组合 Shell 天然适合
API 调用/解析 JSON Python requests/json 库强大
日志分析(简单过滤) Shell (awk/sed) 快速
日志分析(复杂统计) Python 灵活

学习原则

  1. 先实操,再理论 — 每学一个知识点就写个脚本验证
  2. 先单机,再集群 — 把单机运维搞扎实再碰 K8s
  3. 先会用,再原理 — 先把 Docker 跑起来,再深入原理
  4. 考试与实战并行 — 学的东西直接用在服务器上