[Epic] 以全局 WatchManager 解耦 inotify Watch 与采集配置的生命周期 #2650
Replies: 4 comments
-
|
from=agent role=architect action=drafted 已完善路线,请人工审阅;确认无误请回复「确认」或在 Console 点击「确认并拆解」。 |
Beta Was this translation helpful? Give feedback.
-
|
把epic的标题概括成一句话。新增的指标是不是应该放在WatchManager里?WatchManager本身也算是一个runner。然后你的路线不要局限于旧有的A、B、C三个主线+一个D支线,你要按真实的开发需求和并行度来设计路线的个数和每条路线的内容,不是一定要有支线,像边界健壮性与回退可解释完全没必要作为支线的,合并到对应的主线就行了。重新设计一下 |
Beta Was this translation helpful? Give feedback.
-
|
from=agent role=architect action=drafted 已完善路线,请人工审阅;确认无误请回复「确认」或在 Console 点击「确认并拆解」。 |
Beta Was this translation helpful? Give feedback.
-
|
from=agent role=delivery action=broken-down 已拆解子 Issue 并挂载到 Epic #2651 按 3 条主线 10 个可交付步骤拆解,每步一个子 Issue(一步=一 Issue=一 PR,含范围锁与 UT/E2E 验收命令),全部通过 Sub-issues 面板挂在 Epic #2651 下:
依赖已用 Blocked by 显式串联;当前仅起点 W0(#2652) 无未满足前置,已打 agent-ready,其余待前置完成后再放开。 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
以全局 WatchManager 解耦 inotify Watch 与采集配置的生命周期
1. 总览
一句话目标:引入一个作为独立 runner 的全局
WatchManager,以(st_dev, st_ino)为主键统一持有内核 inotify watch、按订阅配置维护引用计数,使目录 watch 的内核生命周期与采集配置生命周期彻底解耦——reload 时只对真正变化的目录做inotify_add_watch/inotify_rm_watch(未变化目录零 syscall),并由 WatchManager 自身的一套自证指标证明每次 reload 是「新增 / inode 替换 / 复用 / 误拆 / 回退」中的哪一种。现状定性(主干 d052b5f):
EventDispatcher::RegisterEventHandler()对「同路径 + inode 未变」的目录直接复用已有wd,不再调用AddWatch()(core/file_server/EventDispatcher.cpp:179-212);仅对真正新增路径调用mEventListener->AddWatch()(EventDispatcher.cpp:242)。同时支持no_inotify/inotify_black_list(core/app_config/AppConfig.cpp:1100-1114)。CollectionPipelineManager→FileServer::Pause()/Resume(true,false)(core/collection_pipeline/CollectionPipelineManager.cpp:56,160)→ConfigManager::RegisterHandlers()(core/file_server/FileServer.cpp:117);LogInput::ProcessLoop()还会周期性RegisterHandlers()(core/file_server/event_handler/LogInput.cpp:433)。移除依赖 timeout 老化(TimeoutHandler→UnregisterAllDir,LogInput.cpp:311-312),而非配置 diff 精确移除。path(mPathWdMap),(dev,ino)只是wd的从属字段做二次校验(EventDispatcher.cpp:177,183),同 dev+inode 不同路径(symlink 指向已注册目录)只被降级为「非 inotify wd」(EventDispatcher.cpp:283-291),缺少以(st_dev, st_ino)为主键的去重;② 无「按配置」的引用计数——全局共用mSharedHandler(ConfigManager.cpp:239-242),无法精确知道「某目录被哪些配置订阅」,因而只能靠 timeout GC 而非 diff 精确增删;③ 无结构化 grace period 的「延迟移除」;④ watch 台账没有独立 runner 归属,也缺少自证指标:与 watch 直接相关的指标只有watched_dirs_total一个 gauge(core/monitor/metric_constants/RunnerMetrics.cpp:69,经LogInput::UpdateCriticalMetric()写入 file_server runner),加一个文件 dump(EventDispatcher::DumpInotifyWatcherDirs,EventDispatcher.cpp:751),无法区分「新增 / inode 替换 / 复用 / 误拆 / 回退轮询」。关键设计决策:WatchManager 是一个 runner,指标归它所有。 现有 runner(file_server、flusher_runner、http_sink、processor_runner…)的统一形态是:持有一个用
METRIC_CATEGORY_RUNNER+runner_namelabel 创建的MetricsRecordRef(范式见FileServer.cpp:38-41)。目录 watch 的生命周期本身就是一个独立的运行时子系统,理应成为独立 runner。因此自证指标不再挂在 file_server 记录下,而是由 WatchManager 自己的MetricsRecordRef(新runner_name = "watch_manager")承载。这带来一个直接后果:要采集这套指标,WatchManager必须先存在——所以本目标把「WatchManager runner 骨架」与「自证指标」合并为同一条起始主线,且该骨架只包裹、不改语义(pass-through),从而在不触碰任何生命周期语义的前提下先建立可观测基线。演进线与终态(按真实开发依赖与并行度划分为 3 条主线,无支线;原「边界健壮性」并入主键线,原「回退可解释」并入生命周期线):
WatchManager作为独立 runner 存在(runner_name = "watch_manager"),包裹EventListener并统一中转AddWatch/RemoveWatch,pass-through 不改语义;每次 add/rm/reuse/recreate/fallback 都有带reason的计数与耗时指标,reload 批量规模与内核耗时可观测;事故可被指标复盘(dev,ino)主键去重 + 路径归一化DevInode(st_dev+st_ino)为主键统一持有内核 watch,EventDispatcher的mPathWdMap/mWdDirInfoMap/计数收敛其后;symlink / bind mount / 容器软链展开、inode 替换被归一到(dev,ino)判定,正确区分「同一目录复用」与「真正变化」,不再误判为「删除再新增」enable_polling_discovery)与失败原因绑定 WatchManager 指标,不再静默降级整体推进顺序:线 1 → 线 2 → 线 3,无独立支线。硬约束(见 §4):更换主键(线 2)与移除语义(线 3)必须晚于线 1 建立「WatchManager runner + 指标基线」(pass-through、语义不变)之后进行;线 2 的主键与归一化落地后,线 3 才能基于
(dev,ino)做精确 diff。2. 分阶段依赖
flowchart TD W0["W0 WatchManager runner 骨架<br/>包裹 EventListener + 注册 MetricsRecordRef,pass-through"] W1["W1 add/rm/reuse/recreate/fallback<br/>计数 + reason 维度"] W2["W2 syscall 耗时 + reload 批量 + 内核耗时<br/>并扩展 dump 上下文"] K0["K0 DevInode 主键索引<br/>(dev,ino) 去重"] K1["K1 EventDispatcher 台账收敛<br/>map/计数迁入 WatchManager"] K2["K2 路径归一化<br/>symlink/bind mount/容器/inode 替换"] L0["L0 目录-配置订阅引用计数"] L1["L1 reload 差集计算<br/>keep / add / remove"] L2["L2 四步切换 + grace period 延迟移除"] L3["L3 轮询回退与 reason 指标绑定"] W0 --> W1 --> W2 W0 --> K0 --> K1 --> K2 K1 --> L0 --> L1 --> L2 K2 --> L1 W2 --> L1 W1 --> L3 L2 --> L3WatchManager类接管AddWatch/RemoveWatch,注册runner_name = "watch_manager"的 MetricsRecordRef;行为与旧实现等价,EventDispatcherDirUnittest全绿mPathWdMap/mWdDirInfoMap由 WatchManager 托管,原 file_server UT 全绿,watched_dirs_total语义不变reason正确3. 各线工作项
线 1 — WatchManager Runner 抽象 + 自证指标
FileServer用WriteMetrics::CreateMetricsRecordRef(mMetricsRecordRef, METRIC_CATEGORY_RUNNER, {{METRIC_LABEL_KEY_RUNNER_NAME, ...}})创建、CommitMetricsRecordRef提交(FileServer.cpp:38-41,67);runner_name取值集中在RunnerMetrics.cpp:27-34。watched_dirs_total(gauge,挂在 file_server runner):RunnerMetrics.cpp:69,由LogInput::UpdateCriticalMetric()用GetHandlerCount()写入(LogInput.cpp:347-355,SET_GAUGE(mRegisterdHandlersTotal, ...))。EventListener::AddWatch/RemoveWatch(core/file_server/event_listener/EventListener_Linux.cpp:44-50);调用点在EventDispatcher.cpp:242(add)与EventDispatcher.cpp:871(rm)。EventDispatcher.cpp:179-212(同路径 inode 未变复用 / inode 变UnregisterAllDir)、:283-291(同 inode 已注册→降级 wd=-1)。EventDispatcher::DumpInotifyWatcherDirs()EventDispatcher.cpp:751。WatchManager成为独立 runner 并中转所有 watch syscall;AcquireWatch/ReleaseWatch内部按结果(New/Reused/Recreated/Fallback)自增带reason的计数与耗时指标,记录 reload 批量与内核耗时,形成事故可复盘的证据链。WatchManager(core/file_server/):包裹EventListener,对外暴露AcquireWatch(path)/ReleaseWatch(...),内部先直通现有行为;构造时创建runner_name = "watch_manager"的MetricsRecordRef(新增METRIC_LABEL_VALUE_RUNNER_NAME_WATCH_MANAGER,RunnerMetrics.cpp:27-34附近)WatchManager.{h,cpp},EventDispatcher.cpp:242/871改为经 WatchManager;新增常量并注册 MetricsRecordRef。验收:新增WatchManagerUnittest,行为与旧实现等价;EventDispatcherDirUnittest(core/unittest/controller/EventDispatcherDirUnittest.cpp:150-152)与core/unittest/monitor/常量存在性 UT 全绿,make编译通过inotify_add_watch_total{reason}/inotify_rm_watch_total{reason}/watch_reused_total/watch_recreated_total,reason ∈ {new_path, inode_changed, config_rebuild, watch_lost, retry, symlink_target_changed}AcquireWatch覆盖EventDispatcher.cpp:242(new_path/retry)、:183-192(inode_changed→recreate)、:194-210(reuse);ReleaseWatch覆盖:871(rm)。验收:新增WatchManagerMetricsUnittest,构造各分支并断言计数增量inotify_add_watch_duration_us、watch_reload_batch_size、watch_reload_kernel_time_us;扩展 dump 上下文:config revision / path hash / dev+inode / old·new wd / 旧·新引用数 / 是否真正 inode 变化AcquireWatch内AddWatch前后计时;ConfigManager::RegisterHandlers()(ConfigManager.cpp:239)统计批量与累计内核耗时;DumpInotifyWatcherDirs()(EventDispatcher.cpp:751-771)扩列(引用数字段待线 3 L0 落地后补齐)。验收:UT 断言 duration/batch 随注册数增长被写入;dump 字段存在且格式向后兼容线 2 —
(dev,ino)主键去重 + 路径归一化EventDispatcher成员:mPathWdMap(path→wd)、mWdDirInfoMap(wd→DirInfo)、mWatchNum/mInotifyWatchNum/mNonInotifyWd/mBrokenLinkSet/mWdUpdateTimeMap(EventDispatcher.h:256-277);DirInfo{mPath,mInode,mIsSymbolicLink,mHandler}(EventDispatcher.h:69-78)。inode仅作为wd的从属字段做二次比对(EventDispatcher.cpp:177,183)。同 inode 不同路径只被降级为「非 inotify wd」(EventDispatcher.cpp:283-291),没有(dev,ino)级去重。EventDispatcher.cpp:183-192(受ignore_dir_inode_changed影响,AppConfig.cpp:1030,IgnoreDirInodeChanged()AppConfig.h:483)。mBrokenLinkSet与CheckSymbolicLink()(EventDispatcher.cpp:773-822);UnregisterEventHandler里的 symlink 分支(:860-866)。ConfigManager::RegisterHandlers()用containerInfo.mRealBaseDirs注册(ConfigManager.cpp:280-294)。AddOneToOneMapEntry/RemoveOneToOneMapEntry(EventDispatcher.cpp:737-749)。DevInode为主键统一持有内核 watch 与元数据,EventDispatcher退化为「事件路由 + 配置匹配」,watch 台账收敛到 WatchManager;注册入口统一解析到(dev,ino)后再判定,容器mRealBaseDirs归一。DevInode主键索引,(dev,ino)→WatchEntry去重;同 dev+inode 的多路径共享一个内核 watchmap<DevInode,WatchEntry>;替换EventDispatcher.cpp:283-291的从属去重。验收:WatchManagerUnittest覆盖「两路径同 dev+inode 只 1 次 AddWatch」mPathWdMap/mWdDirInfoMap/mWatchNum/mInotifyWatchNum/mNonInotifyWd迁入 WatchManager,EventDispatcher通过其查询EventDispatcher.h:256-277成员迁移;GetHandler/IsRegistered/FindAllSubDirAndHandler/DumpInotifyWatcherDirs改走 WatchManager。验收:全部 file_server 相关 UT 通过,watched_dirs_total语义不变(dev,ino),复用 K0 主键判定「同目录 vs 真正变化」,覆盖 symlink 目标切换 / bind mount / 容器软链 / inode 替换RegisterEventHandler入口(EventDispatcher.cpp:157-212)改用 WatchManager 的 DevInode 判定;容器mRealBaseDirs(ConfigManager.cpp:280-294)归一;与ignore_dir_inode_changed(AppConfig.cpp:1030)语义协调。验收:UT——symlink 目标切换 / bind mount / 容器路径 / inode 替换四类各一例,判定与reason(inode_changed/symlink_target_changed)正确线 3 — 引用计数 + 配置 diff 解耦生命周期 + 回退可解释
mSharedHandler(ConfigManager::RegisterHandlers()ConfigManager.cpp:239-242),watch 不区分「被哪些配置订阅」。FileServer::Resume()直接RegisterHandlers()全量重扫(FileServer.cpp:117),LogInput周期性再扫(LogInput.cpp:433);移除靠 timeout(ProcessEvent→UnregisterAllDir,LogInput.cpp:311-312;UnregisterAllDirEventDispatcher.cpp:847-853)。RegisterHandlers(basePath,config)ConfigManager.cpp:469;UnregisterEventHandlerEventDispatcher.cpp:855-876(此处真正RemoveWatch并mInotifyWatchNum--)。enable_polling_discovery下PollingDirFile/PollingModify(FileServer.cpp:62-65,123-126);inotify 数量上限default_max_inotify_watch_num(EventDispatcher.cpp:229)、max_watch_dir_count(EventDispatcher.cpp:214);黑名单/关闭IsInInotifyBlackList(AppConfig.cpp:1657)、NoInotify(AppConfig.cpp:101)。add_watch、不再被任何配置引用的目录经 grace period 延迟rm_watch;严格禁止「先全量 rm 再全量 add」;所有轮询回退与失败原因绑定 WatchManager 指标。WatchEntry增加「订阅配置集合 + refcount」;注册/注销时增减引用WatchEntry增set<configKey>;ConfigManager.cpp:144,518,562,614注册处传入配置身份。验收:UT——同目录被 N 个配置订阅,refcount==N,删 1 个仍保留 watchkeep / add / remove差集(旧∩新=keep 零 syscall),替换全量重扫语义FileServer::Resume()(FileServer.cpp:102-128)与RegisterHandlers()(ConfigManager.cpp:239)引入 diff。验收:UT 断言/a /b(keep) 无 AddWatch、/d(add) 一次 AddWatch、/c(remove) 进入待删;批量与内核耗时经 W2 指标可观测LogInput.cpp:311-312)改造为「refcount 归零 + grace period」延迟移除,UnregisterEventHandler(EventDispatcher.cpp:855-876)由延迟队列驱动;grace period 可配置、可回退到旧 timeout 行为。验收:UT 覆盖 grace period 内被重新引用则取消移除;E2ETestConfigUpdate*验证 reload 无采集中断、无重复 AddWatchwatch_fallback_polling_total{reason}绑定(inotify 上限、AddWatch 失败、no_inotify、黑名单)EventDispatcher.cpp:214-293各失败分支与AppConfig::IsInInotifyBlackList(AppConfig.cpp:1657)/NoInotify(AppConfig.cpp:101)处经 WatchManager 埋点。验收:UT 断言各回退 reason 计数;E2E 验证回退后仍不丢文件事件4. 风险与约束
跨线风险:
EventDispatcher/EventListener/ConfigManager/LogInput均为进程内单例且互相直连(LogInput.cpp直接驱动EventDispatcher)。线 2 的收敛需分步进行,禁止一次性大改,每步保持旧 UT 全绿。LogInput单线程访问,但FileServer::Pause/Resume在配置线程触发(CollectionPipelineManager.cpp:56,160)。新 runner 的 MetricsRecordRef 写入、refcount 增减与延迟移除队列须明确锁边界与 HoldOn 语义(FileServer::PauseInnerFileServer.cpp:84-99)。mWatchNum/mInotifyWatchNum与新 refcount 双台账期间必须保持一致,否则触发max_watch_dir_count误限流(EventDispatcher.cpp:214)。watch_managerrunner 记录与既有 file_server 的watched_dirs_total语义不得冲突——watched_dirs_total保留在 file_server runner 不迁移,新 inotify 生命周期指标只落在watch_managerrunner,避免同名/重复计数。兼容性:
no_inotify、inotify_black_list(AppConfig.cpp:1100-1114)、ignore_dir_inode_changed(AppConfig.cpp:1030)、max_watch_dir_count、default_max_inotify_watch_num(EventDispatcher.cpp:214,229)、enable_polling_discovery。watched_dirs_total现有指标语义与归属(file_server runner)保持;新增指标只增不改。DumpInotifyWatcherDirs/ checkpoint dump 文件格式向后兼容(下游可能解析)。mWatchEventMask(EventListener_Linux.cpp:32-33)与半包处理(:82-93)不得回归。必须遵守的硬约束:
(dev,ino)主键与路径归一化——否则 keep/add/remove 会因 symlink/容器/inode 替换误判而错删或漏建。5. 里程碑
Beta Was this translation helpful? Give feedback.
All reactions