应用日志需增量采集而非仅轮转,fsnotify不可靠因无法区分写入类型,应结合inode/dev/offset状态跟踪实现断点续采。

为什么不用 logrotate 直接配合 syslog?
因为你要采集的是应用自己写的日志文件,不是系统日志;logrotate 只负责轮转,不负责“采集”——它不会把 app.log.2024-05-12 推到 Kafka 或发 HTTP 上报。你真正需要的,是一个能监听文件变化、增量读取、断点续采、不丢不重的轻量级采集端。
用 fsnotify 监听文件变化靠谱吗?
不推荐直接用 fsnotify 做日志采集主逻辑。它只告诉你“文件被修改了”,但无法区分是追加写、覆盖写、还是 rename 重命名(比如 logrotate 的 copytruncate 或 rename 模式)。实际中你会遇到:app.log 突然变成空文件,或被 rename 成 app.log.1,而新日志写进另一个空的 app.log——这时仅靠 inotify 事件会丢数据或重复读。
- 真正要监听的是“文件内容追加”,不是“文件被写入”
-
fsnotify可以辅助判断轮转发生(如检测到app.log被 rename),但主体必须靠os.Stat()+os.Seek()对 inode 和 offset 做状态跟踪 - Linux 下注意:如果日志进程用
O_TRUNC重新打开文件,inode 不变但 size 归零,此时需对比Stat().Size和上次记录的 offset 决定是否重头读
如何安全地增量读取并记录读取位置?
核心是维护一个“文件状态快照”:每个日志文件对应一个结构体,存 inode、dev、offset、lastModTime。每次采集前先 os.Stat(),比对这些字段决定行为:
- inode+dev 相同且
size > offset→ 正常追加读,从offset开始Seek(),逐行读取 - inode+dev 相同但
size → 极大概率被 truncate,重置 offset = 0(或按策略跳过) - inode+dev 不同 → 文件被轮转(rename 或重建),关闭旧句柄,打开新文件,offset 设为 0(或查历史记录看是否已采过)
- 文件不存在 → 等待下次轮询,不做报警(常见于服务刚启、日志还没生成)
示例关键逻辑片段:
fi, _ := os.Stat(filename)
if fi.Sys() != nil {
stat := fi.Sys().(*syscall.Stat_t)
if stat.Ino != lastIno || stat.Dev != lastDev {
// 轮转发生
reopenFile()
offset = 0
} else if fi.Size()
<h3>采集器怎么避免吃光内存或卡死?</h3>
<p>日志文件可能单个几百 MB,也可能每秒写入上万行。不能 <code>ReadAll()</code>,也不能无缓冲 <code>bufio.Scanner</code>(默认缓存 64KB,超长行会 panic)。必须控制节奏:</p>
- 用
bufio.NewReader+ReadString('\n'),手动设 buffer 大小(如 1MB),超长行截断或丢弃(日志行一般不会真超 1MB) - 每次最多读 1000 行或耗时 ≤ 50ms,然后 yield 控制权,防止阻塞调度器
- 采集目标如果是网络后端(如 HTTP API),失败时不要重试全部,只重试当前 batch,并记录失败 offset,下次跳过已上报成功部分
- 状态文件(存 offset)建议用
os.O_SYNC | os.O_WRONLY写,但别每行都刷盘——可每 5 秒或每 100 行 flush 一次
容易被忽略的一点:os.File 的 fd 在 Linux 下默认没开 O_CLOEXEC,如果采集器 fork 子进程(比如 exec shell 命令),fd 会被继承导致文件锁异常。启动时显式加 syscall.FD_CLOEXEC 更稳妥。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











