如何在 Shell 脚本中实现日志轮转切割功能?

文章导读
对于大多数 Linux 服务器场景,优先使用系统自带的 logrotate 工具,只有在需要高度定制逻辑或无法安装额外工具时,才考虑用 Shell 脚本手动实现日志切割。
📋 目录
  1. 基础原理演示脚本
  2. 生产环境完整脚本
  3. 脚本关键逻辑说明
  4. 定时任务配置
  5. 效果验证与排查
  6. 常见风险与规避
  7. 参考来源
A A

对于大多数 Linux 服务器场景,优先使用系统自带的 logrotate 工具,只有在需要高度定制逻辑或无法安装额外工具时,才考虑用 Shell 脚本手动实现日志切割。

先说结论:Shell 脚本可以实现基础的日志切割,但需处理好文件句柄和进程信号,否则可能导致磁盘空间未释放或日志丢失。

  • 适合:轻量级应用或无法使用 logrotate 的嵌入式环境。
  • 先看:应用程序是否支持 HUP 信号重新打开日志文件。
  • 建议:配合 cron 定时任务,并添加文件锁防止并发冲突。

基础原理演示脚本

下面是一个基础原理演示脚本,生产环境请参考后文完整示例。该脚本展示了切割的核心逻辑,但缺少并发控制和清理策略。

如何在 Shell 脚本中实现日志轮转切割功能?
#!/bin/bash
LOG_FILE="/var/log/myapp/app.log"
MAX_SIZE=10485760  # 10MB
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

if [ -f "$LOG_FILE" ]; then
  SIZE=$(stat -c%s "$LOG_FILE")
  if [ "$SIZE" -gt "$MAX_SIZE" ]; then
    mv "$LOG_FILE" "${LOG_FILE}.${TIMESTAMP}"
    touch "$LOG_FILE"
    # 注意:生产环境需考虑权限继承问题,避免硬编码 chmod
    gzip "${LOG_FILE}.${TIMESTAMP}"
    # 如果应用支持,发送信号让其重新打开文件
    # kill -HUP $(cat /var/run/myapp.pid)
  fi
fi

生产环境完整脚本

生产环境脚本需补充文件锁(防止 cron 并发)、错误处理(失败退出)、旧日志清理(防止磁盘满)及权限控制。保存为 /usr/local/bin/rotate_log.sh

#!/bin/bash
set -euo pipefail

# === 配置区域 ===
LOG_FILE="/var/log/myapp/app.log"
LOG_DIR="/var/log/myapp"
PID_FILE="/var/run/myapp.pid"
MAX_SIZE=10485760       # 10MB
KEEP_DAYS=7             # 保留天数
LOCK_FILE="/var/lock/myapp_rotate.lock"

# === 函数定义 ===
log_error() {
  echo "[ERROR] $(date '+%F %T') $1" >&2
}

# === 主逻辑 ===
# 1. 获取独占锁,防止并发执行
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
  log_error "Another instance is running, exit."
  exit 1
fi

# 2. 检查文件是否存在及大小
if [ ! -f "$LOG_FILE" ]; then
  log_error "Log file not found: $LOG_FILE"
  exit 1
fi

SIZE=$(stat -c%s "$LOG_FILE")
if [ "$SIZE" -le "$MAX_SIZE" ]; then
  exit 0
fi

# 3. 切割日志
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${LOG_FILE}.${TIMESTAMP}"

if ! mv "$LOG_FILE" "$BACKUP_FILE"; then
  log_error "Failed to move log file."
  exit 1
fi

# 4. 创建新文件(权限继承原目录 umask,避免硬编码 chmod 导致权限不符)
touch "$LOG_FILE"

# 5. 通知应用重载(需确认 PID 文件存在)
if [ -f "$PID_FILE" ]; then
  PID=$(cat "$PID_FILE")
  if kill -0 "$PID" 2>/dev/null; then
    kill -HUP "$PID" || log_error "Failed to send HUP signal."
  else
    log_error "Process $PID not running."
  fi
fi

# 6. 压缩旧日志
if ! gzip "$BACKUP_FILE"; then
  log_error "Failed to gzip log file."
  exit 1
fi

# 7. 清理旧日志(防止磁盘占满)
find "$LOG_DIR" -name "*.gz" -mtime +"$KEEP_DAYS" -delete

exit 0

脚本关键逻辑说明

  1. 文件锁机制:使用 flock -n 200 确保同一时间只有一个脚本实例运行,避免 cron 定时任务重叠导致文件覆盖。
  2. 错误处理:启用 set -euo pipefail 并在关键步骤检查返回值,失败时记录错误日志并退出,便于排查。
  3. 权限控制:新建日志文件使用 touch 继承目录默认权限,避免硬编码 chmod 644 导致应用无法写入(具体权限取决于 umask 配置)。
  4. 清理策略:使用 find ... -mtime +KEEP_DAYS -delete 自动删除过期压缩文件,防止磁盘空间耗尽。
  5. 信号发送:发送 HUP 信号前检查进程是否存在(kill -0),避免向无效 PID 发送信号报错。

定时任务配置

将脚本加入 crontab,建议每分钟或每小时检查一次(取决于日志增长速度):

# 编辑 crontab
crontab -e

# 添加以下行(每小时检查一次)
0 * * * * /usr/local/bin/rotate_log.sh >> /var/log/myapp/rotate_cron.log 2>&1

效果验证与排查

  • 检查 inode 变化:切割前后运行 ls -li 日志文件路径,确认新文件的 inode 号已改变。
  • 检查磁盘空间:运行 du -sh 日志目录,确认旧文件已被压缩或清理,空间不再持续增长。
  • 检查应用写入:观察新生成的日志文件是否有新内容写入,确认应用没有继续写入旧文件。
  • 检查锁文件:若脚本异常退出,检查 /var/lock/myapp_rotate.lock 是否被占用,必要时手动清理。

常见风险与规避

  • 竞态条件:未加锁时,多个 cron 实例可能同时移动文件导致数据丢失。务必使用 flock
  • 信号无效:部分应用(如某些 Java 程序或容器化应用)可能忽略 HUP 信号,需查阅具体应用文档确认日志重载方式,或改用 copy-truncate 模式。
  • 磁盘满风险:在移动和压缩过程中,如果磁盘空间已满,操作可能失败。确保保留足够的临时空间,并配置严格的清理策略。
  • 权限问题:新建的日志文件权限如果不正确,应用可能无法写入。建议测试 touch 后的文件权限是否符合应用运行用户的要求。

参考来源

  • Linux man-pages: mv, stat, gzip, flock 命令手册
  • Linux Foundation: logrotate 官方文档及行为说明