一、为什么每个开发者都必须掌握Shell?
1.1 真实场景:没有Shell,运维怎么活?
想象这样一个凌晨3点的场景:
生产环境突然告警,50台服务器的磁盘空间同时告急。你坐在电脑前,是选择:
- 方案A :一台台SSH登录,手动执行
df -h,复制粘贴清理命令,预计耗时3小时- 方案B:运行一条命令,30秒内完成全集群巡检和自动清理
Shell脚本就是那个"方案B"。
1.2 Shell不可替代的三大理由
| 对比维度 | 手动操作 | Shell脚本 | Python脚本 |
|---|---|---|---|
| 执行速度 | 慢(人肉操作) | 极快(系统级调用) | 中等(需解释器) |
| 环境依赖 | 无 | 几乎无(Bash内置) | 需安装Python及库 |
| 学习成本 | 低(但重复劳动) | 中(一次学习终身受益) | 中高 |
| 系统亲和度 | - | 原生支持 | 需适配 |
| 适用场景 | 一次性操作 | 自动化、批量化 | 复杂业务逻辑 |
核心结论: Shell是Linux系统的"母语",任何与系统打交道的场景,Shell都是首选。
二、脚本骨架:每个脚本都应该有的标准结构(⭐⭐⭐重点)
2.1 为什么需要标准模板?
先看一个反面教材------新手常写的脚本:
bash
#!/bin/bash
a=$1
b=$2
c=$a+$b
echo $c
问题诊断:
- ❌ 没有错误处理,参数缺失直接报错
- ❌ 没有注释,3天后自己都不认识
- ❌ 没有日志,出问题无法追溯
- ❌ 没有帮助信息,别人不会用
2.2 企业级标准模板(可直接复制使用)
bash
#!/bin/bash
#===============================================================================
#
# FILE: backup_mysql.sh
#
# USAGE: ./backup_mysql.sh [OPTIONS]
#
# DESCRIPTION: MySQL数据库备份脚本,支持全量备份和指定库备份
# 特性:自动压缩、过期清理、钉钉告警、执行日志
#
# OPTIONS: -h 显示帮助信息
# -d 指定数据库名(默认全库)
# -p 指定备份路径(默认: /data/backup/mysql)
# -r 保留天数(默认: 7天)
#
# REQUIREMENTS: mysqldump >= 5.7, gzip, bc
#
# BUGS: 暂无已知问题
#
# NOTES: 1. 建议加入crontab定时执行:0 2 * * * /path/backup_mysql.sh
# 2. 首次运行前请确保备份目录存在且有写权限
# 3. 敏感信息(密码)建议通过环境变量或配置文件传入
#
# AUTHOR: Your Name <your.email@example.com>
# VERSION: 1.0.0
# CREATED: 2024-01-01
# REVISION: 2024-03-15 增加磁盘空间预检查
#===============================================================================
#-------------------------------------------------------------------------------
# 严格模式设置(这三行是脚本的"安全气囊")
#-------------------------------------------------------------------------------
set -euo pipefail
# -e: 命令失败立即退出,防止错误继续执行导致更大问题
# -u: 使用未定义变量时报错,避免变量名拼写错误
# -o pipefail: 管道中任一命令失败则整体失败,防止"静默失败"
#-------------------------------------------------------------------------------
# 全局变量定义(集中管理,拒绝硬编码)
#-------------------------------------------------------------------------------
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly LOG_DIR="/var/log/shell-scripts"
readonly LOG_FILE="${LOG_DIR}/${SCRIPT_NAME%.sh}.log"
readonly LOCK_FILE="/var/run/${SCRIPT_NAME%.sh}.lock"
readonly BACKUP_DIR="/data/backup/mysql"
readonly DATE=$(date +%Y%m%d_%H%M%S)
readonly RETENTION_DAYS=7
# 颜色定义(用于终端输出,生产环境可关闭)
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
#-------------------------------------------------------------------------------
# 日志函数(分级日志,便于排查问题)
#-------------------------------------------------------------------------------
# 确保日志目录存在
mkdir -p "$LOG_DIR"
log_info() {
local message="[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] [$$] $*"
echo -e "${GREEN}${message}${NC}" | tee -a "$LOG_FILE"
}
log_warn() {
local message="[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] [$$] $*"
echo -e "${YELLOW}${message}${NC}" | tee -a "$LOG_FILE"
}
log_error() {
local message="[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] [$$] $*"
echo -e "${RED}${message}${NC}" | tee -a "$LOG_FILE"
}
log_debug() {
# 调试日志,通过环境变量控制是否输出
if [[ "${DEBUG:-0}" == "1" ]]; then
local message="[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] [$$] $*"
echo -e "${BLUE}${message}${NC}" | tee -a "$LOG_FILE"
fi
}
#-------------------------------------------------------------------------------
# 帮助信息
#-------------------------------------------------------------------------------
usage() {
cat << EOF
Usage: ${SCRIPT_NAME} [OPTIONS]
MySQL数据库备份脚本 - 企业级备份解决方案
Options:
-h, --help 显示帮助信息
-d, --database 指定数据库名(默认全库备份)
-p, --path 指定备份路径(默认: ${BACKUP_DIR})
-r, --retention 保留天数(默认: ${RETENTION_DAYS}天)
--debug 开启调试模式,输出详细日志
Examples:
# 全库备份
${SCRIPT_NAME}
# 备份指定数据库
${SCRIPT_NAME} -d mydb
# 指定备份路径和保留天数
${SCRIPT_NAME} -d mydb -p /tmp/backup -r 3
# 调试模式运行
DEBUG=1 ${SCRIPT_NAME} -d mydb
Exit Codes:
0 成功
1 参数错误
2 权限不足
3 依赖缺失
4 备份失败
EOF
}
#-------------------------------------------------------------------------------
# 参数解析(使用手动解析,兼容长选项)
#-------------------------------------------------------------------------------
parse_args() {
local database=""
local backup_path="$BACKUP_DIR"
local retention="$RETENTION_DAYS"
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-d|--database)
if [[ -z "${2:-}" ]]; then
log_error "选项 -d 需要参数"
exit 1
fi
database="$2"
shift 2
;;
-p|--path)
if [[ -z "${2:-}" ]]; then
log_error "选项 -p 需要参数"
exit 1
fi
backup_path="$2"
shift 2
;;
-r|--retention)
if [[ -z "${2:-}" ]]; then
log_error "选项 -r 需要参数"
exit 1
fi
retention="$2"
shift 2
;;
--debug)
DEBUG=1
shift
;;
*)
log_error "未知参数: $1"
usage
exit 1
;;
esac
done
# 参数校验
if ! [[ "$retention" =~ ^[0-9]+$ ]]; then
log_error "保留天数必须是正整数: $retention"
exit 1
fi
# 返回解析结果(使用全局变量)
BACKUP_DATABASE="$database"
BACKUP_PATH="$backup_path"
RETENTION="$retention"
}
#-------------------------------------------------------------------------------
# 前置检查(环境、依赖、权限、锁)
#-------------------------------------------------------------------------------
pre_check() {
log_info "开始前置检查..."
# 检查锁文件(防止重复执行)
if [[ -f "$LOCK_FILE" ]]; then
local pid=$(cat "$LOCK_FILE")
if kill -0 "$pid" 2>/dev/null; then
log_error "脚本正在运行中 (PID: $pid),请勿重复执行"
exit 1
else
log_warn "发现过期锁文件,已清理"
rm -f "$LOCK_FILE"
fi
fi
echo $$ > "$LOCK_FILE"
# 检查是否root运行(根据实际需求)
# if [[ $EUID -ne 0 ]]; then
# log_error "请使用root权限运行此脚本"
# exit 2
# fi
# 检查依赖命令
local deps=("mysqldump" "gzip" "mysql")
for cmd in "${deps[@]}"; do
if ! command -v "$cmd" &> /dev/null; then
log_error "未找到依赖命令: $cmd,请先安装"
exit 3
fi
log_debug "依赖检查通过: $cmd"
done
# 检查磁盘空间(预留10%)
local available=$(df "$backup_path" | awk 'NR==2{print $4}')
local required=1048576 # 1GB = 1048576 KB
if [[ $available -lt $required ]]; then
log_error "磁盘空间不足,可用: ${available}KB,需要: ${required}KB"
exit 2
fi
log_info "磁盘空间检查通过"
# 创建备份目录
mkdir -p "$BACKUP_PATH"
if [[ ! -w "$BACKUP_PATH" ]]; then
log_error "备份目录不可写: $BACKUP_PATH"
exit 2
fi
log_info "前置检查全部通过"
}
#-------------------------------------------------------------------------------
# 核心功能:执行备份
#-------------------------------------------------------------------------------
do_backup() {
local database="$1"
local backup_path="$2"
local backup_file="${backup_path}/backup_${database:-all}_${DATE}.sql.gz"
log_info "开始备份,目标文件: $backup_file"
# 构建mysqldump命令
local dump_cmd="mysqldump --single-transaction --quick --lock-tables=false"
dump_cmd+=" --routines --triggers"
if [[ -n "$database" ]]; then
dump_cmd+=" $database"
else
dump_cmd+=" --all-databases"
fi
log_debug "执行命令: $dump_cmd | gzip > $backup_file"
# 执行备份并压缩
if ! eval "$dump_cmd" 2>/dev/null | gzip > "$backup_file"; then
log_error "备份命令执行失败"
rm -f "$backup_file" # 清理残次文件
return 4
fi
# 验证备份文件
if [[ ! -f "$backup_file" || ! -s "$backup_file" ]]; then
log_error "备份文件不存在或为空"
return 4
fi
local size=$(du -h "$backup_file" | cut -f1)
log_info "备份成功,文件大小: $size"
return 0
}
#-------------------------------------------------------------------------------
# 清理过期备份
#-------------------------------------------------------------------------------
cleanup() {
local backup_path="$1"
local retention="$2"
log_info "清理${retention}天前的备份文件..."
local count_before=$(find "$backup_path" -name "backup_*.sql.gz" | wc -l)
find "$backup_path" -name "backup_*.sql.gz" -mtime +"$retention" -delete
local count_after=$(find "$backup_path" -name "backup_*.sql.gz" | wc -l)
local deleted=$((count_before - count_after))
log_info "清理完成,删除${deleted}个文件,剩余${count_after}个文件"
}
#-------------------------------------------------------------------------------
# 清理函数(脚本退出时执行)
#-------------------------------------------------------------------------------
cleanup_lock() {
rm -f "$LOCK_FILE"
log_debug "锁文件已清理"
}
# 注册退出时的清理操作
trap cleanup_lock EXIT
#-------------------------------------------------------------------------------
# 主函数(程序入口)
#-------------------------------------------------------------------------------
main() {
log_info "========== 脚本开始执行 =========="
log_info "脚本版本: 1.0.0"
log_info "执行用户: $(whoami)"
log_info "工作目录: $(pwd)"
parse_args "$@"
pre_check
if ! do_backup "$BACKUP_DATABASE" "$BACKUP_PATH"; then
exit 4
fi
cleanup "$BACKUP_PATH" "$RETENTION"
log_info "========== 脚本执行完成 =========="
}
# 执行主函数,传入所有参数
main "$@"
模板核心亮点解析:
| 特性 | 作用 | 生产价值 |
|---|---|---|
set -euo pipefail |
严格错误处理 | 防止"静默失败",问题早发现 |
| 锁文件机制 | 防止重复执行 | 避免定时任务重叠导致资源争抢 |
| 分级日志 | INFO/WARN/ERROR/DEBUG | 快速定位问题,支持调试开关 |
| 参数校验 | 类型检查、必填校验 | 防止非法输入导致不可预期行为 |
trap EXIT |
退出时自动清理 | 避免锁文件残留导致后续执行失败 |
| 退出码规范 | 0=成功,1-4=不同错误 | 便于上层系统(如Zabbix)监控 |
三、核心语法深度解析
3.1 变量与引号:一个空格引发的惨案
实验:验证引号的差异
bash
#!/bin/bash
# 创建测试文件
touch "my file.txt" # 文件名包含空格
FILE="my file.txt"
echo "===== 实验1:无引号 ====="
echo $FILE
# 输出:my file.txt(被拆成两个参数!)
# 实际执行效果等同于:echo my file.txt
echo "===== 实验2:双引号 ====="
echo "$FILE"
# 输出:my file.txt(正确,一个整体)
echo "===== 实验3:单引号 ====="
echo '$FILE'
# 输出:$FILE(原样输出,不解析变量)
echo "===== 实验4:反引号(命令替换)====="
echo "`date`"
# 输出:Wed May 13 12:00:00 CST 2026
echo "===== 实验5:\$() 现代命令替换 ====="
echo "$(date)"
# 输出同上,但可嵌套,推荐!
# 危险操作演示(别在生产环境执行!)
# rm $FILE # 错误!会尝试删除"my"和"file.txt"两个文件
# rm "$FILE" # 正确!删除名为"my file.txt"的单个文件
# 清理测试文件
rm -f "my file.txt"
黄金法则:变量永远用双引号包裹!
3.2 条件判断:test、[ ]、[[ ]] 的选择
对比实验:
bash
#!/bin/bash
STR="hello world"
NUM=10
echo "===== 实验1:[ ] 需要引号 ====="
# if [ $STR == "hello world" ]; then # 错误!会被拆成多个单词
# echo "匹配"
# fi
if [ "$STR" == "hello world" ]; then # 正确,必须加引号
echo "[ ] 匹配成功"
fi
echo "===== 实验2:[[ ]] 不需要引号 ====="
if [[ $STR == hello* ]]; then # 正确,不会单词分割
echo "[[ ]] 模式匹配成功: $STR"
fi
echo "===== 实验3:[[ ]] 支持正则 ====="
if [[ $STR =~ ^h.*d$ ]]; then
echo "[[ ]] 正则匹配成功"
fi
echo "===== 实验4:[[ ]] 支持逻辑运算符 ====="
if [[ $NUM -gt 5 && $NUM -lt 15 ]]; then
echo "NUM在5到15之间"
fi
# [ ] 中不能这样写,必须:
# if [ $NUM -gt 5 ] && [ $NUM -lt 15 ]; then
选择建议:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 简单数字比较 | [ ] 或 [[ ]] |
[ ] 更兼容POSIX |
| 字符串匹配、正则 | [[ ]] |
功能更强大 |
| 复杂逻辑组合 | [[ ]] |
&& ` |
| 需要跨平台(sh/dash) | [ ] |
[[ ]] 是Bash扩展 |
3.3 循环结构:不同场景的最佳选择
场景对比实验:
bash
#!/bin/bash
echo "===== 场景1:遍历文件(注意通配符陷阱)====="
# 错误写法:如果目录为空,会输出 "*.log" 字面量
# for file in *.log; do
# echo "$file"
# done
# 正确写法:检查文件是否存在
for file in /var/log/*.log; do
[[ -f "$file" ]] || continue # 跳过不存在的文件
echo "处理: $(basename "$file")"
done
echo ""
echo "===== 场景2:C风格循环(需要索引时)====="
arr=("apple" "banana" "cherry")
for ((i=0; i<${#arr[@]}; i++)); do
echo "[$i] ${arr[$i]}"
done
echo ""
echo "===== 场景3:读取文件每行(while + read)====="
# 正确方式:IFS= 防止去掉首尾空格,-r 防止反斜杠转义
while IFS= read -r line; do
echo "读取: $line"
done << EOF
line with leading spaces
normal line
EOF
echo ""
echo "===== 场景4:带索引的数组遍历 ====="
for i in "${!arr[@]}"; do
echo "索引 $i 的值: ${arr[$i]}"
done
3.4 数组操作:Shell的"高级数据结构"
bash
#!/bin/bash
# 普通数组(索引数组)
echo "===== 普通数组 ====="
SERVERS=("192.168.1.10" "192.168.1.11" "192.168.1.12")
echo "原始数组: ${SERVERS[@]}"
echo "数组长度: ${#SERVERS[@]}"
# 添加元素
SERVERS+=("192.168.1.13")
echo "添加后: ${SERVERS[@]}"
# 数组切片
echo "前两个: ${SERVERS[@]:0:2}"
# 删除元素
unset SERVERS[1]
echo "删除索引1后: ${SERVERS[@]}"
echo ""
echo "===== 关联数组(Bash 4.0+,类似Map/字典)====="
declare -A USER_MAP
USER_MAP["admin"]="管理员"
USER_MAP["guest"]="访客"
USER_MAP["root"]="超级管理员"
echo "admin的角色: ${USER_MAP["admin"]}"
echo "所有键: ${!USER_MAP[@]}"
echo "所有值: ${USER_MAP[@]}"
echo ""
echo "===== 遍历关联数组 ====="
for key in "${!USER_MAP[@]}"; do
printf "%-10s -> %s\n" "$key" "${USER_MAP[$key]}"
done
四、大数据实战脚本
脚本1:Hadoop集群一键启停脚本
bash
#!/bin/bash
#===============================================================================
# Hadoop集群管理脚本
# 功能:一键启动/停止HDFS、YARN、HistoryServer
# 配置:修改HADOOP_HOME和主机名即可使用
#===============================================================================
set -euo pipefail
# 配置区(根据实际环境修改)
readonly HADOOP_HOME="/opt/module/hadoop-3.1.3"
readonly NAMENODE_HOST="hadoop101"
readonly RESOURCEMANAGER_HOST="hadoop102"
# 颜色输出
readonly GREEN='\033[0;32m'
readonly RED='\033[0;31m'
readonly NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
usage() {
echo "Usage: $0 {start|stop|status}"
exit 1
}
# 检查SSH免密是否配置
check_ssh() {
local host="$1"
if ! ssh -o ConnectTimeout=5 "$host" "echo OK" &>/dev/null; then
log_error "无法连接到 $host,请检查SSH免密配置"
exit 1
fi
}
# 启动集群
start_cluster() {
log_info "========== 启动 Hadoop 集群 =========="
check_ssh "$NAMENODE_HOST"
check_ssh "$RESOURCEMANAGER_HOST"
log_info "[1/3] 启动 HDFS..."
ssh "$NAMENODE_HOST" "${HADOOP_HOME}/sbin/start-dfs.sh"
log_info "[2/3] 启动 YARN..."
ssh "$RESOURCEMANAGER_HOST" "${HADOOP_HOME}/sbin/start-yarn.sh"
log_info "[3/3] 启动 HistoryServer..."
ssh "$NAMENODE_HOST" "${HADOOP_HOME}/bin/mapred --daemon start historyserver"
log_info "========== 启动完成 =========="
}
# 停止集群
stop_cluster() {
log_info "========== 停止 Hadoop 集群 =========="
log_info "[1/3] 停止 HistoryServer..."
ssh "$NAMENODE_HOST" "${HADOOP_HOME}/bin/mapred --daemon stop historyserver" 2>/dev/null || true
log_info "[2/3] 停止 YARN..."
ssh "$RESOURCEMANAGER_HOST" "${HADOOP_HOME}/sbin/stop-yarn.sh" 2>/dev/null || true
log_info "[3/3] 停止 HDFS..."
ssh "$NAMENODE_HOST" "${HADOOP_HOME}/sbin/stop-dfs.sh" 2>/dev/null || true
log_info "========== 停止完成 =========="
}
# 查看集群状态
status_cluster() {
log_info "========== 集群进程状态 =========="
local hosts=("$NAMENODE_HOST" "$RESOURCEMANAGER_HOST")
for host in "${hosts[@]}"; do
echo ""
echo "----- $host -----"
ssh "$host" "jps" 2>/dev/null || echo "无法连接"
done
}
# 主程序
case "${1:-}" in
start)
start_cluster
;;
stop)
stop_cluster
;;
status)
status_cluster
;;
*)
usage
;;
esac
使用方式:
bash
chmod +x hadoop-cluster.sh
./hadoop-cluster.sh start # 启动集群
./hadoop-cluster.sh stop # 停止集群
./hadoop-cluster.sh status # 查看状态
脚本2:集群分发脚本(xsync升级版)
bash
#!/bin/bash
#===============================================================================
# 集群文件分发脚本
# 功能:将本地文件/目录同步到集群所有节点
# 特性:自动创建目录、增量同步、权限保持、进度显示
#===============================================================================
set -euo pipefail
# 配置区
readonly HOSTS=("hadoop102" "hadoop103" "hadoop104")
readonly USER=$(whoami)
# 颜色
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly RED='\033[0;31m'
readonly NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
usage() {
cat << EOF
Usage: $0 <file_or_directory> [file_or_directory...]
将文件或目录同步到集群所有节点: ${HOSTS[*]}
Examples:
$0 /opt/module/hadoop-3.1.3/etc/hadoop/core-site.xml
$0 /home/$USER/.bashrc
$0 /opt/module/hadoop-3.1.3/etc/hadoop/
EOF
exit 1
}
# 同步到单台服务器
sync_to_host() {
local host="$1"
local file="$2"
local pdir="$3"
local fname="$4"
# 检查远程目录是否存在,不存在则创建
ssh -o ConnectTimeout=5 "$host" "mkdir -p $pdir" 2>/dev/null || {
log_error "无法连接到 $host"
return 1
}
# 使用rsync进行增量同步
if rsync -avz --progress "$file" "${USER}@${host}:${pdir}/${fname}" 2>/dev/null; then
log_info "[$host] 同步成功: $fname"
return 0
else
log_error "[$host] 同步失败: $fname"
return 1
fi
}
# 主程序
main() {
[[ $# -lt 1 ]] && usage
local total=0
local success=0
local failed=0
for file in "$@"; do
# 检查文件是否存在
if [[ ! -e "$file" ]]; then
log_error "文件不存在: $file"
((failed++))
continue
fi
# 获取父目录和文件名
local pdir=$(cd -P "$(dirname "$file")" && pwd)
local fname=$(basename "$file")
log_info "准备同步: $file"
log_info "目标路径: $pdir/$fname"
# 同步到每台服务器
for host in "${HOSTS[@]}"; do
((total++))
if sync_to_host "$host" "$file" "$pdir" "$fname"; then
((success++))
else
((failed++))
fi
done
done
echo ""
log_info "========== 同步完成 =========="
log_info "总计: $total, 成功: $success, 失败: $failed"
}
main "$@"
脚本3:服务器健康巡检+钉钉告警脚本
bash
#!/bin/bash
#===============================================================================
# 服务器健康巡检脚本
# 功能:检查CPU、内存、磁盘、负载,异常时发送钉钉告警
# 建议:加入crontab,每5分钟执行一次
#===============================================================================
set -euo pipefail
# 配置区
readonly THRESHOLD_CPU=80
readonly THRESHOLD_MEM=80
readonly THRESHOLD_DISK=85
readonly THRESHOLD_LOAD=5.0
readonly ALERT_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"
readonly LOG_FILE="/var/log/server-check.log"
# 确保日志目录存在
mkdir -p "$(dirname "$LOG_FILE")"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# 发送钉钉告警
send_alert() {
local title="$1"
local content="$2"
# 如果没有配置webhook,只记录日志
if [[ "$ALERT_WEBHOOK" == *"YOUR_TOKEN"* ]]; then
log "[ALERT-SKIPPED] 未配置钉钉token: $title"
return
fi
local json="{
\"msgtype\": \"markdown\",
\"markdown\": {
\"title\": \"$title\",
\"text\": \"## 🚨 $title\\n\\n$content\\n\\n> 📍 服务器: $(hostname)\\n> ⏰ 时间: $(date '+%Y-%m-%d %H:%M:%S')\\n> 💻 IP: $(hostname -I | awk '{print $1}')\"
}
}"
curl -s -X POST "$ALERT_WEBHOOK" \
-H "Content-Type: application/json" \
-d "$json" > /dev/null 2>&1 && log "[ALERT-SENT] $title" || log "[ALERT-FAILED] $title"
}
# 检查CPU
check_cpu() {
local cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' || echo "0")
local cpu_usage=$(echo "100 - ${cpu_idle:-0}" | bc)
cpu_usage=${cpu_usage%.*}
log "CPU使用率: ${cpu_usage}%"
if [[ ${cpu_usage:-0} -gt $THRESHOLD_CPU ]]; then
send_alert "CPU告警 🔥" \
"**当前CPU使用率: ${cpu_usage}%**\n**阈值: ${THRESHOLD_CPU}%**\n\n建议: 检查是否有异常进程"
fi
}
# 检查内存
check_memory() {
local mem_info=$(free | grep Mem || echo "0 0 0")
local total=$(echo "$mem_info" | awk '{print $2}')
local used=$(echo "$mem_info" | awk '{print $3}')
[[ -z "$total" || "$total" == "0" ]] && return
local usage=$(echo "scale=2; $used / $total * 100" | bc)
usage=${usage%.*}
log "内存使用率: ${usage}%"
if [[ ${usage:-0} -gt $THRESHOLD_MEM ]]; then
send_alert "内存告警 💾" \
"**当前内存使用率: ${usage}%**\n**阈值: ${THRESHOLD_MEM}%**\n\n建议: 检查内存泄漏或扩容"
fi
}
# 检查磁盘
check_disk() {
while read -r filesystem size used avail usage mount; do
[[ "$filesystem" == "Filesystem" ]] && continue
[[ "$filesystem" == "tmpfs" ]] && continue # 跳过虚拟文件系统
local usage_num=${usage%\%}
log "磁盘 [$mount] 使用率: $usage"
if [[ ${usage_num:-0} -gt $THRESHOLD_DISK ]]; then
send_alert "磁盘告警 💿" \
"**挂载点: $mount**\n**使用率: $usage**\n**阈值: ${THRESHOLD_DISK}%**\n\n建议: 清理日志或扩容"
fi
done < <(df -h | grep -v "^Filesystem")
}
# 检查负载
check_load() {
local load1=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | tr -d ',')
log "1分钟负载: $load1"
if (( $(echo "$load1 > $THRESHOLD_LOAD" | bc -l) )); then
send_alert "负载告警 ⚡" \
"**当前1分钟负载: $load1**\n**阈值: ${THRESHOLD_LOAD}**\n\n建议: 检查是否有CPU密集型任务"
fi
}
# 主程序
main() {
log "========== 巡检开始 =========="
check_cpu
check_memory
check_disk
check_load
log "========== 巡检完成 =========="
}
main
五、调试技巧:脚本出错了怎么办?
5.1 五级调试法
| 级别 | 方法 | 适用场景 |
|---|---|---|
| L1:语法检查 | bash -n script.sh |
执行前检查语法错误 |
| L2:追踪执行 | bash -x script.sh |
查看每条命令的执行结果 |
| L3:局部调试 | set -x / set +x |
只调试脚本中的某一段 |
| L4:变量打印 | echo "DEBUG: var=$var" |
快速查看变量值 |
| L5:错误定位 | trap '...' ERR |
自动捕获错误位置 |
5.2 实战调试示例
bash
#!/bin/bash
# 方法1:整个脚本追踪
# bash -x debug-demo.sh
# 方法2:局部追踪
set -x # 开启追踪
# 需要调试的代码段
result=$(ls /nonexistent 2>/dev/null)
echo "result=$result"
set +x # 关闭追踪
# 方法3:使用trap捕获错误
trap 'echo "[ERROR] 脚本 $0 在第 $LINENO 行失败,退出码: $?"' ERR
# 模拟错误
false # 这条命令会触发trap
# 方法4:调试函数(带调用栈)
debug() {
local msg="$1"
local line=${BASH_LINENO[0]}
local func=${FUNCNAME[1]:-main}
echo "[DEBUG] [$func:$line] $msg" >&2
}
my_function() {
local var="test"
debug "var=$var"
}
my_function
六、高频面试题(建议收藏)
| 题目 | 答案要点 |
|---|---|
set -euo pipefail 各参数含义? |
-e出错即退,-u未定义变量报错,-o pipefail管道失败传递 |
[ ] 和 [[ ]] 的区别? |
[[ ]]是Bash扩展,支持模式匹配、正则、逻辑运算符,不需要引号 |
$* 和 $@ 的区别? |
双引号下:"$*"合并为一个字符串,"$@"保持为独立参数 |
| 如何防止重复执行? | 使用锁文件(flock或自定义PID文件) |
| 如何并行执行多个任务? | 后台进程 & + wait 等待完成 |
| 如何安全地处理文件名含空格? | 变量始终用双引号包裹,IFS=处理read |
| 如何获取脚本所在目录? | $(cd "$(dirname "$0")" && pwd) |
如果觉得本文对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬!你的支持是我持续更新的动力!