ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Shell日常巡检脚本大全:轻量级Linux健康检查实战

Shell日常巡检脚本大全:轻量级Linux健康检查实战

1. 项目概述:为什么一个“Shell日常巡检脚本大全”比你想象中更重要

我干系统运维和自动化开发这行十多年,从最早在机房里手动敲命令查服务器状态,到后来带团队做百台规模的集群监控,踩过的坑、写过的脚本、删过的bug,摞起来能当板凳坐。但直到今天,我依然会定期翻出自己压箱底的几个Shell巡检脚本——不是因为它们多炫酷,而是因为它们稳、快、轻、准,而且不需要装任何额外依赖,只要Linux内核还在跑,它就能执行。

“Shell日常巡检脚本大全”这个标题看着朴素,甚至有点老派,但它背后解决的是一个极其现实的问题:如何用最低成本、最短路径、最小侵入性,持续掌握生产环境的基础健康水位?它不替代Zabbix或Prometheus这类专业监控平台,但恰恰是这些平台失灵时的第一道防线——比如监控Agent挂了、网络分区了、磁盘突然被日志塞爆了,这时候你连Web界面都打不开,但只要还能SSH上去,一个50行的check_disk.sh就能告诉你问题出在哪。

这个“大全”不是堆砌代码,而是按真实运维节奏组织的:晨间快速过一遍(CPU/内存/磁盘/进程/服务端口),交接班前重点盯(日志异常关键词、关键进程存活、定时任务执行状态),深夜告警后秒级定位(最近10分钟的syslog突增、inode耗尽预警、NTP时间偏移)。它覆盖的不是“所有可能”,而是“80%高频故障场景下,你第一眼该看什么”。

关键词里没提具体工具名,但实际内容全部围绕bash+coreutils+procps+net-tools+sysstat这五类Linux发行版默认自带的命令展开——CentOS 7、Ubuntu 20.04、Debian 12、Alpine 3.18,全都能跑;不依赖Python环境,不调用外部API,不读取数据库,所有判断逻辑都在本地完成。新手照着抄就能用,老手拿来改两行参数就能嵌入自己的巡检流水线。它解决的不是“高大上”的架构问题,而是“服务器是不是还活着”“磁盘是不是快满了”“SSH端口是不是被谁悄悄关了”这种每天都要确认三遍的基础事实。

如果你是刚转行的运维新人,这个大全是你建立系统直觉的“肌肉记忆训练包”;如果你是开发同事,它能帮你快速理解线上服务的底层依赖;如果你是技术负责人,它就是你给团队定下的“基础健康检查SOP”——不求花哨,但求每次执行都有确定性输出。下面我就把这些年沉淀下来的、真正用在生产环境里的脚本逻辑、判断阈值、避坑细节,一条条拆给你看。

2. 巡检体系设计与脚本分层逻辑

2.1 为什么不用一个“万能脚本”包打天下?

我见过太多人写巡检脚本,上来就搞个all_in_one.sh,里面塞了300行,从CPU查到DNS解析,最后运行一次要等8秒,报错信息像天书。结果呢?没人愿意天天跑,出了问题翻出来一看,发现某段逻辑在CentOS上正常,在Ubuntu上因ps参数差异直接退出,而报错被重定向到/dev/null里——脚本看似“成功”,实则漏检。

所以我的设计原则很明确:按检查粒度分层、按执行频率分组、按风险等级分级。不是为了炫技,而是为了让每个脚本承担清晰、单一、可验证的职责。

  • L1 基础存活层(5秒内完成):只检查最核心的4项——系统是否响应(ping)、SSH是否可登录(ssh -o ConnectTimeout=3)、关键进程是否存在(pgrep sshd)、根分区是否可写(df / | awk 'NR==2 {print $5}' | sed 's/%//')。这是“服务器还活着吗?”的终极答案,必须快、必须稳、必须无依赖。
  • L2 健康水位层(15秒内完成):检查CPU负载(uptime | awk '{print $10}' | sed 's/,//')、内存使用率(free -m | awk 'NR==2{printf "%.0f", $3*100/$2}')、根分区使用率(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')、inode使用率(df -i / | awk 'NR==2 {print $5}' | sed 's/%//')。这里所有计算都用awk原生处理,避免调用bc或python -c引入兼容性风险。
  • L3 业务感知层(30秒内完成):检查指定端口监听状态(ss -tuln | grep ':8080')、关键服务进程数(pgrep -f 'java.*app.jar' | wc -l)、最近1小时错误日志行数(grep -i 'error\|exception\|fail' /var/log/app.log | awk -v d="$(date -d '1 hour ago' '+%b %d %H:%M')” '$0 > d' | wc -l)、crontab任务是否正常执行(检查/var/log/syslog中最近5分钟是否有对应任务记录)。这一层开始引入业务语义,但所有判断逻辑仍保持纯Shell,不写正则陷阱,不依赖systemctl(有些老系统没这命令)。
  • L4 深度诊断层(按需触发):如磁盘IO等待(iostat -dx 1 2 | awk '/sda/ {print $(NF-2)}' | tail -1)、网络连接数(ss -s | awk '/TCP:/ {print $4}' | cut -d',' -f1)、内核OOM事件(dmesg -T | grep -i 'killed process' | tail -1)。这类检查开销大、频率低,只在L2/L3告警后手动执行,不纳入日常自动巡检。

提示:所有脚本统一采用#!/bin/bash -e开头。“-e”参数是关键——一旦某条命令返回非0状态,脚本立即终止并报错。这比写一堆if [ $? -ne 0 ]; then exit 1; fi更干净,也避免了错误被静默吞掉。但要注意:grep查不到内容时返回1,这属于正常逻辑,所以必须用|| true显式忽略,例如grep 'nginx' /etc/passwd || true。

2.2 脚本命名与调用规范:让团队协作不踩坑

命名不是小事。我见过叫check.sh的脚本在10台服务器上版本各不相同,有人改了没同步,有人加了调试echo忘了删,结果交接班时A说“检查过了”,B一跑发现根本没报磁盘告警——因为A用的是旧版,没包含inode检查。

所以我的命名规则强制三点:

  1. 前缀统一:全部以chk_开头,一眼识别为巡检脚本;
  2. 主题明确:chk_disk.sh、chk_service.sh、chk_logerr.sh,拒绝chk_all.sh这种模糊命名;
  3. 版本标记:文件末尾加注释# v2.3.1 - 20240522,每次修改必须更新日期和小版本号。

调用方式也标准化:

  • 所有脚本接受-q(quiet)参数关闭详细输出,只返回状态码(0=正常,1=警告,2=严重);
  • 接受-w(warning)和-c(critical)参数自定义阈值,例如./chk_disk.sh -w 85 -c 95;
  • 输出格式固定为三段式:
    [INFO] Disk usage on /: 78% (warn:85%, crit:90%) [OK] Inode usage on /: 12% [WARN] /tmp usage: 92% (exceeds warning threshold 85%)
    这样运维同学扫一眼就知道哪行要处理,监控系统也能用grep '\[WARN\]\|\[CRIT\]'直接提取告警行。

注意:绝不允许脚本内部硬编码IP、路径、端口。所有可变参数必须通过命令行传入或读取配置文件。我们有个/etc/shell-check/conf.env,里面只放APP_LOG_PATH="/var/log/myapp"、MONITOR_PORT="8080"这类全局变量,脚本开头source /etc/shell-check/conf.env即可。这样换环境只需改配置,不用动代码。

2.3 阈值设定的实战经验:别信教科书,信你的日志

很多教程说“CPU负载>3就告警”,但我在某电商大促期间看到负载冲到12,服务依然平稳——因为那是16核机器,uptime显示的负载是平均值,真正要看的是load average / CPU cores。所以我的阈值全是动态计算的:

  • CPU负载:$(uptime | awk '{print $10}' | sed 's/,//')获取15分钟负载,再除以nproc结果,>0.7才标WARN;
  • 内存使用率:free -m里available字段才是真实可用内存(不是free字段!),计算公式是(total-available)/total*100,>85%告警;
  • 磁盘使用率:根分区>90%标CRIT,但/var/log分区>80%就WARN——因为日志增长最快,等它到90%可能已经滚不动了;
  • inode使用率:>95%必须CRIT,因为inode耗尽比磁盘满更致命——touch都失败,服务直接崩。

这些数字怎么来的?不是拍脑袋。我用sar -u 1 3600(每秒采样,持续1小时)在业务低峰期跑了三天,统计出各指标的P95值,再往上浮10%作为初始阈值。上线后每周用awk '{print $1,$NF}' /var/log/check-disk.log | sort -k2nr | head -5看TOP5峰值,动态调整。

实操心得:阈值宁可宽松,不要激进。我曾把磁盘告警设成85%,结果某次日志轮转失败,半夜连发27条告警邮件,值班同事直接关了邮箱。后来改成90%+连续3次超阈值才告警,并加入“静默期”逻辑:同一脚本1小时内重复告警只发第一次。这个补丁就加了4行date -d "1 hour ago" +%s和stat -c "%Y" /tmp/chk_disk.last对比,但体验提升巨大。

3. 核心脚本详解与实操实现

3.1chk_disk.sh:磁盘与inode双维度守护

这个脚本是我用得最勤的,没有之一。它不只看df -h,而是同时抓取空间和inode两个维度,因为很多故障是“磁盘还有20%空间,但inode已100%耗尽”,典型场景是Java应用疯狂生成临时文件却没清理。

#!/bin/bash -e # chk_disk.sh v3.2.0 - 20240522 # 默认阈值 WARN_DISK=85 CRIT_DISK=90 WARN_INODE=90 CRIT_INODE=95 # 解析参数 while [[ $# -gt 0 ]]; do case $1 in -w) WARN_DISK=$2 shift 2 ;; -c) CRIT_DISK=$2 shift 2 ;; -i) WARN_INODE=$2 shift 2 ;; -I) CRIT_INODE=$2 shift 2 ;; -q) QUIET=true shift ;; *) echo "Usage: $0 [-w warn_pct] [-c crit_pct] [-i inode_warn] [-I inode_crit] [-q]" exit 1 ;; esac done # 获取根分区信息(兼容不同df输出格式) ROOT_DISK=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//') ROOT_INODE=$(df -i / | awk 'NR==2 {print $5}' | sed 's/%//') # 检查根分区空间 if [[ $ROOT_DISK -ge $CRIT_DISK ]]; then echo "[CRIT] Disk usage on /: ${ROOT_DISK}% (warn:${WARN_DISK}%, crit:${CRIT_DISK}%)" exit 2 elif [[ $ROOT_DISK -ge $WARN_DISK ]]; then echo "[WARN] Disk usage on /: ${ROOT_DISK}% (warn:${WARN_DISK}%, crit:${CRIT_DISK}%)" exit 1 else [[ "$QUIET" != "true" ]] && echo "[OK] Disk usage on /: ${ROOT_DISK}%" fi # 检查根分区inode if [[ $ROOT_INODE -ge $CRIT_INODE ]]; then echo "[CRIT] Inode usage on /: ${ROOT_INODE}% (warn:${WARN_INODE}%, crit:${CRIT_INODE}%)" exit 2 elif [[ $ROOT_INODE -ge $WARN_INODE ]]; then echo "[WARN] Inode usage on /: ${ROOT_INODE}% (warn:${WARN_INODE}%, crit:${CRIT_INODE}%)" exit 1 else [[ "$QUIET" != "true" ]] && echo "[OK] Inode usage on /: ${ROOT_INODE}%" fi # 额外检查/tmp分区(常被忽略的重灾区) TMP_DISK=$(df -h /tmp 2>/dev/null | awk 'NR==2 {print $5}' | sed 's/%//' || echo "0") if [[ $TMP_DISK != "0" ]] && [[ $TMP_DISK -ge $WARN_DISK ]]; then echo "[WARN] /tmp usage: ${TMP_DISK}% (exceeds warning threshold ${WARN_DISK}%)" exit 1 fi exit 0

关键细节说明:

  • df -h /的输出在不同系统上列数可能不同(Ubuntu 20.04是6列,CentOS 7是5列),所以用NR==2精准取第二行,再用awk '{print $5}'取第五列,比awk '{print $NF}'更可靠;
  • /tmp分区检查加了2>/dev/null || echo "0",因为有些系统/tmp是tmpfs,df -h /tmp会报错,这时默认设为0,避免脚本中断;
  • 所有echo前加了[[ "$QUIET" != "true" ]] &&判断,确保-q参数生效;
  • 退出码严格分层:0=全OK,1=存在WARN,2=存在CRIT,监控系统可直接映射为不同级别告警。

实操心得:这个脚本上线后,我们发现某台DB服务器/var/lib/mysql分区inode使用率常年98%,排查发现是MySQL的innodb_file_per_table=OFF导致所有表数据存到一个ibdata1文件里,但ls -i显示该目录下有数万个.frm文件——其实是MySQL崩溃后残留的临时文件。脚本没直接修复问题,但它让这个问题从“偶尔卡顿”变成了“必须处理”的明确信号。

3.2chk_service.sh:端口+进程双保险验证

只检查systemctl is-active nginx是不够的。我遇到过Nginx进程在,但master进程卡死,worker全挂了,curl -I http://localhost直接超时。所以这个脚本必须同时验证:进程是否存在、端口是否监听、服务是否可响应。

#!/bin/bash -e # chk_service.sh v2.4.1 - 20240522 SERVICE_NAME="nginx" PORT="80" CHECK_URL="http://localhost:80/health" # 参数解析同上,略 # 检查进程是否存在(兼容不同ps输出) PID_COUNT=$(pgrep -f "$SERVICE_NAME" | wc -l 2>/dev/null || echo "0") if [[ $PID_COUNT -eq 0 ]]; then echo "[CRIT] Process '$SERVICE_NAME' not found" exit 2 fi # 检查端口监听(用ss,比netstat更轻量) LISTEN_COUNT=$(ss -tuln | grep ":$PORT " | wc -l) if [[ $LISTEN_COUNT -eq 0 ]]; then echo "[CRIT] Port $PORT not listening" exit 2 fi # 检查HTTP响应(超时3秒,只取HTTP状态码) HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$CHECK_URL" 2>/dev/null || echo "000") case $HTTP_CODE in 200|204) [[ "$QUIET" != "true" ]] && echo "[OK] Service '$SERVICE_NAME' responds with $HTTP_CODE" exit 0 ;; 000) echo "[CRIT] HTTP check failed for $CHECK_URL (timeout or connection refused)" exit 2 ;; *) echo "[WARN] HTTP check returned $HTTP_CODE for $CHECK_URL" exit 1 ;; esac

为什么用curl -w "%{http_code}"而不是curl -I?
-I只发HEAD请求,某些服务(如Spring Boot Actuator)对HEAD返回405,但GET是200。-w能拿到真实状态码,且--max-time 3确保不会卡住。

为什么pgrep -f比pidof更可靠?
pidof nginx在Ubuntu上可能返回空(因为进程名是nginx: master process),而pgrep -f "nginx"能匹配完整命令行。但要注意-f可能误匹配,所以SERVICE_NAME必须是精确关键词,比如查Java服务就用pgrep -f 'java.*app.jar',而不是宽泛的java。

注意:curl在极简系统(如Alpine)中可能未安装。我们的解决方案是:脚本开头加检测command -v curl >/dev/null 2>&1 || { echo "[WARN] curl not found, skipping HTTP check"; exit 0; },如果缺失就跳过HTTP检查,只做进程+端口验证。这样保证脚本在任何环境都能跑,只是能力降级,而非失败。

3.3chk_logerr.sh:从海量日志中揪出异常模式

日志巡检最容易陷入“grep全文”的误区。grep 'ERROR' /var/log/app.log会返回几千行,其中99%是“连接超时重试成功”,真正要关注的是“连续5次重试都失败”或“同一IP在1分钟内触发100次404”。所以这个脚本的核心是时间窗口过滤 + 频次统计 + 关键词分层。

#!/bin/bash -e # chk_logerr.sh v1.8.2 - 20240522 LOG_FILE="/var/log/app.log" TIME_WINDOW_MINUTES=10 CRIT_THRESHOLD=50 # 10分钟内错误行数阈值 # 计算时间窗口起始时间(精确到分钟) START_TIME=$(date -d "$TIME_WINDOW_MINUTES minutes ago" '+%b %d %H:%M') # 定义错误关键词(按严重程度分组) FATAL_PATTERNS=("OutOfMemoryError" "Segmentation fault" "killed process") ERROR_PATTERNS=("ERROR" "Exception" "failed" "timeout" "refused") WARN_PATTERNS=("WARN" "warning" "retry") # 统计FATAL级别(最高优先级) FATAL_COUNT=0 for pat in "${FATAL_PATTERNS[@]}"; do COUNT=$(grep -i "$pat" "$LOG_FILE" | awk -v d="$START_TIME" '$0 > d {print}' | wc -l 2>/dev/null || echo "0") FATAL_COUNT=$((FATAL_COUNT + COUNT)) done if [[ $FATAL_COUNT -gt 0 ]]; then echo "[CRIT] Found $FATAL_COUNT FATAL events in last $TIME_WINDOW_MINUTES minutes" exit 2 fi # 统计ERROR级别 ERROR_COUNT=0 for pat in "${ERROR_PATTERNS[@]}"; do COUNT=$(grep -i "$pat" "$LOG_FILE" | awk -v d="$START_TIME" '$0 > d {print}' | wc -l 2>/dev/null || echo "0") ERROR_COUNT=$((ERROR_COUNT + COUNT)) done if [[ $ERROR_COUNT -ge $CRIT_THRESHOLD ]]; then echo "[CRIT] $ERROR_COUNT ERROR-level events in last $TIME_WINDOW_MINUTES minutes (threshold: $CRIT_THRESHOLD)" exit 2 elif [[ $ERROR_COUNT -ge $((CRIT_THRESHOLD / 2)) ]]; then echo "[WARN] $ERROR_COUNT ERROR-level events in last $TIME_WINDOW_MINUTES minutes" exit 1 else [[ "$QUIET" != "true" ]] && echo "[OK] $ERROR_COUNT ERROR-level events in last $TIME_WINDOW_MINUTES minutes" fi

关键技巧:

  • awk -v d="$START_TIME" '$0 > d {print}'是精髓。日志时间格式如May 22 14:30:22,$0 > d能直接字符串比较,因为May 22 14:30:22字典序天然支持时间排序,比用date -d逐行转换快10倍;
  • FATAL_PATTERNS单独处理,因为OOM或进程被杀是立即需要人工介入的,不参与频次统计,发现即CRIT;
  • ERROR_PATTERNS里"timeout"和"refused"是网络层关键词,"failed"是业务层,混在一起统计更反映整体稳定性。

实操心得:某次凌晨告警,脚本报[CRIT] 62 ERROR-level events,我直接grep -i 'timeout' /var/log/app.log | tail -20,发现全是Connection refused to redis:6379——但Redis进程明明在。继续查ss -tuln | grep 6379,发现端口没监听!原来Redis配置文件里bind 127.0.0.1被误删成了bind 127.0.0.2,重启后绑定失败。脚本没告诉你配置错了,但它用62次超时把问题推到了你面前。

4. 自动化集成与生产部署实践

4.1 如何让脚本真正“日常”起来?cron不是唯一答案

很多人把脚本丢进crontab -e就以为万事大吉,结果发现:

  • crontab默认PATH不包含/usr/local/bin,脚本里调用的自定义命令找不到;
  • cron执行时HOME是root,~/.bashrc不加载,环境变量丢失;
  • 没有执行日志,脚本失败了也不知道;
  • 多个脚本并发执行,可能争抢磁盘IO,影响业务。

所以我们用三层机制保障:

第一层:cron基础调度

# /etc/cron.d/shell-check # 每5分钟执行基础巡检(L1+L2) */5 * * * * root PATH=/usr/local/bin:/usr/bin:/bin /usr/local/bin/chk_disk.sh -q >> /var/log/shell-check/disk.log 2>&1 */5 * * * * root PATH=/usr/local/bin:/usr/bin:/bin /usr/local/bin/chk_service.sh -q >> /var/log/shell-check/service.log 2>&1 # 每30分钟执行深度巡检(L3) */30 * * * * root PATH=/usr/local/bin:/usr/bin:/bin /usr/local/bin/chk_logerr.sh -q >> /var/log/shell-check/logerr.log 2>&1

关键点:

  • 显式声明PATH,避免命令找不到;
  • 日志重定向用>>追加,不覆盖,方便追溯;
  • 2>&1把错误也记入日志,否则df报错你永远看不到。

第二层:串行化执行锁
防止多个脚本同时df导致IO飙升,我们用flock加锁:

# 在crontab里这样调用 */5 * * * * root flock -n /tmp/shell-check.lock -c 'PATH=/usr/local/bin:/usr/bin:/bin /usr/local/bin/chk_disk.sh -q >> /var/log/shell-check/disk.log 2>&1'

flock -n是非阻塞模式,如果锁被占用,当前执行直接退出,不排队——宁可漏一次检查,也不让脚本堆积。

第三层:告警聚合与通知
所有脚本输出到/var/log/shell-check/后,我们用一个alert_aggregator.sh每分钟扫描:

#!/bin/bash # 扫描最近1分钟日志,提取WARN/CRIT行 LOG_DIR="/var/log/shell-check" ALERT_FILE="/tmp/shell-alert.tmp" # 清空临时文件 > "$ALERT_FILE" # 搜索所有日志中1分钟内的告警 find "$LOG_DIR" -name "*.log" -mmin -1 -exec grep -H "\[WARN\]\|\[CRIT\]" {} \; >> "$ALERT_FILE" 2>/dev/null if [[ -s "$ALERT_FILE" ]]; then # 发送企业微信机器人(用curl POST) curl -s -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" \ -H 'Content-Type: application/json' \ -d "{\"msgtype\": \"text\", \"text\": {\"content\": \"【Shell巡检告警】\n$(cat $ALERT_FILE | head -10 | sed 's/^.*: //')\"}}" > /dev/null fi

这样就把分散的脚本输出,聚合成一条可读性高的告警消息,避免刷屏。

4.2 权限与安全:脚本不是root万能,而是最小权限够用

所有脚本默认以root运行,但这不意味着它们需要root权限才能工作。比如chk_disk.sh只需要读/proc/mounts和执行df,普通用户也能做。所以我们的安全策略是:

  • 脚本文件权限:chmod 750 /usr/local/bin/chk_*.sh,属组为shell-check,只有运维组成员可读可执行;
  • 日志目录权限:chown root:shell-check /var/log/shell-check && chmod 750 /var/log/shell-check,确保日志不被篡改;
  • 敏感操作隔离:涉及reboot、kill -9的脚本,必须加-f强制参数,且默认禁用。例如chk_reboot.sh只有./chk_reboot.sh -f now才执行,否则只输出[INFO] Reboot would be triggered with -f flag;
  • 配置文件保护:/etc/shell-check/conf.env设为640,只允许root和shell-check组读取,防止API Key等密钥泄露。

提示:在审计时,auditctl -w /usr/local/bin/chk_*.sh -p wa -k shell-check可以监控脚本是否被篡改。但更实用的是每天凌晨用sha256sum /usr/local/bin/chk_*.sh > /etc/shell-check/sha256.sum,然后sha256sum -c /etc/shell-check/sha256.sum校验——一行命令,防君子也防小人。

4.3 版本管理与灰度发布:让变更不再惊心动魄

脚本也是代码,必须版本管理。我们不用Git裸库,而是用最简单的rsync + 时间戳:

# 发布新版本前 tar -czf /backup/shell-check-$(date +%Y%m%d-%H%M%S).tar.gz /usr/local/bin/chk_*.sh /etc/shell-check/ # 灰度发布:先推送到5%服务器 ansible webservers -m copy -a "src=chk_disk.sh dest=/usr/local/bin/chk_disk.sh mode=0750" --limit 5% # 观察2小时日志,无异常再全量 ansible webservers -m copy -a "src=chk_disk.sh dest=/usr/local/bin/chk_disk.sh mode=0750"

所有脚本第一行都加# v3.2.0 - 20240522,grep '# v' /usr/local/bin/chk_disk.sh就能知道线上版本。

实操心得:某次升级chk_service.sh,新加了HTTP健康检查,结果在某台老服务器上curl版本太低,不支持--max-time,脚本直接报错退出。幸好我们灰度了,只影响2台测试机,回滚就是rsync -av /backup/shell-check-20240521.tar.gz .解压覆盖。如果全量发布,就得挨个服务器手动修复,那晚谁都别想睡觉。

5. 常见问题与排障速查表

5.1 脚本执行报错:“command not found”

现象:./chk_disk.sh: line 15: df: command not found
原因:df命令不在PATH中,常见于最小化安装的Alpine或CentOS Stream。
排查:

# 查看当前PATH echo $PATH # 检查df位置 which df || find /usr -name df 2>/dev/null

解决:

  • Alpine系统:apk add util-linux;
  • CentOS Stream:dnf install coreutils;
  • 通用方案:在脚本开头加export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"。

注意:不要用#!/usr/bin/env bash,因为env路径也可能不存在。直接写#!/bin/bash最稳妥。

5.2df输出列数不一致,脚本取错值

现象:chk_disk.sh在Ubuntu上显示磁盘使用率120%,明显错误。
原因:df -h /在Ubuntu 22.04输出6列(含Use%列),在CentOS 7输出5列(Use%是第5列),但某些定制镜像把Use%列移到了第6列。
排查:

df -h / | cat -n # 查看每列编号 df -h / | awk 'NR==2 {for(i=1;i<=NF;i++) print i,$i}' # 打印列号和值

解决:
改用df -P(POSIX格式,列数固定):

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

-P参数强制输出标准格式,Use%永远是第5列,跨平台100%兼容。

5.3grep在日志中匹配不到中文,但肉眼可见

现象:chk_logerr.sh对ERROR能告警,但对错误不告警。
原因:grep默认locale是C,不支持UTF-8中文匹配。
排查:

locale # 查看当前locale echo "错误" | grep -o "错误" # 测试是否匹配

解决:
在脚本开头加:

export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8

或者直接用grep -P(Perl正则,对UTF-8更友好):

grep -P "错误|Exception" "$LOG_FILE"

5.4 脚本在crontab中执行正常,但手动执行报错

现象:./chk_disk.sh手动运行OK,但crontab里执行时报[CRIT] Disk usage on /: (warn:85%, crit:90%),括号里数值为空。
原因:crontab环境变量极少,awk的NR==2可能因df输出头信息变化而失效。
排查:

# 模拟cron环境执行 env -i PATH=/usr/bin:/bin /usr/local/bin/chk_disk.sh

解决:

  • 改用df -P确保格式稳定;
  • 或在脚本中加健壮性判断:
    ROOT_DISK=$(df -P / | awk 'NR==2 {if($5 ~ /%/) print $5; else print "0%"}' | sed 's/%//') [[ -z "$ROOT_DISK" || "$ROOT_DISK" = "0" ]] && ROOT_DISK="0"

5.5 多个脚本同时执行,服务器负载飙升

现象:top看到df、ss、grep进程CPU占满,业务响应变慢。
原因:5个脚本每5分钟同时触发,全在扫磁盘和网络。
解决:

  • 错峰执行:在crontab中加随机秒数延迟
    */5 * * * * root sleep $((RANDOM%60)); /usr/local/bin/chk_disk.sh -q >> /var/log/shell-check/disk.log 2>&1
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进