ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 与__default2.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 握手过程中捕获SNIServer 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 members view across all horizons, not views for all of the members见 split_horizon.h。3.1 正向映射ForwardMappingStringMapHostAndPort horizon name -- host:port把每个 horizon 名称如__default、external映射到该成员在此 horizon 下可达的HostAndPort。正向映射始终至少包含__default项。在源码中它的类型定义为using ForwardMapping StringMapHostAndPort;见 split_horizon.h底层是一个字符串键的有序映射容器。3.2 反向主机映射ReverseHostOnlyMappingstd::mapstring, string hostname -- horizon name把该成员可被访问到的每个主机名不含端口映射回使用它的 horizon 名称。determineHorizon()正是基于这张表用传入的 SNI 名称去匹配 horizon。源码中类型为using ReverseHostOnlyMapping std::mapstd::string, std::string;见 split_horizon.h。关键约束由于查找只依据主机名而非host:port组合对于同一个成员每个主机名在所有 horizon 中必须唯一——两个 horizon 不能共用同一个主机名即使端口不同也不行这点在下一节构造与校验中会看到源码级证据。反向映射的构建逻辑位于 split_horizon.cpp 的computeReverseMappings()先把__defaulthorizon 的主机名预置进去这是为了正确处理__default内部的主机名歧义情况再遍历正向映射逐个emplace。四、构造方式与校验不变量SplitHorizon提供两种构造途径见 split_horizon.h从 BSON 构造SplitHorizon(const HostAndPort host, const boost::optionalBSONObj horizonsObject)用于解析副本集配置replSetInitiate/replSetReconfig。host参数成为__default条目可选的horizonsObject提供额外 horizon。直接从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.cppBSONConstruction用例验证了空horizons对象报horizons field cannot be empty, if present、重复 horizon 名称报Duplicate horizon name found见 split_horizon_test.cppdetermineHorizon用例覆盖了无 SNI 回退__defaultSNI 未命中回退__defaultSNI 命中返回对应 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 的关系MemberConfigmember_config.h持有SplitHorizon实例将getHostAndPort(horizon)与determineHorizon(params)委托给它。构造时通过_splitHorizon SplitHorizon(host, getHorizons())初始化见 member_config.cppreplication_info.cpphello/isMaster处理器连接建立阶段调用SplitHorizon::setParameters()捕获客户端的 SNI 名称随后把SplitHorizon::getParameters()传给复制协调器使hello响应按正确 horizon 的地址构建ReplicationCoordinatorreplication_coordinator_impl.cpp使用来自客户端的 horizon 参数为拓扑响应中的每个成员选择返回哪个HostAndPort5.1 SNI 捕获与参数存取SplitHorizon::Parameters结构体只有一个字段boost::optionalstd::string sniName见 split_horizon.h。参数本身通过Client::declareDecorationSplitHorizon::Parameters()声明为 Client 上的装饰器存储见 split_horizon.cppsetParameters()在持有 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.cpp5.2 horizon 字符串的推导复制协调器内部通过_getHorizonString()见 replication_coordinator_impl.cpp完成最终判定仅在自身是配置中的有效成员时才调用self.determineHorizon(horizonParams)得到 horizon 字符串否则返回boost::none。determineHorizon()的核心逻辑非常简洁见 split_horizon.cpp若存在 SNI 名称且在反向映射中命中则返回对应 horizon否则一律返回__default。5.3 面向每个 horizon 的 hello 响应与拓扑版本从源码结构可以看到ReplicationCoordinatorImpl维护了一个_horizonToTopologyChangePromiseMaphorizon → 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的迭代顺序是不确定的必须先排序。toBSON与BSONRoundTrip两组测试共同验证了该行为前者断言仅__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);配置要点host与horizons.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.hSplitHorizon类与Parameters结构定义split_horizon.cpp正反向映射构建、校验、determineHorizon()与toBSON()实现split_horizon_test.cpp构造、判定、序列化与往返测试member_config.h / member_config.cpp成员配置持有与委托调用replication_info.cppSNI 捕获与hello入口replication_coordinator_impl.cpphorizon 感知的响应构建与SplitHorizonChange处理repl_set_config_checks.cppvalidateAllowingSplitHorizonIP()等配置校验路径。总结Split Horizon 通过正向映射horizon → 地址 反向映射主机名 → horizon两张表与 TLS SNI 机制让 MongoDB 副本集能够按客户端网络区域返回各自可达的成员地址。其核心实现集中在split_horizon模块构造与校验不变量严谨保留名、唯一性、非空等并通过MemberConfig→ReplicationCoordinator的委托链贯通到hello响应最终以确定性的 BSON 序列化落回副本集配置。对于跨内外网部署、多 DNS 入口的 MongoDB 集群正确理解并配置horizons是保证客户端连接高可用与正确路由的关键前提。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表