MongoDB Stable API:API 版本兼容性规则与 IDL 兼容性检查机制全解
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
本文围绕 MongoDB 官方文档 STABLE_API_README.md 展开,讲清楚 Stable API 的核心概念(API 版本、兼容性禁止/允许变更清单、apiVersion/apiStrict/apiDeprecationErrors三个客户端参数),并结合当前仓库源码,深入解析服务器端如何实现版本校验(api_parameters.idl、validate_api_parameters.h、commands.h)以及工程上如何用 IDL 兼容性检查脚本防止破坏性变更落地(idl_check_compatibility.py)。读完后你将掌握:在 MongoDB 服务器开发中如何正确标注 IDL 命令的api_version与stability字段、如何本地运行兼容性检查器、以及 FCV(featureCompatibilityVersion)与 API 版本之间的两条硬性规则。
一、什么是 Stable API 与 "API 版本"
MongoDB 的 API 指的是所有命令的用户可见行为,包括命令的参数与回复字段。所谓"API 版本"(API version)是对 API 的一个子集做出特别强的承诺:对于任一 API 版本 V,如果应用声明了 API 版本 V、且只使用 V 内包含的行为,并与特定版本的官方驱动一起部署,那么只要后续服务器版本仍支持 V,服务器升级就不会带来语义上有显著变化的行为差异。
由此可以推出几个关键设计原则(均来自 STABLE_API_README.md):
- 在同一个 API 版本内部,只允许引入兼容性变更;
- 不兼容的变更必须放到新的 API 版本中;
- 服务器可以同时支持多个 API 版本,不同应用可以使用不同的 API 版本;
- 驱动负责把
apiVersion等参数附加到每个命令调用上(见后文"API 版本参数"一节)。
从仓库源码看,服务器当前实际接受apiVersion: "1"(测试开关下才允许 "2"),这体现在版本解析函数getAPIVersion中:对"1"直接返回 1,对"2"要求allowTestVersion否则抛出APIVersionError,其他任何值直接断言失败并提示API version must be "1"(validate_api_parameters.h)。
二、兼容性规则:哪些变更被禁止,哪些被允许
这是 Stable API 文档中最核心的部分。对于任一 API 版本 V,以下变更被禁止,必须在新的 API 版本 W 中才能引入:
| # | 被禁止的变更 |
|---|---|
| 1 | 删除某个 StableCommand(V 中已存在的命令) |
| 2 | 删除某个已文档化的 StableCommand 参数 |
| 3 | 禁止某个原本允许的 StableCommand 参数值 |
| 4 | 从 StableCommand 的回复中删除字段 |
| 5 | 改变回复字段的类型,或扩大其可能类型集合 |
| 6 | 向回复字段中枚举型固定值集新增取值(例如新的索引类型),除非存在 API version 之外的 opt-in 机制 |
| 7 | 以可能导致现有应用行为异常的方式改变 StableCommand 的语义 |
| 8 | 改变某个错误场景下返回的错误码(如果驱动依赖该错误码) |
| 9 | 移除某个错误场景下、此前带有的错误标签 L |
| 10 | 禁止任何当前允许的 CRUD 语法元素,包括查询与聚合操作符、聚合 stage 与表达式、CRUD 操作符等 |
| 11 | 移除对某个 BSON 类型的支持,或做任何其他 BSON 格式变更(新增类型除外) |
| 12 | 放弃对某个 wire protocol 消息类型的支持 |
| 13 | 提高 StableCommand 的授权要求(使其更严格) |
| 14 | 提高hello.minWireVersion(或降低maxWireVersion——官方承诺不会这样做) |
反过来,在 V 内部被允许的变更包括:
- 新增命令;
- 新增可选命令参数;
- 允许某个此前被禁止的命令参数或参数值;
- 对未文档化命令参数的任何变更;
- 内部分片/复制等协议的任何变更;
- 新增命令回复字段;
- 新增错误码(前提是不错坏与现有驱动和应用的兼容性);
- 给错误新增标签;
- 调整回复文档与子文档中字段顺序;
- 新增 CRUD 语法元素;
- 放宽 StableCommand 的授权要求;
- 新增和移除认证机制(认证机制可能因安全漏洞被移除,因此不对其兼容性做保证);
- 废弃(deprecate)某个行为;
- 提高
hello.maxWireVersion; - V 之外行为的任何变更;
- 性能层面的变更。
这套规则的意义在于:它精确划定了"服务器升级后应用无感知"的边界。开发人员在修改任何 IDL 命令时,都必须对照这两张清单判断自己的变更属于哪一类。
三、兼容性如何被强制:IDL + 兼容性检查脚本
3.1 检查机制总览
为保证新提交不会对当前 API 版本引入破坏性变更,仓库提供了一个兼容性检查脚本 idl_check_compatibility.py。其工作机制(文档描述 + 源码印证):
- 检查对象是 IDL 文件:所有处于 Stable API 版本中的命令,都必须用 IDL 描述其输入与输出,才能被做兼容性检查。唯一的例外是
explain命令——它的输出不属于 API V1 的一部分。 - 与基线和所有历史版本比对:脚本将新提交中的 IDL 文件,与 base commit 以及5.0.0 及之后的所有 release的 IDL 文件做对比。历史 release 的 IDL 由 checkout_idl_files_from_past_releases.py 检出到本地目录。
- 在 CI 中运行:该脚本会运行在 evergreen 的 patch build 和 commit queue 中,CI 入口脚本是 check_idl_compat.sh。
- 命令实现层的双重保险:很多命令实现派生自
TypedCommand,这保证实现实际使用的就是 IDL 规范中声明的字段,而不是绕过 IDL 私自读写字段。仓库中还有一个辅助脚本 check_stable_api_commands_have_idl_definitions.py,用于检查 Stable API 命令确实拥有 IDL 定义。
从源码结构看,idl_check_compatibility.py 从一份 YAML 规则文件加载若干"例外/允许名单",这些名单正是后文第四节各白名单机制的落点:
ALLOW_ANY_TYPE_LIST: list[str] = rules["ALLOW_ANY_TYPE_LIST"] IGNORE_ANY_TO_NON_ANY_LIST: list[str] = rules["IGNORE_ANY_TO_NON_ANY_LIST"] IGNORE_NON_ANY_TO_ANY_LIST: list[str] = rules["IGNORE_NON_ANY_TO_ANY_LIST"] ALLOW_CPP_TYPE_CHANGE_LIST: list[str] = rules["ALLOW_CPP_TYPE_CHANGE_LIST"] IGNORE_STABLE_TO_UNSTABLE_LIST: list[str] = rules["IGNORE_STABLE_TO_UNSTABLE_LIST"] ALLOWED_STABLE_FIELDS_LIST: list[str] = rules["ALLOWED_STABLE_FIELDS_LIST"] IGNORE_COMMANDS_LIST: list[str] = rules["IGNORE_COMMANDS_LIST"] # ... 以及 RENAMED_COMPLEX_ACCESS_CHECKS、ALLOWED_NEW_COMPLEX_ACCESS_CHECKS 等3.2 本地运行兼容性检查器
文档给出了完整的本地操作步骤。第一步,检出历史 release 的 IDL 文件(会在idls目录下创建各历史版本的子目录):
python buildscripts/idl/checkout_idl_files_from_past_releases.py -v idls第二步,选择要对比的旧 release 版本目录,运行检查脚本:
python buildscripts/idl/idl_check_compatibility.py -v \ --old-include idls/<old_release_dir>/src \ --old-include idls/<old_release_dir>/src/mongo/db/modules/enterprise/src \ --new-include src \ --new-include src/mongo/db/modules/enterprise/src \ idls/<old_release_dir>/src src一个真实示例(对比 r6.0.3):
python buildscripts/idl/idl_check_compatibility.py -v \ --old-include idls/r6.0.3/src \ --old-include idls/r6.0.3/src/mongo/db/modules/enterprise/src \ --new-include src \ --new-include src/mongo/db/modules/enterprise/src \ idls/r6.0.3/src src参数含义:--old-include/--new-include分别指定新旧两侧 IDL 搜索目录(企业模块src/mongo/db/modules/enterprise/src也需要单独包含);最后两个位置参数是旧、新 IDL 根目录;-v打开详细输出。
四、如何新增命令、参数与回复字段
这一节是 Stable API 开发流程的核心。任何对 Stable API 的新增都必须由 Stable API PM 审批,并由 Query Optimization Team 进行 code review。
4.1 命令级:api_version字段
新增 IDL 命令需要api_version字段,表示该命令属于哪个 Stable API 版本。规则:
- 默认应为空字符串
""; - 只有明确要把命令加入 Stable API 时,才填写目标版本号(当前为
"1"); - 一旦加入 Stable API,意味着在当前 API 版本存续期间该命令不可被删除。
仓库中的实际例子:explain.idl 声明了api_version: "1",即explain命令属于 API V1。
4.2 字段级:stability字段的三个取值
新增命令参数或回复字段时,必须标注stability字段,取值为unstable、internal、stable三者之一。选择原则:
- 拿不准就标
stability: unstable(文档的明确建议); - 只有确定该字段要进入 Stable API 时才标
stability: stable。同时,必须在 idl_check_compatibility.py 中将字段加入ALLOWED_STABLE_FIELDS_LIST白名单,格式为:
<command_name>-<command_param_or_reply_field>-<field_name>这个白名单存在的目的是强制工程师意识到:字段一旦成为 Stable API 的一部分,就不能以违反 Stable API 指南的任何方式变更。具体地说,在当前 API 版本存续期间,该字段既不能删除,也不能改回stability: unstable或stability: internal。仓库 IDL 中可见stability: stable的实际用法(如 explain.idl 中标注的字段)。
4.3stability: unstable与stability: internal的区别
两者都表示"不属于 Stable API",区别在于mongos 向 shard 转发命令时的解析校验:shard 会执行一次校验,检查命令的所有字段是否都属于 Stable API——字段若标为stability: unstable会抛APIStrict错误,而标为stability: internal不会。internal正是为了"能通过这次校验、但仍不进入 Stable API"的场景而加的。因此通用规则是:默认标unstable;除非该字段会经过上述解析校验,此时应标internal。
4.4 例外名单:IGNORE_STABLE_TO_UNSTABLE_LIST
历史上出现过字段被误加入 Stable API、而后该字段实际上是纯内部/未对用户文档化的情况,于是被(经过同样的审批流程)改为 unstable。正常情况下,把字段从stability: stable改回unstable或internal会触发检查器报错,IGNORE_STABLE_TO_UNSTABLE_LIST就是为这类例外提供的允许名单。向该名单添加条目同样必须经 Stable API PM 审批 + Query Optimization Team review。
4.5 BSON 序列化any类型的处理
bson_serialization_type用于定义 IDL 字段序列化为哪种 BSON 类型。有些命令需要 C++ 中定义的自定义序列化器(用于类型校验或接受多种类型),此时bson_serialization_type会被写成any。由于类型的主要逻辑在 IDL 文件之外,兼容性检查脚本无法对any做类型检查。处理方式不是禁止使用,而是要求把使用该类型的命令加入 idl_check_compatibility.py 中的ALLOW_ANY_TYPE_LIST允许名单;所有标为stability: unstable的字段同样适用此规则——这样可以防止将来把字段从unstable改为stable时出现意料之外的检查错误。这种"显式 opt-in"表达了一种约定:实现者理解使用any的含义并有正当理由。
五、服务器端实现:版本校验与 API 版本参数
5.1Command基类的版本接口
文档指出:所有Command子类实现apiVersions(),返回该命令所属的 API 版本集合;默认情况下命令不属于任何 API 版本,即没有特殊的向后兼容保证。子类还实现deprecatedApiVersions(),返回命令被废弃的 API 版本集合,它是apiVersions()的子集。在 commands.h 中可以确认这两个虚函数,以及一个相关接口:
// Returns the list of API versions that include this command. virtual const std::set<std::string>& apiVersions() const; // Returns the list of API versions in which this command is deprecated. virtual const std::set<std::string>& deprecatedApiVersions() const; // Some commands permit any values for apiVersion, apiStrict, and // apiDeprecationErrors. For internal (server to server) commands we // should skip checking api version. virtual bool skipApiVersionCheck() const { return false; }从源码结构看,skipApiVersionCheck()对应了 IDL 检查脚本中"内部(服务器到服务器)命令跳过 API 版本检查"的设计,与第四节的 shard 侧校验形成呼应。
5.2 三个客户端参数的定义与行为
所有命令都接受三个参数:apiVersion、apiStrict、apiDeprecationErrors。由驱动在每次命令调用时附加,含义分别是:请求哪个 API 版本、是否允许调用不属于任何 API 版本的命令、是否允许已废弃行为。如果某行为在不同 API 版本之间发生变化,服务器根据客户端的apiVersion参数决定如何表现。这三个参数均可选——除非服务器参数requireApiVersion为 true,此时所有命令都必须带apiVersion。
这些参数在仓库中有明确的 IDL 定义(api_parameters.idl):
structs: APIParametersFromClient: description: "Parser for pulling out VersionedAPI parameters from commands" fields: apiVersion: description: "The api version specified by the command" type: string optional: true apiStrict: description: >- With apiVersion: 'V' and apiStrict: true, the server rejects requests to use behaviors not included in V type: bool optional: true apiDeprecationErrors: description: >- With apiVersion: 'V' and apiDeprecationErrors: true, the server rejects requests to use behaviors deprecated in V in the current MongoDB release type: bool optional: true server_parameters: requireApiVersion: description: "Require clients to pass the 'apiVersion' parameter with all commands" test_only: true set_at: ["startup", "runtime"] default: false值得注意的实现细节:
requireApiVersion目前是test_only: true的服务器参数,默认false,支持启动时与运行时设置(变量名为gRequireApiVersion,类型Atomic<bool>)。- 还有一个仅测试用的启动参数
acceptApiVersion2(默认false),用于在测试中放开apiVersion: "2"。 - 版本解析逻辑在 validate_api_parameters.h:当前生产环境只接受
"1","2"需要测试开关放行,其余值报错API version must be "1"。 - 校验入口
validateAPIParameters与enforceRequireAPIVersion也在同一头文件声明;从注释看,enforceRequireAPIVersion对内部客户端发起的hello命令会绕过强制检查。
5.3 默认 apiVersion 的不可变性
当前apiVersion的默认值是"1"。文档说明:未来可以移除默认值、把apiVersion变成所有命令的必填参数,但默认值本身永远不会被改变——这本身就是 Stable API 兼容性承诺的一部分。
六、API 版本的废弃(Deprecation)与下线(Dropping)
废弃(Deprecation):可以在任一服务器 release R 中,在 API 版本 V 内废弃某些行为(包括命令、命令参数等)。如果计划在后来的 API 版本中移除某行为,就应该先废弃它。理想情况是:若 R 同时支持 V 和 W,用户代码在 V 下于 R 上运行没有 deprecation 错误,那么把代码切到 W 运行在 R 上时不需要再改任何代码。但也存在一些 W 中的不兼容变更无法由 V 中的废弃预告覆盖。
下线(Dropping):要废弃(drop)API 版本 U,必须至少发布一个同时支持 U 和某个更新的 API 版本 V的服务器 release R,并且要求它在 R 的完全升级后的 FCV下同时支持这两个版本。这给了用户在无停机(no downtime)情况下更新代码到 V 的窗口。
七、featureCompatibilityVersion(FCV)与 API 版本的两条规则
FCV 与 API 版本的关系由两条硬性规则约束:
规则 1:首个支持新版本 W 的 release,只能在升级 FCV 中启用 W
第一个支持 API 版本 W 的 release,可以在其升级后的 FCV(upgraded FCV)中提供 W,但不能在其降级 FCV(downgraded FCV)中提供 W。原因是某些 API 版本会引入需要磁盘格式变更或集群内协议变更的行为,而这些变更要等到setFCV("R")才生效;为保持一致性,新 API 版本一律等到setFCV("R")之后才可用。
规则 2:至少一个 release 必须在升级 FCV 下同时支持 V 和 W
为了让应用能从 V 无停机升级到 W,至少一个 release 必须在升级后的 FCV 下同时支持 V 和 W。这样零停机升级的流程是:应用先运行在 API 版本 V 上,把服务器升级到 R 并处于 FCV "R";此时再重新部署已改为 W 的应用代码。
规则 2 特别强调:仅在一个二进制的降级 FCV下同时支持 V 和 W 是不够的——因为云用户无法控制二进制升级后的服务器何时执行setFCV("R")。服务器必须在其升级 FCV(即稳态)下同时支持两个 API 版本。
八、小结:Stable API 的三层保障
综合文档与源码,MongoDB Stable API 的工程保障可以归纳为三层:
- 规范层:禁止/允许变更两张清单(第二节),是所有设计决策的基准;
- 流程层:新增 Stable API 命令/字段、修改白名单(
ALLOWED_STABLE_FIELDS_LIST、IGNORE_STABLE_TO_UNSTABLE_LIST)均需 Stable API PM 审批与 Query Optimization Team review,IDL 中api_version与stability字段是强制标注项; - 自动化层:idl_check_compatibility.py 在 evergreen patch build 与 commit queue 中将新提交的 IDL 与 5.0.0 以来所有 release 对比(入口 check_idl_compat.sh),
TypedCommand派生保证实现不脱离 IDL,shard 侧APIStrict校验兜住运行时转发链路。
对于服务器开发者,最实用的两条操作清单是:新字段默认stability: unstable;新命令默认api_version: ""——除非你确实在走 Stable API 审批流程。本地验证改动时,按第三节的命令先检出历史 IDL 再跑兼容性检查器,即可在提交前复现 CI 的拦截结果。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考