监听文件夹新增文件 #
业务需求,需要监听一个文件夹是否有新增文件,文件数目1000w,还要考虑文件是否已经上传完毕,程序跑在docker内。
在 Linux 环境下,处理“大文件异步写入”是一个经典的同步问题。如果你直接在监视到文件创建时就读取,由于文件还在写入,你的程序会读取到不完整的数据。
以下是几种最常用且可靠的解决方案,按推荐程度排序:
采用“写入-重命名”模式(最推荐,最稳健)(我的业务有技术人员私自上传,被pass) #
这是工业界的标准做法。其核心思想是:不要让监听程序直接看到正在写入的文件。
- 流程:写入程序先将文件写到一个临时目录,或者起一个特殊后缀名(例如:data.xml.tmp)。文件完全写完并关闭后,调用 mv (rename) 将文件移动到监听目录,或者重命名为最终文件名(data.xml)。
- 为什么有效: 在同一个文件系统中,mv 操作是原子的(Atomic)。这意味着监听程序感知到文件存在的那一瞬间,文件已经完整地在那儿了。
- **代码逻辑:**监听程序只处理 *.xml,忽略 *.tmp。
配合“标志文件”(Marker Files) #
如果你对写入端有控制权,可以在写完主文件后,再创建一个极小的 .done 文件。
- **流程:**写入 bigfile.dat。写完后,创建一个 bigfile.dat.ok 空文件。
- 监听端: 只监视 .ok 后缀的文件,一旦发现,就去处理同名的 .dat 文件。
观察文件大小和修改时间(轮询法) #
这是最原始的方法,适用于无法获取内核通知的情况(如 NFS 挂载目录)。
- **逻辑:**获取文件当前的 size 和 mtime。等待一段时间(如 1-2 秒)。再次获取 size 和 mtime。如果两次结果完全一致,则初步判定文件写入完成。
- 缺点: 不够优雅,且如果写入方网络很慢,可能会产生误判。
经过测试,不可用,windows在文件写入开始,日期和大小就已经写入,并不会随着数据传输而改变。Linux没有测。
循环遍历问题 #
根据您的需求(找到新增文件并添加),我给您分析几种合理的处理方案:
方案一:文件系统监控(fsnotify)⭐⭐⭐⭐⭐ #
原理:实时监听文件系统事件
优点:
- 实时性最高,文件创建后立即响应
- CPU占用极低,事件驱动
- 适合长期运行的服务
缺点:
- Windows上监控数量有限制(约64K句柄)
- 需要递归添加所有子目录到监控
- 程序重启期间的文件变化会遗漏
适用场景:文件不频繁、目录层级可控
方案二:增量扫描(基于修改时间)⭐⭐⭐⭐ #
原理:只扫描最近修改过的目录
// 记录每个目录的最后修改时间
dirModTimes map[string]time.Time
// 扫描时先检查目录修改时间
if dirInfo.ModTime().After(lastScanTime) {
// 只扫描这个目录
}
优点:
- 实现简单,可靠性高
- 自动跳过未变化的目录(90%+的目录可跳过)
- 不依赖第三方库
缺点:
- 仍需遍历目录树(但大部分只stat不readdir)
- 某些操作系统目录时间更新不及时
适用场景:目录结构稳定、文件不频繁变化
方案三:文件索引缓存(类似数据库)⭐⭐⭐ #
原理:维护文件路径的哈希索引
// 记录目录内容的快照(文件名哈希)
dirSnapshots map[string]map[string]struct{}
// 对比快照找出新增文件
优点:
- 精确知道哪些是新增文件
- 可以检测删除、重命名等变化
缺点:
- 内存占用较大(大量文件时)
- 初始化时需要全量扫描
- 实现复杂度较高
适用场景:需要完整的文件变更追踪
方案四:混合方案(推荐)⭐⭐⭐⭐⭐ #
原理:fsnotify + 定期全量扫描
fsnotify处理99%的实时变化
+
低频全量扫描兜底(如每小时1次)
优点:
- 兼顾实时性和可靠性
- fsnotify失效时有backup
- 程序重启遗漏的文件会被补扫
缺点:
- 代码稍复杂
适用场景:生产环境(推荐)
文件系统监控(fsnotify)的缺点 #
1. Windows 系统限制 ⚠️ #
监控句柄数量限制 #
// Windows 每个进程最多约 64K 个监控句柄
// 假设您的目录结构:
scanPath/
├── case1/ // 1个监控
├── case2/ // 1个监控
│ ├── subdir1/ // 1个监控
│ └── subdir2/ // 1个监控
└── case3/ // 1个监控
// 如果有 10,000 个子目录 = 10,000 个监控句柄
实际影响:
- 您的证据文件可能按案件分目录存储
- 如果案件数量超过几万个,会触发限制
- 错误信息:
too many open files或access denied
性能开销 #
// 每个目录监控都会消耗:
- 内核对象(句柄)
- 内存(缓冲区)
- CPU(事件处理)
// 10,000个目录 ≈ 50-100MB 内存 + 持续的CPU开销
2. 程序重启期间的盲区 🚨 #
时间窗口问题 #
T1: 程序停止 (fsnotify 停止监听)
↓
[文件A.bcp 被复制] ← 这个文件会被遗漏!
[文件B.bcp 被复制] ← 这个文件会被遗漏!
↓
T2: 程序重启
initCache() 只加载数据库已有文件
但文件A、B 不在数据库中!
真实场景:
15:00 - 程序升级重启
15:01 - 办案人员复制了 5 个证据文件
15:02 - 程序启动完成
结果:这 5 个文件永远不会被扫描!
当前代码的问题:
func (s *Scanner) initCache(scanPath string) {
// 只从数据库加载已存在的文件
_, records, err := getRecords(&QueryOptions{})
// ❌ 没有实际扫描文件系统!
// 重启期间新增的文件不会被发现
}
3. 事件丢失风险 💥 #
事件缓冲区溢出 #
// fsnotify 内部有事件队列
watcher.Events (chan Event, 缓冲区大小有限)
// 如果短时间内大量文件变化:
复制 1000 个文件 → 产生 1000 个 CREATE 事件
→ 事件队列满 → 后续事件被丢弃
→ 部分文件永远不会被处理
触发场景:
# 办案人员批量复制案件文件夹
xcopy /s D:\evidence\case_2024_001 E:\scanPath\
# 可能产生:
- 1000 个文件 CREATE 事件
- 100 个目录 CREATE 事件
- 如果处理速度跟不上 → 事件丢失
处理速度瓶颈 #
case event := <-watcher.Events:
// 您的处理逻辑:
GetBCPInfo(filePath) // 解析文件 (可能很慢)
insertFile(bcpInfo, ...) // 数据库插入 (IO 操作)
// 如果单个文件处理需要 100ms
// 每秒只能处理 10 个文件
// 但复制可能每秒产生 100 个事件 → 队列堆积
4. 网络共享目录不支持 🌐 #
// ❌ 不支持的场景
scanPath = "\\\\server\\evidence" // SMB/CIFS 网络共享
scanPath = "Z:\\" // 映射的网络驱动器
// fsnotify 依赖操作系统的文件系统通知
// 网络共享无法可靠触发 CREATE 事件
您的实际情况:
- 如果证据文件存储在 NAS 或文件服务器
- fsnotify 可能完全无法工作
5. 新目录监控延迟 ⏱️ #
监控添加的竞态条件 #
// 时间线:
T1: 检测到目录 "case123" 创建
T2: 开始添加监控 watcher.Add("case123")
↓ (耗时 10-50ms)
T3: 此时如果有文件被复制到 case123/
→ 可能遗漏这个文件的 CREATE 事件
T4: 监控添加完成
// 代码中的问题:
if event.Op&fsnotify.Create == fsnotify.Create {
if info.IsDir() {
time.Sleep(50 * time.Millisecond) // 延迟不够
s.addDirWatch(event.Name) // 监控还没生效
}
}
6. 事件类型复杂性 🔄 #
不同操作系统的事件差异 #
// Windows:
复制文件 → CREATE 事件
// Linux:
复制文件 → CREATE + WRITE + CHMOD 多个事件
// macOS:
复制文件 → CREATE + MODIFY + ATTRIB 多个事件
导致的问题:
// 同一个文件可能触发多次处理
case event := <-watcher.Events:
// Windows: 1 次 CREATE
// Linux: 3 次事件 → 可能尝试插入 3 次数据库
重命名/移动的处理 #
// 用户操作:移动文件
move case1/file.bcp → case2/file.bcp
// fsnotify 事件:
1. case1/file.bcp → RENAME (from)
2. case2/file.bcp → RENAME (to)
// ❌ 您的代码只处理 CREATE,会遗漏这种情况
7. 内存泄漏风险 💾 #
监控目录持续增长 #
type Scanner struct {
watchedDirs map[string]time.Time // 只增不减
}
// 如果案件目录不断增加:
// 1月: 1000 个目录
// 6月: 10000 个目录
// 1年: 50000 个目录
// map 会持续占用内存,即使某些目录已经不再使用
8. 符号链接和硬链接问题 🔗 #
// 如果存在符号链接:
scanPath/link_to_evidence → /other/path/evidence
// 可能导致:
1. 重复监控同一个目录
2. 文件路径解析错误
3. 死循环(循环链接)