☰
Pulsar跨地域复制深度解析:架构、配置与容灾切换
2026/10/22 23:42:34 网站建设 项目流程

Pulsar 的跨地域复制(Geo Replication)是我见过少有的、把“多机房容灾”直接做成原生能力的消息中间件功能,但它也是被误解最多的功能之一。很多人看到“复制”两个字,就下意识以为跟数据库主从同步一样,主库写完从库立刻就有;还有人以为两个集群可以当成一个集群随便读写。这些理解一旦落到生产环境,都会被现实教育一遍。

这篇文章我会用 8 张图拆解的方式,把 Pulsar 跨地域复制的整体架构、消息流转链路、配置方法、消费语义、故障切换和排障经验完整过一遍。不管你是正在选型,还是已经在生产上维护多套 Pulsar 集群,应该都能在里面找到点能直接用的东西。下面没有贴现成的架构截图,但我会把每张图需要哪些框、哪些箭头、关注哪些指标讲清楚,你顺着思路在脑子里把图画出来,理解效果比直接看图更深。

1. 跨地域复制到底复制了什么:先搞懂整体架构

1.1 第1张图:两个独立集群之间的复制模型

Pulsar 的跨地域复制,本质上是“两个独立集群之间的异步数据流复制”,不是共享存储。所谓独立集群,意味着每个集群都有自己的一套 broker、BookKeeper 存储节点和元数据服务,地域A的数据跟地域B的数据是分开保存的,两个机房之间没有存储层面的直接关联。

我最早接触这个功能,是在一个需要把订单事件同时提供给两个机房消费者的项目里。当时我画的第1张图里,就三条主线:左边是地域A的 producer 写入地域A集群,中间是地域A集群 broker 内部跑着的复制器,右边是复制器把消息写到地域B集群,然后地域B的 consumer 从本地读取消息。

这第1张图里最关键的一点是:两个集群的存储层不共享。复制不是让B集群“指到”A集群的数据文件上,而是让B集群真正存一份几乎一样的数据。这样做的好处是网络抖动不会直接影响存储层读写,坏处就是复制一定会有延迟,而且每条消息在两个集群分别写两遍,写放大是实打实的。

你还要记住,复制器不是独立部署的进程,而是跑在 broker 进程内部的组件。它本质上是“一个内部消费者加一个内部生产者”的组合:从本集群的 topic 读取消息,再以生产者的身份把消息发送到远端集群的同一个 topic。架构上少了一个单独组件,部署和运维都省了不少事。

1.2 它不是“消息同步”:对一致性要有清醒认知

Pulsar 的跨地域复制是异步的,这一点再怎么强调都不为过。生产者在A集群发送一条消息,broker 返回 ack,只代表这条消息已经在A集群的 BookKeeper 上持久化成功了,不代表B集群也已经收到了。B集群的数据,是A集群后台慢慢复制过去的。

这就像你在发件城市的快递点寄了个包裹,快递点给你回执,只说明包裹已经在当地揽收系统里了,收件城市那边还没影。中间要经历装车、干线运输、派送、签收,每一步都可能排队。放到跨地域复制上,就是本地写入、复制器读取、网络传输、远端写入确认这四个环节串起来。

所以如果你在业务上要求“两地都确认写入成功,才算发送成功”,那 Pulsar 的跨地域复制不适合,你得自己做双确认,或者换用跨地域强同步的存储方案。Pulsar 的跨地域复制目标是容灾、就近消费、异步分发,不是一个跨地域强一致的消息存储。把期望值放在这里,后面所有设计都不会跑偏。

1.3 第2张图:什么时候该上跨地域复制

第2张图是场景图,我通常画四个框:容灾主备、双活近读、边缘汇聚、集群迁移。

容灾主备最典型:正常情况下只有地域A在生产,地域B通过复制保留一份完整数据。地域A的机房真出了故障,把客户端切到地域B,业务还能继续。备集群平时可以承担只读的流量,比如数据分析、离线任务,别让它完全空转。

双活近读则是两个地域都可以读写,但要注意“双活”不代表“双写同一个 topic”。Pulsar 复制是异步的,如果两个地域同时写同一个 topic,又没有业务层做冲突处理,就会产生混乱。我见过比较稳的双活架构,都是按业务模块切分,比如订单模块的地域A写、库存模块的地域B写,两边各自从对方复制数据做本地消费。

边缘汇聚场景更适合物联网或者多分支架构:每个边缘集群只跟中心集群复制,边缘之间不直接互复。消息从边缘汇到中心,由中心统一分发或者存储。集群迁移也是很多人忽略的用法:新老集群同时跑一段时间,复制策略让新集群把老数据全部拉过去,拉平之后切换客户端,比导数据要平滑得多。

2. 消息在两个集群之间流转:完整链路与延迟构成

2.1 第3张图:从本地 Ledger 到远端 Topic 的复制链路

第3张图是链路图,画出消息从进A集群到出现在B集群之间的完整路径。这条路径分成四段。

第一段是生产者写入A集群。producer 连接地域A broker,把消息写入本地的 BookKeeper ledger,本地副本落盘完成,生产者收到确认。这段是所有消息都必须走的正常路径,不管后面要不要跨地域复制,它都要先持久化。

第二段是复制器读取。地域A broker 上会因为 namespace 配置了复制策略,自动为需要复制的 topic 启动复制器。复制器从本地 topic 读取消息,并且维护自己的复制游标,记录“我已经复制到哪条消息了”。这段读取实际上是走内部消费者逻辑,所以本地订阅的积压、消费位点这些机制都会用到。

第三段是跨地域传输。复制器连接地域B broker,以生产者的身份把消息发过去。如果远端不可达,或者远端写入失败,消息会继续停留在本地,复制器的游标也不推进。表现就是复制积压越来越大,但上游业务完全感受不到,因为producer 早就拿到 ack 了。

第四段是远端写入。地域B broker 收到复制过来的消息,写入地域B的 BookKeeper ledger,落盘失败会重试;成功之后,复制器才更新游标,继续读下一条。

这个链路里最容易忽略的是“复制游标”这个东西。它决定了复制从哪里继续,也决定了延迟是多少。想看复制积压,最直接的方式就是查 topic 统计信息:

pulsar-admin topics stats persistent://tenant/ns/order-events

输出里能找到类似replicationBacklog的字段,它表示还有多少消息等着复制到远端集群。如果这个值长期不为零,或者一直涨,复制链路肯定有问题。

2.2 第4张图:复制延迟从哪里来

第4张图是延迟拆解图。跨地域复制的端到端延迟,可以近似看成四个部分相加:本地写确认延迟、复制器批次延迟、网络往返 RTT、远端写确认延迟。

本地写确认延迟取决于 BookKeeper 的 fsync 策略和副本数,通常几毫秒到几十毫秒。复制器批次延迟取决于 Pulsar broker 上复制相关参数的批处理设置,它是攒一批消息再发,不是来一条发一条,所以吞吐高了延迟自然会被拉高。网络 RTT 是物理距离决定的,跨机房的 RTT 几十毫秒很常见,极端情况可以到几百毫秒。远端写确认延迟跟本地类似,也要过一遍 BookKeeper 持久化。

把这四个加起来,你会明白跨地域复制的延迟在一个正常环境里会达到秒级甚至分钟级,别再拿它跟同机房的 topic 读写延迟做对比。第4张图画出来以后,我建议你把“复制延迟高”的排障思路跟着图走:先看本地写入慢不慢,再看复制积压涨不涨,再看网络 RTT,最后看远端写入负载。按环节切分比瞎调参数有用得多。

还有一个参数容易踩坑,就是 broker 配置里的replicationStartAt。它决定复制器启动时从哪个位置开始读,可以理解为“从最早开始补数据”还是“从最新开始只复制新消息”。如果你在存量数据已经写了很多之后才打开复制,并且它配置的是从最新开始,那么历史消息一条都不会复制到远端。这一点经常被当成“复制不生效”来排查,实际是消费位点没选对。

2.3 复制消息头上的交通标记

Pulsar 在把消息复制到远端集群时,会给消息加一个标记,常见的是replicatedFrom之类的属性,用来标识这条消息是从哪个集群复制过来的。消费者在某个集群上读到消息时,如果看到这个属性有值,就知道它是远端复制过来的,而不是本地生产的。

这个标记有两层用处。

第一层是排查数据来源。双活或者边缘汇聚场景里,一个 topic 里的消息可能一部分是本地生产的,一部分是别的集群复制来的。消费端把消息属性打出来,一眼就能判断消息到底来自哪里,定位问题会快很多。

第二层是防止循环复制。在全互联拓扑下,A集群复制到B集群,B集群可能又复制回A集群,如果没有防循环机制,消息就会在两个集群之间无限转发。Pulsar 的做法就是靠replicatedFrom标记:B集群发现这条消息本身就是从A集群来的,就不会再往A集群回传。了解这个标记,你就不会在配置全互联拓扑的时候担心数据风暴。

3. 复制策略配置与拓扑规划:命令、参数、取舍

3.1 第5张图:Namespace 级别的复制策略配置流程

第5张图是配置流程图。Pulsar 的跨地域复制是 namespace 级别的,不是 topic 级别的。也就是说,你给一个 namespace 配置了复制集群列表,这个 namespace 下面所有 topic 都会按照这个列表复制。这个设计有好处也有坑,好处是批量配置、管理简单,坑是你没法直接说“只复制这几个 topic”。

配置步骤基本就三步。第一步确认两个集群在彼此的集群列表里,能互相访问。第二步给 namespace 设置复制集群列表:

pulsar-admin namespaces set-clusters \ --clusters cluster-a,cluster-b \ public/order-ns

第三步查看策略确认是否生效:

pulsar-admin namespaces get-policies public/order-ns

输出里会看到replication_clusters字段,里面应该包含cluster-a和cluster-b。

这里有个细节要注意:--clusters参数列出的集群集合里,包括本地集群自己。比如cluster-a的 broker 在执行这个命令时,列表里要同时有cluster-a和cluster-b,这个 namespace 的消息才会被复制到cluster-b。如果你图省事只写了目标集群,很可能复制策略是残缺的。

策略改变之后,broker 上生效并不是毫秒级的事,它有一个轮询检查策略变更的机制。所以执行完命令,等个十几秒再看复制是否启动都正常,别一秒钟没动静就开始重启 broker。

如果业务上确实只需要复制某些 topic,我的建议是不要跟别人共用 namespace,单独开一个 namespace 给这批需要跨地域复制的 topic。配置策略和后续维护都会清爽很多。

3.2 三种复制拓扑的选型与配置细节

第5张图的下半部分,其实是三种常见的复制拓扑。

主备拓扑: namespace 的复制集群列表包含主备两个集群,但只有主集群接收生产写入,备集群平时只读。配置最简单,故障切换时人工介入做客户端切换。风险点是备集群平时没有真实流量,一旦切换可能暴露消费代码没验证过的问题。

全互联拓扑:所有集群两两复制,任何一个集群都可以被生产写入。配置上就是在每个集群都设置包含所有集群的复制列表。这种拓扑要特别注意业务层写冲突,我的经验是必须按 Topic 或者按业务模块分配“生产归属地”,不要让两个地域同时写同一个业务 topic。

星型拓扑:中心集群与每一个边缘集群复制,边缘集群之间不建立复制关系。比如边缘集群配置的复制列表是“边缘集群+中心集群”,中心集群配置的复制列表是“中心集群+所有边缘集群”。边缘的消息会汇总到中心,中心的消息会分发到各个边缘。这种结构适合中心化分发和监管场景,但中心集群会成为网络和存储瓶颈,容量规划要提前做。

三种拓扑没有绝对好坏,关键看业务对数据流向的要求。选型时用一张小表格把约束条件列出来:谁能写、谁能读、谁能容忍延迟、谁能容忍重复,比直接照搬网络上的架构图要靠谱得多。

3.3 配置粒度与团队协作的取舍

复制策略是 namespace 级别的,这在多团队共用一个集群的环境里很容易出问题。比如 A 业务只需要复制到备集群,B 业务不想复制,但 A 和 B 共用一个 namespace。只要负责 A 业务的人执行了set-clusters,B 业务的 topic 也会跟着复制,数据被复制到不期望的机房,合规和成本都受影响。

我经手的一个项目里,出现过一次“复制策略被误伤”的事故。当时一个运维执行命令时把整个public/defaultnamespace 加进了跨地域复制集群列表,结果几乎所有共享这个 namespace 的 topic 都开始往备集群同步。后续所有人的消费端都看到了大量历史消息重复消费,光是清理就折腾了很长时间。

从那以后我们定的规矩是:每个业务线至少一个独立 namespace,复制策略必须跟着 namespace 走,变更复制策略要走审批。另外,老集群上的旧 topic 如果还留在 default namespace 里,尽量迁走。跨地域复制并不是“配一下就完事”的静态配置,它需要长期维护和治理。

4. 跨地域消费:订阅状态、复制订阅模式与重复消息

4.1 默认情况下,订阅游标是各集群独立的

配置好复制策略之后,消息确实能从A集群到B集群,但消费端的行为很多人不了解。默认情况下,Pulsar 每个集群上的订阅游标是独立的。A集群的消费者订阅了某个 topic,消费到第 100 条;B集群上同样的消费组名可能根本不存在,或者就算存在,游标也是从自己的初始位点开始算的。A和B之间,订阅状态并不会自动同步。

这个特性带来的直接影响就是故障切换。假设A集群挂了,你把消费者切到B集群,如果B集群上这个订阅名之前没创建过,消费者创建新订阅时只能按照 broker 的默认位点开始读。默认是 earliest 就可能从头消费大量历史数据,默认是 latest 就可能跳过A集群已经复制过来但还没消费的尾部消息。这两种结果,要么重复量巨大,要么丢数据感知上吓人。

所以只配置复制策略,不解决消费端切换的连续性。要让切换更平滑,你得了解并配置 replicated subscription,也就是复制订阅。

4.2 第6张图:Replicated Subscription 两种模式怎么选

复制订阅解决的是“订阅游标在跨地域之间怎么同步”的问题。第6张图里可以画两个分支,左边是 Serialized 模式,右边是 Independent 模式,我用下来之后的体会差别很大。

Serialized 模式的目标是“一个订阅名全局只有一份消费位置”。它通过跨集群协调,保证同一个订阅同一时刻只能在一个集群上拉取消息,其他集群上的消费者会等待。故障切换时,新接管的集群能接着原游标消费,重复范围控制得很小。代价是消息流转被全局串行化,还有一个消息在途之类的限制,跨地域网络一抖动,消费吞吐和延迟立刻受影响。除非业务对重复极其敏感,我一般不推荐一上来就开这个模式。

Independent 模式则是每个集群各自维护消费游标,互不协调。配置和运维都要简单很多,消息重复的可能性更大,但至少不会因为跨集群协调导致消费卡死。第6张图右边我会画一个“幂等层”的框,意思是:用 Independent 模式,业务端必须自己做好幂等。

配置复制订阅时不同版本的 Pulsar 在 namespace 策略里的字段略有差异,字段位置和命名以你当前版本的官方文档为准。我的建议是生产环境优先从 Independent 模式起步,等确实遇到重复消息造成的资损问题,再评估是否升级到 Serialized。不要为了追求“看起来最精确的交付”给自己上一把跨地域锁。

4.3 正确理解重复与丢失的边界

跨地域复制场景下,消息重复是常态,不是 Bug。这句话写出来可能让人觉得我在为系统缺陷开脱,但从原理上确实如此。

从设计目标看,Pulsar 跨地域复制不会丢消息:生产者在源集群收到 ack,说明消息已经持久化,复制器之后会把它读到并推到远端。如果复制器在推进游标之前崩溃,它重启后会把那条消息再读一遍、再推一遍。所以源集群成功写入的逻辑,传到远端时,保守设计一定是“可能重复一次”。

消费者要做的,是在消息处理链路里加上业务幂等。比如用订单 ID、消息 ID 做去重表,或者保证处理操作本身就是幂等的(“设置状态为已支付”比“余额加 10 元”更利于重复消费)。如果你把分布式事务的可靠消息和跨地域复制叠加在一起,幂等的重要性会被进一步放大。

真要讨论极端丢消息,只有一种情况:源集群消息已经写入成功,但在复制到远端之前,整个源集群发生了不可恢复的数据损坏,比如多副本全部丢失。这是任何异步容灾方案都逃不掉的理论边界。你需要做的是在项目评审时把这个边界告诉业务方,而不是等故障发生之后再去解释。

5. 故障切换与灾难恢复:不是简单改个连接地址

5.1 第7张图:切换流程检查清单

第7张图我画的是故障切换流程图,每个步骤都踩过坑,所以拿出来单独讲。

第一步,先确认故障边界。是有任何消息都写不进去了,还是只有某条 topic 写入超时?是 broker 进程挂了,还是整个机房网络都断了?如果只是某台 broker 的问题,Pulsar 自己会做负载转移,根本不需要切换。确认边界这步没做好,后面切换就是一次降低可用性的主动操作。

第二步,检查复制 backlog 是否追平。用pulsar-admin topics stats看远端复制积压。正常情况下切换前应该等积压消息基本清空,可接受的 RPO 是多少要预先跟业务约定。如果源集群已经完全不可用,backlog 追不平,那就得认账:丢失这一部分尾部消息。

第三步,检查目标集群的订阅是否就绪。我在一次演练里吃过亏:当时备集群一切正常,但切过去之后消费者拉不到数据,最后发现是备集群上压根没建过订阅,消费者是从头开始读的,把历史消息全部重放了一遍。从那之后,备集群的订阅预创建成了切换清单里的必检项。

第四步,消费端切换。把读取流量慢慢切到备集群,观察 consumer 的连接数、消息速率、消费 lag。消费端能正常跑起来,再进入下一步,不要一上来就同时切生产和消费。

第五步,生产端切换。让 producer 也指向备集群,新消息开始写入备集群。这时候备集群既是生产写入端,又是消费读取端,同时还有来自旧集群的复制尾巴在赶过来。这一步最需要盯的就是备集群的存储负载和网络带宽。

第六步,观察与回切。确认业务稳定运行一个周期后,再按同样清单反向操作,回到原集群或者保持现在的拓扑。

5.2 切换前就要做好的运维储备

跨地域切换不是故障当下就能全部准备的。很多事情要提前做,否则现场根本没有时间补救。

首先是 namespace、topic、ACL、租户这些元数据,不会自动跟着复制策略从A集群同步到B集群。如果B集群从来没创建过对应的 namespace 和 ACL,消费者切过去会发现连权限都没有。提前把配置同步纳入发布流程,而不是靠某人做个一次性脚本。

其次是认证和授权策略在每个集群要独立维护。Pulsar 的复制策略只负责消息数据,不负责权限元数据。备集群的权限必须跟主集群一致,或者至少在应急预案里有明确的执行步骤。

然后是健康检查。不要只检查 broker 进程是否活着,要定期在备集群做真实的消息写入和读取验证。很多备机隐患,只有真正让数据流经过才知道。

最后是切换演练。这个不能省。我见过太多团队把切换文档写得非常完整,但从来没演练过,真到故障那天,连执行文档的第一步命令都会输错。三个月一次,每次把切换清单跑一遍,发现问题改文档,这是最值得投入的运维成本。

5.3 回切的经验教训

回切很容易被当成“再来一次切换”,实际操作比第一次切换更麻烦,因为备集群运行期间攒了新的数据。

一个比较稳的回切流程是这样的:先让备集群把消费 backlog 全部消化完,或者跟业务确认可以跳过这部分的尾部数据,然后把消费端先切回主集群,观察消费位点是否合理,最后再把生产端切回主集群。不要在备集群还有积压的时候就贸然来回切,否则消费者会看到消息顺序错乱、重复消费一堆。

回切过程中可以使用一个一次性测试订阅来验证主集群的消息可用性。这个测试订阅不会污染正式订阅游标,发现问题可以随时删掉重建。验证通过后,再把正式订阅的消费端迁回来。

还有一条经验:回切后不要马上删掉备集群的订阅或复制策略,至少保留一个观察周期,确认没有逻辑问题再清理。备份一旦提前清理了,第二次故障就没有退路了。

6. 常见问题速查与监控排障

6.1 复制不生效:一个排查表解决八成问题

我整理了一个排查表,覆盖跨地域复制“配了但没反应”的大多数情况。先对号入座,再动手改配置。

现象常见原因排查与解决
新消息不复制namespace 复制列表没包含目标集群执行get-policies看replication_clusters,用set-clusters补全
所有 topic 都不复制broker 配置了disableReplication=true检查 broker.conf,改成 false 并滚动重启
只复制新消息,历史消息没有补replicationStartAt配置为 latest确认业务需求,必要时改为 earliest 并触发补复制
复制断断续续,Backlog 增长跨地域网络不通或带宽打满查看 broker 复制器日志,检查网络 RTT 和丢包
部分 topic 复制,部分不复制策略还没轮到或 namespace 共用导致误配置检查 namespace 粒度,确认 topic 属于哪个 namespace

遇到复制不生效,我建议最先查的是 namespace 策略,因为这是配置最常出错的地方。先get-policies看复制集群列表,不要一上来就看 broker 日志。

6.2 第8张图:复制延迟高怎么定位

复制延迟高,第8张图能帮上大忙。这张图我画了一个监控面板,上面有三根核心曲线:源集群写入速率、复制速率、目标集群写入速率。定位延迟高的问题,先看这三根线谁跟谁不匹配。

如果源集群写入速率正常,但复制速率明显偏低,优先检查复制器所在 broker 的 CPU、内存、网络连接数,另外看一下复制相关批次参数是不是太小。如果复制速率和目标集群写入速率都正常,但复制 backlog 还是持续上涨,大概率是跨地域链路抖动,或者目标集群写远端 BookKeeper 的耗时变大。

如果 backlog 一会儿涨一会儿掉,更像是消息到达不均匀或者批量参数设置导致复制波动。先观察,不要急着重启。复制延迟高不一定是故障,很多情况下就是跨地域带宽打满了,消息在排队。这时候加机器不如先做流量削峰或者压缩消息体积。

监控面板上我会额外放两个指标:待复制消息数(backlog)和落后时长估算。Pulsar 的 topic stats 里有 replication backlog 和复制速率,直接用这两个值算“追平时间”就够了。想象一下,backlog 有 100 万条,复制速率每秒 1 万条,那追平时间大约 100 秒。用这个估算来回答业务方的“还要多久”,比一句“快了”有说服力得多。

6.3 消费者重复消费的排查思路

跨地域场景下消费者重复消费,原因主要分两类:一类是生产端的重复复制,一类是消费端订阅游标切换造成重放。

生产端的重复,多见于复制器游标重置。比如复制器读了一条消息,已经推到远端,但还没更新本地游标,这时进程重启,它会把这条消息再推一次。这种情况通过复制机制本身没办法完全消除,消息端到端必然存在“至少一次”语义,业务幂等是唯一出路。

消费端的重复,则跟切换有关。如果没开复制订阅,或者用的 Independent 模式,故障切换后消费者在备集群的消费起点可能跟旧集群的游标对不上,导致一部分消息被重新拉取。排查时可以对比两个集群上同一个订阅的 LastConsumedTimestamp 或者最近消费位置,看看差在哪里。

如果业务要求极端稳定,建议为每个消费者在消息处理层加上全局去重键。去重键不要用 Pulsar 的 message ID,因为复制到远端后 message ID 可能变化,要用业务里的唯一字段,比如订单号、事件 ID。

6.4 定期切换演练比紧急排障更重要

我在实际项目里踩过最痛的一次,就是演练时发现备集群从未创建过订阅,切换后所有消费者从头到尾把历史消息重新拉了一遍,线上流量瞬间被打挂。这个问题的根子不在 Pulsar,在于我们太久没有做演练,所有备集群配置都是纸上谈兵。

后来我们定了一条规矩:所有启用了跨地域复制的集群,每个月做一次只读演练,每个季度做一次完整切换演练。只读演练只验证消费者能在备集群拉数据,完整切换则是连生产写入一起切过去,让业务真实跑一段时间。

跨地域复制本身不复杂,复杂的是它牵涉的消费端、ACL、网络、订阅策略这些外围环节。每演练一次,都会发现一处文档没覆盖到的细节。等到故障真的来了,你靠的不是临场反应,而是那一叠已经被反复修正过的切换清单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询