Pulsar 跨地域复制原理分析
- Pulsar 提供了跨地域复制的能力,即 GEO Replication,它不仅支持不同地域的不同 Pulsar 集群之间的消息数据的复制,而且支持各集群订阅状态的复制,能够很好地满足系统跨地域容灾架构的需要。
数据的复制
原理
- 当集群配置好了跨地域复制的策略后,broker 内会为每对复制关系(localCluster-topic/partition-remoteCluster)生成一个 replicator 来负责其中 topic/partition 数据的跨地域/集群复制。
- 在 replicator 中有两个关键的组件:cursor 和 producer,其中 cursor 主要负责数据的读取和记录游标,cursor 读取完数据后对消息进行过滤(是否过期,是否本地消息,是否复制到指定集群,是否为 marker message),然后通过 producer 将数据发送至远端集群,producer 发送完数据通过 cursor 更新游标,这样本地集群的数据就能够在保证不丢失的条件下复制到远端集群。
![]()
- 北京的生产者发送消息到北京 pulsar 集群的 topic1;
- 北京 pulsar 集群 replicator 中的 cursor 从 topic 中读取消息数据;
- Cursor 通过 producer 向上海 pulsar 集群的 topic1 发送数据;
- Producer 发送完数据后更新 cursor 的复制游标;
- 上海 pulsar 集群中的订阅就可以读取到来自北京 pulsar 集群中的 topic1 的数据;
- 上海 pulsar 集群的消费者就可以消费到来自北京 pulsar 集群中的 topic1 的数据;
多中心使用场景
单向:failover
- 将数据备份到远端 pulsar 集群,远端集群正常不会有 producer 和 consumer,只有当主集群挂了,producer 和 consumer 才会切换至备集群使用。
![]()
单向:aggregate
- 多个小集群中的消息需要复制到一个大集群中合并起来.
![]()
双向:active active
- 两个中心的 pulsar 集群可以同时正常使用,互为备份及高可用。
![]()
几个问题补充
- 如何解决循环复制的问题?
- Replicator 中的 producer 在向远端集群发送消息数据前会检查消息元数据中的 "replicated from" 字段来过滤掉从其它集群复制来的消息,因此只有本地消息才会被 producer 发送,而且发送消息时会在消息的元数据中设置 "replicated from" 的字段来标识其地域属性。
- 是否支持级联复制?
- 是否存在丢数据的可能?
- Cursor 会通过游标记录复制进度,正常情况下不会丢数据,但会有延迟。极端情况下,集群 A 向集群 B 同步数据在 A 集群发生灾难时可能会有部分数据未被同步到 B 集群,此时消费者切换到 B 集群后无法消费到这部分数据。
- 能否保证多集群中同一 partition 中全局消息的顺序性?
- 无法保证不同集群中同一 partition 中全局消息的顺序性,只能保证不同集群中同一 partition 本地消息的顺序性。
- 多中心切换时消费进度可以切换么?
订阅的复制
难点
- 位置同步
通过 messageId(ledgerId + entryId + batch-index + partition-index) pulsar 可以精确地定位到一条消息在集群中的位置,但是同一条 message 在本地集群的 messageId 在同步到远端之后可能就不一样了,也就意味着远端集群和本地集群无法通过相同的的 messageId 定位到相同的一条 message,这将极大地增加消费位置同步的难度。
原理
- pulsar 实现订阅复制的核心原理简单概括就是周期性地在各个集群之间构建一致性的 messageId 映射快照并相互同步快照信息,各集群根据自己的实际消费情况以快照为步伐在不同集群之间同步订阅中的消费进度信息。
基础原理
- 已知上海和北京两个 pulsar 集群之间对某个的消息进行同步,在某一时刻上海 pulsar 集群该主题下的消息情况如下:
![]()
- 假如此时上海的消费者对 Me-BJ 进行了消费并确认消费成功,根据独立集群分区主题消息的有序性,则可以认为北京集群对应的分区主题中任意 messageId < MeBJ 的消息都已经消费成功。如果我们知道消息 Me-BJ 在北京集群对应的 messageId,那么我们就能够做到让北京集群把当前订阅的 "mark-delete-position" 修改到相应 message 的位置,以此来完成订阅状态的同步。
![]()
通信方式
- 复制订阅过程中必然需要集群之间相互通信,其中涉及了快照相关的数据和订阅状态的数据。此时我们可能想到 rpc,即通过 pulsar 集群中 broker 的管理端口或者其它端口来完成这些数据交互,但是 pulsar 基于自己的考虑,使用了另外一种方式,引入了 Marker Message 来解决这个问题。
- Marker Message 也是 message,只不过是一种特殊的 message,特殊性在于这种 message 并不是用来给消费者消费的,而是用来为复制订阅服务的,同时也解决了通信问题,即集群之间构建 snapshot、复制订阅状态都通过发送 Marker Message 来完成。
- 引入 Marker Message 有以下好处:
- 能够完美地复用原本的消息同步链路进行通信,而且不用考虑 snapshot 数据额外的存储,降低了实现上的复杂度;
- 复制订阅状态原本为阶段性的,使用 Marker Message 正好可以楔入消息流,broker 在处理它们时可以利用它们与正常消息的顺序关系,从而更好地配合订阅状态的阶段性复制;
![]()
- Marker Message 分为一共 4 种,使用 message 的元数据中的 marker_type 字段区分:
// 标识构建 snapshot 请求的 marker message
public static final int REPLICATED_SUBSCRIPTION_SNAPSHOT_REQUEST_VALUE = 10;
// 标识响应构建 snapshot 请求的 marker message
public static final int REPLICATED_SUBSCRIPTION_SNAPSHOT_RESPONSE_VALUE = 11;
// 标识同步 snapshot 的 marker message
public static final int REPLICATED_SUBSCRIPTION_SNAPSHOT_VALUE = 12;
// 标识更新订阅状态的 marker message
public static final int REPLICATED_SUBSCRIPTION_UPDATE_VALUE = 13;
快照的构建
- 第一步:本地集群生成构建 snapshot 请求的 marker message,生产到本地 topic 中;
"ReplicatedSubscriptionsSnapshotRequest" : {
"snapshot_id" : "444D3632-F96C-48D7-83DB-041C32164EC1",
"source_cluster" : "a",
}
- 第二步:远端集群收到请求后构建响应的 marker message,生产到本地 topic 中;
"ReplicatedSubscriptionsSnapshotResponse" : {
"snapshotId" : "444D3632-F96C-48D7-83DB-041C32164EC1",
"cluster" : {
"cluster" : "b",
"message_id" : {
"ledger_id" : 1234,
"endtry_id" : 45678
}
}
}
![]()
- 第三步:本地集群收到响应后生成映射快照,构建 snapshot value 的 marker message,生产到本地 topic 中;
{
"snapshot_id" : "444D3632-F96C-48D7-83DB-041C32164EC1",
"local_message_id" : {
"ledger_id" : 192,
"endtry_id" : 123123
},
"clusters" : [
{
"cluster" : "b",
"message_id" : {
"ledger_id" : 1234,
"endtry_id" : 45678
}
}
],
}
![]()
快照的存储
- 构建好 snapshot 后,相应的 marker message 会被生产到本地 topic 中,然后被同步到远端集群。集群在向各自的消费这发送消息前也会进行过滤,当发现 marker_type 为 snapshot value 的 marker message 时,broker 并不会把它发送给消费者,而会将该消息存储到 snapshotCache 中供后面复制订阅状态使用。
订阅状态的同步
- Pulsar 集群中的 broker 在处理订阅的 ack 更新消费进度时就会和这些 snapshot 进行比较,挑选出满足其 position 小于 mark-delete position 的最大的 snapshot 并清理失效的 snapshot,然后根据这个 snapshot 构造 update value 的 marker message,写入本地 topic。当这个 marker message 被复制到远端集群后,远端集群会根据这个 marker message 更新对应的订阅信息。
{
"subscription_name" : "my-subscription",
"clusters" : [
{
"cluster" : "b",
"message_id" : {
"ledger_id" : 1234,
"endtry_id" : 45678
}
}
],
}
![]()
时间线说明
![]()
![]()
样例
![]()
"ReplicatedSubscriptionsSnapshotRequest" : {
"snapshot_id" : "444D3632-F96C-48D7-83DB-041C32164EC1",
"source_cluster" : "shanghai",
}
![]()
"ReplicatedSubscriptionsSnapshotResponse" : {
"snapshotId" : "444D3632-F96C-48D7-83DB-041C32164EC1",
"cluster" : {
"cluster" : "beijing",
"message_id" : {
"ledger_id+entry_id" : 10
}
}
}
![]()
{
"snapshot_id" : "444D3632-F96C-48D7-83DB-041C32164EC1",
"local_message_id" : {
"ledger_id+entry_id" : 16
},
"clusters" : [
{
"cluster" : "beijing",
"message_id" : {
"ledger_id+entry_id" : 10
}
}
],
}
![]()
{
"subscription_name" : "my-subscription",
"clusters" : [
{
"cluster" : "beijing",
"message_id" : {
"ledger_id+entry_id" : 10
}
}
],
}
几个问题补充
- 切换后是否有可能发生跳过消费,即丢消息的情况?
- 订阅状态同步的时效性如何?
- 基于这种方式实现的订阅状态的同步会有一定的延后,这个具体和集群配置的同步间隔以及实际的网络情况都有关系。
- 这种实现方式的优缺点是什么?
- 优点:简单有效易实现;
- 缺点:有延时,切换后可能发生重复消费;所有集群都健康时才能正常同步订阅状态;
参考文献