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

#! /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 里代码的执行顺序是固定的:
BEGIN { ... }:读取任何输入之前最先执行一次,通常用来初始化变量、打印表头。它不会等输入,像上面获取负载那段没有输入数据,也是靠 BEGIN 跑的{ ... }:每读一行输入就执行一次END { ... }:所有输入都读完之后,最后执行一次
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需要在统计完所有数据后执行一次,而不是一直执行
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 里几个编辑器的区别:

时间块
前面已经讲过五个字段的排布了,这里再放一遍方便对照:
* * * * * 命令
│ │ │ │ │
│ │ │ │ └── 星期几(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 就不会出错,路径、环境变量、权限这些都得自己盯。
完!