
简介面向Linux运维、DevOps工程师及Shell脚本学习者的实用脚本工具集2024年新版收录279个开箱即用案例覆盖DDoS攻击防范与恶意IP自动封禁、Linux系统告警邮件、MySQL单库及多库多表备份、Nginx访问日志按天切割与流量分析、网卡实时流量查看等常见运维场景。脚本按功能场景组织可直接用于日常巡检、安全防护、数据库备份与日志治理帮助读者快速搭建自动化维护流程积累命令组合、日志处理与故障排查思路。压缩包共1个PDF文件大小约4.69MB将脚本示例、关键命令与场景说明集中编排便于按任务检索和对照实践。已有154人学习适合需要提升服务器管理效率、补充脚本案例库的初中级运维人员、后端开发者及相关专业学生参考。1. 279 个开箱即用 shell 脚本它替你省掉的到底是哪一段工时去年在一台对外提供接口的测试机上我把这套「279 个开箱即用 shell 脚本」里的防攻击脚本直接跑了。十分钟后运维群里有人问为什么办公室出口 IP 连不上测试环境了。脚本没错它忠实地把访问频次超过阈值的 IP 塞进了 iptables只是它不知道我们整个办公室走一个 NAT 出口。这件事之后我对「开箱即用」的理解变了——开的是箱能不能用取决于你对自己环境的了解程度。这份合集覆盖的是运维日常里出现频率最高的一批场景自动封禁异常 IP、Nginx 访问日志按天切割与 TOP 统计、MySQL 单库与分表备份、网卡实时流量、系统初始化加固、上百台机器磁盘巡检、/proc 进程状态统计、jstack 定时采集、企业微信告警、FTP 与 SSH 客户端封装。它适合已经能登服务器、会看日志但每次写 awk 都要现场翻资料的人也适合想把零散操作沉淀成 cron 任务的小团队。下面按「怎么落地」的顺序拆。2. 摸清目录与运行基线279 个文件里真正能直接用的不到三成合集是网络收集整理而成文件名和目录结构没有统一规范有的脚本叫1.sh有的叫nginx_log_cut.sh还有一部分是同一个功能的多个版本比如 MySQL 备份就有单循环和多循环两份。这意味着你没法「全部解压然后逐个跑」必须先把它们按副作用分类再挑出自己真正需要的十几个改透。这一步花一两个小时能省掉后面几天的排障。我一般的做法是先建一个空目录把压缩包解到~/shell-lab所有实验都在这个目录里做确认没问题再往/usr/local/sbin或/etc/opsscripts挪。2.1 按副作用分两刀只读脚本和动系统的脚本分类的标准只有一个——这个脚本执行完之后系统的哪个部分被改变了。只读脚本随便跑大不了输出难看动系统的脚本一旦跑错轻则日志断档重则机器登不上。所以第一刀必须切在这里而不是按功能切。类别典型脚本是否需要 root首次执行前必做只读分析访问日志 TOP IP/URL、网卡实时流量、/proc 进程统计否确认日志格式与网卡名定时采集jstack 定时抓取、磁盘巡检、GC 日志统计视情况确认保留策略与磁盘余量落盘备份MySQL 单库备份、MySQL 分表备份、大文件迁移否确认目标目录容量与校验方式改系统状态初始化加固、自动封禁 IP、禁用 root 远程登录是打快照 保留一个已登录会话外部调用企业微信告警、FTP 客户端、SSH 客户端否确认出口连通性与凭据存放位置把「改系统状态」这一类单独挑出来可以先用一条 grep 做粗筛# 在解压目录里找出所有可能改动系统状态的脚本重定向到一个清单 grep -rlE iptables|firewall-cmd|systemctl|sed[[:space:]]-i|rm[[:space:]]-rf|kill[[:space:]]-9|?[[:space:]]*/etc/ \ . --include*.sh | sort | tee dangerous.txt # 统计一下有多少个心里有个数 wc -l dangerous.txt这里的正则不是万能的它只能抓到显式写出来的危险动作。像「系统初始化脚本」那种把cat /etc/sysctl.conf和sed -i混在一起、还顺手关掉防火墙的通篇读一遍比 grep 靠谱。dangerous.txt的作用是给你一个审阅顺序不是安全边界。真正要上生产前我习惯把清单里的脚本逐个读完重点看三处有没有rm -rf后面跟变量、有没有直接改/etc/ssh/sshd_config、有没有把防火墙整个关掉。2.2 运行基线核对awk 实现、bash 版本、cron 的 PATH合集里的脚本大量依赖awk和 bash 的数组语法。问题在于不同发行版默认的 awk 不是同一个实现Debian/Ubuntu 上很多时候是mawk它不支持gensub()、strtonum()这类 gawk 扩展而合集里不少统计脚本用了asort、length(a)之类的写法在 mawk 下要么报错要么静默给出空结果。静默失败比报错更麻烦因为 cron 里你看不到任何输出。第二处基线是 bash 版本。脚本头部写着#!/bin/bash但如果用sh script.sh或者放进 cron 时被/bin/sh解释关联数组、[[ ]]、这些语法会直接失效。cron 的环境比登录 shell 干净得多PATH 通常只有/usr/bin:/biniptables在/usr/sbin下就找不到。这两点合起来构成了「手工执行没问题、cron 跑就没反应」这类问题的绝大多数原因。第三处是外部命令版本。MySQL 备份脚本要求mysqldump客户端版本不低于服务端主版本否则可能报Unknown table COLUMN_STATISTICS之类的错误封 IP 脚本在只用 nftables 的新系统上找不到iptables命令。#!/usr/bin/env bash # preflight.sh —— 执行任何脚本前先跑一遍缺什么补什么 set -uo pipefail for c in bash awk sed grep find xargs ssh crontab gzip; do command -v $c /dev/null 21 || echo [缺失] $c done # awk 实现差异输出第一行就能看出是 gawk 还是 mawk awk -W version 21 | head -1 echo bash 版本: ${BASH_VERSION} # 只在需要封 IP 的机器上查 if command -v iptables /dev/null 21; then iptables -S INPUT 2/dev/null | head -5 else echo [提示] 未找到 iptables封禁类脚本需要换成 nft 或 ipset 方案 fi # 备份机上看客户端版本是否和服务端匹配 command -v mysqldump /dev/null 21 mysqldump --version # cron 的真实环境用它来判断脚本在定时任务里能不能找到命令 env -i PATH/usr/bin:/bin bash -c command -v awk; command -v iptables || echo cron 环境找不到 iptablesset -uo pipefail在这里只开未定义变量和管道错误的检查故意没开-e——预检脚本本来就要把缺失项全部列出来中途退出反而看不全。最后一行用env -i模拟 cron 的最小环境这是我最常用的自检手段很多「手工能跑、定时不跑」的问题在这一步就现形了。awk -W version在 mawk 下也能输出但格式不同看到mawk字样就要留意脚本里有没有 gawk 专有函数。2.3 统一改造把 host、密码、路径抽到一个 conf 里合集里几乎每个脚本都把配置硬编码在开头HOSTlocalhost、PASS123.com、BACKUP_DIR/data/db_backup这种写法到处都是。这在演示里没问题但在真实环境里有三个直接后果改一台机器要改十几个文件、密码散落在各处、脚本没法进版本库。我一般会建一个/etc/opsscripts/common.conf存配置再写一个common.sh被所有脚本source。权限上common.conf设成chmod 600并且属主是 root业务脚本用非 root 跑的时候通过 sudo 白名单调用。#!/usr/bin/env bash # common.sh —— 所有脚本统一 source 这个文件改配置只改一处 set -euo pipefail # cron 里 PATH 不完整显式补齐避免 iptables、mysqldump 找不到 export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CONF_DIR${CONF_DIR:-/etc/opsscripts} # shellcheck source/dev/null source $CONF_DIR/common.conf # 统一日志格式方便后面 grep 和轮转 log() { local level$1; shift printf %s [%s] %s\n $(date %F %T) $level $* ${LOG_FILE:-/var/log/opsscripts.log} }# common.conf —— 权限 600属主 root NGINX_LOG_DIR/var/log/nginx NGINX_PID/run/nginx.pid BACKUP_DIR/data/db_backup KEEP_DAYS7 DISK_THRESHOLD80 LOG_FILE/var/log/opsscripts.log ALERT_MAILopsexample.comset -euo pipefail的含义要拆开看-e让命令返回非零就退出-u让引用未定义变量直接报错-o pipefail让管道中任意一段失败都算整条失败。第三点对备份脚本尤其关键因为mysqldump ... | gzip x.sql.gz这种写法默认只取 gzip 的退出码mysqldump 失败了也看不出来。改造的时候不要贪多。279 个脚本里日常真正用得上的大概十五到二十个日志切割、日志 TOP 统计、MySQL 单库备份、磁盘巡检、进程状态统计、网卡流量、系统初始化、告警。剩下的留在归档目录当参考实现需要的时候翻出来抄一段就行。把常用的那几个改到「换一台机器只改 conf」的程度比把 279 个全部改一遍有价值得多。3. 日志与流量类脚本awk 一行流怎么改成能挂 cron 的版本合集里的日志类脚本是使用频率最高的一批也最容易出问题。原因在于它们大多是从一条命令行演化来的有人在终端里敲了一行awk效果不错就直接存成了.sh。这种脚本在交互式终端里跑没问题一旦变成定时任务路径、时区、字段位置、文件句柄这些隐式假设全都会暴露。3.1 Nginx 日志切割mv 和 kill -USR1 的顺序不能反Nginx 打开日志文件后会一直持有这个文件的句柄。mv只是改了目录项的名字进程手里的句柄还指向原来那个 inode所以 nginx 会继续往被移走的旧文件里写。kill -USR1的语义是「重新打开日志文件」nginx 收到信号后关闭旧句柄、按配置里的路径重新open()一次。顺序必须是先 mv、后发信号。反过来的话nginx 重新打开的仍然是那个还没被移走的旧文件你紧接着 mv 走它nginx 又继续往被移走的那份写——切割看起来做了实际新文件一直是空的。这个坑我在两三个环境里都见过日志目录里躺着一堆 0 字节的access.log。#!/usr/bin/env bash # nginx_cut.sh —— 按天切割 nginx 日志保留原文件名 set -euo pipefail source /etc/opsscripts/common.sh # 关键月份目录要用「昨天」算。每月 1 号切割时昨天属于上个月 YESTERDAY$(date -d yesterday %F) MONTH_DIR$NGINX_LOG_DIR/$(date -d yesterday %Y-%m) mkdir -p $MONTH_DIR for f in access.log error.log; do [[ -f $NGINX_LOG_DIR/$f ]] || continue # 先移走nginx 手里的句柄仍指向旧 inode必须靠 USR1 才会重新打开 mv $NGINX_LOG_DIR/$f $MONTH_DIR/${f}.${YESTERDAY} done # USR1 重新打开日志文件不要用 HUPHUP 是重新加载配置 kill -USR1 $(cat $NGINX_PID)三个参数值得单独说。date -d yesterday依赖系统的时区设置如果服务器是 UTC 而业务按东八区算切割出来的文件名会差一天排查时对不上。NGINX_PID在 CentOS 上通常是/var/run/nginx.pidUbuntu 的包安装版常见/run/nginx.pid容器里可能压根没有这个文件——容器场景更推荐在 entrypoint 里用nginx -s reopen。最后kill -USR1发完不会等你确认新文件是 nginx 收到信号后才创建的紧接着跑ls可能看不到。要验证的话加一句sleep 1再检查文件大小。如果系统里有 logrotate其实不必自己写这个脚本。两者的差别在配置复杂度上方案优点代价自写脚本 cron逻辑完全可控能顺带做归档和上报需要自己处理跨月目录、异常退出、文件句柄logrotate内置copytruncate/create模式、压缩、保留份数配置分散在/etc/logrotate.d排障要看多个文件我的习惯是单机两三份日志用脚本机器多了统一交给 logrotate脚本只负责切割后的归档上报。混合用的时候注意别让两套机制同时作用于同一个文件否则会出现一天切两次、目录里一堆空文件的情况。3.2 TOP IP / TOP URL / 状态码统计字段位置是怎么定下来的合集里的日志分析脚本基本都基于 nginx 默认的combined格式$remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent按空格切分后$1是客户端 IP$4是带方括号的时间$7是请求行含方法、路径、协议$9是状态码$10是响应字节数。这套字段位置是所有写法的前提一旦你在nginx.conf里加了自定义字段比如$request_time、$upstream_addr后面的位置就会整体后移脚本全部失效。所以分析脚本的第一行注释里我一般会写明「依据的 log_format 字段顺序」方便后面接手的人核对。#!/usr/bin/env bash # ngx_top.sh access.log —— 三个最常用的统计口径 set -euo pipefail LOG_FILE${1:?用法: ngx_top.sh /var/log/nginx/access.log} echo TOP 10 客户端 IP awk {c[$1]} END{for(i in c) printf %8d %s\n, c[i], i} $LOG_FILE \ | sort -k1,1nr | head -10 echo TOP 10 请求路径只统计 4xx/5xx awk $9 ~ /^[45]/ {c[$7]} END{for(i in c) printf %8d %s\n, c[i], i} $LOG_FILE \ | sort -k1,1nr | head -10 echo 状态码分布 awk {c[$9]} END{for(i in c) printf %8d %s\n, c[i], i} $LOG_FILE \ | sort -k2,2 echo 指定时间段内的请求数格式必须与 $time_local 一致 awk $4 [10/Mar/2024:00:00:00 $4 [10/Mar/2024:23:59:59 {n} END{print n0} $LOG_FILEsort -k1,1nr里的-k1,1限定只按第一列排序不加这个限定的话行数相同时会拿整行比较结果看起来会很怪。head之前必须排完序awk输出的顺序是哈希遍历顺序不排序直接 head 拿到的不是 TOP。时间段过滤靠的是字符串比较前提是时间格式里的月份缩写和位宽固定——[10/Mar/2024:00:00:00这种格式按字典序排恰好等于按时间排因为日、月、年的位宽一致。如果日志里出现[1/Mar/2024这种单位数日期比较就会错这时候要把$4拆出来先规范化。还有一个常见误用用length(a)当作 UV。awk {a[$1]} END{print length(a)}得到的是「不同 IP 的个数」不是独立访客数。同一个用户换网络、整个公司共用一个出口 IP这两种情况它都算不准。真实 UV 一般靠 cookie 或者前端埋点日志里近似的做法是把 IP 和 User-Agent 拼起来去重a[$1|$12]这类写法但也只是近似。3.3 网卡流量与端口连接数/proc/net/dev 的两列和 netstat 的字段漂移/proc/net/dev是内核暴露的网卡计数器每行一个接口格式是「接口名: 接收 16 个字段 发送 16 个字段」。用 awk 的默认分隔符连续空白切分后$1是带冒号的接口名$2是累计接收字节数$10是累计发送字节数。这两个数是单调递增的累计值所以脚本必须自己算差分取一次快照sleep一段时间再取一次两个值相减除以间隔。合集里的版本有个小问题——它用整数除法$(( ($NEW_IN - $OLD_IN) / 1024 ))间隔 1 秒的情况下低于 1KB/s 的流量会显示成 0跑在低流量机器上看着像脚本挂了。另一个隐患是awk $0~$NIC这种拼接写法如果接口名是eth0机器上同时存在eth0和eth0.100就会两行都匹配上取到的值随机。#!/usr/bin/env bash # nic_mon.sh iface [interval] —— 实时看某个网卡的进出流量 set -euo pipefail NIC${1:?用法: nic_mon.sh eth0 [1]} INTERVAL${2:-1} snapshot() { # 用默认分隔符$1 是 eth0: 带冒号$2 收字节$10 发字节 awk -v n$NIC: $1n {print $2, $10} /proc/net/dev } read -r last_rx last_tx (snapshot) [[ -n ${last_rx:-} ]] || { echo 接口 $NIC 不存在; exit 1; } while true; do sleep $INTERVAL read -r rx tx (snapshot) # 网卡重启或模块重载会清零计数器出现负值直接跳过这一轮 if (( rx last_rx || tx last_tx )); then last_rx$rx; last_tx$tx; continue fi printf %s IN %8.1f KB/s OUT %8.1f KB/s\n $(date %T) \ $(awk -v a$rx -v b$last_rx -v i$INTERVAL BEGIN{printf %.1f, (a-b)/i/1024}) \ $(awk -v a$tx -v b$last_tx -v i$INTERVAL BEGIN{printf %.1f, (a-b)/i/1024}) last_rx$rx; last_tx$tx done用awk $1n精确匹配带冒号的接口名避开了eth0匹配到eth0.100的问题。差分用 awk 来算而不是 bash 算术是为了保留小数——bash 的$(( ))只做整数运算。计数器回绕或清零那段判断不是可有可无的虚拟机热迁移、网卡 down/up、容器重启都会让计数从 0 开始不做保护的话会打出一个几 TB/s 的负数。同一章里的「端口请求数 TOP 20」脚本问题更大。合集版本写的是 netstat -ant | awk -F本文还有配套的精品资源点击获取