news 2026/9/15 2:39:35

MongoDB Split Horizon 深度解析:基于 SNI 的多网络区域副本集地址通告机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MongoDB Split Horizon 深度解析:基于 SNI 的多网络区域副本集地址通告机制

MongoDB Split Horizon 深度解析:基于 SNI 的多网络区域副本集地址通告机制

【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo

导读

Split Horizon(水平分割)是 MongoDB 副本集的一项核心机制,它允许同一个副本集成员根据客户端所处的网络区域,向其通告不同的host:port地址,从而支撑"内部网络 + 外部网络"混合部署场景下的地址可达性。本文以当前仓库中 split_horizon 模块 为骨架,结合 split_horizon.cpp、member_config.cpp 与 replication_coordinator_impl.cpp 等源码实现,完整讲解horizons配置、SNI 驱动的区域判定、正反向映射数据结构、构造校验规则、hello响应集成与序列化细节,让你既能写出可落地的replSetReconfig配置,也能理解其底层工作原理。


一、为什么需要 Split Horizon:多网络区域的地址通告问题

一个典型的 MongoDB 部署会横跨内部网络与外部网络。假设某个副本集成员同时被两类客户端访问:

  • 内部客户端通过internal.example.com:27017访问该成员;
  • 外部客户端则通过另一条 DNS 名称(如external.example.com:25000)访问同一个成员。

如果没有 Split Horizon,服务器在hello(或旧版isMaster)响应中只能通告一个地址。此时必然有一侧客户端拿到的地址在自己的网络区域内无法路由,导致连接失败。Split Horizon 正是为了解决这一矛盾而引入的机制:同一个成员在不同网络视图(horizon)下拥有不同的地址,服务器根据客户端的来源区域返回对应的那组地址

从副本集配置的角度看,这就是 MongoDB 官方文档中副本集成员配置选项horizons背后的实现机制。当前仓库中 repl_set_config.cpp 与 repl_set_config_checks.cpp 承担着该配置项的解析与合法性校验职责。

二、核心概念:Horizon 与__default

2.1 什么是 Horizon

一个horizon(水平线)是"一个命名网络视图"。每个副本集成员始终拥有一个__defaulthorizon,它对应成员配置中的host字段;额外的 horizon 则通过成员配置中可选的horizons子文档声明。例如:

{ "_id": 0, "host": "internal.example.com:27017", "horizons": { "external": "external.example.com:25000" } }
  • host字段internal.example.com:27017自动成为__defaulthorizon 的地址;
  • horizons.external声明了一个名为external的额外视图,地址为external.example.com:25000

__default这个名字是保留字,不能显式出现在horizons子文档中。这一点在源码中有直接体现:在 split_horizon.cpp 的 BSON 解析逻辑中,遇到名为__default的字段会直接抛出BadValue,提示"Horizon name "__default" is reserved for internal mongodb usage";空名字同样会被拒绝("Horizons cannot have empty names")。

2.2 通过 SNI 判定客户端所属 Horizon

当 TLS 连接建立时,服务器会从 TLS 握手过程中捕获SNI(Server Name Indication,服务器名称指示)主机名,并将其以SplitHorizon::Parameters的形式存储在Client对象上。当客户端随后发出hello(或旧版isMaster)命令时,复制协调器(Replication Coordinator)调用SplitHorizon::determineHorizon(),用 SNI 主机名去查询反向映射表,找到与之匹配的 horizon;如果未命中(或根本没有 SNI),则回退到__default

最终确定的 horizon 决定了hello响应中包含哪一组地址——每个成员都从"客户端所在网络区域"的视角返回自己的地址,从而保证客户端拿到的 host list 在其区域内可路由。

三、数据模型:正反向两张映射表

SplitHorizon类建模的是单个成员在所有 horizon 下的地址映射,它不是集群级结构——每个MemberConfig各自持有一个SplitHorizon实例。这一点在头文件注释中写得很明确:SplitHorizonmodels a single member's view across all horizons, not views for all of the members(见 split_horizon.h)。

3.1 正向映射(ForwardMapping

StringMap<HostAndPort> horizon name --> host:port

把每个 horizon 名称(如"__default""external")映射到该成员在此 horizon 下可达的HostAndPort。正向映射始终至少包含__default。在源码中它的类型定义为using ForwardMapping = StringMap<HostAndPort>;(见 split_horizon.h),底层是一个字符串键的有序映射容器。

3.2 反向主机映射(ReverseHostOnlyMapping

std::map<string, string> hostname --> horizon name

把该成员可被访问到的每个主机名(不含端口)映射回使用它的 horizon 名称。determineHorizon()正是基于这张表,用传入的 SNI 名称去匹配 horizon。源码中类型为using ReverseHostOnlyMapping = std::map<std::string, std::string>;(见 split_horizon.h)。

关键约束:由于查找只依据主机名(而非host:port组合),对于同一个成员,每个主机名在所有 horizon 中必须唯一——两个 horizon 不能共用同一个主机名,即使端口不同也不行(这点在下一节"构造与校验"中会看到源码级证据)。

反向映射的构建逻辑位于 split_horizon.cpp 的computeReverseMappings():先把__defaulthorizon 的主机名预置进去(这是为了正确处理__default内部的主机名歧义情况),再遍历正向映射逐个emplace

四、构造方式与校验不变量

SplitHorizon提供两种构造途径(见 split_horizon.h):

  1. 从 BSON 构造SplitHorizon(const HostAndPort& host, const boost::optional<BSONObj>& horizonsObject),用于解析副本集配置(replSetInitiate/replSetReconfig)。host参数成为__default条目,可选的horizonsObject提供额外 horizon。
  2. 直接从ForwardMapping构造SplitHorizon(ForwardMapping forward),供内部逻辑和测试使用。

两条路径最终汇聚到统一构造器SplitHorizon(AllMappings)(见 split_horizon.h),由computeForwardMappings()computeReverseMappings()依次完成正反向映射的构建。构造期间强制执行的不变量包括:

  • __defaulthorizon 必须始终存在;
  • horizon 名称必须非空且唯一;
  • 保留名__default不得出现在horizonsBSON 对象中;
  • 主机名必须在所有 horizon 间唯一(同一成员的两个 horizon 不能共享主机名,即使端口不同);
  • horizonsBSON 对象若存在则不能为空。

违反上述任何一条都会产生BadValue错误。这些规则在 split_horizon_test.cpp 中有大量测试覆盖:

  • basicConstruction用例验证了"两个 horizon 使用相同 host:port""相同 host 不同 port"都会抛出BadValue,且错误信息包含重复的主机名(如Duplicate horizon member found "same.example.com"),而不会误报未冲突的成员(见 split_horizon_test.cpp);
  • BSONConstruction用例验证了空horizons对象报horizons field cannot be empty, if present、重复 horizon 名称报Duplicate horizon name found(见 split_horizon_test.cpp);
  • determineHorizon用例覆盖了"无 SNI 回退__default""SNI 未命中回退__default""SNI 命中返回对应 horizon"以及"主机名冲突导致构造失败"四类场景(见 split_horizon_test.cpp)。

值得一提的是,BSON 构造路径中horizons字段的值必须为字符串类型,否则抛出TypeMismatch,提示horizons.<name> field has non-string value of type ...(见 split_horizon.cpp)。

五、与复制协调器的集成:从 SNI 捕获到hello响应

Split Horizon 的价值最终体现在hello/isMaster响应中。整个链路分布在三个组件中:

组件与 Split Horizon 的关系
MemberConfig(member_config.h)持有SplitHorizon实例,将getHostAndPort(horizon)determineHorizon(params)委托给它。构造时通过_splitHorizon = SplitHorizon(host, getHorizons())初始化(见 member_config.cpp)
replication_info.cpphello/isMaster处理器)连接建立阶段调用SplitHorizon::setParameters()捕获客户端的 SNI 名称;随后把SplitHorizon::getParameters()传给复制协调器,使hello响应按正确 horizon 的地址构建
ReplicationCoordinator(replication_coordinator_impl.cpp)使用来自客户端的 horizon 参数,为拓扑响应中的每个成员选择返回哪个HostAndPort

5.1 SNI 捕获与参数存取

SplitHorizon::Parameters结构体只有一个字段boost::optional<std::string> sniName(见 split_horizon.h)。参数本身通过Client::declareDecoration<SplitHorizon::Parameters>()声明为 Client 上的装饰器存储(见 split_horizon.cpp),setParameters()在持有 Client 锁的情况下写入,getParameters()读取(见 split_horizon.cpp)。

在连接建立阶段,replication_info.cpp 从客户端会话中取出 SNI 名称并写入:

// Set split horizon parameters. auto sniName = client->getSniNameForSession(); SplitHorizon::setParameters(client, std::move(sniName));

hello命令处理时,则取出参数传入复制协调器:

const auto& horizonParams = SplitHorizon::getParameters(opCtx->getClient()); // ... replCoord->awaitHelloResponse(opCtx, horizonParams, clientTopologyVersion, deadline);

(见 replication_info.cpp)

5.2 horizon 字符串的推导

复制协调器内部通过_getHorizonString()(见 replication_coordinator_impl.cpp)完成最终判定:仅在自身是配置中的有效成员时,才调用self.determineHorizon(horizonParams)得到 horizon 字符串;否则返回boost::nonedetermineHorizon()的核心逻辑非常简洁(见 split_horizon.cpp):若存在 SNI 名称且在反向映射中命中,则返回对应 horizon;否则一律返回__default

5.3 面向每个 horizon 的 hello 响应与拓扑版本

从源码结构可以看到,ReplicationCoordinatorImpl维护了一个_horizonToTopologyChangePromiseMap(horizon → promise 的映射,见 replication_coordinator_impl.cpp):每次配置刷新时会为该成员配置中的每个 horizon 建立独立的 promise,可等待的helloawaitable hello)会针对自己所属的 horizon 等待拓扑变化通知;响应构建通过_makeHelloResponse()_topCoord->fillHelloForReplSet(response, *horizonString)完成(见 replication_coordinator_impl.cpp)。

当一次replSetReconfig改变了 horizon 映射时,所有正在等待旧 horizon 拓扑变化的hello请求都会收到ErrorCodes::SplitHorizonChange错误并重新发起(见 replication_coordinator_impl.cpp),同时拓扑版本会被递增以标记 horizon 变更。这也解释了为什么该机制能够与客户端基于topologyVersion的变更流式监听(awaitable hello)无缝配合——它保证了"网络视图变化"对客户端可见且可重试。

此外,启动时会对配置中的非默认 horizon 映射做一次检查:如果某个 horizon 映射可以被解析为合法的 CIDR/IP 地址,会输出启动警告,提示"Found split horizon configuration using IP ..."(见 replication_coordinator_impl.cpp),引导运维使用 DNS 名称而非 IP 配置 horizon。

六、序列化规则:toBSON()输出确定性

SplitHorizon::toBSON()负责生成成员配置中的horizons子文档(见 split_horizon.cpp),其行为规则如下:

  • 若成员只有__defaulthorizon,则不输出horizons字段——因为此时它与host字段完全冗余;
  • __default条目永远不会被序列化进horizons对象;
  • 输出前会按名称字典序对 horizon 条目排序,保证序列化结果确定、可复现(源码注释明确指出StringMap的迭代顺序是不确定的,必须先排序)。

toBSONBSONRoundTrip两组测试共同验证了该行为:前者断言"仅__default时不输出horizons字段",后者验证了"序列化 → 反序列化"往返后正反向映射完全一致,且两次toBSON结果逐字节相同(见 split_horizon_test.cpp)。这意味着由SplitHorizon重写出的配置是幂等且稳定的。

七、实战配置示例与注意事项

7.1 配置一个跨内外网的副本集成员

结合前文,一个完整的rs.reconfig()片段如下:

const cfg = rs.conf(); cfg.members[0].host = "internal.example.com:27017"; cfg.members[0].horizons = { external: "external.example.com:25000" }; rs.reconfig(cfg);

配置要点:

  • hosthorizons.external主机名不能相同(即使端口不同),否则replSetReconfig会以BadValue拒绝;
  • horizons中不要使用__default作为键名;
  • horizons子文档不能为空;值为字符串形式的host:port
  • 建议使用 DNS 名称而非 IP 地址,避免启动时收到 CIDR 形式的警告。

7.2 客户端侧如何自动命中 horizon

当外部客户端以 TLS 方式连接external.example.com:25000时,其 TLS 握手携带的 SNI 名称就是external.example.com。服务端据此匹配反向映射,返回的hello响应中该成员即呈现为external.example.com:25000;内部客户端连internal.example.com:27017时,SNI 匹配__default,响应中呈现internal.example.com:27017。两侧客户端都能在各自网络区域内使用响应中的地址完成后续连接。

7.3 与 awaitable hello / 拓扑版本的关系

从 replication_coordinator_impl.cpp 可以看到,等待中的hello会按其所属 horizon 挂起在对应的 promise 上;一旦 reconfig 改变了 horizon 映射,等待者会收到SplitHorizonChange错误并重试。因此在进行涉及 horizon 的配置变更时,客户端驱动需要具备基于topologyVersion的重试能力——这是 MongoDB 官方驱动标准行为,运维无需额外干预。

八、模块文件索引

若希望深入源码,建议按以下顺序阅读:

  • split_horizon/README.md:模块设计文档(本文骨架来源);
  • split_horizon.h:SplitHorizon类与Parameters结构定义;
  • split_horizon.cpp:正反向映射构建、校验、determineHorizon()toBSON()实现;
  • split_horizon_test.cpp:构造、判定、序列化与往返测试;
  • member_config.h / member_config.cpp:成员配置持有与委托调用;
  • replication_info.cpp:SNI 捕获与hello入口;
  • replication_coordinator_impl.cpp:horizon 感知的响应构建与SplitHorizonChange处理;
  • repl_set_config_checks.cpp:validateAllowingSplitHorizonIP()等配置校验路径。

总结

Split Horizon 通过"正向映射(horizon → 地址)+ 反向映射(主机名 → horizon)"两张表与 TLS SNI 机制,让 MongoDB 副本集能够按客户端网络区域返回各自可达的成员地址。其核心实现集中在split_horizon模块,构造与校验不变量严谨(保留名、唯一性、非空等),并通过MemberConfigReplicationCoordinator的委托链贯通到hello响应,最终以确定性的 BSON 序列化落回副本集配置。对于跨内外网部署、多 DNS 入口的 MongoDB 集群,正确理解并配置horizons是保证客户端连接高可用与正确路由的关键前提。

【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 2:39:28

MAX98357A实战:I2S音频功放从接线到排坑全攻略

做桌面小主机时想给它加个外置功放&#xff0c;做毕业设计想给语音识别模块接个喇叭&#xff0c;或者单纯想把树莓派变成网络电台……只要搜过“I2S音频功放”&#xff0c;MAX98357A这颗芯片基本都会出现在你的搜索结果前列。我用它做过两个小项目&#xff0c;第一次通电没声&a…

作者头像 李华
网站建设 2026/9/15 2:39:24

系统宕机别甩锅黑客:IT基础管理才是真命门

先讲个我经历过好几次的场景&#xff1a;周五下午四点半&#xff0c;业务群突然炸了&#xff0c;财务说报表打不开&#xff0c;仓库说 WMS 系统登不进去&#xff0c;销售在客户面前盯着大屏幕转圈。老板冲过来第一句话就是&#xff1a;“是不是被攻击了&#xff1f;赶紧找安全厂…

作者头像 李华
网站建设 2026/9/15 2:36:43

Unity 3D游戏设计与实现:从场景搭建到完整交付的实战指南

做Unity 3D游戏开发这些年&#xff0c;我接过最多的项目类型&#xff0c;不是那种大型商业游戏&#xff0c;反而是“基于Unity 3D的游戏设计与实现”这类带完整交付物的作品级项目。客户通常不只要一个能跑的工程&#xff0c;还要设计源文件、万字设计报告、讲解演示&#xff0…

作者头像 李华
网站建设 2026/9/15 2:36:24

东莞做网站首选企业铭哪家好?3个方案帮你看清报价猫腻

东莞做网站首选企业铭哪家好?3个方案帮你看清报价猫腻 域名备案卡壳、服务器选型迷茫,这种“域名服务器搞不懂”的焦虑,是不是让你在看建站方案时心里没底?很多老板在东莞找建站公司,最怕的不是价格贵,而是报价单上一堆看不懂的术语,比如“高防IP”、“CDN加速”、“独立部署”,问客服就告诉你“这个必须配”…

作者头像 李华
网站建设 2026/9/15 2:36:09

ASP经典Web系统实战:Windows 11下部署工资条应用

简介&#xff1a;这是一套基于ASP技术实现的企业级工资条查询系统源码&#xff0c;面向Web开发初学者与企业信息化建设人员&#xff0c;解决员工薪酬信息在线安全查询与管理员工薪资数据的典型业务需求。资源共115个文件&#xff0c;包含86个核心ASP脚本&#xff08;如huizong.…

作者头像 李华
网站建设 2026/9/15 2:35:36

GTK4国际化与本地化实战:基于gettext从代码标记到翻译部署全流程

写GTK4应用做得久了&#xff0c;你会越来越意识到一件事&#xff1a;代码再干净、交互再顺手&#xff0c;只要程序里到处都是写死的英文提示&#xff0c;它就没法真正走出你自己的圈子。GTK4的国际化与本地化&#xff0c;说白了就是解决两个问题——让程序能翻译成不同语言&…

作者头像 李华