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