Bash 进阶脚本:错误处理、调试与系统监控
Bash 脚本编程进阶:从错误处理到系统监控
在掌握了 Bash 基础语法后,脚本的健壮性与实用性成为下一个关键目标。本教程聚焦于三个核心进阶主题:错误处理(让脚本优雅应对异常)、调试技巧(高效定位问题)以及系统监控(编写实用的运维脚本)。通过大量可直接运行的代码示例,你将学会如何构建生产级 Shell 脚本。
1. 健壮脚本的基石:错误处理
让脚本在遇到意外时不会静默失败或产生破坏性后果,需要主动检查命令执行状态、处理退出码并制定错误响应策略。
1.1 退出状态码与 set -e
- 每个命令执行后都会返回一个退出码(
$?),0 表示成功,非 0 表示错误。 - 脚本默认会忽略中间命令的失败,直到结束才返回最后一条命令的退出码。
- 启用
set -e可强制脚本在任意命令失败时立即退出,避免连锁错误。
#!/bin/bash
set -e # 命令失败立即退出
cd /nonexistent # 脚本在这里终止,后续命令不会执行
echo "这行不会被执行"
- 注意事项:管道中只有最后一个命令的退出码会被考虑,若需要管道任意环节失败都退出,需配合
set -o pipefail。
set -eo pipefail # 严格模式:单命令失败或管道任意环节失败均退出
1.2 自定义错误处理函数与 trap
仅依赖 set -e 还不够,我们希望捕获错误时执行清理操作(如删除临时文件)或打印友好日志。trap 命令可以捕获信号和脚本退出事件。
#!/bin/bash
set -e
cleanup() {
echo "捕获到错误或脚本退出,执行清理..."
rm -f /tmp/myapp_temp.*
}
trap cleanup ERR EXIT # ERR 捕获任何导致脚本退出的错误(要求 set -e),EXIT 确保正常退出也清理
# 模拟一个可能失败的操作
false # 故意执行失败,触发 trap ERR
- 常用
trap信号:ERR(错误退出)、EXIT(脚本结束)、INT(Ctrl+C 中断)、TERM。 - 避免重复 trap:若函数内多次设置 trap,可使用
trap - ERR EXIT取消之前的捕获。
1.3 手动检查关键命令与重试逻辑
并非所有命令都适合用 set -e 一刀切,部分场景需要更细粒度的控制。
# 尝试创建目录,失败则打印错误并退出
if ! mkdir -p /important/dir; then
echo "错误:无法创建目录" >&2
exit 1
fi
# 简单的重试函数
retry() {
local max_attempts=$1; shift
local cmd=$@
for ((i=1; i<=max_attempts; i++)); do
if $cmd; then
return 0
fi
echo "第 $i 次尝试失败,等待重试..." >&2
sleep 2
done
echo "命令失败,已达最大重试次数" >&2
return 1
}
retry 3 wget -q https://example.com/file.tar.gz
2. 调试技术:让脚本透明化
程序员的日常不只是写代码,更是调试代码。Bash 提供了多层次的调试手段,从简单的打印到专业的跟踪工具。
2.1 内置调试选项
| 选项 | 作用 | 使用方式 |
|---|---|---|
-x |
执行前打印每条命令及展开后的参数 | bash -x script.sh 或在脚本内 set -x |
-v |
打印读入的原始行 | bash -v script.sh |
-n |
仅检查语法,不执行 | bash -n script.sh |
-u |
使用未定义变量时报错退出(强烈推荐) | set -u |
- 选择性调试:用
set -x和set +x包裹可疑区域,避免输出过多噪音。
echo "正常区域"
set -x # 开启详细跟踪
complex_logic
set +x # 关闭跟踪
echo "继续"
2.2 结合 PS4 变量增强调试输出
PS4 是 -x 模式下的提示符,默认为 +。可以自定义为包含行号、函数名、时间戳等信息,极大提升调试效率。
export PS4='+ [${BASH_SOURCE}:${LINENO}] ${FUNCNAME[0]:+(${FUNCNAME[0]})}: '
set -x
# 输出示例:+ [myscript.sh:12] main: echo hello
- 常用
PS4变量:BASH_SOURCE(脚本名)、LINENO(行号)、FUNCNAME(函数调用栈)。
2.3 日志函数代替 echo
在脚本中直接使用 echo 难以统一控制输出格式和级别,封装一个简单的日志函数更实用。
log() {
local level=$1; shift
local msg="[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*"
case $level in
ERROR) echo -e "\033[31m${msg}\033[0m" ;; # 红色
WARN) echo -e "\033[33m${msg}\033[0m" ;; # 黄色
INFO) echo -e "\033[32m${msg}\033[0m" ;; # 绿色
DEBUG) [[ $DEBUG -eq 1 ]] && echo -e "\033[36m${msg}\033[0m" ;; # 仅在DEBUG=1时输出
esac
}
DEBUG=1
log INFO "服务启动成功"
log DEBUG "变量 X 的值为 $X"
log ERROR "文件未找到"
- 可以将日志同时写入文件,通过
tee或重定向扩展。
2.4 利用 shellcheck 静态检查
许多运行时错误可以通过静态工具提前发现。shellcheck 是必装的 Bash 代码质量工具。
# 安装后使用
shellcheck my_script.sh
- 它能检测常见问题:未引用的变量、
rm -rf $DIR/的危险、错误的[ $var = "val" ]写法等。
3. 系统监控脚本实战
真正让 Bash 在运维中大放异彩的是系统监控。我们将编写几个可以直接使用的监控脚本,涵盖 CPU、内存、磁盘、进程等。
3.1 基础指标收集框架
首先建立一个脚本模板,实现定时采样与报警。
#!/bin/bash
set -euo pipefail
# 配置项
LOG_DIR="/var/log/monitor"
INTERVAL=5 # 采样间隔(秒)
CPU_THRESHOLD=80 # CPU 使用率报警阈值
MEM_THRESHOLD=90 # 内存使用率报警阈值
mkdir -p "$LOG_DIR"
# 通用报警函数(可替换为发送邮件或消息推送)
alert() {
local msg="$1"
echo "[ALERT] $(date) $msg" | tee -a "$LOG_DIR/alerts.log"
# 在这里加上你的通知逻辑,例如:curl -X POST -d "text=$msg" webhook_url
}
while true; do
# 收集指标逻辑将放在这里
sleep "$INTERVAL"
done
3.2 CPU 和内存监控
借助 /proc/stat 和 top 命令的批处理模式获取数据。
# 获取 CPU 总利用率(用户态+系统态)—— 需要两次采样计算差值
get_cpu_usage() {
local cpu_line=($(grep '^cpu ' /proc/stat))
local idle1=${cpu_line[4]}
local total1=0
for val in "${cpu_line[@]:1}"; do
((total1 += val))
done
sleep 1
cpu_line=($(grep '^cpu ' /proc/stat))
local idle2=${cpu_line[4]}
local total2=0
for val in "${cpu_line[@]:1}"; do
((total2 += val))
done
local delta_idle=$((idle2 - idle1))
local delta_total=$((total2 - total1))
echo $((100 * (delta_total - delta_idle) / delta_total))
}
# 获取内存使用百分比
get_mem_usage() {
local mem_info=($(free -b | awk '/Mem:/ {print $2,$3}'))
local total=${mem_info[0]}
local used=${mem_info[1]}
echo $((100 * used / total))
}
在监控循环中调用:
while true; do
cpu=$(get_cpu_usage)
mem=$(get_mem_usage)
log INFO "CPU: ${cpu}%, Memory: ${mem}%"
if [[ $cpu -ge $CPU_THRESHOLD ]]; then
alert "CPU 使用率过高: ${cpu}% (阈值: ${CPU_THRESHOLD}%)"
fi
if [[ $mem -ge $MEM_THRESHOLD ]]; then
alert "内存使用率过高: ${mem}% (阈值: ${MEM_THRESHOLD}%)"
fi
sleep "$INTERVAL"
done
3.3 磁盘空间与 inode 监控
df 命令可以直接输出使用百分比,但要注意去除百分号并比较数值。
check_disk() {
local threshold=85
# 监控根分区和任何数据分区
df -h / /data 2>/dev/null | awk 'NR>1 {print $5" "$6}' | while read usage mount; do
local percent=${usage%\%}
if [[ $percent -ge $threshold ]]; then
alert "磁盘 $mount 使用率 ${percent}% 已超过阈值"
fi
done
# Inode 使用检查(防止小文件耗尽 inode 导致“磁盘满”假象)
df -i / | awk 'NR==2 {print $5" "$6}' | {
read usage mount
local ip=${usage%\%}
if [[ $ip -ge 85 ]]; then
alert "磁盘 $mount Inode 使用率 ${ip}% 过高"
fi
}
}
3.4 关键进程存活监控
监控指定进程名是否存在,若不存在则触发报警并尝试自动重启(可选)。
PROCESSES=("nginx" "sshd" "mysql")
check_processes() {
for proc in "${PROCESSES[@]}"; do
if ! pgrep -x "$proc" > /dev/null; then
alert "进程 $proc 未运行!"
# 自动重启逻辑(谨慎使用)
# case $proc in
# nginx) systemctl restart nginx ;;
# mysql) systemctl restart mysql ;;
# esac
fi
done
}
3.5 网络连通性与服务端口检测
通过 ping 和 nc (或 curl) 检测外部连通性与服务可用性。
TARGETS=("8.8.8.8" "1.1.1.1")
SERVICES=("https://example.com" "tcp://db.internal:3306")
check_network() {
# ping 检查
for ip in "${TARGETS[@]}"; do
if ! ping -c 1 -W 1 "$ip" > /dev/null; then
alert "Ping $ip 失败"
fi
done
# 服务端口检查 (需要 nc 命令)
for srv in "${SERVICES[@]}"; do
if [[ $srv == tcp://* ]]; then
local host=${srv#tcp://}
local port=${host#*:}
host=${host%:*}
if ! nc -zv -w2 "$host" "$port" &>/dev/null; then
alert "TCP 端口不可达: $host:$port"
fi
elif [[ $srv == https://* ]]; then
if ! curl -f -s -o /dev/null --connect-timeout 3 "$srv"; then
alert "HTTP 服务异常: $srv"
fi
fi
done
}
3.6 将所有碎片组合成完整监控脚本
将上述函数集成到一个脚本中,加上适当的主循环和日志记录,一个轻量级实时监控守护进程就诞生了。
#!/bin/bash
set -euo pipefail
source ./logger.sh # 引用之前的日志函数
# 配置...(同上)
LOG_DIR="/var/log/monitor"
INTERVAL=10
CPU_THRESHOLD=80
MEM_THRESHOLD=90
# ... 所有函数定义(get_cpu_usage, get_mem_usage, check_disk, check_processes, check_network)
main() {
# 以守护进程方式运行更好的选择是使用系统服务,这里仅示意
while true; do
cpu=$(get_cpu_usage)
mem=$(get_mem_usage)
log INFO "CPU: ${cpu}%, Mem: ${mem}%"
[[ $cpu -ge $CPU_THRESHOLD ]] && alert "CPU 使用 ${cpu}% 超出阈值"
[[ $mem -ge $MEM_THRESHOLD ]] && alert "内存使用 ${mem}% 超出阈值"
check_disk
check_processes
check_network
sleep "$INTERVAL"
done
}
main "$@"
4. 进阶技巧与最佳实践
- 配置与代码分离:将阈值、检查项、通知方式等提取到单独的
.conf文件,并用source加载。 - 守护进程化:使用
systemd或supervisor管理监控脚本,避免手动nohup。 - 性能考虑:频繁调用外部命令(如
awk、grep、sleep)在长时间循环中会有开销。对于极高频监控,可考虑纯 Bash 内置操作或减少外部调用。 - 脚本自检:在脚本开头添加
check_root或依赖检查函数。
require() { command -v "$1" >/dev/null 2>&1 || { echo "需要 $1 但未安装。退出。" >&2; exit 1; } }
require nc
require wget
结语
Bash 进阶编程的核心是让脚本具备应对真实环境复杂性的能力。通过严谨的错误处理、高效的调试手段以及实用的监控逻辑,你可以将简单的命令行脚本提升为可靠的自动化工具。下一步建议:阅读 man bash 中关于 [[、数组、重定向的进阶用法,并动手为自己所在的环境编写一套定制化监控解决方案。