Linux服务器巡检Shell脚本:资源、账号与cron实战
简介这份 Linux 服务器日常巡检脚本面向系统管理员与运维工程师用于把日常人工巡检流程固化为可重复执行的一键脚本适合中级运维人员直接上手使用。资源压缩包仅 118KB内含 1 个 doc 文档完整收录巡检脚本源码与说明轻量却覆盖全面。脚本涵盖磁盘、内存、CPU、进程、文件更改、用户登录等核心巡检项并延伸至监听端口、Selinux、防火墙、用户与空密码账户、相同 UID 用户、sudo 授权、SSH 信任主机与协议版本、root 远程登录、僵尸进程、自启动服务与计划任务、Syslog、SNMP、NTP、JDK 版本等检查点同时统计物理 CPU 个数、逻辑 CPU 数、每 CPU 核心数、CPU 型号与架构以及内存总容量、剩余量与使用率、硬盘与 Inode 总量和占用率、IP、MAC、网关、DNS 等主机信息。脚本通过 echo 拼接报表在 log 目录生成巡检报告并借助 mail 发送到指定邮箱配合环境变量与日志处理可直接部署或按需裁剪。目前已有 1372 人学习便于快速搭建巡检基线、排查隐患。1. 巡检脚本能解决什么从一批跑了三年的服务器说起接手一批跑了三年的 CentOS最先要回答的往往不是“服务活着吗”而是“这批机器现在到底什么状态”。哪台的 /var 只剩不到 5%哪台 inode 已经到 90% 却没人发现哪台上还留着空密码账号cron 里堆了多少条早该下线的历史任务——这些东西监控面板不一定覆盖出了事却要有人负责。这份巡检脚本就是冲着这个场景写的。纯 bash 实现靠 /proc、df、ss、last、chage 这些系统自带命令把 CPU、内存、磁盘与 inode、系统版本、服务自启、网络与监听、计划任务、用户与密码策略、sudoers、僵尸进程、JDK 等十几类信息采一遍输出成一份纯文本报告落到脚本同级 log 目录再通过邮件发到指定邮箱。适合的人群很明确还没上 Zabbix 或 Prometheus 的中小规模环境需要按天留一份可读巡检记录的服务器运维也适合当作自己写自动化采集器之前的参照物。脚本不依赖任何第三方包一台最小化安装的机器就能跑起来。2. 资源层采集CPU、内存、磁盘与 inode 的解析与计算资源层的四个指标决定了这份报告有没有人愿意看。CPU、内存、磁盘空间的采集命令本身很简单难的是解析字段和单位换算稍微糊弄一下使用率就能差出十几个百分点。2.1 CPU 采集physical id 去重与 processor 计数原始脚本这一段是完全可用的直接抄# /proc/cpuinfo 中每个物理插槽有一个 physical id去重计数即物理 CPU 数 Physical_CPUs$(grep physical id /proc/cpuinfo | sort | uniq | wc -l) # 每个逻辑核一条 processor 记录直接计数得到逻辑 CPU 数含超线程 Virt_CPUs$(grep processor /proc/cpuinfo | wc -l) # cores 会在每颗物理 CPU 上重复出现先去重再取值 CPU_Kernels$(grep cores /proc/cpuinfo | uniq | awk -F : {print $2}) # 同一台机器型号通常一致sort|uniq 兜住异构插槽的情况 CPU_Type$(grep model name /proc/cpuinfo | awk -F : {print $2} | sort | uniq) CPU_Arch$(uname -m)逻辑上就是三件事插槽数、逻辑核数、单颗核心数。grep physical id在带超线程的机器上会返回与逻辑核数相同的行数必须sort | uniq | wc -l才能收敛到真实插槽数少一步结果就翻倍。注意ARM 架构的 /proc/cpuinfo 里没有physical id字段grep 返回空wc -l得到 0。容器里读到的也是宿主机裁剪视图。要给这几个变量补默认值否则报表里会出现“物理 CPU 个数:0”。2.2 内存采集free 与 /proc/meminfo 的双通道原脚本按 CentOS 版本分流7 以下用free -mo7 及以上用free -hf。这里有个长期被误读的点先把字段表列清楚。字段含义说明total物理内存总量不含 swapused已用内存新版本 free 已扣除可回收部分free完全空闲通常很小不代表内存不足buff/cache缓冲与页缓存可被回收available实际可用内核 3.14 提供判断内存压力看这个报表变量用的是 /proc/meminfo# 取 KB 单位的原始值便于统一换算 MemTotal$(awk /MemTotal/{print $2} /proc/meminfo) MemFree$(awk /MemFree/{print $2} /proc/meminfo) MemUsed$((MemTotal - MemFree)) # 除零保护容器里 MemTotal 可能为 0 MemPercent$(awk BEGIN{if($MemTotal0){printf 100}else{printf \%.2f\,$MemUsed*100/$MemTotal}})MemFree只统计完全空闲页不含 buff/cache所以这样算出来的使用率会明显偏高一台看着 85% 的机器实际压力可能很小。做阈值告警时要把MemFree换成MemAvailable读法一样只是键名不同。容器场景还要注意 cgroup 限制/proc/meminfo给的是宿主机的值要看/sys/fs/cgroup/memory/memory.limit_in_bytes。2.3 磁盘与 inodedf -hTP 与 join 合并磁盘采集的核心是两个命令加一次合并# -T 输出文件系统类型-P 保证一行一个文件系统不会折行 # 排除 tmpfs它在容器和 /dev/shm 上数量很多会稀释统计结果 df -hTP | sed 1d | awk $2!tmpfs{print} /tmp/disk df -iTP | sed 1d | awk $2!tmpfs{print} /tmp/inode # 表头统一成 Mounted原脚本用 sed 把 Mounted on 改写后再 join 对齐 join /tmp/disk /tmp/inode | column -t-P这个参数很容易被忽略。不加它设备名过长时 GNU df 会折行输出一行数据变两行后面所有按列取值的 awk 全部错位。sed 1d去掉表头$2!tmpfs排除内存文件系统——不排除的话一台跑了十几个容器的机器报表里一半行都是 tmpfs真正要看的根分区反而被淹了。inode 单独统计是这份脚本比较专业的地方。df -i看的是 inode 数量而不是字节数/var/spool/postfix这类目录里堆满小文件时磁盘还剩几百 Ginode 已经 100%一样写不进新文件。原脚本把 inode 总量除以 1000 标成 K 单位其实 df -i 输出的是“个”这个单位标注不太严谨自己用的时候直接按个数展示更清楚。提示coreutils 8.21 以后可以直接df -PT --outputsource,fstype,size,used,avail,pcent,target指定列不用再靠列号硬切挂载点带空格的场景更稳。3. 系统与服务层版本分流、网络监听与计划任务资源层看完接下来是系统身份、网络暴露面和定时任务。这三块决定了报告的“背景信息”是否可信也决定了脚本能不能迁移到非 RHEL 系的机器上。3.1 发行版判定与 systemctl/chkconfig 分流原脚本的版本判定写死了一条命令centosVersion$(awk {print $(NF-1)} /etc/redhat-release)对CentOS Linux release 7.9.2009 (Core)这类输出$(NF-1)取到的是7.9.2009能工作。但/etc/redhat-release是 RedHat 系专有文件Ubuntu、Debian、统信 UOS 上根本不存在变量为空后续所有[[ $centosVersion 7 ]]判断都会走错分支。改造成兼容写法# os-release 是 systemd 时代的标准文件主流发行版都有 if [ -f /etc/os-release ]; then . /etc/os-release os_id$ID # 取值如 centos / ubuntu / debian / uos os_ver${VERSION_ID%%.*} # 只取主版本号做数值比较 else os_ver$(awk {print $(NF-1)} /etc/redhat-release 2/dev/null | cut -d. -f1) fi # 数值比较避免字符串比较下 10 7 为真的陷阱 if [ ${os_ver:-0} -ge 7 ] 2/dev/null; then is_systemd1 else is_systemd0 fi服务采集据此分流systemd 机器走systemctl list-unit-files --typeservice --stateenabled --no-pager统计自启服务走systemctl list-units --typeservice --staterunning --no-pager统计运行中服务老系统走chkconfig和service --status-all。两边的输出格式不一样所以 grep 的关键字也不一样systemd 侧匹配enabledsysvinit 侧要同时匹配:on和:启用——中文环境下 chkconfig 输出是本地化的这个坑在国产化替代的机器上尤其常见。3.2 网络、监听端口与登录记录网络信息分三块取。接口地址在 CentOS 7 之后用ip命令替代 ifconfig# 只取有 BROADCAST 标志的物理/虚拟接口过滤 lo for i in $(ip link | grep BROADCAST | awk -F: {print $2}); do ip add show $i | grep -E BROADCAST|global | awk {print $2} | tr \n echo done # 默认网关从路由表取 GATEWAY$(ip route | grep default | awk {print $3}) # DNS 从 resolv.conf 取过滤注释行 DNS$(grep nameserver /etc/resolv.conf | grep -v # | awk {print $2} | tr \n ,)监听端口用ss -ntul而不是netstat -ntul。ss 走内核 netlink 直接读 socket 表几万条连接的机器上比 netstat 快一个量级而且新内核已经默认不装 net-tools。-n不做反向解析、-tTCP、-uUDP、-l只显示监听态四个参数组合起来正好是要的暴露面清单。登录记录用last | head但更值得加的是时间同步检查。集群里时间不同步会导致日志时间戳对不上、证书校验失败、分布式任务调度错乱。常见做法是补一段chronyc tracking或ntpq -p把偏移量一并写进报告。3.3 计划任务扫描与自启动程序计划任务分两处扫用户 crontab 和系统级 cron 目录Crontab0 # 第一层遍历所有可用 shell for shell in $(grep -v /sbin/nologin /etc/shells); do # 第二层找出使用该 shell 的用户 for user in $(grep $shell /etc/passwd | awk -F: {print $1}); do if crontab -l -u $user /dev/null 21; then echo $user; crontab -l -u $user let CrontabCrontab$(crontab -l -u $user | wc -l) fi done done # 系统级/etc/cron.d /etc/cron.daily 等 find /etc/cron* -type f | xargs -i ls -l {} | column -t let CrontabCrontab$(find /etc/cron* -type f | wc -l)两层循环的设计是为了过滤 nologin 用户——这类系统账号不会有 crontab逐个去crontab -l -u只会刷出成屏的报错。/dev/null 21抑制输出靠返回码判断该用户有没有任务比解析报错文本可靠。自启动程序检查 rc.local 时grep -v ^# | sed /^$/d这两步去掉注释和空行避免把小节标题也算成一条命令。检查项命令关注点用户任务crontab -l -u USER退出码 0 表示有任务系统任务find /etc/cron* -type fcron.d 目录最易被忽略rc.localgrep -v ^# /etc/rc.d/rc.local老系统遗留启动项4. 账号安全巡检空密码、同 UID、密码过期与 sudoers这一章是报告里最有价值的部分也是最容易写错的部分。用户和权限的检查逻辑一旦有偏差要么漏报真实风险要么天天误报最后没人看。4.1 空密码用户与相同 UID 的判定原脚本用一条 awk 找空密码用户# 目标是找出密码字段异常的账号 awk -F: $2!!{print $1} /etc/shadow | grep -w $user这里要拆清楚/etc/shadow第二个字段如果是!!或!表示账号被锁定不是空密码真正的空密码是第二个字段为空字符串也就是冒号紧挨着冒号。判定应该分开写# 真正的空密码第二个字段长度为 0 awk -F: length($2)0 {print $1} /etc/shadow # 被锁定账号单独列出来属于另一类风险 awk -F: $2 ~ /^!/ {print $1, locked} /etc/shadow相同 UID 的检查逻辑是对的# 统计每个 UID 出现次数出现超过一次即存在重复 UIDs$(cut -d: -f3 /etc/passwd | sort | uniq -c | awk $11{print $2}) for uid in $UIDs; do echo -n $uid awk -F: ORS; $3$uid{print :,$1} /etc/passwd echo done重复 UID 意味着两个账号实际拥有同一份文件权限审计上属于必须清理的项。运维脚本创建账号时手写 UID 最容易撞上useradd -u指定已有 UID 时如果不加-o系统会报错加了-o就悄悄建成了重复账号。4.2 chage 批量检测密码过期密码过期检测要遍历用户逐个查 chagefor user in $(echo $pwdfile | grep $shell | cut -d: -f1); do get_expiry_date$(chage -l $user | grep Password expires | cut -d: -f2) if [[ $get_expiry_date ~ never ]]; then printf %-15s 永不过期\n $user else # 把过期日期和当前时间都转成秒级时间戳再相减 expiry_ts$(date -d $get_expiry_date %s) current_ts$(date %s) days$(( (expiry_ts - current_ts) / 86400 )) printf %-15s %s 天后过期\n $user $days fi done日期转时间戳这一步不能省。date -d解析 chage 输出的自然语言日期%s转成 Unix 时间戳再做减法得到秒差除以 86400 取整。直接对日期字符串做减法在跨月和跨年时必然出错。chage 输出里never的写法在不同版本不完全一致用~做正则匹配比严格相等稳。4.3 sudoers 授权与 root 特权账号盘点sudoers 的检查不能只读主文件# 主文件里去掉注释和 Defaults 行剩下的才是授权规则 grep -v ^# /etc/sudoers | grep -v ^Defaults | sed /^$/d # 补充扫描子目录运维习惯把授权拆到 /etc/sudoers.d/ ls -l /etc/sudoers.d/ cat /etc/sudoers.d/* 2/dev/null很多发行版和云镜像会把授权拆进/etc/sudoers.d/只读主文件会漏掉全部实际授权。另外主文件里的#includedir /etc/sudoers.d这行本身以#开头会被grep -v ^#一起过滤掉第一次看容易以为目录没被启用。特权账号的统计更直接# 遍历 passwd 中所有用户UID 为 0 的即为特权账号 for user in $(awk -F: {print $1} /etc/passwd); do [ $(id -u $user) -eq 0 ] echo $user done4.4 用 lastlog 替代逐年回溯的登录查询原脚本里getUserLastLogin用last加-t时间过滤逐年往前找思路是对的但有两个问题last不显示年份只能靠“今天之前登录次数是否变化”来推断wtmp 一旦被 logrotate 轮转历史记录就找不到last只能追到最早一条。更省事的做法是直接读 lastlog# lastlog 一次输出所有用户的最后登录时间读的是 /var/log/lastlog lastlog | awk NR1{print;next} $2!**Never # 只关注 90 天以上没登录的活跃账号 lastlog -b 90-b 90表示只显示 90 天以前登录过的用户这个参数用来找“僵尸账号”非常顺手比在循环里调last快几个数量级也不受 wtmp 轮转影响。提示空密码、重复 UID、UID 为 0 的非 root 账号这三类一旦命中报告标题里就该带个醒目标记别让它们混在几十行输出中间被扫过去。5. cron 环境、邮件投递与阈值改造的实战细节脚本能手动跑通和能进 crontab 稳定运行中间隔着好几道坎。前面提过的 PATH 是第一道邮件通道和阈值改造是后两道。5.1 PATH 与 source /etc/profile 的真实差异原脚本注释里写着“环境变量 PATH 没设好在 cron 里执行时有很多命令会找不到”然后同时做了两件事export PATH...和source /etc/profile。实际上后者往往无效——/etc/profile开头常见一段[ ${-#*i} ! $- ] return非交互 shell 执行时直接返回PATH 根本没被赋值。真正可靠的是显式导出# 覆盖 cron 的精简 PATH把 sbin 目录补全 export PATH/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin # 需要 JAVA_HOME 等变量的显式 source 具体文件而不是 profile [ -f /etc/profile.d/jdk.sh ] . /etc/profile.d/jdk.sh另外要注意 cron 会把脚本的所有标准输出当作邮件正文再发一份。脚本内部采集时最好把中间输出重定向掉只在最后一次性写文件和发信否则收到两封邮件一封是报告一封是采集过程的杂散输出。5.2 邮件投递通道的选择发信命令本身简单# CentOS 7 上 mail 由 mailx 提供 mail -s $(hostname)-巡检报告-$(date %F) opsexample.com $RESULTFILE难点在本机 MTA 的配置。最小方案是装 postfix 并启用本地投递把外部投递交给上游但多数云服务器默认封禁出站 25 端口直连外部 MX 发不出去得配 smtp relay 或者改用 msmtp 走 587/465 端口# msmtp 的最小配置写到 ~/.msmtprc account default host smtp.example.com port 587 auth on tls on user opsexample.com password 授权码 from opsexample.com发信内容里带主机名和日期标题格式统一成主机名-报告类型-日期收件端做规则过滤时才好按主题归档。5.3 阈值判定与日志清理报告是给人看的但阈值可以给机器看。在生成 RESULTFILE 之后追加一段判断命中异常就把主题前缀改成[WARN]WARN # 磁盘使用率去掉百分号后做数值比较 disk_pct${report_DiskUsedPercent%\%} [ ${disk_pct%.*} -ge 85 ] WARN${WARN}[DISK] inode_pct${report_InodeUsedPercent%\%} [ ${inode_pct%.*} -ge 85 ] WARN${WARN}[INODE] [ -n $report_USEREmptyPassword ] WARN${WARN}[EMPTY_PWD] # 有告警时改标题前缀方便收件箱规则分流 subject${WARN:-[OK]}$(hostname)-巡检报告-$(date %F)${var%\%}是去掉尾部百分号${var%.*}是去掉小数点后的部分取整两步之后才能进-ge做数值比较直接拿带%的字符串比会报语法错。报告文件本身也要清理不然 log 目录一年下来就是几千个 txt# 只保留最近 30 天的巡检报告 find $LOGPATH -name HostDailyCheck-*.txt -mtime 30 -delete把生成时间戳和主机名拼进文件名配合这条清理规则日志目录就不会无限膨胀了。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →