Shell腳本開箱即用:分類預(yù)檢、配置統(tǒng)一與cron落地)
簡介面向Linux運(yùn)維、DevOps工程師及Shell腳本學(xué)習(xí)者的實(shí)用腳本工具集2024年新版收錄279個(gè)開箱即用案例覆蓋DDoS攻擊防范與惡意IP自動(dòng)封禁、Linux系統(tǒng)告警郵件、MySQL單庫及多庫多表備份、Nginx訪問日志按天切割與流量分析、網(wǎng)卡實(shí)時(shí)流量查看等常見運(yùn)維場(chǎng)景。腳本按功能場(chǎng)景組織可直接用于日常巡檢、安全防護(hù)、數(shù)據(jù)庫備份與日志治理幫助讀者快速搭建自動(dòng)化維護(hù)流程積累命令組合、日志處理與故障排查思路。壓縮包共1個(gè)PDF文件大小約4.69MB將腳本示例、關(guān)鍵命令與場(chǎng)景說明集中編排便于按任務(wù)檢索和對(duì)照實(shí)踐。已有154人學(xué)習(xí)適合需要提升服務(wù)器管理效率、補(bǔ)充腳本案例庫的初中級(jí)運(yùn)維人員、后端開發(fā)者及相關(guān)專業(yè)學(xué)生參考。1. 279 個(gè)開箱即用 shell 腳本它替你省掉的到底是哪一段工時(shí)去年在一臺(tái)對(duì)外提供接口的測(cè)試機(jī)上我把這套「279 個(gè)開箱即用 shell 腳本」里的防攻擊腳本直接跑了。十分鐘后運(yùn)維群里有人問為什么辦公室出口 IP 連不上測(cè)試環(huán)境了。腳本沒錯(cuò)它忠實(shí)地把訪問頻次超過閾值的 IP 塞進(jìn)了 iptables只是它不知道我們整個(gè)辦公室走一個(gè) NAT 出口。這件事之后我對(duì)「開箱即用」的理解變了——開的是箱能不能用取決于你對(duì)自己環(huán)境的了解程度。這份合集覆蓋的是運(yùn)維日常里出現(xiàn)頻率最高的一批場(chǎng)景自動(dòng)封禁異常 IP、Nginx 訪問日志按天切割與 TOP 統(tǒng)計(jì)、MySQL 單庫與分表備份、網(wǎng)卡實(shí)時(shí)流量、系統(tǒng)初始化加固、上百臺(tái)機(jī)器磁盤巡檢、/proc 進(jìn)程狀態(tài)統(tǒng)計(jì)、jstack 定時(shí)采集、企業(yè)微信告警、FTP 與 SSH 客戶端封裝。它適合已經(jīng)能登服務(wù)器、會(huì)看日志但每次寫 awk 都要現(xiàn)場(chǎng)翻資料的人也適合想把零散操作沉淀成 cron 任務(wù)的小團(tuán)隊(duì)。下面按「怎么落地」的順序拆。2. 摸清目錄與運(yùn)行基線279 個(gè)文件里真正能直接用的不到三成合集是網(wǎng)絡(luò)收集整理而成文件名和目錄結(jié)構(gòu)沒有統(tǒng)一規(guī)范有的腳本叫1.sh有的叫nginx_log_cut.sh還有一部分是同一個(gè)功能的多個(gè)版本比如 MySQL 備份就有單循環(huán)和多循環(huán)兩份。這意味著你沒法「全部解壓然后逐個(gè)跑」必須先把它們按副作用分類再挑出自己真正需要的十幾個(gè)改透。這一步花一兩個(gè)小時(shí)能省掉后面幾天的排障。我一般的做法是先建一個(gè)空目錄把壓縮包解到~/shell-lab所有實(shí)驗(yàn)都在這個(gè)目錄里做確認(rèn)沒問題再往/usr/local/sbin或/etc/opsscripts挪。2.1 按副作用分兩刀只讀腳本和動(dòng)系統(tǒng)的腳本分類的標(biāo)準(zhǔn)只有一個(gè)——這個(gè)腳本執(zhí)行完之后系統(tǒng)的哪個(gè)部分被改變了。只讀腳本隨便跑大不了輸出難看動(dòng)系統(tǒng)的腳本一旦跑錯(cuò)輕則日志斷檔重則機(jī)器登不上。所以第一刀必須切在這里而不是按功能切。類別典型腳本是否需要 root首次執(zhí)行前必做只讀分析訪問日志 TOP IP/URL、網(wǎng)卡實(shí)時(shí)流量、/proc 進(jìn)程統(tǒng)計(jì)否確認(rèn)日志格式與網(wǎng)卡名定時(shí)采集jstack 定時(shí)抓取、磁盤巡檢、GC 日志統(tǒng)計(jì)視情況確認(rèn)保留策略與磁盤余量落盤備份MySQL 單庫備份、MySQL 分表備份、大文件遷移否確認(rèn)目標(biāo)目錄容量與校驗(yàn)方式改系統(tǒng)狀態(tài)初始化加固、自動(dòng)封禁 IP、禁用 root 遠(yuǎn)程登錄是打快照 保留一個(gè)已登錄會(huì)話外部調(diào)用企業(yè)微信告警、FTP 客戶端、SSH 客戶端否確認(rèn)出口連通性與憑據(jù)存放位置把「改系統(tǒng)狀態(tài)」這一類單獨(dú)挑出來可以先用一條 grep 做粗篩# 在解壓目錄里找出所有可能改動(dòng)系統(tǒng)狀態(tài)的腳本重定向到一個(gè)清單 grep -rlE iptables|firewall-cmd|systemctl|sed[[:space:]]-i|rm[[:space:]]-rf|kill[[:space:]]-9|?[[:space:]]*/etc/ \ . --include*.sh | sort | tee dangerous.txt # 統(tǒng)計(jì)一下有多少個(gè)心里有個(gè)數(shù) wc -l dangerous.txt這里的正則不是萬能的它只能抓到顯式寫出來的危險(xiǎn)動(dòng)作。像「系統(tǒng)初始化腳本」那種把cat /etc/sysctl.conf和sed -i混在一起、還順手關(guān)掉防火墻的通篇讀一遍比 grep 靠譜。dangerous.txt的作用是給你一個(gè)審閱順序不是安全邊界。真正要上生產(chǎn)前我習(xí)慣把清單里的腳本逐個(gè)讀完重點(diǎn)看三處有沒有rm -rf后面跟變量、有沒有直接改/etc/ssh/sshd_config、有沒有把防火墻整個(gè)關(guān)掉。2.2 運(yùn)行基線核對(duì)awk 實(shí)現(xiàn)、bash 版本、cron 的 PATH合集里的腳本大量依賴awk和 bash 的數(shù)組語法。問題在于不同發(fā)行版默認(rèn)的 awk 不是同一個(gè)實(shí)現(xiàn)Debian/Ubuntu 上很多時(shí)候是mawk它不支持gensub()、strtonum()這類 gawk 擴(kuò)展而合集里不少統(tǒng)計(jì)腳本用了asort、length(a)之類的寫法在 mawk 下要么報(bào)錯(cuò)要么靜默給出空結(jié)果。靜默失敗比報(bào)錯(cuò)更麻煩因?yàn)?cron 里你看不到任何輸出。第二處基線是 bash 版本。腳本頭部寫著#!/bin/bash但如果用sh script.sh或者放進(jìn) cron 時(shí)被/bin/sh解釋關(guān)聯(lián)數(shù)組、[[ ]]、這些語法會(huì)直接失效。cron 的環(huán)境比登錄 shell 干凈得多PATH 通常只有/usr/bin:/biniptables在/usr/sbin下就找不到。這兩點(diǎn)合起來構(gòu)成了「手工執(zhí)行沒問題、cron 跑就沒反應(yīng)」這類問題的絕大多數(shù)原因。第三處是外部命令版本。MySQL 備份腳本要求mysqldump客戶端版本不低于服務(wù)端主版本否則可能報(bào)Unknown table COLUMN_STATISTICS之類的錯(cuò)誤封 IP 腳本在只用 nftables 的新系統(tǒng)上找不到iptables命令。#!/usr/bin/env bash # preflight.sh —— 執(zhí)行任何腳本前先跑一遍缺什么補(bǔ)什么 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 實(shí)現(xiàn)差異輸出第一行就能看出是 gawk 還是 mawk awk -W version 21 | head -1 echo bash 版本: ${BASH_VERSION} # 只在需要封 IP 的機(jī)器上查 if command -v iptables /dev/null 21; then iptables -S INPUT 2/dev/null | head -5 else echo [提示] 未找到 iptables封禁類腳本需要換成 nft 或 ipset 方案 fi # 備份機(jī)上看客戶端版本是否和服務(wù)端匹配 command -v mysqldump /dev/null 21 mysqldump --version # cron 的真實(shí)環(huán)境用它來判斷腳本在定時(shí)任務(wù)里能不能找到命令 env -i PATH/usr/bin:/bin bash -c command -v awk; command -v iptables || echo cron 環(huán)境找不到 iptablesset -uo pipefail在這里只開未定義變量和管道錯(cuò)誤的檢查故意沒開-e——預(yù)檢腳本本來就要把缺失項(xiàng)全部列出來中途退出反而看不全。最后一行用env -i模擬 cron 的最小環(huán)境這是我最常用的自檢手段很多「手工能跑、定時(shí)不跑」的問題在這一步就現(xiàn)形了。awk -W version在 mawk 下也能輸出但格式不同看到mawk字樣就要留意腳本里有沒有 gawk 專有函數(shù)。2.3 統(tǒng)一改造把 host、密碼、路徑抽到一個(gè) conf 里合集里幾乎每個(gè)腳本都把配置硬編碼在開頭HOSTlocalhost、PASS123.com、BACKUP_DIR/data/db_backup這種寫法到處都是。這在演示里沒問題但在真實(shí)環(huán)境里有三個(gè)直接后果改一臺(tái)機(jī)器要改十幾個(gè)文件、密碼散落在各處、腳本沒法進(jìn)版本庫。我一般會(huì)建一個(gè)/etc/opsscripts/common.conf存配置再寫一個(gè)common.sh被所有腳本source。權(quán)限上common.conf設(shè)成chmod 600并且屬主是 root業(yè)務(wù)腳本用非 root 跑的時(shí)候通過 sudo 白名單調(diào)用。#!/usr/bin/env bash # common.sh —— 所有腳本統(tǒng)一 source 這個(gè)文件改配置只改一處 set -euo pipefail # cron 里 PATH 不完整顯式補(bǔ)齊避免 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 # 統(tǒng)一日志格式方便后面 grep 和輪轉(zhuǎn) log() { local level$1; shift printf %s [%s] %s\n $(date %F %T) $level $* ${LOG_FILE:-/var/log/opsscripts.log} }# common.conf —— 權(quán)限 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讓引用未定義變量直接報(bào)錯(cuò)-o pipefail讓管道中任意一段失敗都算整條失敗。第三點(diǎn)對(duì)備份腳本尤其關(guān)鍵因?yàn)閙ysqldump ... | gzip x.sql.gz這種寫法默認(rèn)只取 gzip 的退出碼mysqldump 失敗了也看不出來。改造的時(shí)候不要貪多。279 個(gè)腳本里日常真正用得上的大概十五到二十個(gè)日志切割、日志 TOP 統(tǒng)計(jì)、MySQL 單庫備份、磁盤巡檢、進(jìn)程狀態(tài)統(tǒng)計(jì)、網(wǎng)卡流量、系統(tǒng)初始化、告警。剩下的留在歸檔目錄當(dāng)參考實(shí)現(xiàn)需要的時(shí)候翻出來抄一段就行。把常用的那幾個(gè)改到「換一臺(tái)機(jī)器只改 conf」的程度比把 279 個(gè)全部改一遍有價(jià)值得多。3. 日志與流量類腳本awk 一行流怎么改成能掛 cron 的版本合集里的日志類腳本是使用頻率最高的一批也最容易出問題。原因在于它們大多是從一條命令行演化來的有人在終端里敲了一行awk效果不錯(cuò)就直接存成了.sh。這種腳本在交互式終端里跑沒問題一旦變成定時(shí)任務(wù)路徑、時(shí)區(qū)、字段位置、文件句柄這些隱式假設(shè)全都會(huì)暴露。3.1 Nginx 日志切割mv 和 kill -USR1 的順序不能反Nginx 打開日志文件后會(huì)一直持有這個(gè)文件的句柄。mv只是改了目錄項(xiàng)的名字進(jìn)程手里的句柄還指向原來那個(gè) inode所以 nginx 會(huì)繼續(xù)往被移走的舊文件里寫。kill -USR1的語義是「重新打開日志文件」nginx 收到信號(hào)后關(guān)閉舊句柄、按配置里的路徑重新open()一次。順序必須是先 mv、后發(fā)信號(hào)。反過來的話nginx 重新打開的仍然是那個(gè)還沒被移走的舊文件你緊接著 mv 走它nginx 又繼續(xù)往被移走的那份寫——切割看起來做了實(shí)際新文件一直是空的。這個(gè)坑我在兩三個(gè)環(huán)境里都見過日志目錄里躺著一堆 0 字節(jié)的access.log。#!/usr/bin/env bash # nginx_cut.sh —— 按天切割 nginx 日志保留原文件名 set -euo pipefail source /etc/opsscripts/common.sh # 關(guān)鍵月份目錄要用「昨天」算。每月 1 號(hào)切割時(shí)昨天屬于上個(gè)月 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 才會(huì)重新打開 mv $NGINX_LOG_DIR/$f $MONTH_DIR/${f}.${YESTERDAY} done # USR1 重新打開日志文件不要用 HUPHUP 是重新加載配置 kill -USR1 $(cat $NGINX_PID)三個(gè)參數(shù)值得單獨(dú)說。date -d yesterday依賴系統(tǒng)的時(shí)區(qū)設(shè)置如果服務(wù)器是 UTC 而業(yè)務(wù)按東八區(qū)算切割出來的文件名會(huì)差一天排查時(shí)對(duì)不上。NGINX_PID在 CentOS 上通常是/var/run/nginx.pidUbuntu 的包安裝版常見/run/nginx.pid容器里可能壓根沒有這個(gè)文件——容器場(chǎng)景更推薦在 entrypoint 里用nginx -s reopen。最后kill -USR1發(fā)完不會(huì)等你確認(rèn)新文件是 nginx 收到信號(hào)后才創(chuàng)建的緊接著跑ls可能看不到。要驗(yàn)證的話加一句sleep 1再檢查文件大小。如果系統(tǒng)里有 logrotate其實(shí)不必自己寫這個(gè)腳本。兩者的差別在配置復(fù)雜度上方案優(yōu)點(diǎn)代價(jià)自寫腳本 cron邏輯完全可控能順帶做歸檔和上報(bào)需要自己處理跨月目錄、異常退出、文件句柄logrotate內(nèi)置copytruncate/create模式、壓縮、保留份數(shù)配置分散在/etc/logrotate.d排障要看多個(gè)文件我的習(xí)慣是單機(jī)兩三份日志用腳本機(jī)器多了統(tǒng)一交給 logrotate腳本只負(fù)責(zé)切割后的歸檔上報(bào)?;旌嫌玫臅r(shí)候注意別讓兩套機(jī)制同時(shí)作用于同一個(gè)文件否則會(huì)出現(xiàn)一天切兩次、目錄里一堆空文件的情況。3.2 TOP IP / TOP URL / 狀態(tài)碼統(tǒng)計(jì)字段位置是怎么定下來的合集里的日志分析腳本基本都基于 nginx 默認(rèn)的combined格式$remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent按空格切分后$1是客戶端 IP$4是帶方括號(hào)的時(shí)間$7是請(qǐng)求行含方法、路徑、協(xié)議$9是狀態(tài)碼$10是響應(yīng)字節(jié)數(shù)。這套字段位置是所有寫法的前提一旦你在nginx.conf里加了自定義字段比如$request_time、$upstream_addr后面的位置就會(huì)整體后移腳本全部失效。所以分析腳本的第一行注釋里我一般會(huì)寫明「依據(jù)的 log_format 字段順序」方便后面接手的人核對(duì)。#!/usr/bin/env bash # ngx_top.sh access.log —— 三個(gè)最常用的統(tǒng)計(jì)口徑 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 請(qǐng)求路徑只統(tǒng)計(jì) 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 狀態(tài)碼分布 awk {c[$9]} END{for(i in c) printf %8d %s\n, c[i], i} $LOG_FILE \ | sort -k2,2 echo 指定時(shí)間段內(nèi)的請(qǐng)求數(shù)格式必須與 $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限定只按第一列排序不加這個(gè)限定的話行數(shù)相同時(shí)會(huì)拿整行比較結(jié)果看起來會(huì)很怪。head之前必須排完序awk輸出的順序是哈希遍歷順序不排序直接 head 拿到的不是 TOP。時(shí)間段過濾靠的是字符串比較前提是時(shí)間格式里的月份縮寫和位寬固定——[10/Mar/2024:00:00:00這種格式按字典序排恰好等于按時(shí)間排因?yàn)槿铡⒃隆⒛甑奈粚捯恢?。如果日志里出現(xiàn)[1/Mar/2024這種單位數(shù)日期比較就會(huì)錯(cuò)這時(shí)候要把$4拆出來先規(guī)范化。還有一個(gè)常見誤用用length(a)當(dāng)作 UV。awk {a[$1]} END{print length(a)}得到的是「不同 IP 的個(gè)數(shù)」不是獨(dú)立訪客數(shù)。同一個(gè)用戶換網(wǎng)絡(luò)、整個(gè)公司共用一個(gè)出口 IP這兩種情況它都算不準(zhǔn)。真實(shí) UV 一般靠 cookie 或者前端埋點(diǎn)日志里近似的做法是把 IP 和 User-Agent 拼起來去重a[$1|$12]這類寫法但也只是近似。3.3 網(wǎng)卡流量與端口連接數(shù)/proc/net/dev 的兩列和 netstat 的字段漂移/proc/net/dev是內(nèi)核暴露的網(wǎng)卡計(jì)數(shù)器每行一個(gè)接口格式是「接口名: 接收 16 個(gè)字段 發(fā)送 16 個(gè)字段」。用 awk 的默認(rèn)分隔符連續(xù)空白切分后$1是帶冒號(hào)的接口名$2是累計(jì)接收字節(jié)數(shù)$10是累計(jì)發(fā)送字節(jié)數(shù)。這兩個(gè)數(shù)是單調(diào)遞增的累計(jì)值所以腳本必須自己算差分取一次快照sleep一段時(shí)間再取一次兩個(gè)值相減除以間隔。合集里的版本有個(gè)小問題——它用整數(shù)除法$(( ($NEW_IN - $OLD_IN) / 1024 ))間隔 1 秒的情況下低于 1KB/s 的流量會(huì)顯示成 0跑在低流量機(jī)器上看著像腳本掛了。另一個(gè)隱患是awk $0~$NIC這種拼接寫法如果接口名是eth0機(jī)器上同時(shí)存在eth0和eth0.100就會(huì)兩行都匹配上取到的值隨機(jī)。#!/usr/bin/env bash # nic_mon.sh iface [interval] —— 實(shí)時(shí)看某個(gè)網(wǎng)卡的進(jìn)出流量 set -euo pipefail NIC${1:?用法: nic_mon.sh eth0 [1]} INTERVAL${2:-1} snapshot() { # 用默認(rèn)分隔符$1 是 eth0: 帶冒號(hào)$2 收字節(jié)$10 發(fā)字節(jié) 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) # 網(wǎng)卡重啟或模塊重載會(huì)清零計(jì)數(shù)器出現(xiàn)負(fù)值直接跳過這一輪 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精確匹配帶冒號(hào)的接口名避開了eth0匹配到eth0.100的問題。差分用 awk 來算而不是 bash 算術(shù)是為了保留小數(shù)——bash 的$(( ))只做整數(shù)運(yùn)算。計(jì)數(shù)器回繞或清零那段判斷不是可有可無的虛擬機(jī)熱遷移、網(wǎng)卡 down/up、容器重啟都會(huì)讓計(jì)數(shù)從 0 開始不做保護(hù)的話會(huì)打出一個(gè)幾 TB/s 的負(fù)數(shù)。同一章里的「端口請(qǐng)求數(shù) TOP 20」腳本問題更大。合集版本寫的是 netstat -ant | awk -F本文還有配套的精品資源點(diǎn)擊獲取