Shell脚本实战从日志分析到自动化运维常见坑点与解决方案详解新手必收藏
嘿,朋友!看到这篇文章,说明你已经开始和Shell脚本”打交道”了。别紧张,我也曾经对着满屏的 $? 和 ${} 发愁。今天我想跟你聊聊我在运维路上踩过的坑,以及如何用Shell脚本把日志分析和自动化运维搞定。
从第一行脚本说起
让我先给你一个真正能在生产环境跑的脚本,不是那种”hello world”级别的玩具代码。
#!/bin/bash
# 这是一个真实的日志分析脚本,用于分析Nginx访问日志
# 作者:一个曾经和你一样的新手
# 用途:找出访问最频繁的前10个IP地址
LOG_FILE="/var/log/nginx/access.log"
REPORT_DIR="/home/admin/reports"
DATE_STAMP=$(date +%Y%m%d_%H%M%S)
# 检查日志文件是否存在
if [[ ! -f "$LOG_FILE" ]]; then
echo "错误:日志文件 $LOG_FILE 不存在"
exit 1
fi
# 创建报告目录(如果不存在)
mkdir -p "$REPORT_DIR"
# 分析日志,提取IP地址并统计频率
echo "开始分析日志..."
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10 > "$REPORT_DIR/top_ips_$DATE_STAMP.txt"
# 检查命令是否成功执行
if [[ $? -eq 0 ]]; then
echo "分析完成!结果已保存到:$REPORT_DIR/top_ips_$DATE_STAMP.txt"
cat "$REPORT_DIR/top_ips_$DATE_STAMP.txt"
else
echo "分析过程中出错,请检查日志文件权限"
exit 2
fi
这段代码有几个关键点我想跟你单独说说:
set -e 的使用:很多新手会忽略这个,但如果你不加 set -e,脚本会在某个命令失败后继续执行,这可能导致一系列连锁错误。
变量引用加引号:注意 "$LOG_FILE" 而不是 $LOG_FILE。这是新手最容易踩的坑之一。如果你的路径里有空格,比如 /var/log/my access.log,不加引号脚本就崩了。
错误码设计:我用了 exit 1 和 exit 2 来区分不同类型的错误。这在大型自动化脚本里非常重要,能让你知道到底是”文件找不到”还是”命令执行失败”。
日志分析的实战场景
让我分享几个我真正用过的分析场景。
场景一:查找特定时间段的错误日志
#!/bin/bash
# 查找指定时间范围内的错误日志
# 使用方式:./find_errors.sh "2024-01-15 10:00:00" "2024-01-15 12:00:00" /var/log/syslog
START_TIME=$1
END_TIME=$2
LOG_FILE=$3
# 参数检查
if [[ $# -ne 3 ]]; then
echo "用法:$0 <开始时间> <结束时间> <日志文件路径>"
echo "时间格式:YYYY-MM-DD HH:MM:SS"
exit 1
fi
# 时间转换(转换为秒级时间戳)
START_TS=$(date -d "$START_TIME" +%s 2>/dev/null)
END_TS=$(date -d "$END_TIME" +%s 2>/dev/null)
if [[ -z "$START_TS" || -z "$END_TS" ]]; then
echo "错误:时间格式不正确"
exit 2
fi
# 检查日志文件
if [[ ! -f "$LOG_FILE" ]]; then
echo "错误:日志文件 $LOG_FILE 不存在"
exit 3
fi
# 执行分析
echo "正在分析 $LOG_FILE,时间范围:$START_TIME 至 $END_TIME"
echo "----------------------------------------"
# 提取错误日志
grep -i "error" "$LOG_FILE" | while IFS= read -r line; do
# 提取日志时间(假设格式为:Mon DD HH:MM:SS)
log_time=$(echo "$line" | awk '{print $1, $2, $3}')
# 转换日志时间(假设是当年)
log_ts=$(date -d "$log_time" +%s 2>/dev/null)
if [[ -n "$log_ts" ]] && [[ "$log_ts" -ge "$START_TS" ]] && [[ "$log_ts" -le "$END_TS" ]]; then
echo "$line"
fi
done | tee /tmp/error_analysis_$$.log
# 统计错误数量
ERROR_COUNT=$(wc -l < /tmp/error_analysis_$$.log)
echo "----------------------------------------"
echo "在指定时间范围内找到 $ERROR_COUNT 条错误日志"
# 清理临时文件
rm -f /tmp/error_analysis_$$.log
这个脚本里有一个我很在意的细节:临时文件的命名。注意我用了 $$,这是当前进程的PID。这样做可以避免多个脚本实例同时运行时产生文件冲突。新手经常会用 /tmp/errors.log 这种固定名称,结果多个人跑脚本就乱了。
场景二:自动分析磁盘占用并清理
#!/bin/bash
# 自动分析磁盘占用并进行安全清理
# 这个脚本设计得很谨慎,因为删除文件是不可逆操作
THRESHOLD=80 # 磁盘使用率阈值
CLEAN_DIRS=("/tmp" "/var/cache" "/var/log")
DRY_RUN=true # 设为false才会真正删除
# 检查权限
if [[ $EUID -ne 0 ]]; then
echo "这个脚本需要root权限运行"
echo "请尝试:sudo $0"
exit 1
fi
echo "===== 磁盘空间分析报告 ====="
echo "执行时间:$(date '+%Y-%m-%d %H:%M:%S')"
echo "运行模式:$([ "$DRY_RUN" = true ] && echo '仅预览' || echo '实际执行')"
echo "----------------------------------------"
# 检查各个分区
df -h | awk 'NR>1 {print $5, $6}' | while read -r usage mount; do
# 提取数字部分
use_pct=$(echo "$usage" | tr -d '%')
if [[ "$use_pct" -ge "$THRESHOLD" ]]; then
echo "[警告] 分区 $mount 使用率已达 ${use_pct}%"
# 查找该分区下最大的目录
echo " 正在分析占用空间最大的目录..."
du -h --max-depth=1 "$mount" 2>/dev/null | sort -rh | head -5 | while read -r size dir; do
echo " $size $dir"
# 如果超过1G,建议清理
if [[ "$(echo "$size" | grep -o '[0-9]*')" -gt 1024 ]]; then
echo " --> 建议检查此目录"
fi
done
echo ""
fi
done
# 自动清理缓存(仅预览模式不执行)
if [[ "$DRY_RUN" = false ]]; then
echo "===== 开始执行清理 ====="
for dir in "${CLEAN_DIRS[@]}"; do
if [[ -d "$dir" ]]; then
echo "清理 $dir..."
find "$dir" -type f -mtime +7 -delete 2>/dev/null
echo " 清理完成"
fi
done
else
echo "===== 预览模式,未执行任何删除操作 ====="
echo "如需实际执行,请将 DRY_RUN 改为 false"
fi
echo ""
echo "===== 清理后磁盘空间 ====="
df -h
这段代码里我有一个重要的设计理念:安全机制。你看我用了 DRY_RUN 变量,默认是 true。这意味着你第一次运行这个脚本时,它只会告诉你”我会做什么”,而不会真的删除文件。这是运维脚本设计的重要原则——给新手一个”安全网”。
场景三:分析Web日志中的异常请求
#!/bin/bash
# 分析Nginx日志,检测异常请求模式
# 可以检测:暴力破解、扫描器、异常HTTP方法等
LOG_FILE="/var/log/nginx/access.log"
ALERT_LOG="/var/log/abnormal_access.log"
MAX_RETRY=5 # 同一IP失败次数阈值
# 颜色输出(让结果更易读)
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
check_log_file() {
if [[ ! -f "$LOG_FILE" ]]; then
echo -e "${RED}错误:找不到日志文件 $LOG_FILE${NC}"
exit 1
fi
}
detect_brute_force() {
echo -e "${YELLOW}[检测暴力破解尝试]${NC}"
# 分析401和403状态码
awk '$9 == "401" || $9 == "403" {print $1}' "$LOG_FILE" | \
sort | uniq -c | sort -rn | \
while read -r count ip; do
if [[ "$count" -ge "$MAX_RETRY" ]]; then
echo -e " ${RED}[疑似暴力破解]${NC} IP: $ip - 失败次数: $count"
# 记录到告警日志
echo "[$(date)] 暴力破解检测: IP=$ip 次数=$count" >> "$ALERT_LOG"
fi
done
}
detect_scanners() {
echo -e "${YELLOW}[检测扫描器行为]${NC}"
# 检测常见的扫描模式
awk '$7 ~ /(\.\.\/|\/etc\/|\/proc\/|wp-admin|phpmyadmin|\.env)/ {print $1, $7}' "$LOG_FILE" | \
sort -u | \
while read -r ip path; do
echo -e " ${YELLOW}[可疑请求]${NC} IP: $ip 请求路径: $path"
echo "[$(date)] 扫描器检测: IP=$ip 路径=$path" >> "$ALERT_LOG"
done
}
detect_abnormal_methods() {
echo -e "${YELLOW}[检测异常HTTP方法]${NC}"
# 检测非标准的HTTP方法
awk '$6 ~ /(DELETE|PUT|PATCH|OPTIONS|TRACE)/ && $9 !~ /(200|204|301|302)/ {print $1, $6, $9, $7}' "$LOG_FILE" | \
sort | uniq -c | sort -rn | \
while read -r count method status path; do
if [[ "$count" -ge 3 ]]; then
echo -e " ${YELLOW}[异常方法]${NC} 方法: $method 状态: $status 次数: $count 路径: $path"
fi
done
}
analyze_all() {
echo "===== 开始分析Web日志 ====="
echo "日志文件:$LOG_FILE"
echo "分析时间:$(date '+%Y-%m-%d %H:%M:%S')"
echo "----------------------------------------"
detect_brute_force
echo ""
detect_scanners
echo ""
detect_abnormal_methods
echo ""
echo "===== 分析完成 ====="
# 显示告警日志
if [[ -f "$ALERT_LOG" ]]; then
echo "最近的告警记录:"
tail -5 "$ALERT_LOG"
fi
}
# 主程序
check_log_file
analyze_all
这个脚本展示了几个高级技巧:
颜色输出:用ANSI颜色码让输出更直观。你在终端里看到红色和黄色的文字,比纯文本更容易抓住重点。
模块化设计:每个检测功能都是一个独立的函数。这样的好处是你可以单独调用某个检测,也可以组合使用。
告警日志:所有检测到的异常都会记录到
$ALERT_LOG。这个设计很重要——让脚本有”记忆”,方便后续追踪。
自动化运维脚本的设计原则
让我跟你分享一些我用多年运维经验总结出来的原则。
原则一:幂等性
#!/bin/bash
# 幂等性示例:确保多次执行结果一致
SERVICE_NAME="myapp"
CONFIG_FILE="/etc/myapp/config.yml"
# 错误1:非幂等操作
# systemctl restart $SERVICE_NAME # 每次运行都重启服务,可能导致服务抖动
# 正确做法:检查状态后再操作
check_and_restart() {
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo "服务 $SERVICE_NAME 正在运行"
else
echo "服务 $SERVICE_NAME 未运行,尝试启动..."
systemctl start "$SERVICE_NAME"
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo "服务启动成功"
else
echo "服务启动失败"
exit 1
fi
fi
}
# 错误2:非幂等的文件操作
# cp new_config.yml /etc/myapp/config.yml # 无条件覆盖
# 正确做法:比较内容后再覆盖
update_config() {
if cmp -s "$CONFIG_FILE" "new_config.yml"; then
echo "配置文件未发生变化,跳过更新"
else
echo "检测到配置变更,更新配置文件..."
cp "$CONFIG_FILE" "${CONFIG_FILE}.bak.$(date +%s)"
cp new_config.yml "$CONFIG_FILE"
echo "配置更新完成"
check_and_restart
fi
}
幂等性是什么意思呢?简单来说,就是同一个操作执行一次和执行多次,结果应该是一样的。这在自动化运维里非常重要,因为脚本可能会被重复执行。
原则二:日志记录
#!/bin/bash
# 良好的日志记录方式
LOG_FILE="/var/log/myapp/automation.log"
LOG_LEVEL="INFO" # DEBUG, INFO, WARN, ERROR
# 确保日志目录存在
mkdir -p "$(dirname "$LOG_FILE")"
log() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 根据日志级别决定是否输出
case "$level" in
DEBUG)
[[ "$LOG_LEVEL" == "DEBUG" ]] && echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
;;
INFO)
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
;;
WARN)
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
;;
ERROR)
echo "[$timestamp] [$level] $message" >&2 | tee -a "$LOG_FILE"
;;
esac
}
# 使用示例
log "INFO" "脚本开始执行"
log "DEBUG" "当前工作目录:$(pwd)"
log "INFO" "正在检查服务状态..."
# 模拟一个操作
if some_command; then
log "INFO" "操作成功"
else
log "ERROR" "操作失败,退出码:$?"
fi
好的日志记录能让你在问题发生时快速定位。注意我用了 tee -a,这样日志既会显示在终端,也会写入文件。这在调试时特别有用。
原则三:错误处理
#!/bin/bash
# 完整的错误处理机制
# 启用严格模式
set -euo pipefail
# -e: 遇到错误立即退出
# -u: 使用未定义变量时报错
# -o pipefail: 管道中任何一个命令失败则整个管道失败
# 定义错误处理函数
error_handler() {
local exit_code=$?
local line_number=$1
echo "错误:脚本在第 $line_number 行出错,退出码:$exit_code"
# 这里可以添加告警逻辑,比如发送邮件或通知
send_alert "脚本执行失败"
exit $exit_code
}
# 注册错误处理
trap 'error_handler $LINENO' ERR
# 定义清理函数
cleanup() {
local exit_code=$?
echo "执行清理操作..."
# 清理临时文件
rm -f /tmp/myapp_*.tmp
# 关闭数据库连接等
exit $exit_code
}
# 注册清理函数
trap cleanup EXIT
# 定义告警函数
send_alert() {
local message=$1
# 这里可以调用邮件、钉钉、企业微信等
echo "发送告警:$message"
# mail -s "告警" admin@example.com <<< "$message"
}
# 主程序
log "INFO" "开始执行主流程"
# 模拟一系列操作
command1 || {
log "ERROR" "command1 失败"
exit 1
}
command2 || {
log "ERROR" "command2 失败"
exit 1
}
log "INFO" "所有操作完成"
这个错误处理机制看起来很复杂,但它是生产环境脚本的标配。trap 命令是Shell的一个强大功能,可以在特定事件(如错误、退出)发生时执行指定的操作。
常见坑点及解决方案
让我跟你聊聊我在运维路上踩过的坑,以及我是怎么解决的。
坑点一:特殊字符处理
#!/bin/bash
# 正确处理包含特殊字符的文件名
# 错误做法
filename="my file (2024).txt"
cat $filename # 这会报错,因为空格和括号会被解释
# 正确做法1:使用引号
cat "$filename"
# 正确做法2:使用数组处理多个文件
files=("my file (1).txt" "my file (2).txt" "my file (3).txt")
for file in "${files[@]}"; do
echo "处理文件:$file"
done
# 正确做法3:使用find命令配合-print0和xargs
find /path/to/dir -type f -name "*.log" -print0 | \
xargs -0 grep "error" | wc -l
特殊字符是新手最容易踩的坑。文件名里的空格、括号、引号都会导致脚本出错。记住两个原则:变量引用加引号,使用 -print0 和 -0 处理特殊字符。
坑点二:时间zone问题
#!/bin/bash
# 正确处理时区问题
# 问题:服务器时间和本地时间不一致
echo "当前时间:$(date)" # 这显示的是服务器时区的时间
# 解决方案1:指定时区
echo "北京时间:$(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M:%S')"
echo "纽约时间:$(TZ=America/New_York date '+%Y-%m-%d %H:%M:%S')"
# 解决方案2:在脚本开头统一设置时区
export TZ=Asia/Shanghai
echo "统一时区后的时间:$(date)"
# 解决方案3:使用UTC时间并转换
log_time=$(date -d "2024-01-15 10:00:00" +%s) # 转换为时间戳
echo "时间戳:$log_time"
echo "转换回时间:$(date -d "@$log_time" '+%Y-%m-%d %H:%M:%S %Z')"
时区问题在跨时区团队协作时特别容易出问题。我的建议是:日志统一用UTC时间存储,展示时再转换为本地时间。
坑点三:管道中的变量作用域
#!/bin/bash
# 管道变量作用域问题
count=0
# 错误做法:管道中的变量在子shell中,父shell无法访问
echo -e "line1\nline2\nline3" | while read line; do
((count++))
done
echo "计数结果:$count" # 输出0,因为count在子shell中修改
# 解决方案1:使用进程替换
while read line; do
((count++))
done < <(echo -e "line1\nline2\nline3")
echo "计数结果:$count" # 输出3
# 解决方案2:使用临时文件
echo -e "line1\nline2\nline3" > /tmp/temp_input.txt
while read line; do
((count++))
done < /tmp/temp_input.txt
rm -f /tmp/temp_input.txt
echo "计数结果:$count" # 输出3
# 解决方案3:使用awk(推荐)
result=$(echo -e "line1\nline2\nline3" | wc -l)
echo "计数结果:$result" # 输出3
管道会产生子shell,这是Shell编程中一个比较隐蔽的问题。如果你需要在管道后使用变量,记得用上面的解决方案。
坑点四:后台任务的陷阱
#!/bin/bash
# 正确处理后台任务
# 错误做法:后台任务可能因为终端关闭而终止
my_function &
echo "后台任务已启动"
# 解决方案1:使用nohup
nohup my_function > /tmp/my_function.log 2>&1 &
echo "后台任务已启动,日志文件:/tmp/my_function.log"
# 解决方案2:使用disown
my_function &
disown
echo "后台任务已启动(不会因终端关闭而终止)"
# 解决方案3:使用screen或tmux
# screen -dmS mytask my_function
# tmux new -d -s mytask 'my_function'
# 完整的后台任务管理脚本
run_background_task() {
local task_name=$1
local log_file="/var/log/background/${task_name}.log"
# 确保日志目录存在
mkdir -p "$(dirname "$log_file")"
# 启动后台任务
nohup bash -c "$task_name" > "$log_file" 2>&1 &
local pid=$!
echo "任务 $task_name 已启动,PID: $pid"
echo "日志文件: $log_file"
# 记录PID到文件,方便后续管理
echo "$pid" > "/tmp/${task_name}.pid"
}
stop_background_task() {
local task_name=$1
local pid_file="/tmp/${task_name}.pid"
if [[ -f "$pid_file" ]]; then
local pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
echo "任务 $task_name 已停止"
else
echo "任务 $task_name 未运行"
fi
rm -f "$pid_file"
else
echo "未找到任务 $task_name 的PID文件"
fi
}
# 使用示例
# run_background_task "analyze_logs"
# stop_background_task "analyze_logs"
后台任务管理是自动化运维的重要组成部分。记住:用nohup或disown确保任务不随终端关闭而终止,并记录PID方便后续管理。
完整的自动化运维案例
让我给你一个完整的自动化运维脚本示例,这个脚本整合了前面提到的所有最佳实践。
#!/bin/bash
# 自动化运维脚本:服务健康检查与自动恢复
# 版本:1.0
# 作者:一个走过弯路的运维人员
# 使用方法:./health_check.sh
# 配置区域
readonly SCRIPT_NAME="health_check"
readonly LOG_DIR="/var/log/automation"
readonly LOG_FILE="${LOG_DIR}/${SCRIPT_NAME}.log"
readonly PID_FILE="/tmp/${SCRIPT_NAME}.pid"
readonly ALERT_LOG="${LOG_DIR}/alerts.log"
readonly CONFIG_FILE="/etc/automation/health_check.conf"
readonly DRY_RUN=false # 设为true只预览不执行
# 服务配置
declare -A SERVICES=(
["nginx"]="nginx"
["mysql"]="mysqld"
["redis"]="redis-server"
)
# 启用严格模式
set -euo pipefail
# 确保日志目录存在
mkdir -p "$(dirname "$LOG_FILE")"
mkdir -p "$(dirname "$ALERT_LOG")"
# 写入PID文件
echo $$ > "$PID_FILE"
# 日志函数
log() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# 告警函数
alert() {
local message=$1
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message" >> "$ALERT_LOG"
log "ALERT" "$message"
# 这里可以添加邮件、钉钉等告警逻辑
}
# 错误处理
error_handler() {
local exit_code=$?
local line_number=$1
log "ERROR" "脚本在第 $line_number 行出错,退出码:$exit_code"
alert "健康检查脚本出错:第${line_number}行,退出码${exit_code}"
exit $exit_code
}
trap 'error_handler $LINENO' ERR
# 清理函数
cleanup() {
local exit_code=$?
log "INFO" "正在清理..."
rm -f "$PID_FILE"
log "INFO" "清理完成"
exit $exit_code
}
trap cleanup EXIT
# 检查配置文件
check_config() {
if [[ -f "$CONFIG_FILE" ]]; then
log "INFO" "加载配置文件:$CONFIG_FILE"
source "$CONFIG_FILE"
else
log "WARN" "配置文件不存在,使用默认配置"
fi
}
# 检查单个服务
check_service() {
local service_name=$1
local process_name=${SERVICES[$service_name]}
log "INFO" "检查服务:$service_name"
# 检查服务是否运行
if systemctl is-active --quiet "$process_name"; then
log "INFO" "服务 $service_name 运行正常"
return 0
else
log "WARN" "服务 $service_name 未运行"
return 1
fi
}
# 尝试重启服务
restart_service() {
local service_name=$1
local process_name=${SERVICES[$service_name]}
if [[ "$DRY_RUN" = true ]]; then
log "INFO" "[预览模式] 将重启服务 $service_name"
return 0
fi
log "INFO" "尝试重启服务:$service_name"
# 尝试启动服务
if systemctl start "$process_name"; then
sleep 2 # 等待服务启动
# 验证服务是否启动成功
if systemctl is-active --quiet "$process_name"; then
log "INFO" "服务 $service_name 重启成功"
alert "服务 $service_name 已自动重启"
return 0
else
log "ERROR" "服务 $service_name 重启后仍未运行"
alert "服务 $service_name 重启失败,请手动检查"
return 1
fi
else
log "ERROR" "无法重启服务 $service_name"
alert "服务 $service_name 重启命令执行失败"
return 1
fi
}
# 检查磁盘空间
check_disk() {
log "INFO" "检查磁盘空间..."
df -h | awk 'NR>1 {
usage=$5+0;
mount=$6;
if (usage > 85) {
printf " [警告] 分区 %s 使用率 %d%%\n", mount, usage;
}
}'
}
# 检查内存使用
check_memory() {
log "INFO" "检查内存使用..."
local mem_info=$(free | awk '/^Mem:/ {printf "总内存: %s, 已用: %s, 使用率: %.1f%%", $2, $3, $3/$2*100}')
log "INFO" "内存状态:$mem_info"
# 检查是否使用率超过90%
local usage_pct=$(free | awk '/^Mem:/ {printf "%.0f", $3/$2*100}')
if [[ "$usage_pct" -gt 90 ]]; then
alert "内存使用率超过90%,当前:${usage_pct}%"
fi
}
# 检查系统负载
check_load() {
log "INFO" "检查系统负载..."
local load_avg=$(uptime | awk -F'load average:' '{print $2}' | xargs)
local cpu_count=$(nproc)
local threshold=$((cpu_count * 2))
log "INFO" "系统负载:$load_avg (CPU核心数:$cpu_count)"
# 检查1分钟负载是否超过阈值
local load_1min=$(echo "$load_avg" | cut -d',' -f1 | xargs)
if (( $(echo "$load_1min > $threshold" | bc -l) )); then
alert "系统负载过高:${load_1min} (阈值:$threshold)"
fi
}
# 主程序
main() {
log "INFO" "===== 开始执行健康检查 ====="
log "INFO" "执行时间:$(date '+%Y-%m-%d %H:%M:%S')"
log "INFO" "运行模式:$([ "$DRY_RUN" = true ] && echo '预览模式' || echo '实际执行')"
echo "----------------------------------------"
# 检查配置
check_config
# 检查各项指标
check_disk
check_memory
check_load
# 检查服务
for service_name in "${!SERVICES[@]}"; do
if ! check_service "$service_name"; then
restart_service "$service_name"
fi
done
echo "----------------------------------------"
log "INFO" "===== 健康检查完成 ====="
log "INFO" "完成时间:$(date '+%Y-%m-%d %H:%M:%S')"
}
# 入口
main "$@"
这个脚本整合了我在前面提到的所有最佳实践:
- 严格的错误处理:
set -euo pipefail+ trap - 完善的日志记录:分级日志 + tee输出
- 安全机制:
DRY_RUN模式 - 资源清理:cleanup函数
- 告警机制:alert函数
让脚本更易维护的建议
最后,我想给你一些让脚本更容易维护的建议。
1. 注释要写清楚”为什么”,不只是”做什么”
# 错误示范:注释只是重复代码
# 计算总数
total=$(wc -l < "$file")
# 正确示范:注释解释原因
# 使用wc -l而不是grep -c,因为grep -c在文件为空时返回0而不是实际行数
total=$(wc -l < "$file")
2. 使用函数封装可重用逻辑
# 把通用逻辑封装成函数
send_notification() {
local message=$1
local channel=$2 # email, dingtalk, wechat
case "$channel" in
email)
echo "$message" | mail -s "告警" admin@example.com
;;
dingtalk)
curl -X POST "https://oapi.dingtalk.com/robot/send" \
-H "Content-Type: application/json" \
-d "{\"msgtype\": \"text\", \"text\": {\"content\": \"$message\"}}"
;;
wechat)
# 企业微信 webhook
curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send" \
-H "Content-Type: application/json" \
-d "{\"msgtype\": \"text\", \"text\": {\"content\": \"$message\"}}"
;;
esac
}
3. 版本控制和文档
#!/bin/bash
# 版本:1.2.3
# 最后更新:2024-01-15
# 更新日志:
# v1.2.3 - 修复内存检查的精度问题
# v1.2.2 - 添加企业微信告警支持
# v1.2.1 - 优化日志轮转逻辑
# v1.2.0 - 新增磁盘空间检查
# v1.1.0 - 添加预览模式(DRY_RUN)
# v1.0.0 - 初始版本
#
# 使用方法:
# ./health_check.sh # 执行健康检查
# ./health_check.sh --dry-run # 预览模式,不执行实际操
4. 测试你的脚本
# 使用bash -n检查语法
bash -n health_check.sh
# 使用shellcheck进行静态分析
shellcheck health_check.sh
# 在测试环境验证
./health_check.sh --dry-run
结语
写到这里,我想跟你说:Shell脚本学习 curve 确实有点陡,但一旦你掌握了,它能让你从”手动运维”升级为”自动化运维”,这是质的飞跃。
记住几个关键点:
- 安全第一:多用
DRY_RUN,多检查权限 - 错误处理不能少:
set -euo pipefail是标配 - 日志要写好:将来排查问题全靠它
- 函数化封装:让代码更清晰、更易维护
- 持续学习:Shell世界很大,慢慢探索
希望你从这篇文章里找到对自己有用的东西。如果有什么问题,随时交流!
