设置 Active-Active Replication

功能成熟度提示

Active-Active 模式处于 alpha,且仅适用于 Redis 7.2。Redis 6.0 不支持该模式 — admission 会拒绝它。

设置 Active-Active replication 包含两个部分:在每个 Redis 实例上启用 Active-Active 模式,以及为每个成员创建一个 ActiveRedisMesh,以便成员能够彼此发现。有关该模式的工作方式 — 已发现的成员关系、两个端口、冲突解决和时钟要求 — 请先阅读 架构。有关它与 Disaster Recovery 的比较,请参阅选择模式。

前提条件

  • 每个成员运行 Redis 7.2。
  • 每个成员都有一个位于 [0-15] 范围内的唯一 serviceID。
  • 每个成员都必须显式设置可从外部路由的 announceAddress — 请参阅步骤 2。不会默认将其设置为跨数据中心地址,而 admission 只会发出警告,因此未设置该地址的成员会公布一个其对等成员无法访问的地址。
  • 每个成员的代理端点都必须可从其他每个成员访问,并且访问其代理 Service 发布的每个端口,每个端口均使用自身的端口号 — 而不仅仅是公布的端口。
  • 所有成员的系统时钟必须保持同步,即在组内每个集群的每个节点上运行时间同步守护进程(ntpd、chronyd 或平台对应的组件)。这是硬性前提条件,而不是建议 — 请参阅时钟同步。

步骤 1:在每个数据中心配置对等认证凭证

Active-Active 组中的每条对等连接都需要进行身份验证,并且每个成员都必须提供相同的凭证:只有当入站对等成员提供的用户名等于该成员自身的对等认证用户名,且密码通过 Redis ACL 验证时,成员才会接受该入站对等成员。每个实例通过 spec.activeRedis.redisUserRef 绑定该凭证,该字段指定实例自身命名空间中的一个 RedisUser 对象。

无论绑定哪个帐户,该帐户都必须:

  • 引用本地实例 — 指向其他实例的 RedisUser 会被拒绝;
  • 是 custom 或 default 帐户;system 帐户始终会被拒绝;
  • 拥有一个包含 password 键的密码 Secret;
  • 能够达到 status.phase: Success — 在此之前绑定会被拒绝;
  • 在每个数据中心携带相同的用户名和相同的密码值。

专用帐户(推荐)

在每个数据中心创建一个用于此目的的 custom RedisUser。这样可以缩小凭证授予的权限范围 — 否则,任何持有默认密码的对象都可以充当 mesh 对等成员 — 并且可以独立于实例密码轮换复制凭证。

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

密码会在 admission 时进行策略检查:长度为 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: aa-peer-auth-dc1
stringData:
  password: <the shared peer-auth password>
---
apiVersion: redis.middleware.alauda.io/v1
kind: RedisUser
metadata:
  name: aa-peer-auth-s72-dc1
spec:
  accountType: custom
  arch: sentinel
  redisName: s72-dc1
  username: aa-peer
  aclRules: +@all ~*
  passwordSecrets:
  - aa-peer-auth-dc1
EOF
字段描述
accountTypecustom。
arch所引用实例的架构 — sentinel、cluster 或 standalone。
redisName此帐户所属的本地实例。
username对等认证用户名。在每个数据中心都必须相同。
aclRules+@all ~* — 对等成员会针对每个键重放完整的命令操作面。
passwordSecrets一个不被其他任何 RedisUser 使用的 Secret,其 password 键包含所有数据中心共享的值。

等待帐户就绪后再绑定:

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

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

也可以绑定实例自身的默认帐户 — 这是 Disaster Recovery 使用的同一凭证,因此如果这些实例已经存在绑定,可以直接跳到步骤 2。

仅当实例具有密码时

每个实例都有一个默认帐户 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

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

默认帐户 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
绑定默认帐户会使 mesh 与实例密码关联

此后每个成员都必须携带相同的 default 密码,而默认密码是按实例生成的 — 请在启用该模式前将它们设置为相同的值。任何持有该密码的对象都可以充当 mesh 对等成员,并且轮换实例密码也会随之轮换复制凭证。专用帐户可以避免这两种情况。

步骤 2:在每个实例上启用 Active-Active 模式

启用复制时设置模式

这些操作步骤假设实例从一开始就要加入 Active-Active 组。对于已经在进行复制的实例,准入控制不会阻止将 spec.activeRedis.mode 从 peerof 更改为 mesh,但将已建立的 Disaster Recovery 组转换为 Active-Active 不属于本操作步骤的范围,且未经验证。请使用尚未接入 Disaster Recovery 组的实例构建 Active-Active 组。

启用该模式会重启数据 Pod

启用 Active-Active 会将该模块的 init container 添加到实例,因此 operator 会滚动重启 Redis Pod,并且实例会在再次处于 Ready 状态前重新经过 Initializing。请先完成步骤 1:当实例不处于 Ready 状态时,无法针对该实例创建 custom RedisUser — 准入控制会拒绝请求并返回 redis failover <instance> is not ready — 因此在帐户创建之前启用该模式,会让您只能等待重启完成后才能创建帐户。

成员必须公布一个可从外部路由的地址,因为对等成员每次拨号时都会重新解析该地址。这就是设置在代理 Service 配置中的 announceAddress。

$ kubectl -n default patch redis s72-dc1 --type=merge --patch='
spec:
  activeRedis:
    serviceID: 0
    mode: mesh
    redisUserRef: aa-peer-auth-s72-dc1
    proxy:
      service:
        type: LoadBalancer
        announceAddress: redis-dc1.example.com
        announcePort: 6379
'
字段描述
modemesh。
serviceID在 mesh 内唯一,范围为 [0-15],且不可变更。
redisUserRef步骤 1 中的对等身份验证 RedisUser 绑定。必填。
proxy.service.announceAddress对等成员拨号以访问此成员时使用的外部可达主机 — 外部负载均衡器或 VIP 上的主机,与集群内 Service 解耦。必须是裸主机:不能包含 scheme、端口或路径。
proxy.service.announcePort此成员代理端点的外部可达 RESP 端口。它与 announceAddress 共同构成对等成员为此成员放入 seed 列表的地址对,也是该成员用于识别自身 seed 条目的匹配对。默认值为代理 Service 端口 6379;仅当外部负载均衡器使用其他端口号暴露该端口时才覆盖此值。
请在 Redis 实例上设置这些字段,而不是在 ActiveRedis 资源上设置

ActiveRedis 暴露相同的 spec.proxy.service 字段,但该资源是派生资源:operator 会在每次 reconcile 时,根据 Redis 实例重写其 spec.proxy — 在 mesh 模式下,会将已解析的公布地址写入其中 — 因此在该资源上进行的编辑会被覆盖。请始终修改 Redis 对象。

announceAddress 规则
  • 建议使用 DNS 名称:该模块会在每次拨号时重新解析该名称,因此可以适应 IP 变化;包含多个 A 记录的 DNS 名称还可以覆盖多个 NodePort 地址。当公布地址为 DNS 名称时,operator 会通过 ExternalDNS 将其绑定到代理 Service。
  • 允许使用裸 IP,但会发出警告 — IP 发生变化后不会重新解析,因此仅适用于稳定的 VIP。
  • 留空会产生警告,之后的行为取决于 operator:如果 operator 配置了受管 DNS 区域,则会派生出 <instance>.<namespace>.<zone>,并通过 ExternalDNS 将其公布在代理 Service 上;如果未配置区域,则完全不发送公布地址,模块会公布Pod IP,而该地址无法跨数据中心路由。不会有其他机制填充此字段。
  • 包含 scheme(tcp://…)、端口(host:6379)或路径的地址会被拒绝。端口应填写在 announcePort 中。
announcePort 不是传输端口 — 对等流量通过对等端口传输

announcePort 是一个身份值:它用于向对等成员命名此成员的端点,本身不承载任何流量。gossip 和复制都不会由它引导,它也不会覆盖模块寻址对等成员的方式。

代理仅运行单个监听器 — 应用流量、gossip 和复制流量都会到达该监听器,并按会话进行区分 — 但代理 Service 会将多个端口发布到该监听器上。当外部负载均衡器或防火墙位于代理前端时,请转发该 Service 发布的每个端口,并分别使用各自的端口号。将其中任何端口重新映射到其他端口号都会破坏 mesh。

步骤 3:在每个集群中创建 ActiveRedisMesh

每个 Kubernetes 集群为其本地实例管理一个 ActiveRedisMesh。spec.seeds 用于指定其他数据中心成员公布的端点 — 即这些成员在步骤 2 中获得的 announceAddress 和 announcePort。

先等待实例进入 Ready 状态

步骤 2 会滚动 Pod,模块也会随 Pod 一起到位。如果在滚动更新到达分片的主节点之前创建 ActiveRedisMesh,mesh 会进入 Failed,并报告模块自身的命令不存在:

set mesh config on pod rfr-s72-dc1-0: ERR Unknown option or number of arguments for CONFIG SET
- 'activeredis.mesh-seeds'; shard 0 mesh info: ERR unknown command 'as.mesh'

此状态会自行清除 — 主节点加载模块后,mesh 会转为 Pending,随后转为 Healthy — 但在此期间,它与实际故障无法区分。请等待 kubectl get redis 报告 Ready。

$ cat << EOF | kubectl -n default create -f -
apiVersion: redis.middleware.alauda.io/v1alpha1
kind: ActiveRedisMesh
metadata:
  name: mesh-dc1
spec:
  instance: s72-dc1
  seeds:
  - redis-dc2.example.com:6379
  - redis-dc3.example.com:6379
EOF

Seed 语法

Seed 是 host:port — 远程成员的 announceAddress:announcePort,必须完全按照该成员公布自身信息的方式填写。它是一个可从外部路由的端点,因此当负载均衡器或 VIP 位于该成员代理的前端时,seed 应填写负载均衡器的地址和端口,而不是集群内代理 Service。请原样填写这一对值:成员会将 seed 的地址和端口与自身公布的地址和端口进行比较,以识别自己的条目;如果两者不匹配,则自我跳过机制会失效。

请将 seed 写成普通的 host:port,由 operator 完成其余处理。operator 会先将列表规范化为模块内部的 seed 语法,然后注入模块,因此 CONFIG GET activeredis.mesh-seeds 报告的每个条目会比你填写的形式更长。这种补全是 seed 能够使用的必要条件,其中没有任何需要你设置的内容。

Seed 是引导列表,而不是成员列表

每个远程数据中心只需一个可访问的 seed 即可 — gossip 会发现其他所有成员。可以包含本地成员自身的条目,因此每个数据中心都可以使用相同的统一 seed 列表;模块会跳过自身记录。

operator 会将规范化后的集合注入每个节点,包括副本节点,因此 mesh 会在每个 Pod 上自动启动。因此,重启和故障转移后的重新加入都无需手动操作。

可选字段

字段描述
pause暂停参与 mesh。默认为 false。
enableTLS为对等传输启用 TLS。
tuning可选的 gossip 和成员关系计时器。请参阅调优。
secretName已弃用且会被忽略。 对等身份验证凭证是实例级的 RedisUser 绑定。

第 4 步:验证收敛

$ kubectl -n default get activeredismesh
NAME       INSTANCE   PHASE     MEMBERS   ALIVE   AGE
mesh-dc1   s72-dc1    Healthy   3         3       2m
$ kubectl -n default get activeredismesh mesh-dc1 -o yaml
...
status:
  phase: Healthy
  instance: s72-dc1
  memberCount: 3
  aliveCount: 3
  fleetFloorVersion: "7.2"
  versionConverged: true
  members:
  - uid: 9f2c1a...
    serviceID: 0
    shardID: 0
    state: alive
    version: "7.2"
  - uid: 4b81d3...
    serviceID: 1
    shardID: 0
    address: redis-dc2.example.com:6379
    state: alive
    clockOffsetMs: -5
    version: "7.2"
    moduleVersion: 10
  - uid: 07d65b...
    serviceID: 2
    shardID: 0
    address: redis-dc3.example.com:6379
    state: alive
    clockOffsetMs: -2
    version: "7.2"
    moduleVersion: 10
本地成员的条目比其他成员更短

上面的第一条记录是此实例自身的记录,其中没有 address、clockOffsetMs 或 moduleVersion。这是预期行为,并不表示成员缺失:gossip 会报告其他成员,因此 operator 会根据实例对自身的了解补全本地记录,而这三个字段没有本地来源。每个远程成员都会携带这些字段。值为零的计数 — suspectCount、deadCount — 会被省略,而不是打印出来。

Mesh 阶段

阶段含义
Healthy成员关系已收敛,所有已知成员均处于活动状态。
Degradedmesh 正在有可疑或已失效成员的情况下运行。
Pending尚未能够访问任何就绪的主节点来读取成员关系 — 可能处于引导窗口期,也可能发生了中断。
Pausedspec.pause 为 true。
Failed无法配置 mesh;status.message 会提供原因(例如 invalid mesh seed)。

成员状态

成员的状态为 alive、suspect 或 dead — 请参阅成员关系生命周期。

clockOffsetMs 是每个成员相对于此实例的时钟偏移量,单位为毫秒。请参阅时钟同步。

版本字段

status.fleetFloorVersion 报告已知版本的非失效成员中的最低 Redis 版本 — 这是整个 mesh 的功能上限。所有非失效成员运行相同的 Redis 和模块版本后,status.versionConverged 为 true。

滚动升级期间 versionConverged 为 false

这是预期行为,由模块负责处理;它不会导致 mesh 阶段失败。整个成员集收敛后,它会再次变为 true。

后续步骤

Day-2 操作 — 监控时钟偏移、调整 gossip 计时器,以及添加或移除成员 — 详见运维。