自动归档日志的脚本怎么写?从零到生产级的完整指南
目录导读
- 为什么需要自动归档日志? —— 磁盘爆满的痛与合规需求
- 核心原理 —— 归档脚本必须解决的三个问题
- 实战脚本 —— 三套直接可用的Shell/Python方案
- 生产级优化 —— 压缩、轮转、异常通知与安全
- 常见问题与FAQ —— 你踩过的坑都在这里
- 总结与下一步 —— 从脚本到日志管理平台
为什么需要自动归档日志?
当你的应用日志以每天500MB的速度增长,而磁盘分区只有80GB时,不归档的后果只有一个:30天后服务崩溃,更严重的是,在金融或医疗行业,法律要求日志保留至少180天——归档不是选择,而是强制项。

手动用crontab执行tar看似简单,但真正的痛点在于:
- 日志文件被进程占用:直接压缩会丢失最新写入的数据
- 归档与清理脱节:只打包不删除,磁盘一样会满
- 无失败重试机制:半夜脚本挂了没人发现
核心原理:归档脚本的三个关键决策
一个成熟的自动归档脚本,需要依次回答这三个问题:
- 何时归档? —— 基于文件大小(如>100MB)还是基于时间(如每天凌晨3点)?
- 如何安全归档? —— 使用
copytruncate还是先rename再重建? - 归档后如何处理原文件? —— 立即删除、保留N天、还是转存冷存储?
经验值:90%的场景采用“按大小触发 +
rename+ 延迟删除”组合,既保证数据完整性,又避免频繁IO。
实战脚本:三套直接可用的方案
方案A:纯Shell + logrotate(最推荐,零依赖)
其实logrotate已内置归档逻辑,你只需写一个配置:
# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
daily # 按天归档
rotate 7 # 保留7份历史
compress # 压缩为.gz
delaycompress # 延迟一天压缩,避免文件被占用
copytruncate # 先复制再清空原文件(适合不重启的进程)
missingok # 文件不存在不报错
notifempty # 空文件不归档
create 0640 www-data www-data # 新文件权限
}
配套cron任务(每天凌晨执行):
0 2 * * * /usr/sbin/logrotate -v /etc/logrotate.d/myapp >> /var/log/logrotate.log 2>&1
方案B:纯Shell脚本(可完全自定义)
#!/bin/bash
# 自动归档30天前的日志,并压缩删除
LOG_DIR=/var/log/myapp
ARCHIVE_DIR=/data/log_archive
RETENTION_DAYS=30
# 1. 创建归档目录
mkdir -p $ARCHIVE_DIR
# 2. 找到30天前的.log文件,打包后删除原文件
find $LOG_DIR -name "*.log" -mtime +$RETENTION_DAYS | while read file
do
base=$(basename "$file")
date_str=$(date -r "$file" +%Y%m%d)
tar -czf "$ARCHIVE_DIR/${base}_${date_str}.tar.gz" -C "$LOG_DIR" "$base"
if [ $? -eq 0 ]; then
rm -f "$file" && echo "[OK] Archived and removed: $file"
else
echo "[FAIL] tar failed: $file" >> /var/log/archive_error.log
fi
done
方案C:Python脚本(更灵活,适合复杂规则)
#!/usr/bin/env python3
import os, gzip, shutil, datetime, glob
LOG_DIR = "/var/log/myapp/"
ARCHIVE_DIR = "/data/archive/"
RETENTION_DAYS = 15
def rotate_file(path):
"""将日志文件压缩到归档目录,并清空原文件"""
base = os.path.basename(path)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
target = os.path.join(ARCHIVE_DIR, f"{base}.{timestamp}.gz")
with open(path, 'rb') as f_in, gzip.open(target, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out, length=1024*1024)
# 安全清空原文件(保留inode,进程不断写)
open(path, 'w').close()
print(f"Archived: {path} -> {target}")
if __name__ == "__main__":
for log_file in glob.glob(LOG_DIR + "*.log"):
# 检查大小超过100MB就归档
if os.path.getsize(log_file) > 100 * 1024 * 1024:
rotate_file(log_file)
生产级优化:必须补上的四个细节
压缩选型:gzip vs zstd
- gzip:兼容性最好,但压缩速度慢(100MB/s)
- zstd:比gzip快10倍,压缩率高30%,推荐用于大日志
# 使用zstd替换gzip tar --zstd -cf "$file.tar.zst" -C "$LOG_DIR" "$base"
异常告警(一个都不能少)
在脚本关键步骤后增加退出码判断,并接入钉钉/邮件通知:
if [ $? -ne 0 ]; then
curl -X POST -H "Content-Type: application/json" \
-d '{"msgtype":"text","text":{"content":"日志归档失败: '$file'"}}' \
https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN
fi
处理文件被占用的终极方案
用 rename + 信号触发(适合Java/Tomcat等会持有文件句柄的进程):
# 将当前日志改名,然后向进程发送USR1信号让应用重新开日志文件 mv app.log app.log.$(date +%Y%m%d) kill -USR1 $(pgrep -f myapp.jar) # 应用需实现USR1处理 sleep 2 gzip app.log.$(date +%Y%m%d) &
清理策略:双保险
- 本地保留天数:例如只留本地7天压缩档
- 远端存储同步:用
aws s3 sync或rsync推送到对象存储
常见问题与FAQ
Q1:为什么我的copytruncate会丢失少量日志?
答:copytruncate是“先复制再清空”,这两步之间写入的数据会丢失,如果应用允许,优先使用rename方案,若必须用,请配合delaycompress降低风险。
Q2:crontab里执行脚本,为什么总是找不到tar或gzip?
答:crontab的PATH环境变量极小,每个命令需写全路径:
# crontab里这样写
0 2 * * * /usr/bin/find /var/log/myapp -name "*.log" -exec /usr/bin/gzip {} \;
或脚本开头加:export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Q3:归档后,应用日志中出现了空白或乱码?
答:大概率是日志文件被移动位置后,应用通过文件名重新创建了新文件,但旧文件的文件描述符还在写,解决:检查应用是否支持日志文件“rename后自动重建”,否则只能用copytruncate。
Q4:如何做“归档前校验数据完整性”?
答:在生产脚本中,压缩前先用md5sum生成校验文件,压缩后再次计算原文件的哈希,对比一致才删除原文件。
Q5:多个应用共用磁盘,如何全局容量预警?
答:在脚本最前面加一段磁盘配额检测:
DISK_USE=$(df /data | awk 'NR==2 {print $5}' | tr -d '%')
if [ $DISK_USE -gt 85 ]; then echo "磁盘超85%,强行清理5天前日志"; fi
总结与下一步
自动归档日志的脚本核心就三句话:什么时候归档、怎么归档、归档后原文件怎么处理,从简单的logrotate到自研Python脚本,没有银弹,关键是匹配你的应用类型和日志频率。
如果你还在手工敲
tar命令,我强烈建议今天就把logrotate部署上——它已经足够应对80%的需求,更复杂的场景(如实时海量日志、多级归档策略),推荐转向ELK/Loki等日志平台,脚本只作为最后的兜底。
下次遇到日志爆盘,你该先检查这三件事:
- 是否已经配置了归档策略?
- 归档命令的退出码有被检查吗?
- 归档后的压缩包是否同步到了远端?
打开你的终端,写第一版脚本吧——别忘了先测试,再上生产。