来自 #130 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
🛑 [阻塞 · 并发] lastTS 的锁持有期覆盖整个脱敏和编码路径 service/ingesthook/filter.go:97
问题根因:Filter.Handle 在时间戳校验分支(第 97 行)加锁 f.mu.Lock(),锁的临界区仅覆盖 lastTS 的读-改-写操作。但锁在 f.mu.Unlock() 后立即释放,而脱敏和 encodePut 在锁外执行——这本身是正确的。真正的问题是:同一设备的多帧同时被不同 worker(work-stealing 下极易发生)执行到时间戳校验分支时,锁保护的临界区过短,导致「先到后判」的窗口竞争:假设设备 X 的帧 ts=100 和 ts=101 同时进入两个 worker:worker A 检查 lastTS[dev]=→发现无记录→设 lastTS[dev]=100→放行;worker B 同时检查 lastTS[dev]=→也读不到(A 还没写)→设 lastTS[dev]=101→放行。这导致 ts=100 的帧在竞争窗口内实际跳过了单调校验。根因是锁保护了 lastTS 的写入互斥,但没有保护「读-比较-写」的原子性(虽然锁本身提供了互斥,但第二个 goroutine 可能在第一个 goroutine 的写入生效前通过了读比较)。这个问题在单次竞争窗口内概率不高,但在高频采集下(数千 Hz × 多 worker)是确定性丢帧的温床。
为什么低级解法不够:简单把 lastTS 换成 sync.Map 甚至 atomic 都解决不了「读-比较-写」的原子性问题——sync.Map 的 LoadOrStore 不是「比较旧值后才决定是否更新」的语义,atomic.CompareAndSwap 只适用于单 int64 值,不能原子地做「ts > last → 替换」。加锁方式正确,但锁的粒度和临界区边界需要调整。
架构级方案:将锁的临界区扩大到整个「读-then-写」操作:在锁内完成 if seen && ts <= last { return HookDrop } 的判断和写入,确保一个 goroutine 的「看到旧值→判断→更新」不会被其他 goroutine 的并行操作打断。具体就是将第 96–101 行的锁区域扩展到整个单调校验逻辑。更进一步,如果希望消除锁竞争,可以将 lastTS 按 device hash 分片(per-shard sync.Mutex + map),将热路径上的锁争用降到 1/N。
代价/收益:代价:分片增加代码复杂度(需设计哈希分片函数 + 分片锁数组),对小规模设备数(数十台)可能收益有限。将锁临界区扩大到整个操作是低成本高收益的即时修复。收益:消除「先到后判」窗口,单调校验在 work-stealing 下仍正确工作。
🛑 [阻塞 · 并发] lastTS 的锁持有期覆盖整个脱敏和编码路径
service/ingesthook/filter.go:97问题根因:Filter.Handle 在时间戳校验分支(第 97 行)加锁
f.mu.Lock(),锁的临界区仅覆盖lastTS的读-改-写操作。但锁在f.mu.Unlock()后立即释放,而脱敏和encodePut在锁外执行——这本身是正确的。真正的问题是:同一设备的多帧同时被不同 worker(work-stealing 下极易发生)执行到时间戳校验分支时,锁保护的临界区过短,导致「先到后判」的窗口竞争:假设设备 X 的帧 ts=100 和 ts=101 同时进入两个 worker:worker A 检查 lastTS[dev]=→发现无记录→设 lastTS[dev]=100→放行;worker B 同时检查 lastTS[dev]=→也读不到(A 还没写)→设 lastTS[dev]=101→放行。这导致 ts=100 的帧在竞争窗口内实际跳过了单调校验。根因是锁保护了lastTS的写入互斥,但没有保护「读-比较-写」的原子性(虽然锁本身提供了互斥,但第二个 goroutine 可能在第一个 goroutine 的写入生效前通过了读比较)。这个问题在单次竞争窗口内概率不高,但在高频采集下(数千 Hz × 多 worker)是确定性丢帧的温床。为什么低级解法不够:简单把
lastTS换成sync.Map甚至atomic都解决不了「读-比较-写」的原子性问题——sync.Map的 LoadOrStore 不是「比较旧值后才决定是否更新」的语义,atomic.CompareAndSwap只适用于单 int64 值,不能原子地做「ts > last → 替换」。加锁方式正确,但锁的粒度和临界区边界需要调整。架构级方案:将锁的临界区扩大到整个「读-then-写」操作:在锁内完成
if seen && ts <= last { return HookDrop }的判断和写入,确保一个 goroutine 的「看到旧值→判断→更新」不会被其他 goroutine 的并行操作打断。具体就是将第 96–101 行的锁区域扩展到整个单调校验逻辑。更进一步,如果希望消除锁竞争,可以将lastTS按 device hash 分片(per-shardsync.Mutex+ map),将热路径上的锁争用降到 1/N。代价/收益:代价:分片增加代码复杂度(需设计哈希分片函数 + 分片锁数组),对小规模设备数(数十台)可能收益有限。将锁临界区扩大到整个操作是低成本高收益的即时修复。收益:消除「先到后判」窗口,单调校验在 work-stealing 下仍正确工作。