現(xiàn)日志按天切割的完整方案與避坑指南)
一位朋友在做幾個小系統(tǒng)的日志整理時被單個日志文件動輒幾個GB的問題搞得焦頭爛額查起問題來非常吃力。他把需求丟給我核心就一句話怎么用Shell把日志按天切到不同文件去。這問題看著簡單但真正落地時涉及時間格式、切割策略、歷史清理、容錯處理一堆細(xì)節(jié)。這篇文章就把我實(shí)際的寫法和踩過的坑完整分享出來。1. 為什么日志按天切割是最務(wù)實(shí)的選擇做日志管理的同學(xué)應(yīng)該都有體會日志文件不切的話三個月甚至半年前的歷史記錄和今天的日志混在一個文件里體積直奔幾個GB甚至十幾GB。用view或者tail去翻日志加載起來卡頓嚴(yán)重用grep搜索某個報(bào)錯關(guān)鍵字要在海量數(shù)據(jù)里掃一遍慢到懷疑人生。更麻煩的是某天系統(tǒng)出問題你想定位到具體某一天的某幾個小時在沒有時間切分的情況下只能自己估算偏移量然后用sed -n去硬跳效率極低。按天切割最直接的好處有三點(diǎn)。第一單個文件體積可控一天的量最多幾百M(fèi)Bgrep和tail都很流暢。第二排查問題可以非常精準(zhǔn)地定位到日期比如grep ERROR /var/log/app/app-2025-03-18.log一分鐘之內(nèi)就能圈定異常范圍。第三歸檔和清理非常自然刪除或者壓縮三個月前的舊日志只需要對文件名做日期比較一條find命令搞定不用寫復(fù)雜的腳本去計(jì)算偏移量。那切割方案怎么選業(yè)界常見的思路大致有三種用系統(tǒng)自帶的logrotate、用第三方工具比如cronolog、以及自己寫Shell腳本配合crontab調(diào)度。之前很多項(xiàng)目用logrotate它確實(shí)很成熟支持按大小、按時間輪轉(zhuǎn)配置也不復(fù)雜。但在一些自定義場景下比如日志文件名需要嵌入特定業(yè)務(wù)標(biāo)識、切割后需要馬上壓縮、或者某些服務(wù)啟動方式比較特殊導(dǎo)致信號通知麻煩logrotate的靈活度就不太夠。所以說自己寫一套Shell腳本配合系統(tǒng)的cron調(diào)度反而是最可控、最容易定制、也最好移植的辦法——這也是我推薦大多數(shù)人嘗試的路線。自研方案還有一個容易被忽視的好處不依賴額外軟件包。很多內(nèi)網(wǎng)環(huán)境和容器基礎(chǔ)鏡像都是精簡過的連cronolog和logrotate都沒裝。而Shell腳本只要有bash、date、mv、gzip這些基礎(chǔ)工具就能跑幾乎零依賴。這在部署層面能省掉很多溝通成本。2. 切割方案的核心思路與流程設(shè)計(jì)在寫腳本之前先把整體流程在腦子里推演一遍。按天切割的本質(zhì)邏輯就是每天在固定時間點(diǎn)把當(dāng)前正在寫入的日志文件復(fù)制一份歸檔并記錄下歸檔日期然后清空原文件讓服務(wù)繼續(xù)往原文件里寫。注意這里用的是“復(fù)制后清空”而不是“直接改名”要說清楚為什么否則新手非常容易踩坑。很多服務(wù)進(jìn)程在啟動時就持有日志文件的文件描述符寫入時會通過這個文件描述符往后追加內(nèi)容。如果直接用mv把原文件改名文件描述符不會跟著名字走它還是指向原來的inode。改名之后服務(wù)再寫日志內(nèi)容依然會落到那個已經(jīng)被移走的文件里而新創(chuàng)建的以原名命名的文件反而一直沒有內(nèi)容。這就是常見的“改名后日志不寫了”的坑。另外一種情況是如果你先刪除文件再創(chuàng)建新文件舊文件的inode在進(jìn)程沒有關(guān)閉它之前不會釋放文件內(nèi)容也不會消失但磁盤空間卻一直被占用你以為是清理掉了結(jié)果磁盤還是滿的。所以正確的做法是先cp一份當(dāng)前日志內(nèi)容到帶日期的歸檔文件然后用: file或者truncate -s 0 file把原文件變成空文件。這樣進(jìn)程持有的文件描述符不變寫入不會中斷同時歸檔文件已經(jīng)拿到了完整的歷史數(shù)據(jù)。這個方案唯一的小瑕疵是切割瞬間可能會有極少量的日志丟失進(jìn)程寫入正在發(fā)生而cp已經(jīng)執(zhí)行完的情況下但對于絕大多數(shù)普通應(yīng)用來說這種亞秒級的遺漏完全可以忽略。如果你做的是銀行交易系統(tǒng)、支付流水那種不容丟失的場景那就不該用Shell腳本而應(yīng)該引入專門的日志采集框架。切割的時間點(diǎn)怎么定我一般選在凌晨的00:00:01也就是0 0 * * *這個cron時間點(diǎn)。為什么選這個時刻而不是美其名曰“業(yè)務(wù)低峰期”夜里相對空閑切割動作雖然很輕量但萬一當(dāng)天日志量特別大cp會占用一部分IO這個點(diǎn)用戶基本不受影響。同時日志天然按“自然日”來分凌晨0點(diǎn)整切割文件名中的日期就是昨天邏輯一目了然。如果你跑的業(yè)務(wù)是跨夜的結(jié)算任務(wù)0點(diǎn)前后都會有一波高峰也可以把切割時間往后挪半小時比如30 0 * * *根據(jù)自己的業(yè)務(wù)節(jié)奏靈活調(diào)整就好。還有一個容易忽略的點(diǎn)處理cron環(huán)境變量。cron運(yùn)行時的PATH往往很精簡可能只有/usr/bin:/bin但某些腳本里用到的命令比如gzip如果裝在/usr/local/bin會找不到導(dǎo)致腳本靜默失敗。穩(wěn)妥的做法是在腳本開頭強(qiáng)制定義PATH或者直接用絕對路徑調(diào)用關(guān)鍵命令。我習(xí)慣在腳本里顯式寫上export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin一勞永逸。3. 完整實(shí)現(xiàn)從基礎(chǔ)版到進(jìn)階版先寫一個最核心的切割腳本不搞花活邏輯直白適合快速上手。#!/bin/bash # 日志按天切割腳本 - 基礎(chǔ)版 # 適用場景應(yīng)用程序持續(xù)寫入單一日志文件需要每日切割歸檔 # 關(guān)鍵變量定義 LOG_DIR/var/log/myapp LOG_NAMEapp.log STAMP$(date %F) # 當(dāng)前日期格式 2025-03-18 ARCHIVE_FILE${LOG_DIR}/${LOG_NAME%.log}-${STAMP}.log # 第一步復(fù)制當(dāng)前日志到歸檔文件 cp -p ${LOG_DIR}/${LOG_NAME} ${ARCHIVE_FILE} # 第二步清空原日志文件注意不是刪除 : ${LOG_DIR}/${LOG_NAME} # 可選壓縮歸檔文件節(jié)省空間 gzip ${ARCHIVE_FILE}這段腳本的每一行都有講究。date %F是date %Y-%m-%d的簡寫輸出格式穩(wěn)定不會因?yàn)檎Z言環(huán)境不同而出現(xiàn)月份英文縮寫。${LOG_NAME%.log}是Shell參數(shù)擴(kuò)展把a(bǔ)pp.log中的.log后綴去掉得到app這樣歸檔文件名就是app-2025-03-18.log語義清晰。如果日志文件名是access.log那歸檔就是access-2025-03-18.log一眼能看出對應(yīng)關(guān)系。cp -p這個-p參數(shù)很多人會忽略但非常重要。它表示保留原始文件的屬性時間戳、所有者等歸檔文件的時間戳?xí)辉O(shè)置為日志文件當(dāng)前的修改時間這樣你看到歸檔文件名里的日期和文件實(shí)際修改時間是一致的查問題時會少很多困惑。然后的: 這里是Shell內(nèi)置的空命令:后面跟一個重定向作用是打開文件并以空內(nèi)容覆蓋它。對進(jìn)程來說這個操作只是把原文件截?cái)嚅L度變?yōu)?文件描述符和inode都沒變所以后續(xù)寫入會依然正常追加。強(qiáng)調(diào)一遍絕對不要在這里用rm否則就會出現(xiàn)上一節(jié)說的服務(wù)繼續(xù)往已刪除文件里寫數(shù)據(jù)的問題?;A(chǔ)版完成后我會建議在此基礎(chǔ)上加兩個能力一個是不壓縮的按天保留策略一個是手動執(zhí)行時的時間校驗(yàn)。因?yàn)榛A(chǔ)版在真實(shí)環(huán)境中只能算“能跑”距離“好維護(hù)”還有一段距離。#!/bin/bash # 日志按天切割腳本 - 進(jìn)階版 # 新增能力壓縮自動清理N天前的舊日志手動執(zhí)行時的時間校驗(yàn) LOG_DIR/var/log/myapp LOG_NAMEapp.log RETENTION_DAYS90 # 手動執(zhí)行時可以通過環(huán)境變量傳入日期比如 STAMP2025-03-17 ./cut_log.sh if [ -z $STAMP ]; then STAMP$(date %F) fi ARCHIVE_FILE${LOG_DIR}/${LOG_NAME%.log}-${STAMP}.log # 保證目標(biāo)目錄存在 [ -d $LOG_DIR ] || { echo ERROR: $LOG_DIR 不存在; exit 1; } # 切割 cp -p ${LOG_DIR}/${LOG_NAME} ${ARCHIVE_FILE} : ${LOG_DIR}/${LOG_NAME} # 壓縮歸檔 if [ -f ${ARCHIVE_FILE} ]; then gzip ${ARCHIVE_FILE} fi # 清理N天前的歸檔日志只清理本應(yīng)用前綴的文件 find ${LOG_DIR} -name ${LOG_NAME%.log}-*.log.gz -mtime ${RETENTION_DAYS} -delete進(jìn)階版里加入了保留天數(shù)的清理邏輯。find里先用-name ${LOG_NAME%.log}-*.log.gz限定文件名模式確保只清理這個應(yīng)用自己的歷史日志不會誤傷同目錄下其他文件。-mtime 90的意思是從當(dāng)前時間往前推90天以前修改過的文件這里用號注意它是“大于90天”而不是“第90天”邊界值差了24小時放在腳本里影響不大追求精確可以省略號直接用-mtime 90。關(guān)于gzip這一步壓縮時機(jī)放在切割動作完成之后。如果當(dāng)天日志量特別大會有一點(diǎn)耗時但gzip的壓縮速度很快一天幾個GB的文本壓下來通常也就十幾秒放在凌晨完全無感。壓縮后歸檔文件從幾個GB變成幾百M(fèi)B磁盤空間壓力驟減。配合上90天的保留策略磁盤占用就能維持在一個相對穩(wěn)定的水位。4. 調(diào)度配置與真實(shí)場景落地腳本寫好了剩下的關(guān)鍵一步就是讓系統(tǒng)按計(jì)劃執(zhí)行它。這里用crontab來完成調(diào)度。# 編輯當(dāng)前用戶的crontab crontab -e # 加入以下一行 0 0 * * * /usr/local/bin/cut_log.sh /var/log/cut_log.log 21這行cron表達(dá)式從左到右依次是分鐘、小時、日、月、周。0 0 * * *表示每天0點(diǎn)0分執(zhí)行一次。執(zhí)行路徑有兩個重點(diǎn)第一腳本要放在固定位置比如/usr/local/bin/不要隨手丟在家目錄里。因?yàn)閏ron執(zhí)行時的工作目錄和登錄Shell不一樣如果你在腳本里用了相對路徑大概率找不到文件。寫腳本時也建議把所有路徑都寫成絕對路徑。第二執(zhí)行日志要重定向到獨(dú)立文件 /var/log/cut_log.log 21。很多人寫cron任務(wù)時會忽略輸出結(jié)果腳本運(yùn)行出錯時完全無跡可尋。加上這行之后腳本的stdout和stderr都會被記錄到日志文件里排查時能省很多力氣。腳本里我還習(xí)慣性地加上關(guān)鍵操作的回顯比如echo [$(date %F %T)] 切割完成: ${ARCHIVE_FILE}.gz這樣看著執(zhí)行日志就能確認(rèn)切割動作是否如期發(fā)生。說完配置再來聊幾個真實(shí)場景里的細(xì)節(jié)。場景一應(yīng)用自己會重開日志文件。有些Java應(yīng)用默認(rèn)使用log4j2之類的日志框架配置了滾動策略日志文件名甚至?xí)蛱?。這時候你再去cp一個固定文件名的日志就不一定對了。遇到這種情況先把應(yīng)用的日志策略改成“單文件持續(xù)寫入”或者“只按大小滾動”然后在Shell層做統(tǒng)一切割如果應(yīng)用本身已經(jīng)按天滾動那你根本不需要再寫Shell腳本直接讓日志框架側(cè)完成歸檔Shell腳本只負(fù)責(zé)壓縮和清理就好。盲目疊加兩層切割邏輯會出現(xiàn)重復(fù)歸檔、文件名混亂影響排查。場景二多個服務(wù)共享一個日志目錄。假設(shè)/var/log/myapp/下跑了好幾個子服務(wù)每個服務(wù)一個日志文件。這種情況下我會建議把切割參數(shù)提取出來做成“一個腳本傳參復(fù)用”。比如腳本寫成cut_log.sh 服務(wù)名 日志名通過$1、$2接收參數(shù)這樣cron里寫多行每行調(diào)一次腳本傳不同參數(shù)即可不需要把同一個邏輯復(fù)制粘貼好幾份。場景三容器環(huán)境。容器里通常沒有cron守護(hù)進(jìn)程你也不應(yīng)該為了跑cron特意在容器里裝額外軟件。換成在宿主機(jī)上配置cron通過掛載的日志目錄卷去執(zhí)行切割腳本或者用Kubernetes的CronJob在Pod里執(zhí)行同樣的腳本。核心腳本本身不需要任何改動因?yàn)樗僮鞯闹皇俏募湍夸洸魂P(guān)心容器還是虛擬機(jī)。5. 常見問題與排查技巧實(shí)錄按天切割這個需求本身原理不復(fù)雜但在真實(shí)操作中幾乎每個來找我?guī)兔Φ呐笥讯疾冗^不同的坑。我把最常見的幾個問題和排查思路整理成了速查表直接對照著查就行?,F(xiàn)象可能原因排查與解決切割后原日志文件不再增長腳本用了mv或rm而不是截?cái)嗲蹇諜z查腳本中是否用了: 或truncate確認(rèn)應(yīng)用進(jìn)程是否仍指向舊inode必要時重啟應(yīng)用讓日志句柄復(fù)位歸檔文件日期比實(shí)際晚一天cron執(zhí)行時間設(shè)在0點(diǎn)前或時區(qū)設(shè)置不對用date命令確認(rèn)服務(wù)器時區(qū)把切割時間改到0點(diǎn)后如5 0 * * *手動執(zhí)行腳本時用STAMP環(huán)境變量覆寫日期cron任務(wù)沒執(zhí)行但腳本手動執(zhí)行正常環(huán)境變量或PATH問題腳本依賴的gzip不在cron的PATH中在腳本開頭顯式聲明PATHcron任務(wù)輸出重定向到日志文件查看是否有“command not found”報(bào)錯歸檔文件重復(fù)生成或內(nèi)容為空腳本被重復(fù)調(diào)度或者日志文件本來就為空檢查crontab是否有重復(fù)行檢查應(yīng)用是否真的產(chǎn)生了日志為空時cp后原文件為空歸檔自然也空建議在腳本里加一個非空判斷find清理誤刪了非日志文件-name匹配范圍過寬先加ls或find的-print參數(shù)預(yù)覽確認(rèn)文件列表無誤后再換成-delete磁盤空間依然緩慢增長應(yīng)用不斷打開多個文件句柄未釋放用lsof或ls -l /proc/pid/fd查看進(jìn)程打開的日志文件確認(rèn)是否有刪除但未釋放句柄的情況除了上面的速查表還有兩個我踩過坑之后總結(jié)的心得值得單獨(dú)強(qiáng)調(diào)。第一個是空日志文件問題。有時候某個服務(wù)當(dāng)天確實(shí)沒有產(chǎn)生任何日志文件為空切割時cp一個空文件過去再gzip就會留下一個幾十字節(jié)的空壓縮包。看著不礙事但積累多了有點(diǎn)煩人。我現(xiàn)在的腳本里會加一個判斷if [ -s ${LOG_DIR}/${LOG_NAME} ]; then-s表示文件存在且非空再做切割歸檔否則直接跳過這樣就不會把空文件也留檔。第二個是腳本冪等性問題。我曾經(jīng)遇到過cron因?yàn)橄到y(tǒng)重啟剛好錯過執(zhí)行時間于是手動補(bǔ)跑了一次結(jié)果因?yàn)槊麤_突出現(xiàn)了兩個同名歸檔文件腳本沒做覆蓋控制。后來我在切割前加了一個判斷如果歸檔文件已存在就追加時間戳后綴如app-2025-03-18-150001.log.gz確保每次手動補(bǔ)跑不會把之前的歸檔覆蓋掉。# 冪等性處理如果歸檔文件已存在追加時分秒后綴 if [ -e ${ARCHIVE_FILE}.gz ] || [ -e ${ARCHIVE_FILE} ]; then ARCHIVE_FILE${ARCHIVE_FILE%.log}-$(date %H%M%S).log fi這一小段邏輯不復(fù)雜但讓我少掉了很多頭發(fā)。6. 優(yōu)化擴(kuò)展與個人經(jīng)驗(yàn)總結(jié)基礎(chǔ)腳本跑通之后還可以根據(jù)自己的業(yè)務(wù)情況做一些優(yōu)化讓整套機(jī)制更順手。如果你不想讓腳本總是從cp整個大文件開始可以改用tail -c結(jié)合日志文件的字節(jié)數(shù)做增量復(fù)制比如tail -c 1 ${LOG_DIR}/${LOG_NAME} ${ARCHIVE_FILE}這樣在大文件場景下能減少IO壓力。不過這個做法在極端并發(fā)下可能會丟失少量數(shù)據(jù)普通場景下用起來沒問題。個人建議先用cp方案跑一陣確實(shí)有性能瓶頸再優(yōu)化不必一開始就上復(fù)雜方案。另外如果你的日志寫入量特別大凌晨瞬間切割時cp整個文件可能耗時較長??梢钥紤]在接近0點(diǎn)時先做一次“軟切割”把0點(diǎn)前5分鐘作為分界點(diǎn)先復(fù)制前一天的尾部日志再清空原文件。但這里涉及跨天的邊界判斷腳本復(fù)雜度會上一層。大多數(shù)情況下凌晨的負(fù)載很低直接cp一天的量也不會造成明顯影響沒必要過度設(shè)計(jì)。還有一個小技巧值得分享把歸檔文件的權(quán)限和屬主設(shè)置對避免后續(xù)其他賬號讀不了。腳本里可以在cp之后加一句chown appuser:appgroup ${ARCHIVE_FILE}或者直接在cp的-p之后追加chmod 640之類的權(quán)限控制。否則有些服務(wù)以root運(yùn)行會產(chǎn)生root屬主的日志導(dǎo)致普通運(yùn)維賬號無法查看歷史日志。最后說一下我對這套方案的整體感受。用純Shell做日志切割很多人第一反應(yīng)是“有現(xiàn)成工具為什么要再造輪子”但真正深入之后會發(fā)現(xiàn)輪子固然好卻未必合自己的腳。自研腳本能讓你完全掌控文件名規(guī)則、壓縮時機(jī)、保留策略和清理邊界而且代碼總共不過二十多行沒有學(xué)習(xí)成本出問題也知道從何查起。我在后續(xù)好幾個項(xiàng)目里幾乎都是從這套腳本基礎(chǔ)上改的換機(jī)器部署基本就是復(fù)制粘貼改一下變量名就上線。如果你手頭正好有日志越滾越大的困擾我建議別急著引入重型日志系統(tǒng)先用這個簡單方案跑兩周試試。日志一天天一清磁盤空間穩(wěn)住了查問題的效率也會有明顯提升。按天切割只是日志治理的第一步等這個機(jī)制穩(wěn)定了你再去考慮日志采集、全文檢索、告警分析那些更高階的玩法就有了一個干凈整潔的數(shù)據(jù)底座。