设置灾难恢复复制

灾难恢复(spec.activeRedis.mode: peerof)是一种热备拓扑:上游实例将定向复制链路扇出到一个或多个下游实例;如果上游数据中心发生故障,每个下游实例都可以随时提升为主实例。设置灾难恢复包括两部分 — 在每个 Redis 实例上启用跨数据中心复制,以及建立从下游实例到上游实例的连接。

对于每个数据中心都接受写入的替代拓扑,请参阅设置 Active-Active 复制。

集群角色说明

在灾难恢复组中,每个 Redis 实例的角色并不固定,可以是上游实例,也可以是下游实例。下游实例不会限制数据写入,但写入下游实例的数据将被从上游实例同步的数据覆盖,或者由于数据类型冲突而导致同步失败。

选择适用于 Redis 版本的操作步骤

两个模块版本的设置操作步骤不同。请按照与 Redis 版本匹配的部分执行。

Redis 版本操作步骤链路身份验证
7.2Redis 7.2 — 新模块显式绑定 peer-auth RedisUser — 建议使用专用的 custom 账户
6.0Redis 6.0 — 旧模块自动绑定实例的 default 账户
两端必须运行相同的 Redis 版本

不支持 Redis 6.0 与 Redis 7.2 之间的复制。请参阅模块版本及适用性。

Redis 7.2 — 新模块

新模块需要 peer-auth 凭证:复制组中的每个实例都通过 spec.activeRedis.redisUserRef 绑定一个 RedisUser。operator 会将该凭证推送到每个节点,入站和出站 peer 链路都会使用它进行身份验证。未绑定凭证时启用复制会在准入阶段被拒绝,并且该模块会拒绝所有入站 peer 复制。

每个数据中心都需要相同的凭证

只有当入站 peer 提供的凭证与本地 peer-auth 凭证匹配时,模块才会接受该 peer。因此,组中的每个成员都必须携带相同的用户名和相同的密码值。如果两个数据中心的凭证不同,则在准入阶段会拒绝建立它们之间的链路:

ActiveRedisConnection validation Connection failed with reason
ConnectionFailed(Failed to connect to the upstream: ERR WRONGPASS invalid username-password pair)

绑定的账户可以是为此创建的 custom,也可以是实例自身的 default 账户。system 账户始终会被拒绝。

步骤 1:在每个数据中心创建 peer-auth 凭证

专用账户(建议)

custom RedisUser 可使复制凭证与实例密码分离:绑定默认账户会扩大该密码的授权范围 — 任何持有该密码的实体都可以充当复制 peer — 并使凭证轮换与实例密码轮换关联起来。

在每个数据中心创建相同的账户。密码 Secret 与 RedisUser 对象是一对一关系,因此每个数据中心都需要自己的 Secret 对象;跨数据中心需要匹配的是密码值,而不是 Secret 名称。

密码会在准入阶段进行策略检查:长度为 8 到 32 个字符,并且必须同时包含字母、数字和特殊字符。不符合要求的值会在创建 RedisUser 时被 password should consists of letters, number and special characters 拒绝。

$ cat << EOF | kubectl -n default create -f -
apiVersion: v1
kind: Secret
metadata:
  name: dr-peer-auth-dc1
stringData:
  password: <the shared peer-auth password>
---
apiVersion: redis.middleware.alauda.io/v1
kind: RedisUser
metadata:
  name: dr-peer-auth-s72-dc1
spec:
  accountType: custom
  arch: sentinel
  redisName: s72-dc1
  username: dr-peer
  aclRules: +@all ~*
  passwordSecrets:
  - dr-peer-auth-dc1
EOF
字段描述
accountTypecustom。
arch所引用实例的架构 — sentinel、cluster 或 standalone。
redisName此账户所属的本地实例。
usernamepeer-auth 用户名。在每个数据中心都必须相同。
aclRules+@all ~* — peer 会针对每个键重放完整的命令面。
passwordSecrets一个不被其他 RedisUser 使用的 Secret,其 password 键保存供每个数据中心共享的值。

等待账户就绪后再绑定 — 除非其阶段为 Success,否则绑定会被拒绝:

$ kubectl -n default get redisusers dr-peer-auth-s72-dc1
NAME                   INSTANCE   USERNAME   PHASE     AGE
dr-peer-auth-s72-dc1   s72-dc1    dr-peer    Success   1m

实例的默认账户(备用方案)

也可以改为绑定实例自身的默认账户。这是 Redis 的 6.0 链路一直使用的账户,因此现有组中的每个成员都已经携带匹配的密码。

仅适用于设置了实例密码的情况

每个实例都有默认账户 RedisUser,但只有在创建实例时使用了 spec.passwordSecret,该账户才会持有密码 Secret。对于未设置密码创建的实例,账户仍然存在,并报告 Success,但不携带凭证;绑定该账户会使实例的 ActiveRedis 保持为 Failed:

peer-auth RedisUser "rfr-acl-s72-dc1-default" has no password secret: the module rejects credential-less peers

请先设置实例密码,或按上述方式绑定专用账户。

绑定该账户要求组中的每个成员都携带相同的 default 密码。这不会自动完成 — 每个实例的密码取决于其自身的 spec.passwordSecret — 并且每个成员都需要自己的 Secret 对象:由多个实例共享的 Secret 会被它们各自的 RedisUser 控制器争用,永远无法稳定下来。在连接链路之前,请先在每个成员上设置相同的密码值。

默认账户 RedisUser 按实例命名:

实例架构RedisUser 名称
Clusterdrc-acl-<instance-name>-default
Sentinel、standalonerfr-acl-<instance-name>-default

检查它是否已在两个数据中心完成配置:

$ kubectl -n default get redisusers rfr-acl-s72-dc1-default
NAME                      INSTANCE   USERNAME   PHASE     AGE
rfr-acl-s72-dc1-default   s72-dc1    default    Success   3m

绑定本身将在步骤 2 中与其余复制配置一起设置。

步骤 2:在上游实例上启用复制

$ kubectl -n default patch redis s72-dc1 --type=merge \
    --patch='{"spec": {"activeRedis": {"serviceID": 0, "mode": "peerof", "redisUserRef": "dr-peer-auth-s72-dc1"}}}'
WARNING

serviceID 的取值范围为 [0-15],并且在复制组内必须唯一。之后无法更改该值,启用复制后也无法关闭复制。

启用复制会重启数据 Pod

首次启用复制时,实例会对其数据 Pod 执行滚动重启 — 此时会创建 proxy,并将模块交付到每个 Pod。实例会离开 Ready,其 ActiveRedis 会在 Pod 滚动期间处于 Pending:

instance s72-dc1 not ready

继续之前请等待两者都稳定下来。步骤 4 中的下游实例也适用同样情况,而步骤 5 会连接上游实例,因此上游实例也必须先稳定下来。

$ kubectl -n default get redis s72-dc1
$ kubectl -n default get activeredis

实例还必须满足新模块的限制条件,这些条件由准入机制强制执行:

  • customConfig.appendonly 不得为 yes — 新模块仅支持 RDB。
  • customConfig.databases 必须为 <= 16 — 模块拒绝加载超过 16 个数据库,否则 Pod 会进入崩溃循环。

步骤 3:公开上游 proxy

启用复制后,proxy 默认提供一个 NodePort 访问地址。单个基于 NodePort 的访问地址不具备高可用性。在生产环境中,请使用 LoadBalancer 地址:

$ kubectl -n default patch redis s72-dc1 --type=merge \
    --patch='{"spec": {"activeRedis": {"proxy": {"service": {"type": "LoadBalancer"}}}}}'

每个实例都会创建一个名为 activeredis-proxy-<instance-name> 的 proxy Service。

对于 Sentinel 实例,实例名称需要添加 rfr- 前缀。

$ kubectl -n default get svc activeredis-proxy-rfr-s72-dc1
NAME                            TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)          AGE
activeredis-proxy-rfr-s72-dc1   LoadBalancer   10.96.145.32    192.168.1.10    6379:31234/TCP   45s
必须确保一个端点可访问

peerof 实例的 proxy Service 公开一个端口 6379。proxy 在该端口上承载 RESP 控制平面和复制流,并按会话区分二者,因此下游只需要访问一个地址 — 即填入 spec.addresses 的地址。灾难恢复模式不会创建其他端口,数据中心之间也无需开放其他端口。

连接准入会连接其为复制流解析出的端点;如果该端点不可访问,则拒绝连接。当 spec.peerPort 未设置时,该端点就是 spec.addresses[0] 中的地址本身。

步骤 4:在下游实例上启用复制

按照步骤 1 创建下游实例的 peer-auth 账户 — 用户名和密码值必须与上游实例相同。然后使用不同的 serviceID 启用复制:

$ kubectl -n default patch redis s72-dc2 --type=merge \
    --patch='{"spec": {"activeRedis": {"serviceID": 1, "mode": "peerof", "redisUserRef": "dr-peer-auth-s72-dc2"}}}'

这也会重启下游实例的数据 Pod。请等待实例达到 Ready 后再执行步骤 5。

步骤 5:在下游侧创建连接

ActiveRedisConnection 创建在下游集群中。spec.instance 指定本地(下游)实例,spec.addresses 指向上游 proxy 的 RESP 端点。

$ cat << EOF | kubectl -n default create -f -
apiVersion: redis.middleware.alauda.io/v1alpha1
kind: ActiveRedisConnection
metadata:
  name: conn-dc2-to-dc1
spec:
  instance: s72-dc2
  addresses:
  - 192.168.1.10:6379
  teardownPolicy: Detach
EOF
字段描述
instance本地(下游)实例名称。
addresses上游 proxy 的 RESP 端点。第一项是复制流连接的端点。
peerPort保持未设置。上游 proxy 在同一个端点上提供复制流和 RESP 控制平面,因此使用 addresses[0] 的端口。仅当外部负载均衡器将两者拆分到不同端口号时才设置此项。架构中有意不提供 schema 默认值。
teardownPolicyDetach(默认值)或 Decommission。请参阅移除连接。
连接名称会原样用作模块 peer 名称

连接名称会成为模块的 peer 名称,其命名限制比 Kubernetes 更严格:

  • 必须以字母开头 — 1conn 会被拒绝;
  • reset、clear、all 和 list 是保留字,会被拒绝。
每个实例只能有一个连接

一个实例最多可以持有一个 ActiveRedisConnection — 即其自身的上游链路。一个上游连接多个下游的扇出拓扑,是通过在每个下游集群中创建一个连接来构建的,并且这些连接都指向同一个上游实例。

步骤 6:验证

$ kubectl -n default get activeredisconnections conn-dc2-to-dc1
NAME              INSTANCE   STATUS    MESSAGE   AGE
conn-dc2-to-dc1   s72-dc2    Healthy             40s

$ kubectl -n default get activeredis
NAME                  INSTANCE   SERVICEID   SHARDS   PEERS   DPEERS   PHASE     MESSAGE   AGE
s72-dc1-activeredis   s72-dc1    0           1                1        Healthy             16m
s72-dc2-activeredis   s72-dc2    1           1        1                Healthy             12m

PEERS 统计上游链路数量,DPEERS 统计下游链路数量,因此每一侧都会显示一个值,并将另一项留空 — 零值不会显示。上游实例的扇出为 status.downstreamPeerCount。

检查连接以查看每个 shard 的同步详细信息:

$ kubectl -n default get activeredisconnections conn-dc2-to-dc1 -o yaml
...
status:
  credentialMode: peer-auth-global
  instance: s72-dc2
  shards:
  - index: 0
    offset: "488"
    opId: "20480"
    status: Connected
    syncStatus: PartialSync
    versionState: verified
  status: Healthy
  upstreamPeer:
    service_id: 0

status.shards[].status 表示 shard 的连接状态,status.shards[].syncStatus 表示数据同步状态:PartialSync(Oplog 的增量同步)或 FullSync(正在同步 RDB)。流被应用时,offset 和 opId 会持续推进 — 如果某个 Connected shard 中这两个值停滞不前,就说明需要进一步检查。

status.shards[].versionState 记录模块自身对上游版本的判断 — unknown、verified 或 violating。因版本不一致而被挂起的 shard 还会携带非零的 versionHoldSince。

status.credentialMode: peer-auth-global 表示模块侧的 peer 记录不包含内联凭证 — 出站连接会读取节点本地的 peer-auth 凭证,因此凭证轮换会在下一次重新拨号时生效。

原生 Redis Sentinel 模式没有 shard 的概念。此处将 Sentinel 的主节点-从节点对抽象为 shard 0,以便与 Redis cluster 模式使用相同的数据结构。

Redis 6.0 — 旧版模块

旧版实现

Redis 6.0 包含冻结的旧版模块,为兼容现有实例而保留。它仅支持 Disaster Recovery,不支持 Active-Active 模式,也不支持模块级 peer 身份验证。在 Redis 6.0 上启用复制时会返回准入警告,建议使用 Redis 7.2。对于新部署,请使用 Redis 7.2。

上游侧

首先需要创建 Redis 实例。

CLI
Web Console

启用 Disaster Recovery

# Enable ActiveRedis
$ kubectl -n default patch redis s6 --type=merge --patch='{"spec": {"activeRedis":{"serviceID":0 }}}'

此命令会返回准入警告,建议使用 Redis 7.2;随后应用补丁。实例接着会对其数据 pod 执行滚动重启以启动代理,因此请等待实例再次处于 Ready 状态后再继续。

使用 LoadBalancer 作为上游 Proxy 的访问地址

为实例启用灾难恢复支持后,灾难恢复 Proxy 默认提供 NodePort 访问地址。单个基于 NodePort 的访问地址不具备高可用性。在生产环境中,可以使用 LoadBalancer 地址为 Proxy 提供访问。

CLI
# Enable ActiveRedis with LoadBalancer as Proxy access type
$ kubectl -n default patch redis s6 --type=merge --patch='{"spec": {"activeRedis":{"proxy": {"service": {"type": "LoadBalancer"}}}}}'

每个灾难恢复实例都会创建一个 Proxy Service,其名称遵循以下格式:activeredis-proxy-<instance-name>。

对于 Sentinel 实例,实例名称需要添加 rfr- 前缀

下游侧

CLI
Web Console

启用 Disaster Recovery

# Enable ActiveRedis
$ kubectl -n default patch redis s6-dest --type=merge --patch='{"spec": {"activeRedis":{"serviceID":1 }}}'
WARNING

serviceID 的范围为 [0-15]。在同一个灾难恢复集群中,serviceID 不能重复。

等待实例和 peer-auth 绑定

此侧也会滚动重启其数据 pod。在实例仍处于滚动重启状态时,随后创建的连接会被拒绝 — 它进行身份验证时使用的 RedisUser 必须报告 Success,而节点恢复期间不会报告该状态:

peer-auth binding not usable: peer-auth RedisUser not ready: RedisUser
"rfr-acl-s6-dest-default" phase is "Pending", want Success

operator 会在后续 reconcile 中添加绑定,而绑定仍为空时连接会被拒绝。请等待实例处于 Ready 状态,并且此命令输出一个名称:

$ kubectl -n default get redis s6-dest
$ kubectl -n default get redis s6-dest -o jsonpath='{.spec.activeRedis.redisUserRef}'
rfr-acl-s6-dest-default

配置 Disaster Recovery 连接

# create activeredisconnections
$ cat << EOF | kubectl -n default create -f -
apiVersion: redis.middleware.alauda.io/v1alpha1
kind: ActiveRedisConnection
metadata:
name: conn-s6-dest-to-s6-src
spec:
addresses:
- 192.168.1.10:30010
instance: s6-dest
EOF

注意替换上游地址。

链接凭证绑定在实例上,而不是连接上

Redis 6.0 链接使用实例的默认账户进行身份验证。operator 会为你记录这一点:它首次 reconcile 启用了复制但没有绑定的实例时,会将 spec.activeRedis.redisUserRef 设置为该实例自身默认账户的 RedisUser — 集群实例为 drc-acl-<instance-name>-default,Sentinel 实例为 rfr-acl-<instance-name>-default。不会预置任何内容:该账户已经存在,其中的密码就是链接使用的密码。因此,两个数据中心都必须使用相同的 default 密码 — 密码的值相同,但分别位于各自的 Secret 对象中。

连接上没有凭证字段。spec.secretName 已被移除,仍设置该字段的清单会因 unknown field "spec.secretName" 而被拒绝。

来自 v5.1 之前的版本

在 v5.1 之前,Redis 6.0 上的 Disaster Recovery 是该产品提供的唯一跨数据中心复制方式,链接凭证位于连接上:ActiveRedisConnection.spec.secretName 指向一个包含 password 且不包含其他内容的 Secret。

其中从未有过用户名。未设置用户名时,旧版模块会发送一个单参数的 AUTH <password>,而 Proxy 会将其解析为实例的默认账户 — 因此 Redis 6.0 链接始终使用 default 进行身份验证,无论 Secret 的名称是什么。因此,两个数据中心一直都必须使用相同的 default 密码。

v5.1 从 ActiveRedisConnection 和 ActiveRedisInspection 中移除了 spec.secretName,并且仅从 spec.activeRedis.redisUserRef 获取凭证。现有组无需进行任何干预:升级后的首次 reconcile 中,operator 会在每个复制实例上写入绑定,使其指向该实例自身默认账户的 RedisUser。链路上的任何内容都不会改变 — 仍是同一个账户、同一个密码,只是读取位置发生了变化 — 运行中的链接在升级期间会保持正常。

发生变化的是清单。已存储的 ActiveRedisConnection 仍可正常工作,但下一次 apply 时,仍设置 secretName 的清单会被拒绝,并且该字段已从 kubectl explain 中移除。删除该字段,并通过实例的绑定管理凭证。

此处也可以使用专用账户,但有一个注意事项

也可以在 Redis 6.0 上绑定 custom RedisUser — 按照 Redis 7.2 操作步骤中的第 1 步 创建该账户,并在启用复制的同一个补丁中将 spec.activeRedis.redisUserRef 设置为该账户。已经设置的绑定不会被自动绑定替换,之后链接会使用该账户而不是 default 进行身份验证。

但你无法获得轮换安全性。Redis 6.0 链接会将凭证保存在模块的 peer 记录中,链接建立时凭证即被冻结其中,而旧版路径没有自动修复重连机制。更改密码后,运行中的链接会继续工作,直到其会话下一次断开 — 此后链接会保持断开状态:

Failed   shard 0 status is Disconnected

要恢复连接,需要删除 ActiveRedisConnection 并重新创建它,以便使用当前凭证重新配置 peer。在 Redis 6.0 上规划凭证轮换时,应将其视为链接的短暂中断;或者升级到 Redis 7.2,其会在每次拨号时读取凭证。

检查 Disaster Recovery 连接状态

$ kubectl -n default get activeredisconnections conn-s6-dest-to-s6-src -o yaml
apiVersion: redis.middleware.alauda.io/v1alpha1
kind: ActiveRedisConnection
metadata:
annotations:
  cpaas.io/creator: admin
  cpaas.io/updated-at: "2025-08-26T10:16:49Z"
creationTimestamp: "2025-08-26T10:16:49Z"
generation: 1
labels:
  cpaas.io/activeredis: s6-dest-activeredis
  cpaas.io/activeredis-instance: s6-dest
name: conn-s6-dest-to-s6-src
namespace: default
resourceVersion: "18872971"
uid: 283ac8fa-d693-46ff-989f-68d018888584
spec:
addresses:
- 192.168.1.10:30010
instance: s6-dest
pause: false
teardownPolicy: Detach
status:
instance: s6-dest
shards:
- index: 0
  offset: "0"
  opId: "0"
  status: Connected
  syncStatus: PartialSync
status: Healthy
upstreamPeer:
  service_id: 0
  service_metadata:
    instance: default/s6-src

这里,status.shards[0].status 表示 shard 的连接状态,status.shards[0].syncStatus 表示数据同步状态。同步状态可以是 PartialSync(表示 Oplog 的增量同步)或 FullSync(表示正在同步 RDB)。

原生 Redis Sentinel 模式没有 shard 的概念。此处将 Sentinel 的主节点-从节点对抽象为 shard 0,以便与 Redis cluster 模式使用相同的数据结构。

预检查

在接受连接之前,平台会对上游执行一组预检查。Web Console 通过 Inspect 按钮提供这些检查;在 ActiveRedisConnection 准入期间也会自动执行这些检查,如果检查失败,连接将被拒绝,并显示相应消息。这些检查由 ActiveRedisInspection 资源提供支持。

检查项检查内容处理方法
网络连接网络连通性检查、密码检查确认上游地址正确;该地址可从下游访问;凭证正确。
架构Redis 实例架构检查确认上游和下游实例的实例架构一致。
集群模式分片检查集群模式分片数量和槽位分布检查确认上游和下游实例的分片数量和槽位分布相同。如果不同,可以参考初始化集群实例槽位分布 创建新实例。
要求检查实例资源规则检查需要确保下游实例的内存资源大于或等于上游实例。
配置检查实例 Service ID 检查需要确保在复制组中,ServiceID 唯一且位于 [0-15] 范围内。

在 Redis 7.2 上,检查会使用数据路径实际使用的 peer-auth 凭证进行拨号,因此检查成功还可以确认两个数据中心使用相同的凭证。此外,检查还会对其为复制流解析出的端点执行普通 TCP 拨号,即 spec.addresses[0] 中的地址,除非 spec.peerPort 将两者拆分。

删除连接

删除 ActiveRedisConnection 会根据其 spec.teardownPolicy 拆除链路:

策略效果可逆
Detach(默认)中止并删除对等链路。模块会保留对等端的记录信息,因此稍后重新创建连接时,会从保留的状态恢复。是
Decommission永久停用对等端。除了删除链路外,模块还会停止为该对等端保留 Oplog、失效键墓碑和堆积量记录。否
停用是单向操作

Decommission 是永久离线的数据中心应使用的正确拆除方式——已分离但永不返回的对等端会持续占用其他实例的垃圾回收下限。它无法改回为 Detach:

spec.teardownPolicy is one-way: cannot change "Decommission" back to "Detach"

该决定会记录在对等端的 serviceID 中,并在重启后保留。它改变的是此实例为该对等端保留的内容:该 serviceID 的 Oplog 保留、墓碑垃圾回收和堆积量记录都会被释放,且不会再次启用。重新创建与该对等端的连接并不会受到阻止,立即创建的连接仍会从两端当时持有的状态恢复——因此,重新创建链路并不能撤销停用;如果对等端预计会恢复,应将其分离,而不是停用。

凭证轮换

在 Redis 7.2 上,对等链路不携带内联凭证:出站拨号会读取节点本地的 peer-auth 凭证,该凭证由 operator 在每次 reconcile 时重新推送。要进行轮换,请原地更新每个数据中心的 peer-auth RedisUser 的密码 Secret,使其使用相同的新值,并逐个数据中心操作。RedisUser controller 会重新应用 ACL,operator 会重新推送凭证,proxy 会在下一次重建时接受新凭证。已建立的链路会在整个过程中保持健康并持续复制,包括数据中心持有不同值的时间窗口;轮换后断开的链路会使用当前凭证重新拨号。

不过,该窗口并非毫无限制:值不同时,创建链路会被拒绝,因为准入检查会使用数据路径将使用的凭证拨号连接上游。在为任何成员接入新链路之前,请先完成所有成员的轮换。

绑定默认账户后,两次轮换实际上是同一次轮换

绑定专用 custom 账户时,轮换实例自身的密码 Secret 是独立操作,不会影响对等链路。

绑定实例的 default 账户时,其密码 Secret 就是 peer-auth Secret:轮换实例密码时,复制凭证也会随之轮换,复制组中的其他成员也必须跟进。这正是专用账户可以避免的耦合。

在 Redis 6.0 上,轮换会使链路中断

以上内容均不适用于 Redis 6.0。其链路会在模块的对等端记录中内联保存凭证,该凭证在链路接通时即已固定;此路径没有自动修复式的重新接线机制——因此,轮换后的链路只能运行到其会话下一次断开为止,随后会报告 shard 0 status is Disconnected 并保持该状态。请删除并重新创建 ActiveRedisConnection,以使用当前凭证重新接线。