logrotate无法满足go进程内日志轮转需求,因其依赖外部定时与信号机制,易导致日志丢失、竞态和时区错位;真正可控的方式是在go进程内实现轮转逻辑,核心是安全控制文件句柄生命周期与切换时机。

logrotate 无法满足 Go 进程内日志轮转需求
Go 程序默认不支持按时间(如每天、每小时)自动切分日志文件,log 标准库本身无轮转能力;用外部 logrotate 虽可行,但存在信号处理延迟、进程重定向竞争、丢失日志等风险——尤其在高并发或容器环境下更明显。
真正可控的方式是:在 Go 进程内实现轮转逻辑,核心是控制文件句柄生命周期 + 时间判断 + 安全切换。
用 lumberjack 实现按天轮转最稳妥
lumberjack 是 Go 生态中最成熟、被 gin / gRPC 等广泛采用的日志轮转库,它原生支持按大小和按时间双策略,且线程安全、无竞态、可配置压缩与保留数量。
关键参数说明:
-
Filename:日志主文件路径,如"logs/app.log",轮转后生成app.log.2024-05-20类似格式 -
MaxAge:保留多少天的旧日志(单位:天),设为7即只留近 7 天 -
LocalTime:必须设为true,否则轮转时间按 UTC 计算,导致本地时区错位 -
Compress:设为true可自动对归档日志做gzip压缩(节省空间,但首次读取需解压)
示例初始化:
import "gopkg.in/natefinch/lumberjack.v2"
logger := log.New(&lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 100, // MB
MaxAge: 7, // days
LocalTime: true,
Compress: true,
}, "", log.LstdFlags)
自定义按小时轮转需改写切割逻辑
lumberjack 默认只支持按天(MaxAge)或按大小轮转,不支持“每小时一个文件”。若需按小时切分(如 app.log.2024-05-20-14),必须绕过其内置时间判断,自行控制文件名生成与切换时机。
可行做法是封装一个带时间戳检查的 io.Writer:
- 每次写入前检查当前小时是否变化(用
time.Now().Format("2006-01-02-15")) - 若变化,先关闭旧文件,再以新时间戳打开新文件
- 务必加
sync.Mutex锁,避免多 goroutine 并发写时重复 open/close - 注意:不能依赖
os.File.WriteString后立即 flush,要显式调用file.Sync()防止缓冲丢失
简略结构示意:
type HourlyWriter struct {
mu sync.Mutex
file *os.File
pattern string // e.g., "logs/app.log.2006-01-02-15"
curHour string
}
func (w *HourlyWriter) Write(p []byte) (n int, err error) {
w.mu.Lock()
defer w.mu.Unlock()
hour := time.Now().Format("2006-01-02-15")
if w.file == nil || hour != w.curHour {
if w.file != nil {
w.file.Close()
}
name := fmt.Sprintf(w.pattern, hour)
w.file, _ = os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
w.curHour = hour
}
return w.file.Write(p)
}
轮转时注意文件句柄泄漏与权限问题
常见崩溃点不是逻辑,而是系统资源层面:
- Linux 默认单进程最多打开 1024 个文件,频繁轮转 + 未 close 旧文件 → 快速触发
too many open files - 容器中运行时,若日志目录挂载为只读或权限不足(如非 root 用户写
/var/log),lumberjack会静默失败,日志直接丢弃,且不报错 -
lumberjack的Cleanup不自动执行,旧文件删除依赖下一次写入触发;若程序长期低流量,过期日志可能堆积 - 测试时务必用
lsof -p <pid> | grep logs</pid>检查句柄数,用ls -l logs/确认文件属主与权限
建议上线前加一句健康检查:
if _, err := os.Stat("logs"); os.IsNotExist(err) {
os.MkdirAll("logs", 0755)
}
按时间轮转的关键不在“怎么命名”,而在“何时关、何时开、关得干净吗”——时间只是触发条件,文件生命周期管理才是实操难点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











