资讯动态

MongoDB 模块化架构实战指南:Modules API、可见性标记与扫描工具链全解析

发布时间:2026/9/13 2:39:33 来源:尧图企业网站定制
MongoDB 模块化架构实战指南Modules API、可见性标记与扫描工具链全解析【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo导读本文以 MongoDB 服务器代码库中的模块化Modules体系为核心系统讲解如何将一个庞大的 C 单体代码库src/mongo/下数千个头文件与源文件拆分为具有清晰公共/私有 API 边界的模块并借助clang::annotate属性、Bazel aspect 扫描与一组 Python 工具merge_decls.py、browse.py、private_headers.py、mod_diff.py实现跨模块访问的自动化检测。读完本文你将掌握 MongoDB 模块定义文件 modules_poc/modules.yaml 的编写规则、全部可见性级别OPEN/PUBLIC/PARENT_PRIVATE/PRIVATE/FILE_PRIVATE等的语义与宏放置位置、IDL 类型可见性标记方法以及一套可直接复用的标记-扫描-校验-评审PR 工作流。什么是模块ModuleMongoDB 对模块的定义包含三个要素提供连贯的公共 APIcoherent public API模块对外暴露的接口是完整、自洽、有明确意图的内部细节不可从模块外部直接访问实现细节被封装在模块内部外部代码不应依赖它们是一组文件的集合涵盖 API 的头文件、实现头文件 cpp 文件以及测试。从本质上说模块是对类这个粒度之上的一层封装抽象。类将成员区分为public和private模块则将整个代码库中的头文件、源文件区分为模块公共 API与模块实现细节粒度更大、适用范围更广。子模块Submodules子模块机制用于在大型模块内部再做细分。在 modules_poc/modules.yaml 中用**点号.**创建子模块例如core、core.bson、core.idl、core.service、core.servers、core.commands、core.unittestquery、query.knobscatalog_and_routing、catalog_and_routing.shard_role、catalog_and_routing.global_catalog、catalog_and_routing.router_role、catalog_and_routing.topologynetworking、networking.core、networking.commands、networking.execution、networking.mirrored_reads、networking.mongo_bridgereplication.*系列replication.oplog、replication.configs、replication.replication_coordinator等。子模块的元数据如 slack 频道、jira 团队若未显式声明会从父模块继承。fully_marked是一个例外——它不会被子模块继承这样可以让父模块先达到 fully_marked 状态再逐步标记各子模块。为什么要做模块化在 MongoDB 这种规模仅src/mongo/下就有数千个源文件的代码库中公共与私有 API 之间缺乏清晰边界会带来两个直接问题模块拥有者不敢自由演进内部实现因为不知道外部有多少代码在直接依赖某个看起来像内部的符号改动内部细节的风险无法评估消费者不清楚哪些 API 是设计给他们的很容易误用本应属于实现细节的符号导致耦合蔓延。通过明确的公共/私有划分团队可以在不影响消费者的前提下自由演进模块内部实现消费者也能从文档化的 API 边界中明确什么该用、什么不该用从而显著提升整个代码库的可维护性与开发速度velocity。如何将文件分配给模块文件与模块的映射关系集中维护在 modules_poc/modules.yaml约 1250 行中。其结构规则如下顶层键是模块名点号用于创建子模块每个模块有一个meta段slack、jira、fully_marked和一个files段属于该模块的 glob 列表每个文件必须且只能属于一个模块模块分配不需要与团队所有权严格对应。meta 字段说明字段含义slack该模块的答疑 Slack 频道。在访问私有 API 的错误信息中会包含它告诉开发者该去哪里询问替代方案。由于#在 YAML 中是注释符应写server-foo而非#server-foojira为模块提交 ticket 时应使用的 Jira Assigned Teamfully_marked若为true则视同该模块内所有头文件都已完成标记——未标记的 API 一律按 private 处理无论其是否 include 了modules.h。这用于锁定标记进度防止新文件加入后破坏 fully marked 状态。该字段不被子模块继承glob 匹配规则最长 glob 优先当多个 glob 同时匹配同一文件时当前规则是最长 glob 胜出longest glob wins。这是最具体 glob 胜出的一种简化实现未来可能切换为后者。这一规则在源码中有据可查modules_poc/mod_mapping.py将模块 glob 转换为类似 CODEOWNERS 的规则行后执行lines.sort(keylambda l: len(l.split()[0]))——规则越长越靠后而 CODEOWNERS 语义是后匹配者胜出从而实现了更长更具体的 glob 优先。在files中可以使用精确路径、通配符*、[._]*与递归通配**例如core: meta: slack: server-programmability jira: Server Programmability fully_marked: true files: - src/mongo/base/ - src/mongo/platform/ - src/mongo/stdx/ - src/mongo/util/ - src/mongo/db/partitioned* - src/mongo/db/field_parser[._]* - src/mongo/db/generic_argument_util.* - src/mongo/executor/task_executor[._]*如何标记 API 可见性标记 API 可见性的核心是MONGO_MOD_*宏族其规范定义在 src/mongo/util/modules.h。这些宏最终展开为 clang 属性#define MONGO_MOD_ATTR_(attr) clang::annotate(mongo::mod:: #attr)即在编译器层面为声明打上clang::annotate注解供扫描工具读取。可见性级别从最开放到最受限宏语义MONGO_MOD_OPEN可在代码库任意位置使用并继承可被外部子类化。注意 openness不会递归应用每个需要支持外部子类的类型都必须单独标记MONGO_MOD_PUBLIC可在代码库任意位置使用对于类型子类只能在同模块内定义。默认所有继承体系都在模块内封闭除非显式标记为OPENMONGO_MOD_NEEDS_REPLACEMENT模块拥有者希望它是私有的但目前没有合适的替代方案。允许像PUBLIC一样外部使用但会给模块拥有者警告MONGO_MOD_USE_REPLACEMENT(replacement)模块拥有者希望它是私有的但存在尚未清理的历史外部使用。允许外部使用但会给每个调用者警告。replacement是自由文本通常写替代 API如Foo::bar()也可以写提示信息MONGO_MOD_PARENT_PRIVATE与PRIVATE类似但允许父模块中任意文件使用包括其他子模块。例如foo.bar模块中的PARENT_PRIVATE类可被foo、foo.bar、foo.baz中的代码使用。仅能用于子模块模块名中含点号MONGO_MOD_PRIVATE只能从当前模块或其子模块使用MONGO_MOD_FILE_PRIVATE只能从当前文件家族大致为 header cpp tests使用即使同模块的其他文件也不能用MONGO_MOD_PUBLIC_FOR_TECHNICAL_REASONS仅用于因扫描器限制而无法标记为 private 的场景例如只能通过公共宏使用的声明。绝大多数情况下应优先使用NEEDS_REPLACEMENT另有MONGO_MOD_UNFORTUNATELY_OPEN用于只能在模块边界内继承、但不幸存在其他模块扩展的类——当前行为等同OPEN主要作用是提供可 grep 的文档化标记。公共 vs 私有的直觉可以像看待一个class的 public/private 分区一样看待模块可见性——前者表示设计给外部使用的 API后者表示实现细节。区别仅在于粒度PRIVATE对整个模块含子模块开放实现细节FILE_PRIVATE则只对文件家族开放。宏的放置位置MONGO_MOD宏是 C 属性需要放在特定位置取决于被标记对象// 1. 头文件 include 之后第一行单独成行设置该头文件的默认可见性 // 只允许 PUBLIC、PARENT_PRIVATE、FILE_PRIVATE MONGO_MOD_PUBLIC; // 2. 命名空间不支持在单个声明中用嵌套形式如 namespace mongo::repl namespace MONGO_MOD mongo { // 3. 类/枚举/结构体/联合体 class MONGO_MOD Foo { // 4. 函数与变量 MONGO_MOD void func(...); MONGO_MOD int var; // 5. 概念concept concept isFooable MONGO_MOD {对于放在行首的宏如果 clang-format 选择了不理想的位置断行通常的做法是撤销格式化把宏单独放到声明上一行。包含 modules.h 后的默认行为按头文件逐个标记 API在头文件中包含mongo/util/modules.h该头文件即被视为已模块化modularized产生如下效果该头文件中的所有声明不含传递包含进来的内容默认是PRIVATE——也就是说公共 API 是必须被显式标记出来的类中private:段的成员默认PRIVATE无论类本身的可见性如何。语言层面唯一能让模块外使用它们的方式是跨模块 friend这通常应避免。若临时需要优先用NEEDS_REPLACEMENT而非PUBLIC以_forTest结尾的声明默认FILE_PRIVATE以支持仅供测试该类的常见场景。如果它们实际意图是支持消费者测试而非仅测试其所在类型可显式标记为PUBLIC或PRIVATE内部internal和 detail 命名空间默认PRIVATE且不能放宽但可以进一步收紧为FILE_PRIVATE。命名空间内的单个声明可以按需暴露但不能批量暴露——除非把命名空间改名为不暗示 private 的名字。对于模块内不贡献公共 API 的内部头文件只包含modules.h就够了有 private header marker 工具可自动化此过程。是否再把某些 API 标记为FILE_PRIVATE是可选的。不完全标记的例外极少数情况下你只想用到modules.h中的宏但还不想把头文件标记为完全标记此时应包含 src/mongo/util/modules_incompletely_marked_header.h 而非modules.h。头文件级默认可见性MONGO_MOD_PUBLIC;单独放在 include 之后的第一行可以为整个头文件设置默认可见性。此时只允许PUBLIC、PARENT_PRIVATE、FILE_PRIVATE三种取值因为头文件级别的默认值没有更开放与更封闭之外的中间语义。IDL 文件中的可见性标记对于 IDL 文件通过mod_visibility选项标记整个类型struct、enum、command的可见性。取值与MONGO_MOD宏同名但小写、去掉前缀例如mod_visibility: public。可以在global:段设置该 IDL 文件中所有类型的默认可见性。无法控制类型内部单个函数的可见性。仓库中的实际用法示例# src/mongo/bson/bson_validate.idl mod_visibility: public# src/mongo/client/sasl_aws_client_options.idl mod_visibility: private更多实例可参见 src/mongo/client/read_preference.idl、src/mongo/client/client_api_version_parameters.idl 等文件。有哪些工具可用所有工具都应在配置好的 Python 虚拟环境中运行并先执行buildscripts/uv_sync.sh确保依赖正确mod_mapping.py甚至会启动时校验codeowners库版本过旧则报错提示先跑 uv_sync。工具脚本全部位于 modules_poc/ 目录。扫描器与合并器Scanner and Merger合并器merger即 modules_poc/merge_decls.py它生成所有 first-party 代码互相引用的交叉引用表存储到merged_decls.json供其余工具使用同时它也是校验非法跨模块访问的地方。浏览器请求重新扫描时会自动调用它也可手动运行./modules_poc/merge_decls.py其内部实现会执行bazel build --configmod-scanner //src/mongo/...对整个代码库运行扫描器或仅扫描上次扫描后有变更的部分并利用 Bazel 远程执行达到极高的并行度源码中最多重试 3 次以容忍瞬时失败。扫描动作本身由 Bazel aspect 驱动定义在 modules_poc/mod_scanner.bzl对每个 C/C 目标提供CcInfo、非 external、不带no-mod-scantag生成ModScanneraction只扫描src/mongo/下且不在third_party中的.c/.cc/.cpp/.cxx/.c/.C源文件头文件通过#include被间接分析每个翻译单元输出一个*.mod_scanner_decls.json.zstzstd 压缩合并器再统一解压合并。合并器还负责执行可见性校验跨模块访问私有声明会报错mod_diff.py中对此有^^ERROR^^: private declaration above has ... direct external usages!的错误输出路径。分析merged_decls.json时jq 是强大工具也可以直接用 Python。例如官方进度报告的生成方式遍历每个 mod 和 TOTAL统计无UNKNOWN可见性的文件比例jq map(., .mod TOTAL) | group_by(.mod)[] | group_by(.loc | split(:)[0]) | {mod: .[0].[0].mod, total: length, marked: map(select(any(.visibility UNKNOWN) | not)) | length} | .done (1000 * .marked / .total | round) / 10 | \(.mod): \( * (.mod | 40-length)) \(.done)% (\(.marked) / \(.total)) -r merged_decls.json浏览器The Browser核心交互工具是 modules_poc/browse.py基于 Textualtextual tree-sitter-cpp rich实现的全终端 TUI./modules_poc/browse.py若尚未扫描过代码库启动时会询问是否扫描需几分钟修改源码后可随时按r重新扫描且只重扫被修改的文件或其传递包含链上的文件浏览器主要服务于给公共 API 打标签因此文件按未标记声明unknowns数量最多排序按f搜索文件按m按模块过滤快捷键列表显示在右侧按?可切换显示。其他常用键g跳转到当前高亮声明/位置仅在 vscode 或 nvim 终端内有效vscode 下调用code -gnvim 下通过--remote-send定位光标p切换内联代码预览Tab在树与代码预览间切换鼠标完全支持滚动与展开另有 vim 风格别名hjkl移动、/搜索。浏览器界面中每个声明会显示可见性徽标绿色pub/open、黄色priv/file_priv/parent_priv、橙色needs_repl/use_repl、红色¿vis?表示 UNKNOWN以及 direct usages、direct and transitive usages、semantic children、lexical children 等维度。Private Header Marker自动标记私有头文件在完成扫描并生成merged_decls.json之后modules_poc/private_headers.py 会找出所有当前未检测到外部使用的头文件与 IDL 文件并自动将它们标记为模块内完全私有写入#include mongo/util/modules.h必要时把modules_incompletely_marked_header.h替换为modules.h。注意自动标记并不等于这些头文件理应私有——必须由人工复核确保标记结果符合意图。脚本支持按 module / owning team / path glob 任意组合过滤对于命中的头文件还会警告_forTest用法超出文件家族的情况这类声明默认仅限文件家族若需整个模块可用应显式标记PRIVATE。修改 C 文件后记得运行buildscripts/clang_format.py format-my或bazel run format。示例用法./modules_poc/private_headers.py --teamserver_programmability --modulecore --globsrc/mongo/executor/*加--dry-run或-n可以只预览所有变更而不实际应用。PR 评论生成器PR Comment Generatormodules_poc/mod_diff.py 会为当前分支中每个被修改的文件输出所有 API含可见性级别与使用次数的简要摘要作为 PR 评审辅助./modules_poc/mod_diff.py默认通过git merge-base origin/master HEAD与git diff --name-only找出变更文件也可在命令行直接传入文件列表输出按可见性从开放到受限排序visibility_rankUNKNOWN → open → public → use_replacement → needs_replacement → parent_private → private → file_private递归展示语义子节点并标注N usages, M external输出与 C 非常接近PR 评论中应放入 cpp 代码块使其更易读本地可用bat -lcpp做彩色高亮它使用最近一次扫描的结果因此修改过任何头文件后应先重新扫描再运行此工具。标准工作流Workflow每个 PR 的一般流程确保处于 Python 虚拟环境中必要时创建一个并运行buildscripts/uv_sync.sh更新依赖运行合并器扫描代码库modules_poc/merge_decls.py标记一些头文件重跑合并器确保没有违规并更新merged_decls.json运行 PR 评论生成器展示已标记的 API仔细检查输出确认一切符合预期提交 PR并在评论中放入 cpp 代码块 形式的生成结果建议保持 PR 小而精比如每次不超过 10 个文件便于评审者审查作为例外把大量头文件自动标记为私有可以放在单个 PR 中但必须与任何手动标记的 PR 分开。开始标记一个新模块首次开始标记某个模块时建议先以--dry-run-n运行private_headers.py./modules_poc/private_headers.py --dry-run --moduleYOUR_MODULE对于较大模块尤其是query巨型模块可以加--glob先聚焦于代码的子集。这样你会得到一个概览哪些文件被模块外部使用即今天的事实公共 API哪些没有可自动标记为私有实现细节。如果所有事实私有的头文件看起来都确实应该私有去掉--dry-run即可自动标记它们。务必验证其内容确实意图私有——人工标记的意义就在于正确捕获意图。可选地可以在每个头文件中把实现细节标记为FILE_PRIVATE防止它们在模块内其他文件被使用。随后打开浏览器modules_poc/browse.py查看剩余头文件它会显示谁在用、从哪里用对看起来该私有却被外部使用的情况尤其有用。内部 API 正在被使用时怎么办若只有少量外部文件在使用先检查这些文件是否本来就应属于你的模块。第一阶段phase 1尽力正确映射了所有文件但可能有文件被分配到了错误的模块。此时调整modules_poc/modules.yaml中的 glob 即可若已有应被调用者使用的公共 API将其标记为USE_REPLACEMENT(better_api)。参数接受任意 C 标记但意图是尽可能写替代 API 的名字。这会为所有使用该代码的团队生成 ticket如果使用者极少考虑直接清理掉这些使用若其他模块确实需要该功能、且这是唯一途径重新考虑是否将其设为公共 API否则若没有满足调用者需求的公共 API但你不希望该 API 长期保持公共使用NEEDS_REPLACEMENT——这会为拥有该代码的团队生成 ticket如果该 API 显然本意是私有的例如位于details命名空间且调用者完全可以自己实现该功能比如自己写一份用USE_REPLACEMENT(do not use internal details)是合理的。注意事项与已知限制Caveats and Limitations总原则即使当前工具不会强制也始终按真实意图标记声明。这既为人类读者提供正确信息也避免未来工具改进消除这些限制时出现问题。以下限制较为技术化大多数读者除非遇到诡异现象否则不必深究。这些限制主要影响 core 类模块——因为代码库其余部分很少通过宏和模板暴露 API也很少有仅被模板消费的 API。不追踪命名空间本身只追踪命名空间内的声明。命名空间被标记可见性时影响的是该命名空间块内所有声明的默认可见性命名空间每次重开都是独立块其他块上的标记不适用扫描器只知道被使用的声明出于实现原因它只能通过每个使用点用了什么来反推发现声明这既可能是某些限制的原因也可能是结果模板中的使用可能不可见尤其是依赖类型与值编译器在模板实例化前无法确定的东西。函数参数是依赖类型时可能无法确定选择哪个重载对无限定调用f(blah)而非ns::f(blah)或x.f(blah)更严重——由于 ADL重载决议总是被延迟宏展开产生的一切都视为在展开点书写对声明和使用都成立。若某 API 只应通过已定义宏使用标记为MOD_PUBLIC_FOR_TECHNICAL_REASONS提醒读者避免直接使用即使工具不会阻止模板变量被完全忽略clang 的 bug但仍应正确标记未来可能修复方法调用归属调用点的静态类型两个重要影响子类重写的方法若只通过基类指针/引用调用可能看似未被使用通过基类指针/引用的调用被计为该类方法的调用而非接口的默认成员defaulted members含默认构造/析构视为对类本身的使用clang 无法区分隐式/显式 defaulted模板规范化难题尽力把声明报告为模板fooT而非fooint、foostring等实例除非是显式特化实例有不同于主模板的定义。clang 在此表现不佳有大量 workaround最重要的影响函数/变量模板的显式特化被忽略一律转换为主模板类型显式特化按独立实体处理启发式位置与主模板不同因为其形态与 API 可能不同但通常应与主模板可见性一致除非实例化使用了本应对消费者不可用的私有类型clang 常把大量位置指派给显式模板实例化与 extern template 声明点即使有更好的位置所幸这种情况较少using声明与类型别名的解析目标clang 有时报告解析目标但通常报告using声明本身。几个值得注意的案例是趋势而非绝对using Base::foo;暴露基类成员解析为对Base::foo的使用而非Derived::foo——当Base意图是私有实现细节时尤其显著需要把所有暴露的方法标记为 publicusing Base::Base;引入基类构造器恰好相反记录为对Derived::Base(args)的使用——这样的声明实际并不存在internal/details 命名空间匹配正则(detail|internal)s?$在包含modules.h时默认可见性隐式为 private不能设为公共可见性但可进一步限制为FILE_PRIVATE。若希望其中的声明对模块外可用必须显式标记其子声明或改名去掉暗示仅供内部的后缀。常见场景仅意图通过宏使用的内部声明标记为PUBLIC_FOR_TECHNICAL_REASONS谨慎对待前向声明forward declarations尽可能避免除非收益显著尤其避免前向声明其他模块的任何东西。必须使用前向声明时确保其可见性与真实定义一致。例外若每个看到前向声明的 TU 也会看到定义如同头文件内或前向声明位于被定义头文件包含的私有实现细节头中可省略对前向声明的标记。注意如果前向声明是 TU 中看到的唯一声明隐式可见性标记也适用于它永远不要为了省一个 include 而前向声明函数——无论对 C 一般规则还是此工具函数都比类型麻烦得多位置选取头文件中定义的类型尽量用定义位置其余一律用canonical locationclang 术语指当前 TU 中见到的第一个声明类型定义在 .cpp 中时用 canonical location只考虑头文件中的声明从不考虑 .cpp 中的声明注意_forTest函数默认FILE_PRIVATE通常只意图测试其所在类型。若确实意图作为测试消费者的 API可显式标记PUBLIC模块外可用或PRIVATE模块内可用隐式使用如隐式转换运算符即使未在调用点具名也被计为使用合并多 TU 信息时定义总是替换仅见声明的 TU 收集到的元数据注意并不保证看到每个定义尤其在定义所在的 TU 中未被调用的函数因此不能借此发现删了定义却忘了删声明的问题反正也只追踪被使用的东西未定义的东西无法真正被使用类的private成员隐式为PRIVATE如需更改必须显式标记。它们大概永远不应被设为PUBLIC那意味着跨模块 friend。现存少数案例均标记为不幸公共的变体NEEDS_REPLACEMENT或USE_INSTEADprivate类型的public成员不会继承隐式PRIVATE遵循寻找最近带显式标记的语义父级的常规规则因此可能是PUBLIC。但语言规则依然生效只要类型的实例从未交给消费者他们就无法访问这些成员protected成员默认不是PRIVATE但因为只允许从OPEN类继承语言可见性规则会阻止模块外访问除非用OPEN类或friend主动放开。注意任何子类标记为OPEN都会暴露父类的全部protected成员除非它们被标记为PRIVATEfriend声明基本被忽略除非它同时也是定义。因此hidden friend模式下的定义会被追踪但定义在 .cpp 中的则被忽略。关于语义父级的补充clang 区分语义semantic与词法lexical父级二者的主要差异是类的成员包括成员类型即使行外out-of-line定义也是该类的语义子级反之friend声明不是它们被视为最近命名空间的语义子级。总结MongoDB 的模块化体系是一套约定modules.yaml 编译器注解MONGO_MOD 宏 Bazel 扫描 Python 工具链四位一体的工程实践modules.yaml定义文件归属与元数据modules.h提供可见性标记语言mod_scanner.bzlmerge_decls.py实现分布式扫描与跨模块访问校验browse.py提供交互式标记界面private_headers.py加速私有化进程mod_diff.py辅助 PR 评审。这套流程不仅适用于 MongoDB 自身对于任何大型 C 代码库建立公共 API 契约 自动违规检测的模块化治理都具有直接的参考与借鉴价值。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价