首页  /  Linux 进阶  /  正文

Shell 运维脚本实战之巡检与菜单集成

Linux 进阶 2026-10-02📖 19 分钟👁 —
🐚 Linux 进阶 · Shell 编程第 10 / 10 页12345678910📖 完整导航 →

一、系统巡检脚本

前面写的都是单点监控,缺一个整体的健康报告,巡检脚本就是干这个的:

系统巡检脚本和服务监控脚本的区别
#! /bin/bash

echo -e  "\n=========获取磁盘使用状态=========\n"
df -h|awk '{print $1, $5}'|grep '^/dev/'|tr -d '%'|while read disk disk_used; do
  if [ $disk_used -gt 80 ]; then
    echo -e "磁盘$disk的容量告警:$disk_used%\n"
  else
    echo -e "磁盘$disk的容量:$disk_used%\n"
  fi
done

sleep 1

echo -e "=========获取服务器内存状态=========\n"
free -m | awk 'NR==2 {total=$2; available=$NF;
mem_usage=(total - available) / total
if (mem_usage > 0.8) {
  printf "服务器内存不足(超过0.8),使用占比:%.2f\n", mem_usage
} else {
  printf "服务器内存正常(未超过0.8),使用占比:%.2f\n", mem_usage
}

}'

sleep 1

echo -e "=========获取服务器服务状态=========\n"
# 服务列表
SERVICES="nginx ufw ssh mysql"
# 有空格,变量括起来
for service_name in $SERVICES; do

SERVICE_STATUS=$(systemctl is-active "$service_name")

if [ "$SERVICE_STATUS" = "active" ]; then
        echo -e "$service_name正常运行\n"
        sleep 1
else
        echo -e "$service_name未启动\n"
        sleep 1

        RETURN=$(systemctl restart "$service_name" 2>&1)
        
        if [ $? -ne "0" ]; then
                echo -e "$RETURN\n"
        else
                echo -e "服务重启成功\n"
        fi
fi
done

sleep 1

echo -e  "=========获取系统负载状态=========\n"
echo -e "本服务器为2h,4g\n"

# uptime 负载分别是1、5、15min的,涉及到小数用awk比较

# 获取1min的负载
one_min_load=$(uptime |awk '{print $10}'|tr -d ',')

# 需要给awk传入参数
awk -v one_load="$one_min_load" ' BEGIN {
if (one_load > 2.00) {
printf "系统负载异常,当前负载%.2f\n", one_load
} else {
printf "系统负载正常,当前负载%.2f\n", one_load
}
}'

sleep 1

SER_NAME=$(uname -n)
echo -e "=========获取服务器$SER_NAME的用户=========\n"

 # 无人登录输出为空的情况(防止卡住)
if [ -z "$(who)" ]; then
  echo "无人"
else  
  who| awk '{print $1}'| while read USERS; do 
    echo -e "当前用户: $USERS\n"
  done
fi

写这个脚本的时候碰到两个语言混用的问题,记一下:

一是变量计算。shell 里 MEM_REM=mem_used/total 只是字符串赋值,不会真的做除法,而且子 shell 里算出来的变量退出来就没了。正确思路是直接在 awk 里算好再输出,awk 里多条语句用分号 ; 隔开。

二是列表的 for 循环,一定要区分 awk 的语法和 shell 的语法,awk 是 C 的风格,和 shell 差别很大。变量里有空格要用 "" 包起来,但变量作为列表的时候不能用 "",那样整个列表会被当成一个变量传进去。

awk 里代码的执行顺序是固定的:

awk 里没法直接使用 shell 变量,这是两种语言,底层虽然都是 C 但语法风格不一样,所以做了区分。跨语种传数据用 -v,写法是 -v awk变量="$变量"。


二、端口存活检测

核心就一条 ss -tunlp 加 grep,判断命令成没成功看 $?:

端口检测用到的几条核心命令
#! /bin/bash

read -p "传入多个端口,空格分隔" PORT
# 列表不加引号
for port in $PORT; do
service=$(lsof -i :$port | awk 'NR==2 {print $1}')
# 获取所指定端口的状态码
# 精确匹配端口加空格(:80 /:8080 )
if ss -tunlp | grep -q ":$port "; then
  echo "端口$port监听中,启动服务:$service"
else
  echo "端口$port未监听"
fi

done


root@VM-0-12-ubuntu:~/scripts# bash port_check.sh 
传入多个端口,空格分隔80 443 3306 2222 777 88 8080    
端口80监听中,启动服务:nginx
端口443未监听
端口3306监听中,启动服务:mysqld
端口2222监听中,启动服务:sshd
端口777未监听
端口88未监听
端口8080监听中,启动服务:java

不要试图用 $(... | echo $?) 去获取管道里某个命令的退出码,管道的退出码永远属于最后一个命令,而不是你想要的那个:

# 第一步:先跑一个一定能成功的命令,让 $? 变成 0
true

# 第二步:故意搜一个不存在的词 "notfound"
status_code=$(echo "hello world" | grep -q "notfound" | echo $?)

# 第三步:看结果
echo "最终结果:$status_code"

root@VM-0-12-ubuntu:~/scripts# true
root@VM-0-12-ubuntu:~/scripts# status_code=$(echo "hello world" | grep -q "notfound" | echo $?)
root@VM-0-12-ubuntu:~/scripts# echo "最终结果:$status_code"
最终结果:0

明明 grep 什么都没搜到,最后拿到的却是 0,就是因为 $? 取的是管道最后一个 echo 的返回值。


三、综合日志分析

把 nginx 日志玩明白,基本就这几件事:

日志分析要统计的五项内容

nginx 日志格式

Nginx 默认(Combined)格式,每一列是什么得先搞清楚:

192.168.1.100 - - [10/Oct/2023:13:55:36 +0000] "GET /index.html HTTP/1.1" 200 2326
│             │ │ │                            │    │              │      │    │
$1            $2$3$4                           $5   $6             $7     $8   $9
# nginx的访问日志很多,下面只是实验环境的数据,之前删了很多日志轮转文件
root@VM-0-12-ubuntu:/var/log/nginx# cat access.log|wc -l
434

脚本:

#! /bin/bash

echo "不要在当前shell执行脚本,可能弹出ssh\n"

echo -e "\n================================================\n"
LOG_DIR="/var/log/nginx"
LOG_FILE="$LOG_DIR/access.log"

echo -e "============日志文件校验============\n"

if [ ! -d "$LOG_DIR" ]; then
    echo "错误:目录 $LOG_DIR 不存在"
    exit 1
fi

if [ ! -f "$LOG_FILE" ]; then
    echo "错误:日志文件 $LOG_FILE 不存在"
    exit 1
fi

if [ ! -r "$LOG_FILE" ]; then
    echo "错误:没有权限读取 $LOG_FILE"
    exit 1
fi

if [ ! -s "$LOG_FILE" ]; then
    echo "错误:日志文件 $LOG_FILE 是空的"
    exit 1
fi

FILE_SIZE=$(du -sh "$LOG_FILE" | awk '{print $1}')
echo "日志文件大小:$FILE_SIZE\n"
echo "校验通过\n"


echo -e "============统计日志总条数============\n"

# 如果wc -l太慢可以换成awk统计
TOTAL_REQUEST=$(cat $LOG_FILE|awk 'END {print NR}')
echo -e "访问日志请求总数:"$TOTAL_REQUEST"\n"


echo -e "============请求最多前十个IP============\n"


cat $LOG_FILE | awk '{count[$1]++} 
END {for(req_ip in count) 
print count[req_ip],req_ip}'|sort -k1 -nr | head -10


echo -e "============请求最多前十个路径============\n"

cat $LOG_FILE | awk '{count[$7]++} 
END {for(url in count) 
print url,count[url]}'|sort -k2 -nr | head -10


echo -e "============状态码次数统计============\n"

cat $LOG_FILE | awk '{print $9}' | sort | uniq -c | sort -nr

变量命名上有个习惯:环境变量用大写,临时变量用小写,一眼能区分开。

awk 的关键语法

两条完整的语句写在同一行时,必须用 ; 分隔;一个 {} 里写两段代码就用 ;,分两行写就不用:

awk 里 BEGIN、{}、END 各自的执行时机
# 计算总行数时,awk需要在统计完所有数据后执行一次,而不是一直执行
aaa
bbb
ccc
# 不加END
awk '{print NR}'
# 输出
1
2
3

# 加了之后输出
3

统计状态码

状态码各自代表什么、运维要重点关注哪些,这张表挺全的:

常见状态码的含义和运维关注度

关于日志文件校验

分析之前先确认文件是不是正常,能不能拿来统计数据:

校验日志文件的四个检查项

上面是查哪几项,下面是查的时候要用的判断符,别搞混了:

常用的文件判断符

效果概览

root@VM-0-12-ubuntu:~/scripts# bash log_analysis.sh 
不要在当前shell执行脚本,可能弹出ssh\n

================================================

============日志文件校验============

日志文件大小:80K\n
校验通过\n
============统计日志总条数============

访问日志请求总数:443

============请求最多前十个IP============

49 20.245.121.3
49 103.46.186.85
12 45.91.64.8
11 156.225.1.106
7 180.101.245.253
6 94.183.174.95
6 66.132.172.143
6 66.132.172.107
6 180.101.244.14
6 16.5.0.236
============请求最多前十个路径============

/ 158
/wp-admin/install.php?step=1 30
400 26
/favicon.ico 13
/SDK/webLanguage 8
/robots.txt 8
/admin/config.php 6
/taffy.png 5
/mudan.png 5
/leimi.png 5
============状态码次数统计============

    205 404
    180 200
     26 "-"
     15 166
      8 499
      7 400
      2 405

四、把九个脚本攒成一个菜单

前面写的 9 个脚本,做成一个集成工具放一起,主要用到 case 语法:

#! /bin/bash

while true; do
clear
SCRIPTS_DIR=/root/scripts
echo -e "=====================================================
         1--获取目录文件数量最多的十个目录\n
         2--获取Nginx日志访问次数最多的前十个IP\n
         3--日志自动清理(日志轮转文件)\n
         4--磁盘监控\n
         5--服务存活检测和自动重启\n
         6--文件备份脚本\n
         7--系统巡检脚本\n
         8--端口存活检测\n
         9--Nginx综合日志分析\n
===================================================="

read -p "输入脚本编号(1--9)" SCRIPTS

case "$SCRIPTS" in
  1) # 这里有些脚本没有使用read传参,需要改成交互,当然这并不代表read就一定好
    read -p "输入要统计的目录的绝对路径(/root/scripts)" dir
    bash "$SCRIPTS_DIR"/file_cnt.sh "$dir"
    ;;
  2)
    bash "$SCRIPTS_DIR"/nginx_ip.sh
    ;;
  3)
    bash "$SCRIPTS_DIR"/clear_var_log.sh
    ;;
  4)
    bash "$SCRIPTS_DIR"/dev_monitor.sh
    ;;
  5)
    read -p "输入服务(一个)" service
    bash "$SCRIPTS_DIR"/service_status.sh "$service"
    ;;
  6)
    bash "$SCRIPTS_DIR"/bak_file.sh
    ;;
  7)
    bash "$SCRIPTS_DIR"/sys_inspect.sh
    ;;
  8)
    bash "$SCRIPTS_DIR"/port_check.sh
    ;;
  9)
    bash "$SCRIPTS_DIR"/log_analysis.sh
    ;;
  *)
    echo -e "输入错误\n"
    exit 1
    ;;
esac
  read -p "===按回车返回菜单==="
done

case 的语法就长这样,;; 相当于别的语言里的 break,*) 是兜底的默认分支:

case "$变量" in
    值1)
        命令1
        ;;
    值2)
        命令2
        ;;
    *)
        默认命令
        ;;
esac

再想提升 shell 水平,就可以去翻大佬写的脚本了。


五、crontab 定时任务

crontab 这个名字拆开看就懂了:cron 是一个后台守护进程,table 代表清单,合起来就是定时任务的清单。

crontab -e    # 编辑定时任务
crontab -l    # 查看当前定时任务
crontab -r    # 删除所有定时任务(慎用)

root@VM-0-12-ubuntu:~# ps -ef|grep cron
# 系统的
root         788       1  0 May15 ?        00:01:05 /usr/sbin/cron -f -P
# grep产生的
root      245584  245538  0 21:43 pts/0    00:00:00 grep --color=auto cron

# 切换crontab编辑器
root@VM-0-12-ubuntu:~# select-editor

Select an editor.  To change later, run 'select-editor'.
  1. /bin/nano        <---- easiest
  2. /usr/bin/vim.basic
  3. /usr/bin/vim.tiny
  4. /bin/ed

Choose 1-4 [1]: # 这里输入数字即可

select-editor 里几个编辑器的区别:

select-editor 可选编辑器各自的特点

时间块

前面已经讲过五个字段的排布了,这里再放一遍方便对照:

* * * * * 命令
│ │ │ │ │
│ │ │ │ └── 星期几(0-7,0 和 7 都代表周日)
│ │ │ └──── 月份(1-12)
│ │ └────── 日(1-31)
│ └──────── 小时(0-23)
└────────── 分钟(0-59)
# 分时日月周

分钟位的范围是 0~59。*/5 * * * * [命令] 是每隔五分钟执行,不加 /5 就是每分钟执行,/ 放在其余时间位也是同样的意思。

crontab -e

*/5 * * * * /bin/bash /root/scripts/dev_monitor.sh >> /var/log/disk_monitor.log 2>&1
# 每五分钟检查磁盘追加输出到该日志,并获取crontab的错误信息

脚本本身写得对,不代表 crontab 就不会出错,路径、环境变量、权限这些都得自己盯。

完!