为什么运维要学 Shell 脚本?

运维工作里有一个"三次原则":同一个操作如果你手动做了三次,就应该把它写成脚本。Shell 脚本是 Linux 运维最基本的自动化手段——不需要额外安装任何软件,每台服务器原生支持,学会了从此告别重复劳动。

一、第一个脚本

1.1 创建脚本文件

#!/bin/bash
# 我的第一个运维脚本

echo "=== 服务器基本信息 ==="
echo "主机名:$(hostname)"
echo "系统版本:$(cat /etc/os-release | head -1)"
echo "运行时间:$(uptime -p)"
echo "当前内存:$(free -h | awk '/^Mem/ {print $3 "/" $2}')"
echo "磁盘用量:$(df -h / | awk 'NR==2 {print $3 "/" $2}')"
# 保存为 server-info.sh,然后执行
chmod +x server-info.sh
./server-info.sh

#!/bin/bash 是 shebang,告诉系统用哪个解释器来运行这个脚本。写脚本第一步,先想清楚这个脚本能帮你省多少事。

1.2 变量与系统信息采集

#!/bin/bash
# 服务器巡检脚本

# 定义变量
DATE=$(date +%F_%H:%M)
LOG_DIR="/var/log/audit"
THRESHOLD_DISK=80   # 磁盘阈值 %
THRESHOLD_MEM=90    # 内存阈值 %

# 创建日志目录
mkdir -p "$LOG_DIR"

# 系统信息采集函数
get_system_info() {
    echo "=== 巡检时间: $DATE ==="
    echo "CPU 负载: $(uptime | awk -F'load average:' '{print $2}')"
    echo "内存使用: $(free -m | awk '/Mem:/ {printf "%.1f%%", $3/$2 * 100}')"
    echo "磁盘使用: $(df -h / | awk 'NR==2 {print $5}')"
    echo "连接数: $(ss -tun | wc -l)"
}

# 调用函数
get_system_info >> "$LOG_DIR/server-check-$(date +%F).log"

变量使用要点:

  • VAR=value 赋值,等号两边不能有空格
  • $VAR${VAR} 引用变量
  • $(command) 获取命令输出
  • 引用变量时养成加双引号的习惯,避免空格和特殊字符出问题

二、条件判断——让脚本变聪明

2.1 if 语句

#!/bin/bash
# 磁盘告警脚本

DISK_USED=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')

if [ "$DISK_USED" -gt 90 ]; then
    echo "⚠️  磁盘使用率 ${DISK_USED}%,告警!"
    # 发送告警(示例:写入日志)
    echo "$(date): DISK WARNING at ${DISK_USED}%" >> /var/log/disk-alert.log
elif [ "$DISK_USED" -gt 75 ]; then
    echo "⚠️  磁盘使用率 ${DISK_USED}%,需要注意"
else
    echo "✅ 磁盘使用率 ${DISK_USED}%,正常"
fi

常用判断操作符:

操作符说明示例
-gt大于[ "$a" -gt 10 ]
-lt小于[ "$a" -lt 10 ]
-eq等于[ "$a" -eq 10 ]
-z字符串为空[ -z "$var" ]
-n字符串非空[ -n "$var" ]
-f文件存在[ -f /etc/nginx.conf ]
-d目录存在[ -d /var/log ]

2.2 实战:检查服务是否在运行

#!/bin/bash
# 服务健康检查脚本

SERVICES=("nginx" "mysqld" "php-fpm")

for svc in "${SERVICES[@]}"; do
    if systemctl is-active --quiet "$svc"; then
        echo "✅ $svc 运行正常"
    else
        echo "❌ $svc 未运行,尝试重启..."
        systemctl restart "$svc"
        if [ $? -eq 0 ]; then
            echo "   ✅ $svc 已重启成功"
            echo "$(date): $svc was down, restarted" >> /var/log/service-recovery.log
        else
            echo "   ❌ $svc 重启失败,请手动排查"
        fi
    fi
done

三、循环——批量操作的利器

3.1 for 循环

#!/bin/bash
# 批量检查多台服务器

SERVERS=("web01" "web02" "db01" "cache01")

for host in "${SERVERS[@]}"; do
    echo "正在检查 $host ..."
    ssh "$host" "uptime && df -h / | tail -1" 2>/dev/null
    if [ $? -ne 0 ]; then
        echo "  ❌ $host 连接失败"
    fi
    echo "---"
done

3.2 实战:批量清理30天前的日志

#!/bin/bash
# 批量日志清理脚本

LOG_PATHS=(
    "/var/log/nginx"
    "/var/log/mysql"
    "/var/log/php"
    "/opt/app/logs"
)

for path in "${LOG_PATHS[@]}"; do
    if [ -d "$path" ]; then
        count=$(find "$path" -name "*.log" -mtime +30 | wc -l)
        if [ "$count" -gt 0 ]; then
            echo "清理 $path: 发现 $count 个30天前日志文件"
            find "$path" -name "*.log" -mtime +30 -exec rm -f {} \;
            echo "  ✅ 清理完成,释放空间: $(du -sh "$path" | awk '{print $1}')"
        else
            echo "✅ $path 无需清理"
        fi
    else
        echo "⚠️  $path 目录不存在,跳过"
    fi
done

四、运维脚本最佳实践

4.1 脚本模板

一个合格的运维脚本,应该包含以下要素:

#!/bin/bash
#=============================================
# 脚本名称: example.sh
# 功能描述: 一句话说明这个脚本干什么
# 使用方式: ./example.sh [参数1] [参数2]
# 作者: 运维工程师
# 创建日期: 2024-01-01
# 最后修改: 2026-07-29
#=============================================

set -euo pipefail  # 遇到错误立即退出,变量未定义报错

# 常量定义
readonly SCRIPT_NAME=$(basename "$0")
readonly VERSION="1.0.0"
readonly LOG_FILE="/var/log/scripts/${SCRIPT_NAME%.sh}.log"

# 日志函数
log_info() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" | tee -a "$LOG_FILE"
}
log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" | tee -a "$LOG_FILE" >&2
}

# 使用说明
usage() {
    cat << EOF
用法: $SCRIPT_NAME [选项]

选项:
  -h          显示帮助
  -v          显示版本
  -m MODE     运行模式 (check|clean|report)

示例:
  $SCRIPT_NAME -m check    # 执行检查
  $SCRIPT_NAME -m clean    # 执行清理
  $SCRIPT_NAME -m report   # 生成报告
EOF
    exit 0
}

# 主逻辑
main() {
    log_info "脚本开始执行,参数: $*"
    
    # 在这里写你的核心逻辑
    log_info "脚本执行完成"
}

# 参数解析
while getopts "hvm:" opt; do
    case $opt in
        h) usage ;;
        v) echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
        m) MODE="$OPTARG" ;;
        *) usage ;;
    esac
done

# 执行入口
main "$@"

4.2 常用技巧总结

1. set -euo pipefail
写脚本时强烈建议加上这行,它会让脚本在遇到任何错误时立即退出,避免错误被忽略导致更严重的问题。

2. 日志记录
所有的操作都要有日志,不然出了问题你都不知道脚本什么时候跑过。

3. 返回值检查

if ! command_that_might_fail; then
    log_error "命令执行失败"
    exit 1
fi

4. 临时文件管理

TMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT  # 脚本退出时自动清理

5. 幂等性
同一个脚本重复执行多次,结果应该是一样的。比如"清理30天前的日志"重复跑也不会误删当天的日志。

6. 给脚本加锁防止并发

LOCKFILE="/var/lock/$(basename $0).lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "脚本已在运行"; exit 1; }

五、三个可以直接用的脚本

5.1 服务器巡检脚本

#!/bin/bash
# daily-check.sh — 每天早上8点发送服务器状态报告

REPORT="/tmp/server-report-$(date +%F).txt"

{
    echo "======================================"
    echo "  服务器巡检报告 - $(date +%F)"
    echo "  主机: $(hostname)"
    echo "======================================"
    echo ""
    echo "【系统负载】"
    uptime
    echo ""
    echo "【CPU TOP 5 进程】"
    ps aux --sort=-%cpu | head -6
    echo ""
    echo "【内存 TOP 5 进程】"
    ps aux --sort=-%mem | head -6
    echo ""
    echo "【磁盘使用情况】"
    df -h | grep -v tmpfs
    echo ""
    echo "【网络连接数】"
    echo "ESTABLISHED: $(ss -tun state established | wc -l)"
    echo "TIME_WAIT:   $(ss -tun state time-wait | wc -l)"
    echo ""
    echo "【监听端口】"
    ss -tnlp
} > "$REPORT"

# 可将报告通过邮件或企业微信发送
cat "$REPORT"

5.2 网站批量部署脚本

#!/bin/bash
# deploy.sh — 从 Git 拉取代码并部署

set -euo pipefail

APP_DIR="/var/www/myapp"
BACKUP_DIR="/backup/app"
DATE=$(date +%Y%m%d_%H%M%S)

# 1. 备份当前版本
echo ">>> 备份当前版本..."
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/myapp-$DATE.tar.gz" -C "$APP_DIR" .

# 2. 拉取最新代码
echo ">>> 拉取最新代码..."
cd "$APP_DIR"
git pull origin main

# 3. 安装依赖
echo ">>> 安装依赖..."
composer install --no-dev --optimize-autoloader

# 4. 执行数据库迁移
echo ">>> 执行数据库迁移..."
php artisan migrate --force

# 5. 清理缓存
echo ">>> 清理缓存..."
php artisan optimize

# 6. 设置权限
echo ">>> 设置目录权限..."
chown -R www-data:www-data "$APP_DIR"
chmod -R 755 "$APP_DIR/storage"

# 7. 重启服务
echo ">>> 重启 PHP-FPM..."
systemctl reload php8.1-fpm

echo "✅ 部署完成!保留备份: myapp-$DATE.tar.gz"

5.3 日志轮转手动触发脚本

#!/bin/bash
# force-logrotate.sh — 对指定服务的日志进行轮转

if [ $# -eq 0 ]; then
    echo "用法: $0 <服务名>"
    echo "示例: $0 nginx"
    exit 1
fi

SERVICE=$1
CONF_FILE="/etc/logrotate.d/$SERVICE"

if [ ! -f "$CONF_FILE" ]; then
    echo "❌ 未找到 $SERVICE 的 logrotate 配置"
    exit 1
fi

echo ">>> 正在轮转 $SERVICE 日志..."
logrotate -vf "$CONF_FILE"

if [ $? -eq 0 ]; then
    echo "✅ 日志轮转成功"
else
    echo "❌ 日志轮转失败,检查配置"
fi

总结

Shell 脚本是运维自动化的基础工具,学会它之后:

  1. 重复性工作交给脚本,自己可以干更有价值的事
  2. 操作的流程被固化下来,不会出现"这次忘了步骤"的情况
  3. 脚本可以轻松在几十台服务器上批量执行
  4. 一份好的脚本,换个新人也能正确操作

记住三个原则:

  • 手动操作三次就写成脚本
  • 脚本必须有日志和错误处理
  • 核心脚本纳入版本管理(Git)