首页 / 资讯中心 / 文章详情

Bash脚本实现Linux服务器日常巡检与自动化报告

Bash脚本实现Linux服务器日常巡检与自动化报告 ★ FEATURED ARTICLE
简介面向Linux服务器运维与系统管理员这份实操文档以完整Shell脚本实现日常自动化巡检覆盖磁盘、内存、CPU、进程、文件更改、用户登录等关键维度并利用ifconfig、free、cat、ps、find、last等系统命令采集状态经awk解析后生成结构化巡检报告再通过mail自动发送至指定邮箱同时支持日志记录与环境变量加载。脚本结构清晰内置环境变量加载、日志目录自动创建等细节可直接复制部署或按需扩展降低日常巡检门槛。资源包为单个doc文档体积仅118KB内容包含脚本源码、注释与调用思路便于理解脚本逻辑并快速接入现有运维体系。已有1375人学习下载适合希望降低手工检查负担、提升异常发现与排查效率的初中级运维人员参考使用尤其适合纳入每日定时巡检场景。1. 为什么要用脚本接管 Linux 服务器日常巡检一台台敲命令太浪费人了我最早接触 Linux 服务器日常巡检脚本是在帮某公司看一批线上机器的时候。十几台服务器磁盘、内存、CPU、进程、登录记录每周人工过一遍光收集信息就要一上午更别提还要整理成报告发出来。后来拿到这份巡检脚本才意识到这些重复劳动完全可以用一个 bash 脚本标准化掉它把磁盘、内存、CPU、进程、文件更改、用户登录这些检查项全部收敛成一份文本报告落到 log 目录顺手还能通过 mail 发到指定邮箱。对系统管理员和运维来说这就是把「每周手工检查」变成「每天早上看一封邮件」的典型工具也是入行时最值得先改造的一类 Linux 脚本。适合谁刚接手服务器巡检、想快速建立检查清单的新手以及嫌手动命令太碎的熟手。它不解决监控告警那种实时问题解决的是「定期要交一份巡检记录」这种周而复始的活儿。下面我按自己拆脚本的习惯把结构、模块、坑和落地方式逐层说清楚。2. 拆脚本结构函数组织、全局变量与报表变量的定义方式2.1 先看懂脚本骨架从 shebang 到主流程拿到一份 bash 脚本我习惯先不看具体函数先看它定义了什么全局变量、声明了哪些函数、最后怎么调用。这份脚本的结构很清晰开头定义 IP 获取、环境变量、版本号和日志路径中间是一批 getXxxStatus 函数各自负责一类巡检项结尾应该有调用入口。常见做法是在文件末尾加一段顺序调用把所有函数串起来再统一生成报告类似下面这样# 定义完所有函数后按检查顺序调用 getSystemStatus getCpuStatus getMemStatus getDiskStatus getNetworkStatus getListenStatus getServiceStatus getProcessStatus getUserStatus getPasswordStatus getSudoersStatus getLoginStatus getCronStatus getFirewallStatus getJDKStatus getSyslogStatus getInstalledStatus # 生成报告文件 { echo 巡检时间: $report_DateTime echo 主机名: $report_Hostname echo IP 地址: $report_IP echo 内存使用率: $report_MemUsedPercent echo 磁盘使用率: $report_DiskUsedPercent echo 僵尸进程数: $report_DefunctProsess } $RESULTFILE # 邮件发送 mail -s Host Daily Check Report your_emailexample.com $RESULTFILE逻辑说明脚本把每个巡检项封装成独立函数最后统一收集全局变量生成报告。这样做的好处是新增一个检查项时不用动其他函数只要加一个函数、在调用区加一行、在报告输出区加一个变量即可。参数说明RESULTFILE是报告文件路径模板里包含 IP 和日期避免多台机器同一天的报告互相覆盖。PROGPATH通过解析$0拿到脚本所在目录这是 bash 脚本里比较常见的自定位方式好处是用绝对路径执行脚本时log 目录依然能落在脚本旁边不会跟着当前工作目录跑偏。2.2 报表变量的初始化一份报告长什么样的关键脚本里定义了一批report_开头的全局变量从report_DateTime到report_JDK共二十多个。这些变量是所有巡检函数共享的输出通道。每个采集函数在自己的逻辑里把数据填进对应变量最后主流程统一写入报告文件。这里有个细节值得注意变量名的设计基本就是报告模板的字段名。比如report_MemUsedPercent存内存使用率report_DiskUsedPercent存磁盘使用率report_DefunctProsess存僵尸进程数量。这种命名方式对后续扩展非常友好你加一个新的巡检项时先想好报告里要展示什么字段然后定义同名变量让采集函数只负责赋值报告部分只负责打印职责是分开的。# 报告文件内容示例巡检日期和核心指标 echo 主机基本信息 echo 报告时间: 2024-06-01 08:30:22 echo 主机名: node-01 echo IP: 192.168.1.101 echo 资源使用情况 echo 内存使用率: 67.25% echo 磁盘使用率: 82.14% echo Inode 使用率: 3.82% echo 僵尸进程数量: 0逻辑说明先填充结构化信息再填资源指标这是巡检报告的基本排版逻辑。如果你希望报告更易读可以在这段基础上加一行分隔线或者按模块分区打印。参数说明report_MemUsedPercent这类变量在赋值时已经带了%符号打印时不需要再拼字符串。这个设计细节能避免在输出阶段到处补单位也让报告里的数据看起来更整齐。我在自己改脚本时通常会保留这个风格所有涉及百分比的变量在源头就完成格式化。2.3 log 目录与执行权限为什么脚本一定要能跨目录执行脚本里对PROGPATH做了处理先去掉脚本名如果处理完发现$PROGPATH还是文件就置为当前目录.然后创建$LOGPATH/log。这保证了你不管在哪个目录执行脚本日志都稳定落在脚本旁边的 log 目录。# 脚本自定位到所在目录并创建 log 目录 PROGPATHecho $0 | sed -e s,[\\/][^\\/][^\\/]*$,, [ -f $PROGPATH ] PROGPATH. LOGPATH$PROGPATH/log [ -e $LOGPATH ] || mkdir $LOGPATH RESULTFILE$LOGPATH/HostDailyCheck-$IPADDR-date %Y%m%d.txt逻辑说明sed在这里的作用是去掉最后一个斜杠及后面的脚本名拿到纯目录路径。[ -f $PROGPATH ]判断的是如果$0只包含脚本名而没有路径前缀sed 处理后会剩下空字符串或原字符串这时把目录改为当前目录。参数说明RESULTFILE文件名里的$IPADDR来自ifconfig eth0解析适用于 eth0 作为主网卡的场景。如果你的机器网卡命名是 ens33、enp0s3这里解析不到 IP文件名会变成HostDailyCheck--20240601.txt。我在后面避坑章会专门讲这个。3. 核心巡检模块逐个拆磁盘、内存、CPU 与进程的采集逻辑3.1 磁盘巡检df 与 inode 的两次快照合并磁盘检查的函数是整份脚本里信息量最大的部分。它先跑两次df一次查磁盘空间df -hTP一次查 inodedf -hiP然后用join命令把两份结果按文件系统挂载点合并输出成易读的表格。# 磁盘空间与 inode 信息合并展示 df -hiP | sed s/Mounted on/Mounted/ /tmp/inode df -hTP | sed s/Mounted on/Mounted/ /tmp/disk join /tmp/disk /tmp/inode | awk {print $1,$2,|,$3,$4,$5,$6,|,$8,$9,$10,$11,|,$12} | column -t逻辑说明这里的核心是字段对齐。df -hTP输出包含文件系统、类型、总大小、已用、可用、挂载点df -hiP输出包含 inode 总量、已用、剩余、挂载点。直接 cat 两份文件对不齐所以用join以挂载点为连接键再通过awk重排字段顺序。参数说明-h表示人类可读-T显示文件系统类型-P使用 POSIX 输出格式避免换行错位-i显示 inode 信息。sed s/Mounted on/Mounted/是为了让两个文件的表头一致join 才不会因为表头字符串不同而报错。再往后是磁盘使用率的统计逻辑它用df -TP排除 tmpfs 后累加总量和已用量# 汇总磁盘总量、已用、剩余和使用率 diskdata$(df -TP | sed 1d | awk $2!tmpfs{print}) disktotal$(echo $diskdata | awk {total$3}END{print total}) diskused$(echo $diskdata | awk {total$4}END{print total}) diskfree$((disktotal-diskused)) diskusedpercent$(echo $disktotal $diskused | awk {if($10){printf 100}else{printf %.2f,$2*100/$1}})逻辑说明先过滤掉表头和 tmpfs再分别对第 3 列总块数和第 4 列已用块数做累加。磁盘总使用率 已用 / 总量这样算出来的百分比是整机视角而不是某个分区的使用率。参数说明这里有个容易被忽略的点df -TP的数值列默认单位是 KB脚本最后除以 1024 再除以 1024 转成 GB。如果你拿到脚本后发现报告里磁盘总量偏小先检查是不是有分区挂载在 tmpfs 之外的虚拟文件系统上被awk $2!tmpfs过滤掉了比如 overlay、squashfs。3.2 内存巡检free 命令的版本兼容处理内存检查函数里有一个典型的版本分支代码对 CentOS 6 和 CentOS 7 用了不同的free参数# CentOS 6 使用 -moCentOS 7 使用 -h if [[ $centosVersion 7 ]]; then free -mo else free -h fi逻辑说明CentOS 6 的 free 没有-h人类可读选项-m以 MB 为单位展示-o关闭缓冲区显示。CentOS 7 开始-h成为标配直接用人类可读格式。这个分支思路值得保留因为现在还有不少存量机器停留在 CentOS 6/7脚本要在不同主版本上跑最稳妥的方式就是先探测系统版本再决定命令参数。内存使用率计算则统一走/proc/meminfo跟 free 的显示层解耦# 从 /proc/meminfo 原始数据计算使用率 MemTotal$(grep MemTotal /proc/meminfo | awk {print $2}) # KB MemFree$(grep MemFree /proc/meminfo | awk {print $2}) # KB let MemUsedMemTotal-MemFree MemPercent$(awk BEGIN{if($MemTotal0){printf 100}else{printf \%.2f\,$MemUsed*100/$MemTotal}})逻辑说明/proc/meminfo的前两列永远是MemTotal和MemFree用awk {print $2}取的就是数值部分。let MemUsedMemTotal-MemFree算出已用量再用 awk 做浮点除法避免 bash 整数运算直接截断小数。如果 MemTotal 为 0按 100% 处理这也是一个防御性写法。参数说明MemFree并不等于系统真实可用内存它不含 buffers/cache。严格来说可用内存应该看MemAvailable但这份脚本基于 CentOS 6/7 时代写的当时更习惯用 MemFree。如果你想调可以把 MemFree 换成 MemAvailable但要注意 CentOS 6 的/proc/meminfo不一定有这个字段改了得做版本判断。3.3 CPU 巡检从 /proc/cpuinfo 读物理核与逻辑核CPU 检查走的是/proc/cpuinfo这块写法比较经典。物理 CPU 个数看physical id字段去重后的行数逻辑 CPU 个数看processor字段的行数每颗 CPU 核心数看cores字段型号看model name。# CPU 物理个数、逻辑个数、核心数与型号 Physical_CPUs$(grep physical id /proc/cpuinfo | sort | uniq | wc -l) Virt_CPUs$(grep processor /proc/cpuinfo | wc -l) CPU_Kernels$(grep cores /proc/cpuinfo | uniq | awk -F : {print $2}) CPU_Type$(grep model name /proc/cpuinfo | awk -F : {print $2} | sort | uniq) CPU_Arch$(uname -m)逻辑说明grep physical id出来的每一行是一个物理 CPU 槽位sort | uniq去掉同一颗物理 CPU 的重复编号。grep processor数的是逻辑 CPU 数超线程开启时逻辑核会多于物理核乘以核心数。cores字段在开了超线程的机器上仍然显示物理核心数所以物理核数 Physical_CPUs × CPU_Kernels而逻辑核数就是 Virt_CPUs。这两个数字差一倍说明开了超线程。参数说明awk -F : {print $2}里分隔符是冒号加空格不是冒号。这个细节容易看错如果写成-F :取出来的值会带前导空格后面拼字符串时排版会多一个空格。3.4 进程巡检僵尸进程与内存、CPU 占用 TOP10进程检查函数有两个固定输出僵尸进程列表、内存占用 TOP10、CPU 占用 TOP10。这几个数据是巡检报告里最能反映机器健康状态的。# 僵尸进程统计与列表 if [ $(ps -ef | grep defunct | grep -v grep | wc -l) -ge 1 ]; then echo 僵尸进程 ps -ef | head -n1 ps -ef | grep defunct | grep -v grep fi逻辑说明grep defunct匹配僵尸进程grep -v grep去掉 grep 自身那条进程记录。如果统计数大于等于 1说明存在僵尸进程就打出来。这里用的是-ge 1而不是-gt 0效果一样但写成-ge 1更符合直觉。# 内存占用 TOP10 与 CPU 占用 TOP10 echo -e PID %MEM RSS COMMAND$(ps aux | awk {print $2, $4, $6, $11} | sort -k3rn | head -n 10) | column -t top -b -n1 | head -17 | tail -11逻辑说明ps aux输出的第 2 列是 PID第 4 列是 %MEM第 6 列是 RSS物理内存占用第 11 列是命令。sort -k3rn按 RSS 数值降序排这里的-k3对应的是 awk 输出的第 3 列 RSS。CPU TOP10 直接用top -b -n1非交互模式跑一轮截取第 7 到 17 行。-b是 batch 模式适合脚本里调用-n1指只刷新一次。参数说明内存 TOP10 里之所以不用sort -k2rn按 %MEM 排是因为 %MEM 是百分比很多进程数值相同排序不稳定。按 RSS 排更能反映实际内存占用绝对值这个取舍是合理的。3.5 用户与登录巡检last 命令与空密码检测用户检查函数比较长核心是遍历 /etc/passwd 里所有可登录用户逐个查最近登录时间和密码过期时间。其中获取用户最近登录时间用了 last 命令配合年份推断# 推断用户最近一次登录时间last 命令不直接显示年份 loginBeforeToday$(last $username | grep $username | wc -l) loginBeforeNewYearsDayOfThisYear$(last $username -t $thisYear0101000000 | grep $username | wc -l) if [ $loginBeforeToday -gt $loginBeforeNewYearsDayOfThisYear ]; then echo 该用户今年有登录记录 fi逻辑说明last 命令默认只显示最近若干条登录记录如果要判断某用户是否在今年登录过比较两个数字即可。如果「截至今天」的登录次数大于「截至今年元旦」的登录次数说明今年有登录记录。这是一条非常取巧的思路也是脚本里唯一一段带算法性质的逻辑。空密码用户检测走的是 /etc/shadow 里密码位为!!的标记# 检测空密码用户 r$(awk -F: $2!!{print $1} /etc/shadow | grep -w $user) if [ ! -z $r ]; then echo $r fi逻辑说明/etc/shadow第二列是加密后的密码新创建但未设置密码的用户该字段一般是!!。awk -F: $2!!精确匹配这一状态grep -w $user确保不是前缀匹配避免漏掉或误判用户名类似的账户。参数说明判断条件用的是[ ! -z $r ]即非空就执行。这里如果$r为空字符串[ ! -z ]在部分 bash 版本下会有告警更严谨的写法是[ -n $r ]加双引号。4. 落地避坑从 cron 报错到邮件发不出的 5 条记录4.1 cron 里执行报错 command not found现象脚本手动执行一切正常丢到 crontab 里定时跑之后日志文件里全是ifconfig: command not found、free: command not found。原因cron 的执行环境 PATH 极简默认只有/usr/bin:/bin很多系统命令在/sbin和/usr/sbin下。脚本里的 ifconfig、chkconfig、sestatus 都依赖这些路径。解决脚本里已经显式导出了完整 PATHexport PATH/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin同时source /etc/profile重新加载系统环境变量。如果你拿到的版本没有这行在 crontab 的脚本执行行前补上也可以0 8 * * * source /etc/profile /path/to/check.sh。4.2 CentOS 7 上 ifconfig 解析不到 IP现象报告文件名变成HostDailyCheck--20240601.txt登录检查里网络模块的 IP 字段为空。原因CentOS 7 开始默认不装 net-toolsifconfig 可能不存在就算装了主网卡名称也未必是 eth0可能是 ens33、enp0s3 或 eno1。脚本开头用ifconfig eth0硬编码网卡名拿不到 IP。解决把 IP 获取方式改成 ip 命令兼容任意网卡名称# 兼容新老系统的 IP 获取方式 IPADDR$(ip -4 addr show | grep -v 127.0.0.1 | grep inet | awk {print $NF} | head -n1)如果机器有多个网卡这个命令取的是第一个非回环地址。想精确到指定网卡可以把ip -4 addr show换成ip -4 addr show eth0或ip -4 addr show ens33按实际环境改。4.3 mail 命令发不出邮件现象脚本手动执行后报告文件正常生成但邮箱里收不到信终端报mail: command not found或cannot send message: No such file or directory。原因机器上没装 mailx / mailutils或者装了但系统没有配置可用的 MTA。很多精简安装的服务器默认不带邮件客户端即使带了也没有可用的 smtp 配置。解决没有 mail 命令就装一个CentOS 7 的安装命令是# CentOS 7/RHEL 7 安装 mailx yum install -y mailx装完还要确认能发外域邮件。常见做法是配置 mailx 走外部 SMTP在/etc/mail.rc末尾追加set smtpsmtp.example.com:465 set smtp-auth-useryour_accountexample.com set smtp-auth-passwordyour_password set smtp-authlogin set ssl-verifyignore参数说明smtpsmtp.example.com:465的 465 是 SSL 端口smtp-authlogin表示登录认证方式ssl-verifyignore跳过证书校验适合内网自建邮件服务器或测试环境。配置完后可以先手动跑一次echo test | mail -s test your_emailexample.com验证。4.4 磁盘汇总与 df 实际输出对不上现象报告里磁盘总容量比df -h里所有分区加起来小很多或者 Inode 使用率算出一个很不合理的数字。原因脚本用df -TP排除 tmpfs 后累加但有些机器的/dev/shm、/run、/sys/fs/cgroup挂在 tmpfs、overlay、squashfs 上这些都被过滤了本身没问题问题往往出在有的分区是 NFS 挂载df 出来的数值单位不统一。另外join /tmp/disk /tmp/inode时如果某个文件系统的挂载点包含空格join 的字段分隔会错位导致 awk 取列取错。解决遇到 NFS 挂载时在awk $2!tmpfs后面追加过滤条件把网络文件系统也排除掉awk $2!tmpfs $2!nfs $2!nfs4。至于挂载点带空格的情况检查时先看一眼df -TP原始输出如果挂载点有空格脚本需要改用-PPOSIX 格式并以冒号分隔后处理属于个别场景不用过度改。4.5 chage 对 root 用户报错导致密码检查中断现象密码检查函数在部分机器上输出异常chage -l root显示Permission denied后续用户密码状态全部没打出来。原因chage -l查询其他用户密码信息时要么需要 root 权限要么用户本身是 root 时某些系统版本会限制查询。脚本没有对每个用户做权限判断遇到权限不足直接退出当前循环。解决在调用 chage 前确认执行身份保留脚本开头那段[ $(id -u) -gt 0 ] echo 请用 root 用户执行此脚本 exit 1的校验同时把 chage 查询的 stderr 丢弃避免报错信息污染报告get_expiry_date$(/usr/bin/chage -l $user 2/dev/null | grep Password expires | cut -d: -f2)5. 接入邮件与定时任务把巡检报告变成每天早上的第一份日志到了这一步脚本本身已经能跑、能出报告了剩下的问题是让它自动化运转起来。我的习惯是加两层第一层是定时任务每天早上固定时间自动执行第二层是验证逻辑确认报告真的生成、真的发出去了。cron 配置我建议这么写注意路径和环境变量一起带上# crontab 定时执行巡检脚本 0 8 * * * source /etc/profile /usr/local/bin/host_daily_check.sh /var/log/host_check.log 21逻辑说明0 8 * * *表示每天早上 8 点执行。source /etc/profile在脚本外先加载一次系统环境变量避免脚本里 export 不完整时 cron 环境缺东西。重定向日志是必要的出了错能第一时间看到迹。邮件发送的配置需要在脚本主流程末尾补一段我这里给出一个带判断的版本# 报告生成后检查文件是否非空再决定是否发邮件 if [ -s $RESULTFILE ]; then mail -s Host Daily Check Report - $report_Hostname $(date %F) your_emailexample.com $RESULTFILE else echo 报告文件为空可能巡检失败 | mail -s Host Check ERROR your_emailexample.com fi逻辑说明-s判断文件存在且非空。报告为空说明前面某个环节挂了这时候发一封错误提醒邮件比发一封空报告有用得多。邮件标题带上主机名和日期在多台服务器同时巡检时收件箱里一眼就能分辨是哪台机器。报告内容本身也可以做点小优化。脚本把所有 report 变量拼成一个文本我一般会在头部加一个「健康概览」块把内存使用率、磁盘使用率、僵尸进程数这几个最关键的指标放最前面避免收件人翻完整份报告才发现问题# 报告头部增加健康概览 echo 健康概览 echo 内存使用率: $report_MemUsedPercent echo 磁盘使用率: $report_DiskUsedPercent echo Inode 使用率: $report_InodeUsedPercent echo 僵尸进程数: $report_DefunctProsess echo 计划任务数: $report_Crontab echo 完整巡检信息 参数说明$report_Crontab是脚本统计的当前用户计划任务总数数值偏大不一定代表异常但每次对比这个数字能发现是否有新增的恶意定时任务或业务任务误添加。验证这套东西是否真正跑通我的做法是手动执行一次脚本确认报告文件出现再手动执行一次 mail 发送命令确认邮箱能收到最后把 cron 时间临时调成两分钟后等它真跑完一遍再改回 8 点。从那以后我每次接手一批新机器都强制先跑一遍这个流程确认报告里所有字段都有值、邮件能收到再做其他优化。如果哪天早上邮件没来第一件事不是翻脚本而是看/var/log/host_check.log里 cron 的执行记录——这个习惯帮我省了不止一次半夜查机器的功夫。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站