50·Shell 脚本进阶高级

实用脚本编写实战

scriptingautomation

第50课:实用脚本编写实战

所属模块:Shell 脚本进阶 难度:高级 标签:scripting, automation 上一课:Shell 脚本调试技巧 下一课:ps 进程查看

1. 学习目标

通过本课学习,你将能够:

  • 理解实战脚本的设计思路:从需求分析到功能分解的完整流程。
  • 掌握高级脚本要素:包括错误处理、日志记录、交互式提示和配置文件。
  • 编写健壮的自动化脚本:能够处理异常情况并提供清晰的运行状态反馈。
  • 构建可维护的代码结构:学习使用函数、注释和模块化来组织脚本。
  • 完成一个完整的监控脚本案例:实战演练编写一个服务守护进程监控脚本。

2. 核心概念

在实际运维工作中,脚本的“可运行”和“实用”之间存在巨大差距。一个实用的脚本不仅要能完成核心任务,还必须具备:

  • 健壮性:能应对输入错误、服务中断等异常情况。
  • 可读性:代码清晰,有充分注释,方便自己和他人后续维护。
  • 可配置性:关键参数(如路径、端口、阈值)应易于修改,而非硬编码。
  • 可观察性:运行过程应有明确的日志输出,便于问题排查。

我们将通过一个**“进程守护监控脚本”** 来贯穿这些概念。这个脚本的目标是定期检查指定的进程(如 nginx, mysql)是否在运行,如果发现进程停止,则尝试自动重启并记录日志。

3. 代码示例:进程守护监控脚本 (process_guardian.sh)

#!/bin/bash

# ==========================================
# 脚本名称: process_guardian.sh
# 功能描述: 守护式监控指定进程,挂掉后自动重启并记录日志
# 使用方式: ./process_guardian.sh -p <进程名> [-l <日志文件>] [-c <最大重启次数>]
# ==========================================

# ---------- 初始化与默认值 ----------
PROCESS_NAME=""          # 要监控的进程名
LOG_FILE="/var/log/process_guardian.log"  # 默认日志文件
MAX_RESTARTS=5           # 单次循环内最大重启次数
CHECK_INTERVAL=10        # 检查间隔(秒)

# ---------- 函数定义 ----------
# 显示帮助信息
show_help() {
    echo "用法: $0 -p <进程名> [-l <日志文件>] [-c <最大重启次数>]"
    echo "  -p  指定要监控的进程名称 (必选)"
    echo "  -l  指定日志文件路径 (默认: $LOG_FILE)"
    echo "  -c  单次监控循环内最大重启尝试次数 (默认: $MAX_RESTARTS)"
    exit 1
}

# 记录日志(同时输出到屏幕和文件)
log_message() {
    local level="$1"   # 日志级别:INFO, WARN, ERROR
    local message="$2"
    local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
    # 格式化并输出
    echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

# 检查进程是否存活
is_process_alive() {
    # 使用 `pgrep` 更安全地查找进程,避免 `grep` 本身的匹配
    if pgrep -x "$1" > /dev/null; then
        return 0  # 进程存在
    else
        return 1  # 进程不存在
    fi
}

# 重启指定进程
restart_process() {
    local proc_name="$1"
    log_message "WARN" "尝试重启进程: $proc_name"

    # 尝试使用 systemctl (适用于 systemd 管理的服务)
    if systemctl is-active "$proc_name" &>/dev/null; then
        systemctl restart "$proc_name"
    # 如果 systemctl 不适用,则尝试直接使用进程名
    else
        # 注意:这是一个示例,实际中需要知道启动命令。
        # 对于 nginx,通常是 `nginx`;对于 mysql,可能是 `mysqld`。
        # 更安全的方式是维护一个进程名到启动命令的映射表。
        command -v "$proc_name" && "$proc_name" || log_message "ERROR" "找不到进程 $proc_name 的启动命令"
    fi
}

# ---------- 主程序入口 ----------
# 1. 参数解析
while getopts ":p:l:c:h" opt; do
    case $opt in
        p) PROCESS_NAME="$OPTARG" ;;
        l) LOG_FILE="$OPTARG" ;;
        c) MAX_RESTARTS="$OPTARG" ;;
        h) show_help ;;
        \?) echo "无效选项: -$OPTARG" >&2; show_help ;;
        :) echo "选项 -$OPTARG 需要一个参数" >&2; show_help ;;
    esac
done

# 2. 验证必要参数
if [ -z "$PROCESS_NAME" ]; then
    echo "错误:必须使用 -p 指定进程名!" >&2
    show_help
fi

# 确保日志目录存在
log_dir=$(dirname "$LOG_FILE")
if [ ! -d "$log_dir" ]; then
    mkdir -p "$log_dir" 2>/dev/null || {
        echo "警告:无法创建日志目录 $log_dir,将日志输出到 /tmp" >&2
        LOG_FILE="/tmp/process_guardian_$(date +%s).log"
    }
fi

log_message "INFO" "进程守护监控启动,目标进程: $PROCESS_NAME"

# 3. 主监控循环
restart_count=0
while true; do
    # 检查进程状态
    if ! is_process_alive "$PROCESS_NAME"; then
        log_message "ERROR" "进程 $PROCESS_NAME 未运行!"
        restart_count=$((restart_count + 1))

        # 检查是否超过重启限制
        if [ "$restart_count" -le "$MAX_RESTARTS" ]; then
            restart_process "$PROCESS_NAME"
            # 重启后等待几秒再检查
            sleep 5
        else
            log_message "ERROR" "进程 $PROCESS_NAME 在本次循环中已重启 $MAX_RESTARTS 次,停止监控并退出。"
            exit 1
        fi
    else
        # 进程正常,重置重启计数器
        if [ "$restart_count" -gt 0 ]; then
            log_message "INFO" "进程 $PROCESS_NAME 已恢复运行,重置重启计数。"
            restart_count=0
        fi
    fi

    # 等待下一次检查
    sleep "$CHECK_INTERVAL"
done

代码解析

  1. 结构化设计:脚本开头有清晰的元数据注释。使用 getopts 处理命令行参数,支持 -h 显示帮助。
  2. 函数封装:将“日志记录”、“检查进程”、“重启进程”等独立功能封装成函数,提高复用性和可读性。
  3. 错误处理
    • 使用 local 关键字限制函数内变量的作用域。
    • 检查关键目录是否存在,不存在则创建或使用备用方案。
    • 重启逻辑内置了最大尝试次数限制,避免陷入无限重启循环。
  4. 日志系统:统一使用 log_message 函数,同时输出到屏幕和文件,并带有时间戳和级别,便于追溯。
  5. 进程检查:使用 pgrep -x 精确匹配进程名,避免 ps aux | grep 可能匹配到自身的陷阱。

4. 实践练习

练习一(基础):修改上面的脚本,使其支持同时监控多个进程。提示:将 -p 参数的值改为用逗号分隔的字符串(如 nginx,mysql),然后在循环中拆分并逐个检查。

练习二(进阶):为脚本添加一个“配置文件模式”。创建一个配置文件 /etc/process_guardian.conf,每行定义一个要监控的进程,格式为 进程名:最大重启次数:检查间隔。修改脚本,使其优先读取配置文件,命令行参数可以覆盖配置文件中的默认值。

练习三(设计):假设你需要编写一个“磁盘空间清理”脚本,要求如下:

  • 监控指定目录(如 /tmp, /var/log)的使用率。
  • 当使用率超过设定阈值(如 80%)时,自动删除超过一定时间(如 30 天)的 .log.tmp 文件。
  • 操作前需记录日志,并保留被删除文件的列表。
  • 脚本应能通过 cron 定时任务每天运行。 请写出该脚本的设计大纲核心伪代码,无需完整实现。

5. 常见错误

  1. 硬编码路径与用户名:将 /usr/bin/nginx/home/user 等直接写在脚本里。应使用 which nginx 动态查找,或通过变量/配置文件定义。
  2. 忽略返回值和错误:执行关键命令(如 rm, mv, 网络请求)后不检查 $?。应该使用 if command; then ... else ... fi|| { echo "命令失败"; exit 1; }
  3. 日志记录过度或不足:只在开头和结尾记录日志,关键分支和错误点没有记录。或者打印大量无用的调试信息。应区分日志级别(INFO, WARN, ERROR),在关键决策点和错误发生处记录。
  4. 缺乏并发安全考虑:如果脚本可能同时运行多个实例(如通过 cron 触发),却没有加锁机制(如使用 flock),可能导致数据损坏。
  5. 脚本不够“沉默”:成功的脚本应该只输出必要的信息。避免使用过多的 echo,尤其是成功时的大量输出,这会干扰 cron 邮件或日志分析。

6. 小结

本课我们从实战角度出发,通过一个进程守护监控脚本,学习了编写高质量运维脚本的关键要点:

  • 设计先行:明确脚本的目标、输入、输出和边界条件。
  • 模块化与函数:将复杂逻辑分解为小函数,便于测试和维护。
  • 健壮性三要素:参数校验、错误处理、日志记录,三者缺一不可。
  • 可配置化:通过命令行参数、配置文件和环境变量让脚本灵活适应不同环境。
  • 代码风格:良好的注释、清晰的变量命名和一致的代码格式是团队协作的基础。

记住,一个“能用”的脚本可能只需要几分钟,但一个“实用”的脚本需要投入数倍的时间进行设计、加固和测试。这正是专业脚本编写与随意粘贴命令的核心区别。在接下来的课程中,我们将开始学习如何使用 ps 等命令来获取脚本所需运行的系统状态信息。

练习编辑器

rust
Loading...

继续学习

完成本课后,建议继续学习下一课「ps 进程查看」 以巩固所学知识。