文件自動(dòng)化清理:從腳本到容器環(huán)境的完整方案)
先說點(diǎn)實(shí)在話。臨時(shí)文件這東西幾乎每個(gè)用電腦的人都會(huì)碰到但真正把它當(dāng)回事的人不多。我曾經(jīng)在一臺(tái)測試服務(wù)器上見過 /tmp 目錄里堆了接近 30GB 的垃圾里面全是各種安裝包殘留、編譯中間產(chǎn)物和半年前的日志切片。更麻煩的是這臺(tái)機(jī)器磁盤已經(jīng)告警了可沒人敢手動(dòng)刪——因?yàn)檎l都不確定里面有沒有哪個(gè)文件正在被某個(gè)服務(wù)使用。這種場景相信不少運(yùn)維和開發(fā)朋友都遇到過。臨時(shí)文件自動(dòng)化方案說白了就是用一套確定的規(guī)則和工具代替“人工不定期清理”這個(gè)不靠譜的習(xí)慣。它解決的是三個(gè)層面的問題磁盤空間被不知不覺吃光、臨時(shí)文件混亂導(dǎo)致排障困難、以及手動(dòng)刪除時(shí)誤刪正在使用的文件造成事故。這篇文章適合運(yùn)維工程師、開發(fā)者、以及想把自己電腦整理干凈但不想天天操心的朋友。我會(huì)從方案選型、腳本設(shè)計(jì)、定時(shí)任務(wù)配置、誤刪防護(hù)一直講到 CI/CD 和容器環(huán)境里的進(jìn)階玩法全程給可復(fù)現(xiàn)的命令和配置。1. 臨時(shí)文件為什么需要一套自動(dòng)化清理方案1.1 臨時(shí)文件是怎么悄悄長大的臨時(shí)文件不是只有一個(gè) /tmp 目錄那么簡單。按照來源大致能分成幾類操作系統(tǒng)運(yùn)行時(shí)會(huì)產(chǎn)生的套接字文件、PID 文件、軟件安裝包殘骸包管理器緩存比如 apt、pip、npm、yarn 留下的緩存開發(fā)工具的編譯產(chǎn)物像 dist、build、target 目錄以及 node_modules/.cache還有日志切割后的歷史文件。每一類單獨(dú)看都不大但架不住積少成多。我見過一個(gè)很典型的例子一個(gè) Java 項(xiàng)目的編譯構(gòu)建每次會(huì)在 /tmp 里留下幾百 MB 的中間文件。開發(fā)環(huán)境跑了幾周/tmp 就被撐到了 20GB。還有個(gè)前端項(xiàng)目node_modules/.cache 里緩存了幾百 MB 的 webpack 持久化緩存磁盤空間莫名其妙就少了。這種問題最大的迷惑性在于你根本不知道是誰寫的也不知道什么時(shí)候?qū)懙呐挪槠饋硖貏e費(fèi)勁。如果是在 Linux 服務(wù)器上inode 耗盡比磁盤滿更隱蔽。一個(gè)目錄里塞了上百萬個(gè)小文件df -h 看著還有空間但 df -i 已經(jīng) 100% 了這時(shí)候任何新文件都創(chuàng)建不了系統(tǒng)行為變得非常詭異。這類問題一旦發(fā)生手動(dòng)清理難度極大沒有自動(dòng)化方案幾乎只能靠重建文件系統(tǒng)來恢復(fù)。1.2 手動(dòng)清理的三個(gè)硬傷手動(dòng)清理臨時(shí)文件看起來簡單實(shí)際做起來有三個(gè)繞不過去的硬傷。第一個(gè)是頻率問題。人一定會(huì)忘記。磁盤告警才想起來去清理往往已經(jīng)是火燒眉毛的時(shí)刻。我見過很多人平時(shí)不關(guān)心等到發(fā)現(xiàn)磁盤滿了才臨時(shí)抱佛腳地刪文件刪完過兩個(gè)星期又滿了陷入無限循環(huán)。第二個(gè)是判斷問題。哪些臨時(shí)文件可以刪、哪些不能刪需要根據(jù)文件的修改時(shí)間、訪問時(shí)間、當(dāng)前是否被進(jìn)程占用來判斷。手動(dòng)操作時(shí)沒人愿意逐項(xiàng)分析結(jié)果要么一刀切亂刪要么干脆一個(gè)都不敢動(dòng)。第三個(gè)是路徑問題。臨時(shí)文件分布在系統(tǒng)的各個(gè)角落從 /tmp、/var/tmp 到用戶目錄下的 .cache、項(xiàng)目目錄里的構(gòu)建產(chǎn)物甚至容器運(yùn)行時(shí)的 overlay 層。手動(dòng)清理很難覆蓋全而且每次執(zhí)行的操作命令可能都不一樣最終效果完全不可控。1.3 自動(dòng)化方案解決的核心問題自動(dòng)化清理最大的價(jià)值不是“替你點(diǎn)了刪除按鈕”而是把清理這回事從“隨機(jī)事件”變成“確定性行為”。確定性的含義包括定期執(zhí)行、規(guī)則明確、操作可審計(jì)、結(jié)果可追蹤。規(guī)則明確指的是你可以精確指定哪些目錄、按什么條件、刪除多老的文件。比如“刪除 /tmp 下超過 7 天沒修改過的文件”這是一條可執(zhí)行、可驗(yàn)證的規(guī)則。操作可審計(jì)指的是每次執(zhí)行都留下日志刪了多少文件、釋放了多少空間都記錄在案出問題能回溯。結(jié)果可追蹤則意味著你能通過日志和告警知道清理動(dòng)作是否成功、是否有異常。一旦這套東西搭建起來它就能自己運(yùn)轉(zhuǎn)不再依賴人工的臨場判斷。我自己的習(xí)慣是“先盤家底、再定規(guī)則、后上自動(dòng)化”下面幾個(gè)章節(jié)就是圍繞這個(gè)思路展開的。2. 方案選型從“能用”到“好用”2.1 系統(tǒng)自帶方案不用白不用如果只看 Linux 系統(tǒng)本身其實(shí)已經(jīng)內(nèi)置了幾套基礎(chǔ)的臨時(shí)文件管理機(jī)制其中最值得了解的是 systemd-tmpfiles 和 tmpreaper。systemd-tmpfiles 通過配置文件來聲明哪些目錄要清理、按什么規(guī)則清理。配置文件放在 /etc/tmpfiles.d/ 下語法很直白。舉個(gè)例子# /etc/tmpfiles.d/clean-tmp.conf d /tmp 1777 root root 7d D /var/tmp 1777 root root 30d第一行表示 /tmp 目錄保留 7 天超過這個(gè)期限的文件會(huì)被 systemd-tmpfiles --clean 處理。第二行的 D 表示目錄本身和里面的內(nèi)容一起清理保留周期 30 天。這個(gè)方案的好處是零額外依賴、聲明式管理適合規(guī)則不復(fù)雜的場景。tmpreaper 則是傳統(tǒng) tmpwatch 的增強(qiáng)版用起來更靈活。比如這條命令tmpreaper 7d /tmp含義是刪除 /tmp 下 7 天未訪問的文件。tmpreaper 的優(yōu)勢在于它考慮了很多邊界情況比如不會(huì)去動(dòng)正在被進(jìn)程使用的文件、知道怎么處理符號(hào)鏈接、還支持排除規(guī)則。不過在不少現(xiàn)代 Linux 發(fā)行版里systemd 已經(jīng)成為默認(rèn) init 系統(tǒng)systemd-tmpfiles 明顯是更“原汁原味”的選擇。還有一個(gè)很實(shí)用的系統(tǒng)機(jī)制是 logrotate。它管理的是日志文件不是普通臨時(shí)文件但日志和臨時(shí)文件經(jīng)?;煸谝黄稹=o日志配置好按大小或日期切割再配合 rotate 份數(shù)限制能從源頭上避免日志文件無限增長。很多生產(chǎn)事故其實(shí)就是日志把磁盤寫滿了。這兩類系統(tǒng)方案的共同問題是“夠用但不夠細(xì)”。如果只有幾條靜態(tài)規(guī)則那直接用 systemd-tmpfiles 就夠了如果你需要按目錄、按條件、按項(xiàng)目自定義復(fù)雜的清理邏輯就得用腳本。2.2 腳本方案最靈活也最危險(xiǎn)腳本方案的核心就是兩個(gè)工具find 負(fù)責(zé)查找cron 或 systemd timer 負(fù)責(zé)定時(shí)執(zhí)行。它靈活在哪里你可以自由組合路徑、時(shí)間條件、文件類型、大小條件還可以在刪除前做任何你想做的預(yù)處理。但是腳本方案也是最危險(xiǎn)的。原因在于find 命令是一個(gè)純粹的“執(zhí)行者”它不會(huì)理解文件是否重要。一條命令寫錯(cuò)可能把不該刪的全刪了。比如很多人剛接觸時(shí)會(huì)寫 find / -name *.log -exec rm {} ;這行命令會(huì)去系統(tǒng)里找所有 .log 文件然后刪掉包括某些應(yīng)用正在寫入的日志后果不堪設(shè)想。因此腳本方案的準(zhǔn)入門檻是你必須嚴(yán)格遵守“先 dry-run、再真實(shí)執(zhí)行”的原則。dry-run 就是只打印會(huì)刪除哪些文件不真正執(zhí)行刪除。這一步能幫你提前發(fā)現(xiàn)規(guī)則寫錯(cuò)的問題。下面這行命令就是 dry-run 的典型寫法find /tmp -type f -mtime 7 -print先看輸出確認(rèn)列出來的文件全是該清的再把 -print 換成 -delete或者接上 -exec rm 來真正執(zhí)行。2.3 工具鏈方案處理特定生態(tài)的殘留系統(tǒng)方案和腳本方案解決的是通用臨時(shí)文件。但在真實(shí)場景里最有價(jià)值的是針對特定生態(tài)做清理。我舉例說明幾類項(xiàng)目構(gòu)建產(chǎn)物。node 項(xiàng)目里的 node_modules/.cache、dist、buildJava 項(xiàng)目里的 targetPython 項(xiàng)目里的pycache。這些目錄量大、增長快而且重建成本低。清理它們通常不用猶豫??梢詫懸粋€(gè)項(xiàng)目級清理腳本當(dāng)項(xiàng)目不再活躍時(shí)自動(dòng)清掉這些構(gòu)建緩存。包管理器緩存。pip 的 ~/.cache/pipnpm 的 ~/.npmapt 的 /var/cache/apt。這些緩存刪掉之后下次安裝包會(huì)重新下載但在磁盤緊張的時(shí)候它們是最好的“出血點(diǎn)”。我通常在 CI 鏡像構(gòu)建的最后階段清掉這些緩存能顯著減小鏡像體積。Docker 相關(guān)的殘留。docker system prune -f 能清理懸空鏡像和停止的容器/var/lib/docker/overlay2 里堆積的層文件是磁盤大戶。配合日志輪轉(zhuǎn)和鏡像保留策略能避免 Docker 占據(jù)大量磁盤。每種生態(tài)都有自己的一套清理規(guī)則腳本的靈活性在這里體現(xiàn)得淋漓盡致。但要注意工具鏈方案往往涉及特定業(yè)務(wù)邏輯最好能結(jié)合項(xiàng)目實(shí)際情況做保留策略而不是一刀切全刪。2.4 選型建議與對比我直接給一個(gè)決策表格方便你對號(hào)入座方案優(yōu)勢劣勢適用場景systemd-tmpfiles零依賴、聲明式、系統(tǒng)原生規(guī)則表達(dá)能力有限常規(guī) /tmp 和 /var/tmp 清理tmpreaper安全邊界處理較好、主動(dòng)檢查占用部分發(fā)行版需額外安裝傳統(tǒng)服務(wù)器、復(fù)雜排除規(guī)則自寫腳本 cron/timer完全可控、可定制業(yè)務(wù)邏輯風(fēng)險(xiǎn)高依賴編寫者水平多目錄、混合規(guī)則的項(xiàng)目環(huán)境docker system prune專攻容器殘留、集成度高只處理 Docker 生態(tài)CI 跑機(jī)、容器化部署環(huán)境商業(yè)/平臺(tái)級工具集中管理、告警完善引入成本高多機(jī)、多團(tuán)隊(duì)的大規(guī)模治理選型的核心原則是盤子里有多少菜決定你用多大鍋。單機(jī)服務(wù)器上幾條規(guī)則別一上來就上重量級平臺(tái)方案而真正的大規(guī)模集群環(huán)境光靠幾個(gè) cron 腳本也撐不住這時(shí)候才需要集中管理和統(tǒng)一的度量體系。3. 實(shí)操搭一套最小可用的自動(dòng)清理系統(tǒng)這一節(jié)我會(huì)完整走一遍實(shí)際搭建流程從盤家底、寫規(guī)則到掛定時(shí)任務(wù)全部是可復(fù)現(xiàn)的命令和配置。我先說明一下我的環(huán)境假設(shè)一臺(tái) Ubuntu 22.04 服務(wù)器Docker 和 Node 環(huán)境都有目標(biāo)是每天自動(dòng)清理各類臨時(shí)文件。3.1 第一步盤點(diǎn)目錄和設(shè)計(jì)保留策略在動(dòng)任何命令之前先花半小時(shí)做一次“家底盤點(diǎn)”。你需要明確知道這臺(tái)機(jī)器上哪些目錄是真正需要清理的以及每個(gè)目錄的合理保留周期。我推薦的盤點(diǎn)方法是先全盤掃一遍目錄占用定位最大的幾個(gè)目錄du -h --max-depth1 /tmp 2/dev/null | sort -hr | head -20 du -h --max-depth1 /var/tmp 2/dev/null | sort -hr | head -20 du -h --max-depth2 ~/.cache 2/dev/null | sort -hr | head -20然后根據(jù)目錄實(shí)際內(nèi)容和業(yè)務(wù)要求給每個(gè)目錄設(shè)定保留周期。我給一套通用的初始策略你可以直接抄目錄保留周期說明/tmp1~3 天系統(tǒng)臨時(shí)文件生命周期極短/var/tmp7~30 天相對持久但也不應(yīng)長期堆積~/.cache/pip、~/.npm30 天重新下載成本低但沒必要天天刪項(xiàng)目 target、dist、node_modules/.cache7 天構(gòu)建產(chǎn)物可重建日志切割后的舊文件7~14 天由 logrotate 管理更佳Docker 懸空鏡像立即清理docker system prune 處理注意保留周期的設(shè)定不是拍腦袋。太短可能導(dǎo)致正在使用的文件被刪太長則清理沒意義。我的經(jīng)驗(yàn)是“寧保守勿激進(jìn)”初次設(shè)定可以給到雙倍周期運(yùn)行一兩周觀察后再逐步縮短。3.2 第二步寫核心清理腳本腳本是整套系統(tǒng)的核心。我給出一個(gè)我實(shí)際在用的版本做了精簡但核心邏輯完整。它支持日志輸出、支持排除規(guī)則、也支持 dry-run 模式。#!/usr/bin/env bash # 臨時(shí)文件自動(dòng)清理腳本 # 用法: # ./clean-tmp.sh --dry-run # 只打印將刪除的文件不實(shí)際刪除 # ./clean-tmp.sh # 真實(shí)執(zhí)行清理 set -euo pipefail DRY_RUNfalse if [[ ${1:-} --dry-run ]]; then DRY_RUNtrue fi LOG_FILE/var/log/clean-tmp.log # 需要清理的目錄列表空格分隔 CLEAN_DIRS(/tmp /var/tmp /root/.cache/pip /root/.npm) # 各目錄保留天數(shù), 默認(rèn)7天 declare -A KEEP_DAYS KEEP_DAYS[/tmp]3 KEEP_DAYS[/var/tmp]14 # 排除規(guī)則的正則表達(dá)式 EXCLUDE_PATTERN(\.dockerenv|\.X11-unix|\.ICE-unix|systemd-private.*-(systemd.*)/) log() { echo $(date %Y-%m-%d %H:%M:%S) $* $LOG_FILE } clean_dir() { local dir$1 local keep_days${KEEP_DAYS[$dir]:-7} local threshold${keep_days} if [[ ! -d $dir ]]; then log SKIP: $dir not exists return 0 fi log START: $dir keep_days$keep_days if [[ $DRY_RUN true ]]; then # dry-run: 只打印 find $dir -type f -mtime $threshold \ -regextype posix-extended ! -regex $EXCLUDE_PATTERN \ -printf %p %s bytes\n else # 真實(shí)刪除刪除前統(tǒng)計(jì)空間 local before0 local after0 before$(du -sb $dir 2/dev/null | awk {print $1}) find $dir -type f -mtime $threshold \ -regextype posix-extended ! -regex $EXCLUDE_PATTERN \ -delete after$(du -sb $dir 2/dev/null | awk {print $1}) log DONE: $dir released $((before - after)) bytes fi } for d in ${CLEAN_DIRS[]}; do clean_dir $d done log ALL DONE這個(gè)腳本有幾個(gè)關(guān)鍵設(shè)計(jì)我說一下理由。set -euo pipefail 是必須的。set -e 讓腳本在遇到錯(cuò)誤時(shí)立即退出set -u 直接爆出未定義變量pipefail 保證管道中任一環(huán)節(jié)失敗都會(huì)讓整個(gè)命令失敗。這三個(gè)組合起來能讓你第一時(shí)間發(fā)現(xiàn)腳本本身的問題而不是讓它帶病運(yùn)行。find 命令中 -type f 避免了誤刪目錄。如果直接 -delete目錄也會(huì)被遞歸刪除風(fēng)險(xiǎn)極大。保留目錄結(jié)構(gòu)只刪除文件是更穩(wěn)妥的路徑。-delete 相比于 -exec rm {} 更安全因?yàn)?find 內(nèi)置的 -delete 會(huì)在內(nèi)部處理一些邊界情況比如先檢查目錄是否為空避免刪非空目錄時(shí)出錯(cuò)。排除規(guī)則用的是 -regextype posix-extended 和 -regex它的作用是保護(hù)特殊路徑。比如 systemd-private 動(dòng)態(tài)生成的私有目錄里面可能存有運(yùn)行時(shí) socket不能亂刪。腳本里的 du 統(tǒng)計(jì)可以幫你直觀看到每次釋放了多少空間日志對排查很有幫助。3.3 第三步配置定時(shí)任務(wù)腳本寫好后剩下的就是讓它按時(shí)跑起來。Linux 下有兩種主流方式cron 和 systemd timer。cron 是最傳統(tǒng)的方式配置很簡單。編輯 /etc/crontab 或使用 crontab -e加入一行0 2 * * * root /opt/scripts/clean-tmp.sh /var/log/clean-tmp.log 21這表示每天凌晨 2 點(diǎn)執(zhí)行一次清理腳本。為什么選這個(gè)時(shí)間因?yàn)榕R時(shí)文件清理的最佳窗口是業(yè)務(wù)低峰期大多數(shù)服務(wù)在凌晨的負(fù)載最低。如果服務(wù)器上跑著夜間批處理任務(wù)就需要避開那段時(shí)間。systemd timer 是更現(xiàn)代的選擇。我需要先創(chuàng)建一個(gè) service 單元和一個(gè) timer 單元# /etc/systemd/system/clean-tmp.service [Unit] DescriptionClean temporary files Afternetwork.target [Service] Typeoneshot ExecStart/opt/scripts/clean-tmp.sh# /etc/systemd/system/clean-tmp.timer [Unit] DescriptionRun clean-tmp daily at 02:00 [Timer] OnCalendar*-*-* 02:00:00 Persistenttrue [Install] WantedBytimers.target然后執(zhí)行systemctl daemon-reload systemctl enable --now clean-tmp.timersystemd timer 相比 cron 有幾個(gè)優(yōu)勢可以設(shè)置 Persistenttrue即使機(jī)器當(dāng)時(shí)關(guān)機(jī)下次開機(jī)后也會(huì)補(bǔ)跑錯(cuò)過的任務(wù)可以用 systemctl status 查看任務(wù)最近執(zhí)行情況日志集中管理方便排查。我個(gè)人的建議是新環(huán)境優(yōu)先用 systemd timer老環(huán)境沿用 cron 也沒問題。如果你用的是 Windows 服務(wù)器最常用的方式是“任務(wù)計(jì)劃程序”配合 PowerShell 腳本。PowerShell 里頭找臨時(shí)文件同樣簡單$threshold (Get-Date).AddDays(-7) Get-ChildItem -Path C:\Windows\Temp -Recurse -File | Where-Object { $_.LastWriteTime -lt $threshold } | Remove-Item -Force -ErrorAction SilentlyContinue在任務(wù)計(jì)劃程序里設(shè)置每天執(zhí)行一次即可。注意 C:\Windows\Temp 有很多文件被系統(tǒng)占用刪除時(shí)要帶上 -ErrorAction SilentlyContinue 忽略失敗否則任務(wù)會(huì)報(bào)錯(cuò)。3.4 第四步先演練再上崗腳本和定時(shí)任務(wù)都配置好了但我不建議直接讓它以真實(shí)刪除模式運(yùn)行。正確做法是先 dry-run 跑幾天。第一輪演練手動(dòng)執(zhí)行 --dry-run仔細(xì)觀察輸出內(nèi)容。重點(diǎn)看兩個(gè)問題是否有不該刪的文件被列出來了是否有應(yīng)該清理的目錄被排除規(guī)則擋掉了。發(fā)現(xiàn)異常就調(diào)整腳本改完再跑。第二輪演練真實(shí)刪除但選擇業(yè)務(wù)最低峰期手動(dòng)執(zhí)行一次。執(zhí)行前確保已經(jīng)備份哪怕只是備份腳本并且開啟了日志。等腳本連續(xù)跑了一周沒有異常再把它交給 cron 或 systemd timer 去自動(dòng)執(zhí)行。這時(shí)候才算真正上崗。我特別想強(qiáng)調(diào)一點(diǎn)自動(dòng)清理系統(tǒng)的“上線”不是一錘子買賣。它應(yīng)該像監(jiān)控系統(tǒng)一樣需要持續(xù)迭代。磁盤增長趨勢變化了、業(yè)務(wù)新增了臨時(shí)文件目錄、某個(gè)服務(wù)生命周期變短了這些都要反映到清理規(guī)則里。我基本上每季度會(huì)重盤一遍目錄占用同步更新一次腳本。4. 誤刪與安全清理工具的底線工程自動(dòng)化清理的恐怖之處在于它不會(huì)疲倦也不會(huì)猶豫它是按照你的規(guī)則去執(zhí)行每一個(gè)刪除動(dòng)作。如果規(guī)則有誤它會(huì)毫不猶豫地把所有命中的文件殺掉。所以守住安全底線是這套系統(tǒng)的生死線。4.1 正在被使用的臨時(shí)文件會(huì)怎樣這個(gè)現(xiàn)象值得單獨(dú)講。很多人在 Linux 上發(fā)現(xiàn)一個(gè)詭異的事情某個(gè)文件明明被刪除了但磁盤空間并沒有釋放。這是因?yàn)橛幸粋€(gè)進(jìn)程仍然持有該文件的打開句柄。在 Linux 的文件系統(tǒng)語義里文件刪除只是把目錄項(xiàng)移除但如果文件還被進(jìn)程打開著它的 inode 和磁盤塊并不會(huì)立刻釋放要等所有句柄關(guān)閉之后才真正回收。這意味著什么如果服務(wù)在運(yùn)行時(shí)寫了一個(gè)日志文件你把它刪了服務(wù)并不會(huì)立刻崩潰日志會(huì)繼續(xù)寫入這個(gè)已經(jīng)“不存在”的文件里但你再也找不到這個(gè)文件了。直到服務(wù)重啟文件才被真正刪除期間磁盤空間一直占用著。排障的時(shí)候這種情況非常隱蔽。你 df -h 看磁盤空間沒少但目錄里文件又不見了很容易懷疑是被病毒入侵或者有日志清理任務(wù)出了問題。其實(shí)根因可能就是有人在 /tmp 里刪了某個(gè)正在使用的文件。避免這種問題最直接的辦法是用 lsof 檢查文件是否被占用。生產(chǎn)環(huán)境的清理腳本里我建議加一段# 檢查目錄中是否有被進(jìn)程打開的文件 find /var/tmp -type f -mtime 7 -print0 2/dev/null | xargs -0 -r lsof 2/dev/null如果 lsof 輸出有結(jié)果說明有文件正在使用中應(yīng)該調(diào)整保留策略或者將這些文件排除掉。4.2 權(quán)限、符號(hào)鏈接和變量空值三個(gè)經(jīng)典大坑清理腳本里最容易翻車的三個(gè)點(diǎn)權(quán)限、符號(hào)鏈接和變量空值。權(quán)限問題很好理解很多臨時(shí)目錄屬于 root普通用戶清理不了這需要用 root 或 sudo 執(zhí)行清理腳本。但問題在于一旦用 root 執(zhí)行腳本里哪怕有一個(gè)地方邏輯混亂后果會(huì)被無限放大。我的建議是腳本執(zhí)行的用戶權(quán)限要有明確邊界比如用獨(dú)立用戶跑清理任務(wù)不給不必要的 root 權(quán)限。符號(hào)鏈接坑是最隱蔽的。想象一下 /tmp/link 是一個(gè)指向 /etc 的符號(hào)鏈接。如果你執(zhí)行 find /tmp -type d -name link -delete危險(xiǎn)不大但如果你在腳本里寫了 rm -rf /tmp/*在某個(gè)特殊情況下rm 會(huì)對符號(hào)鏈接指向的目標(biāo)進(jìn)行操作尤其是配合 --no-preserve-root 之類參數(shù)時(shí)可能把系統(tǒng)關(guān)鍵目錄刪掉。我見過一次事故有人把 find 的 -exec rm -rf {} ; 誤應(yīng)用于一個(gè)包含符號(hào)鏈接的目錄結(jié)果把鏈接指向的應(yīng)用目錄整個(gè)刪了。所以腳本里涉及目錄刪除時(shí)務(wù)必加上 -type d 的限制并且顯式排除符號(hào)鏈接find ... ! -type l。變量空值是最容易被新手忽視的經(jīng)典坑。在 bash 腳本中如果某個(gè)變量沒有賦值默認(rèn)就是空字符串。假設(shè)你寫了 rm -rf $DIR/而 $DIR 因?yàn)槟撤N原因是空的那么命令行就變成 rm -rf /結(jié)果是災(zāi)難性的。防范手段很簡單腳本開頭用 set -u 讓未定義變量直接報(bào)錯(cuò)或者在刪除前斷言變量非空[[ -n $DIR ]] || exit 1。順便說一個(gè)和空格相關(guān)的坑。文件名里帶空格是合法的但很多腳本在拼接路徑時(shí)沒有加引號(hào)find 的輸出在管道傳給 xargs 時(shí)會(huì)被錯(cuò)誤分隔。處理辦法是使用 -print0 和 xargs -0或者直接用 -delete。我在 3.2 的腳本里使用 -delete就是考慮到這個(gè)。4.3 從“刪”到“移”更穩(wěn)妥的回收站模式對很多場景來說直接物理刪除是不必要的冒險(xiǎn)。更穩(wěn)妥的方式是“移動(dòng)”也就是先設(shè)置一個(gè)回收桶目錄把該清理的文件移動(dòng)進(jìn)去觀察一段時(shí)間沒有異常再真正刪除。這在生產(chǎn)環(huán)境里非常實(shí)用。比如我可以把臨時(shí)文件先移到 /var/tmp/trash保留 14 天后再自動(dòng)徹底刪除。這樣即使有誤判你還有回旋余地可以手工恢復(fù)到原位置。實(shí)現(xiàn)起來也不復(fù)雜在之前的清理腳本基礎(chǔ)上把 -delete 替換成 -exec mv {} /var/tmp/trash/ 即可。我實(shí)際用下來這個(gè)模式既滿足了清理需求又大幅降低了誤刪造成的不可逆風(fēng)險(xiǎn)。唯一要注意的是回收桶目錄自身的清理別讓它變成另一個(gè)堆積點(diǎn)。給回收桶設(shè)置一個(gè)更長的保留周期比如 30 天然后定期清一遍。4.4 日志、告警與問題速查自動(dòng)化清理必須留痕。日志是事后排查的唯一依據(jù)。我在腳本里已經(jīng)把每次清理的路徑和釋放空間寫入 /var/log/clean-tmp.log。如果你有集中日志平臺(tái)建議把這份日志同步上去如果沒有至少確保日志會(huì)按天切割避免單個(gè)文件無限增大。告警也很重要。單純把任務(wù)掛到 cron 里有效期是“靜默失敗”如果腳本出錯(cuò)了可能只會(huì)默默寫個(gè)日志就完事。我的做法是在腳本末尾加上簡單的健康檢查如果本次釋放空間為 0或者腳本執(zhí)行異常就通過 webhook 發(fā)送告警到值班群。這不需要復(fù)雜的監(jiān)控平臺(tái)curl 一條 webhook 就能搞定。梳理一下常見問題方便你快速定位癥狀可能原因排查方法磁盤空間沒減小文件被進(jìn)程占用未釋放lsof 檢查句柄、重啟服務(wù)后觀察日志顯示刪除了但目錄還在find 匹配的是文件目錄未被刪除檢查排除規(guī)則、目錄層級某些目錄從未被清理名稱包含空格/特殊字符路徑拼接失敗用 -print0 驗(yàn)證開啟 set -u清理任務(wù)不執(zhí)行cron 環(huán)境變量或 systemd timer 狀態(tài)異常systemctl status clean-tmp.timer誤刪了業(yè)務(wù)文件排除規(guī)則不完整、保留周期過短立即停止任務(wù)檢查回收桶恢復(fù)這些坑我基本上都踩過一遍。最有價(jià)值的教訓(xùn)是永遠(yuǎn)先 dry-run永遠(yuǎn)先移動(dòng)后刪除永遠(yuǎn)要留日志。記住這三條自動(dòng)清理就不會(huì)成為災(zāi)難制造機(jī)。5. 進(jìn)階多機(jī)、CI 與容器環(huán)境里的臨時(shí)文件治理5.1 給 CI 跑機(jī)做磁盤減負(fù)CI 系統(tǒng)是臨時(shí)文件堆積的重災(zāi)區(qū)。尤其是自建 GitLab Runner 或 Jenkins 節(jié)點(diǎn)每跑一次構(gòu)建都會(huì)在 workspace 和 /tmp 里留下大量中間產(chǎn)物。如果構(gòu)建頻率高磁盤空間兩三天就能告警。我處理過一臺(tái) GitHub Actions 自建 runner每天跑幾十次構(gòu)建每次構(gòu)建產(chǎn)生的臨時(shí)文件從幾十 MB 到數(shù) GB 不等。最初磁盤 100GB半個(gè)月就滿了。后來加了雙重機(jī)制解決第一重是定時(shí)清理用上面說的腳本每天早上 4 點(diǎn)清一次第二重是構(gòu)建結(jié)束鉤子在每一個(gè) job 收尾時(shí)主動(dòng)清理該構(gòu)建產(chǎn)生的臨時(shí)目錄。構(gòu)建結(jié)束鉤子其實(shí)就是 CI 配置文件里的一段代碼比如 GitHub Actions 的 post 步驟- name: Cleanup workspace if: always() run: | docker system prune -f sudo rm -rf /tmp/* /var/tmp/* || true sudo rm -rf /home/runner/work/*/node_modules /home/runner/work/*/*/dist || true注意這里用的是 if: always()保證即使構(gòu)建失敗清理也會(huì)執(zhí)行。CI 跑機(jī)的治理思路是定時(shí)清理兜底構(gòu)建鉤子主動(dòng)清理雙保險(xiǎn)。另外 CI 鏡像構(gòu)建階段清掉包管理器緩存能顯著減小鏡像體積、縮短推送時(shí)間。5.2 多機(jī)統(tǒng)一治理從“單機(jī)腳本”到“批量執(zhí)行”當(dāng)服務(wù)器數(shù)量多到一定程度逐臺(tái)配置 cron 是不可維護(hù)的。這時(shí)候需要把清理規(guī)則集中管理。我常用的方案有兩種。一種是用 Ansible 之類的配置管理工具。把清理腳本和 timer 單元作為基礎(chǔ)設(shè)施代碼推送到所有目標(biāo)機(jī)器。好處是配置變更可以快速批量生效而且可以統(tǒng)一版本管理。另一種是搭建一個(gè)簡單的中心化日志和告警管道每臺(tái)機(jī)器跑完清理后把結(jié)果匯總到同一處比如收集日志到 Elasticsearch 或用簡單的 Webhook 上報(bào)。批量執(zhí)行時(shí)有一個(gè)重要細(xì)節(jié)不要在同一時(shí)間讓所有機(jī)器同時(shí)清理否則可能會(huì)同時(shí)觸發(fā)集中監(jiān)控平臺(tái)的告警風(fēng)暴也可能造成集中的 I/O 沖擊。給每臺(tái)機(jī)器的清理時(shí)間加上隨機(jī)偏移量比如 2 點(diǎn)到 3 點(diǎn)之間隨機(jī)執(zhí)行是一個(gè)成熟的做法。在 Kubernetes 集群里情況稍有不同。節(jié)點(diǎn)的臨時(shí)目錄和容器的 emptyDir 卷由 kubelet 管理kubelet 會(huì)根據(jù) eviction 閾值自動(dòng)驅(qū)逐容器釋放臨時(shí)存儲(chǔ)。但這是“兜底式”的治理主要目的是避免整個(gè)節(jié)點(diǎn)磁盤寫滿。業(yè)務(wù)層面仍然需要自己管理鏡像殘留和構(gòu)建緩存。docker system prune 在容器節(jié)點(diǎn)上仍然很有用配合定期執(zhí)行能顯著減少磁盤占用。5.3 容器場景的特殊考量容器里的 /tmp 和宿主機(jī)上的 /tmp 有本質(zhì)不同。容器內(nèi)每個(gè)文件系統(tǒng)層都是臨時(shí)的容器刪除后層也隨之消失。但也因此產(chǎn)生了一個(gè)問題如果你在容器里堆積了大量臨時(shí)文件而容器長期不重建這些文件會(huì)一直占著內(nèi)存或磁盤。兩個(gè)實(shí)踐建議第一容器內(nèi)盡量避免使用 /tmp 做持久化存儲(chǔ)應(yīng)該使用掛載卷或者 emptyDir第二對跑長時(shí)間任務(wù)的有狀態(tài)容器定期重建是比清理內(nèi)部臨時(shí)文件更徹底的辦法。docker system prune 這個(gè)命令我在前面提到了展開說幾個(gè)參數(shù)docker system prune -f --volumes --filter until72h-f 表示不交互確認(rèn)--volumes 表示同時(shí)清理未使用的卷--filter until72h 只清理 72 小時(shí)前創(chuàng)建的懸空資源。這個(gè)命令特別適合 CI 跑機(jī)清理效果立竿見影。但注意默認(rèn)情況下 docker system prune 不會(huì)清理還在使用的鏡像和容器這一點(diǎn)安全性還可以不過加上 --volumes 之后要確認(rèn)沒有重要的持久卷被誤傷。還有一類常見容器殘留是構(gòu)建緩存。BuildKit 的緩存目錄可能非常龐大。對應(yīng)清理命令是 docker builder prune -f建議在 CI 構(gòu)建完成后定期執(zhí)行??偨Y(jié)一下容器的清理哲學(xué)優(yōu)先重建其次治理鏡像最后才是容器內(nèi)部清理。5.4 建立長效機(jī)制從“清理”到“預(yù)防”臨時(shí)文件治理的終極目標(biāo)不是“清得多”而是“產(chǎn)生得少”。當(dāng)自動(dòng)化方案穩(wěn)定運(yùn)行之后我會(huì)建議你花更多時(shí)間從源頭減少臨時(shí)文件的產(chǎn)生。具體可以做的事情很多規(guī)范日志切割配置 logrotate 的 maxsize 和 rotate統(tǒng)一包管理器緩存目錄并設(shè)置定期清理在 CI 流水線里對構(gòu)建產(chǎn)物做歸檔并自動(dòng)刪除舊版本使用 tmpfs 掛載 /tmp讓系統(tǒng)重啟時(shí)臨時(shí)空間自動(dòng)清空對需要的臨時(shí)數(shù)據(jù)直接寫入內(nèi)存文件系統(tǒng)而不是寫盤再刪。以 tmpfs 為例把 /tmp 掛載到內(nèi)存中讀寫速度快是一個(gè)好處更關(guān)鍵的是重啟后內(nèi)容自動(dòng)清空完全不需要人為干預(yù)。但有個(gè)前提內(nèi)存要夠大否則 /tmp 占滿內(nèi)存反而影響系統(tǒng)穩(wěn)定性。在內(nèi)存資源充足的服務(wù)器上這是一個(gè)很優(yōu)雅的方案。預(yù)防思維的本質(zhì)是把“清理臨時(shí)文件”這件苦差事從“事后補(bǔ)救”變成“設(shè)計(jì)時(shí)考慮”。你可以在系統(tǒng)設(shè)計(jì)階段就把臨時(shí)文件的存儲(chǔ)路徑、保留時(shí)長、清理責(zé)任劃分清楚。能做到這一步之后維護(hù)的負(fù)擔(dān)會(huì)直線下降?;氐轿易约旱捏w會(huì)臨時(shí)文件自動(dòng)化治理最被低估的價(jià)值其實(shí)不是省了多少 GB 磁盤而是讓系統(tǒng)行為變得可預(yù)期。定時(shí)任務(wù)每天都在跑日志每天都在記磁盤占用曲線慢慢變得平緩。那種“不知道哪一天磁盤會(huì)滿”的焦慮感消失之后你才有余力去做更有價(jià)值的事。這套方案本身不復(fù)雜但它需要你一點(diǎn)點(diǎn)打磨從單機(jī)腳本到多機(jī)批量從被動(dòng)清理到主動(dòng)預(yù)防每一步都是在為系統(tǒng)的長期穩(wěn)定夯實(shí)基礎(chǔ)。最后再分享一條個(gè)人經(jīng)驗(yàn)每次清理腳本變更后至少保留一周的 dry-run 日志不要急著刪除。這些日志是你在排查“是不是清理腳本誤刪了文件”時(shí)最有力的證據(jù)。臨時(shí)文件清理這種操作寧可慢一點(diǎn)、啰嗦一點(diǎn)也千萬別拿生產(chǎn)環(huán)境做賭注。