TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

服务器为什么无法通过两台节点实现 HA(高可用)2:一个见证节点所带来的改变与残留的陷阱

状态:实际测量(2026-10-31)。 此数值在运行于 3 台 TT Lab 实践 VM(KubeVirt,Ubuntu 24.04)上的 Docker 中测得。每项测试进行了一次,未进行 VM 电源直接关闭测试(未测量)。配置与测试方法请参考第 1 篇。 编写规则:以实测数据为基础,未测出的标记为“未测量”,原因未确认的解释标记为“推测”。

目录

1. 概览

在第 1 篇中,我们看到仅有 2 台数据节点时,丢失一台节点会导致全部 5 个服务停止。这次我们在引入了仅参与投票的见证 VM 的架构(数据节点 db1、db2 + 见证节点)中引入了 3 种类型的故障。

首先记录结果。

2. 按故障类型划分的最长写入中断

最长写入中断是指成功写入与下一次成功写入之间最长的时间间隔(秒)。括号内为失败次数/尝试次数。 ClickHouse 为默认插入, quorum=2 是通过 insert_quorum=2 插入的, S3 是 HAProxy 后端的 2 个 SeaweedFS S3 网关。

증인이 있을 때: 장애 종류별 최장 쓰기 중단 장애는 모두 90초. 고립 = 세션망 차단, 정지 = 컨테이너 전부 docker stop db1 고립 db1 정상 정지 증인 정지 0 30 60 90 120 150 초 PostgreSQL 23.7 3.7 0.1 Redis 105 134.8 0.1 OpenSearch 66.3 0.7 0.1 ClickHouse 5.3 2.4 5.4 ClickHouse quorum=2 60.6 123.6 5.3 SeaweedFS S3 95.7 131.1 27.6 점선 = 장애 90초. 0.1 은 끊김 없음(쓰기 간격 수준)
图:存在见证节点时:按故障类型划分的最长写入中断
服务 db1 隔离 90秒 db2 隔离 90秒 db1 隔离 40秒 db2 正常停止 90秒 db1 正常停止 90秒 见证节点停止 90秒
PostgreSQL 23.7 (400/5910) 28.3 (491/5915) 33.7 (601/3120) 3.4 (25/5944) 3.7 (34/5943) 0.09 (0/4780)
Redis 105.0 (471/4364) 105.1 (1964/5855) 55.2 (371/2463) 268.8 ※ (5349/5968) 134.8 (2673/5970) 0.09 (0/4782)
OpenSearch 66.3 (16/2347) 64.7 (15/2364) 40.2 (9/1196) 0.3 (0/2990) 0.7 (0/2986) 0.13 (0/2392)
ClickHouse 5.3 (1/1476) 5.2 (1/1476) 5.2 (1/776) 2.5 (2/1491) 2.4 (2/1490) 5.4 (1/1175)
ClickHouse quorum=2 60.6 (303/1475) 59.9 (300/1451) 10.0 (51/752) 122.9 (601/1488) 123.6 (602/1486) 5.3 (1/1175)
SeaweedFS S3 95.7 (19/898) 111.2 (11/955) 61.2 (11/505) 122.4 (11/898) 131.1 (12/856) 27.6 (2/1055)

※ db2 正常停止测试中的 Redis 268.8 秒,是由于官方镜像缺陷导致重启的 Redis 陷入重启死循环而最终未能恢复的值(4.5 节)。修复后在 db1 上执行相同测试的结果位于右侧第二列。写入间隔为 PostgreSQL 与 Redis 0.05 秒,因此 0.09 秒意味着没有中断。

读取(查询)失败几乎没有发生。OpenSearch _count 在每次测试中为 0 到 1 次,ClickHouse 与 S3 在所有带见证节点的测试中为 0 到 1 次。

2.1 主节点切换过程

从注入故障时起计算的秒数。

db1 隔离 90秒 db2 隔离 90秒 db1 隔离 40秒 db2 正常停止 db1 正常停止
Patroni 新主节点 (primary) 22.6 27.4 32.7 3.1 2.8
Sentinel 新主节点 (master) 6.5 6.6 7.0 7.9 7.8
OpenSearch 节点移除 (yellow) 64.8 65.2 移除前恢复 0.6 0.5
SeaweedFS 新 master 领导者 12.1 领导者不是被隔离节点 领导者不是被隔离节点 无影响 15.0
恢复后加入 PG 15.5(包含 pg_rewind 与重启),OpenSearch green 5.1 PG 6.6,OpenSearch green 2.8 PG 14.2,OpenSearch green 6.9 OpenSearch green 23.2(以开始为准) OpenSearch green 26.3

2.2 按服务查看读取情况

2.3 关闭见证节点的情况

PostgreSQL Redis OpenSearch ClickHouse 默认 / quorum=2 S3
失败 0 / 4780 失败 0 / 4782 失败 0 / 2392 5.4秒 1条 / 5.3秒 1条 27.6秒,2条

ClickHouse 和 S3 的短暂中断是 因为当时 Keeper 领导者和 SeaweedFS master 领导者正好在见证节点上。重新选举领导者分别花费了约 5 秒和 12 秒,而 S3 在卷服务器重新连接到新领导者之前中断得更久。虽然见证节点只是一个“仅用于投票”的角色,但值得记住的是,在 Raft 协议中它也有可能成为领导者。

3. 网络分区:被隔离的老主节点是否会接收写入

实现了自动故障转移并不意味着万事大吉。如果断网一侧的老主节点在此期间接收了写入并在随后丢弃,应用程序在收到成功响应后仍会丢失数据。 将 db1(当时的主节点)隔离 40 秒,并在 db1 内部 每隔 0.5 秒直接写入其本地服务。恢复后,对比了这些写入是否被保留。

服务 被隔离的老主节点接收到的写入 恢复后保留的数据 防范机制
PostgreSQL 0。隔离后立即失败 不适用 同步复制(等待副本确认)+ 丢失共识存储则降级
Redis 隔离后 0 秒到 10.7 秒之间 22 条 0 条(22 条全部丢失) min-replicas-max-lag 10 过去后才 NOREPLICAS。这 10 秒即为丢失窗口
OpenSearch 100 条 100 条 ※ 这是刚创建的索引,副本尚未进入 in-sync 集合(常规索引的写入被阻止)。没有发生分支
ClickHouse 0。由于丢失 Keeper 过半数,插入挂起 不适用 Keeper 过半数
SeaweedFS filer 0。超时 不适用 master 过半数

严格遵循过半数规则的 PostgreSQL、ClickHouse、SeaweedFS 拒绝了被隔离一侧的写入。唯独 Redis 出现了数据丢失。

4. 运维人员必须了解的陷阱

以下内容是在本次测量中实际经历过的。

4.1 Redis min-replicas-max-lag 本质上就是网络分区时的丢失窗口

Redis 분할: 고립된 옛 주인이 받은 22건 db1(주인)의 세션망을 끊고, db1 안에서 0.5초마다 직접 썼다 0 +5 +10 +15 +20초 db1 옛 주인(고립) 쓰기 성공 22건 NOREPLICAS db2 새 주인 복제본 +7.0초 Sentinel 이 승격 min-replicas-max-lag 10 복구하자 db1 은 db2 의 복제본이 되어 db2 의 데이터로 덮였다. 성공 응답을 받은 22건이 복구 뒤 0건(전부 소실) 그 사이 db2 쪽의 확인받은 쓰기 손실은 0건
图:Redis 网络分区:被隔离的老主节点接收到的 22 条数据

Redis 的复制是异步的。主节点接收写入后会立即返回成功,并随后发送给副本。 min-replicas-to-write 1 与 min-replicas-max-lag 10 意味着“仅当最近 10 秒内有至少 1 个副本响应时才接受写入”。被隔离的主节点在副本的最后响应时间超过 10 秒之前,会继续接收写入。

在实际测量中,这个窗口完全如预期般开启。被隔离的 db1 在最初的 10.7 秒内成功接收了 22 条写入,而在此期间的 +7.0 秒时,Sentinel 已将 db2 立为新主节点。恢复后,db1 成为 db2 的副本并被 db2 的数据覆盖, 这 22 条写入没有一条留存下来。 但从应用端来看,这是收到了“成功”响应的写入。

有三种选择,没有一种是免费的。

本次配置采用了第一种设置(1, 10),其代价经测算表现为两点:“分区时最多约 10 秒的写入丢失”以及“丢失一个数据节点时写入暂停”。

4.2 返回失败但实际已存储的写入,以及幂等键

실패로 응답했는데 저장된 쓰기 시간 초과·정족수 미달 응답이 와도 한쪽에는 이미 써졌다 앱 id=42 삽입 ① 삽입 ③ 실패 응답(시간 초과) DB ② 복제본 하나에 저장 다시 보내면 멱등 키 없음 id=42 가 두 줄 (같은 행이 중복된다) 다시 보내면 멱등 키 있음 id=42 가 한 줄 OpenSearch: _id 지정 ClickHouse: 중복 제거 토큰 실측(증인 있는 구성): OpenSearch 고립 시험마다 9–16건, ClickHouse quorum=2 실패 응답의 거의 전부, S3 2–4건
图:返回失败但实际已存储的写入

确认收到的写入丢失数量为 0,但出现了反方向的偏差。 收到失败响应但实际上已经存储的写入。

带证人节点的配置测试 OpenSearch ClickHouse quorum=2 S3
db1 隔离 90 秒 16 / 失败 16 303 / 失败 303 4 / 失败 19
db2 隔离 90 秒 15 / 失败 15 300 / 失败 300 3 / 失败 11
db1 隔离 40 秒 9 / 失败 9 51 / 失败 51 4 / 失败 11
db2 正常停止 90 秒 0 / 失败 0 601 / 失败 601 2 / 失败 11
db1 正常停止 90 秒 0 / 失败 0 601 / 失败 602 2 / 失败 12

(表中的值为“失败响应却已存储的数量 / 失败响应数量”)

如果客户端因为失败而重新发送,会导致同一行数据被写入两次。因此, 重试写入需要用到幂等键。OpenSearch 由应用指定文档的 _id 并写入,而 ClickHouse 则附加了 insert_deduplication_token 。首先需要确认应用的重试逻辑是否假设了“因为失败了所以肯定没写进去”。

4.3 ClickHouse Keeper 强制恢复后的法定人数窗口

这是在第 1 篇 7.1 节中看到的内容。在只有 2 个节点的配置中,将 Keeper 以 --force-recovery 作为单服务器恢复后的大约 20 秒内,尽管对方副本仍处于隔离状态,但 insert_quorum=2 有 98 个插入操作响应为成功。这之后,有 TOO_FEW_LIVE_REPLICAS 个操作以 失败。据推测(推测),恢复后的 Keeper 在会话超时(30 秒)之前,依然保留着对方旧的“存活”标志。在强制恢复后,在会话超时之前,切勿盲目相信法定人数插入会成功。

4.4 OpenSearch 察觉到隔离节点花去了 65 秒钟

在正常停止的情况下,节点会主动离开集群,因此写入中断时间不到 1 秒。但隔离则不同。节点被剔除(变为 yellow)花了 64.8 秒和 65.2 秒,在此期间, 发往该节点上拥有 primary 分片的索引的写入操作陷入了停滞。 搜索(_count)在每次隔离测试中仅失败了 0 到 1 次。

OpenSearch 使用默认的故障检测配置(单次检查超时时间 10 秒,连续失败 3 次判定为故障)来监视节点。由于在隔离情况下此判定需要经过两个阶段,因此耗时大约为 65 秒。 cluster.fault_detection.* 虽然减小

的值可以缩短这段时间,但我们并未对调整后的值进行过测量(未实测)。由于暂时的网络抖动也可能导致节点被剔除,因此如果缩短该值,必须重新对其进行测量。 redis:8 4.5 官方

镜像与 Sentinel 的重启死循环

  1. 这对应了第 2 节表格中的 268.8 秒。其原因是多种因素交织在一起导致的。 redis:8 官方 .so 镜像的入口点会将模块文件夹中的所有 --loadmodule 以
  2. 的形式附加并启动 Redis。 CONFIG REWRITE Sentinel 在发生故障转移时会向 Redis 发送 loadmodule 。这样一来,正在运行的配置会被重新写入文件,导致这些 redis.conf 行被追加到
  3. 中。 Can't load module ... server aborting 在下一次启动时,入口点附加的模块与文件中写入的模块发生重叠,导致同一模块被加载两次,从而因

而崩溃。容器随后重启,然后再次崩溃。

  redis:
    image: redis:8.8.4
    command: ["redis-server", "/conf/redis.conf"]
    tmpfs:
      - /usr/local/lib/redis/modules   # 진입점이 붙일 모듈이 없게 한다
sed -i '/^loadmodule /d' /path/to/redis.conf   # 이미 박힌 줄 지우기

由于该问题仅在使用 Sentinel、经历过一次故障转移并重新启动该 Redis 时才会暴露出来,因此在刚安装完的测试中是无法看到的。如果不使用模块,解决方法就很简单:用一个空的 tmpfs 覆盖模块文件夹,彻底不加载任何模块。并将已经写入的行删除。 min-replicas 修复后在同样的测试(db1 正常停止)中,Redis 恢复正常,剩余的 134.8 秒中断则是 4.1 节中redis/docker-library-redis#468, redis/redis#14178所带来的代价。该问题已向上游报告(redis/redis#16009),Redis 本体的修复已于 2026-10-29 合并,预计将在下一个版本中发布。

同根源还有一个小陷阱。 Sentinel 会将 requirepass 改写为 ACL 哈希行。 由于从配置文件读取密码的健康检查会失败,这次我们让认证前的响应(NOAUTH)也被视为“存活”。

4.6 将见证节点放在第三个故障域

在本次测试中,见证节点 VM 与 db2 处于 同一物理服务器 上。由于只引入了 VM 级别的故障,因此结果没有受到影响,但如果该物理服务器宕机,db2 和见证节点将同时丢失,3 票中会失去 2 票。这样就会像只有 2 台机器的配置一样停止运行。

见证节点的价值在于“不与任何一方数据节点同时宕机”。在生产环境中,应将见证节点部署在 第三个故障域,即不同的机架、不同的机房、不同的电源与交换机,或者是云上的小型 VM。见证节点所做的工作仅仅是投票,因此小型 VM 就足够了。本次的见证节点使用的是 2 vCPU 和 2 GiB 内存,在安装后立即使用了 1.5 GiB。不过,正如 2.3 节所看到的,见证节点也可能成为 Raft 领导者,因此如果与数据节点之间的往返延迟太大,可能会延长重新选举领导者期间的中断时间(未实际测量)。

4.7 SeaweedFS:无法仅凭一个卷服务器运行,但也不会立即失败

在复制 001 中,如果只剩下一个卷服务器,就没有地方放置新的写入。到这里为止都符合预期。问题在于,请求在挂起约 10 秒后才返回失败。这里有两个因素交织在了一起。

这两个问题都已提交给上游。针对卷服务器迟迟找不到新领导者的问题,我们提交了修复 PR(seaweedfs#12259,复现中的时间从 25 秒缩短至 4 秒);针对恢复的 master 拉低领导者的问题,我们提交了 Issue(seaweedfs#12260)。在同样的实验中,如果切换到 -raftHashicorp(开启了 PreVote 的 Raft),领导者就不会被扰乱。如果在生产环境中使用 2 个数据节点来运行 SeaweedFS,请将“当其中一台机器离线时,上传不会立即失败而是会挂起”这一情况计入应用程序的超时设置中。如果是整个卷服务器磁盘丢失的情况, volume.fix.replication -apply 在 30.7 秒内匹配了全部 24 个卷,在此期间 S3 写入失败 1 次,读取失败 0 次。在让单个 filer 停止 60 秒的测试中,748 次 S3 写入的失败次数为 0 次。

4.8 常见短小陷阱汇总

陷阱 症状 应对
OpenSearch node.roles: ${OS_ROLES}(yml 中的环境变量替换) 变成了一个单一字符串而不是列表,导致节点之间无法组成集群。仅在 DEBUG 日志中可见 通过环境变量 node.roles=cluster_manager,data,ingest(逗号分隔列表)传入
OpenSearch 3.9 见证节点堆内存 320m·上限 700m OOM 重启死循环 堆内存 384m,限制 1100m
SeaweedFS 镜像以降 uid 1000 运行 无法读取 root 600 的 S3 配置文件,导致 S3 崩溃 将文件所有者设为 1000
SeaweedFS master 限制 128m 上升到 89 MiB 192m
ClickHouse 26.8 HTTP 插入的默认值 async_insert=1, wait_for_async_insert=1 出现了 法定人数测试 async_insert=0 单独
ClickHouse Keeper Raft 端口 listen_host 即使指定了,也会在所有地址 (9234) 上打开 使用主机防火墙阻止外部接口
iptables -m multiport 单条规则最多 15 个端口(“too many ports specified”) 将规则拆分为两条
容器中 /dev/watchdog 没有 Patroni 隔离仅依赖于共识存储的 TTL。如果主服务器没有死机而是卡住并超过了 TTL,则可能会短暂出现两个主服务器 传递宿主机 watchdog 或 softdog(未实测)
docker stop 宽限 30 秒 由于容器耗尽了宽限期,导致整个节点停止花费了 30 秒 将其计入正常停止流程的时间预算中

5. 本次测量的局限性

在套用具体数据之前,请先确认相关条件。

6. 总结:采用“2 台 + 见证节点”方案时的检查清单

检查项 原因(本文的实际测量结果)
将见证节点部署在与其他数据节点不同的故障域中 如果部署在同一位置且同时宕机,则等同于只有 2 台的架构
根据对 Redis 的 min-replicas-* 的考量来选择 (1, 10) 在网络分区时丢失 22 条写入,丢失一个节点 90 秒会导致 105 秒写入暂停
为重试的写入操作添加幂等键 在 OpenSearch·ClickHouse·S3 中出现了返回失败响应但实际已保存写入的情况
在强制恢复 ClickHouse 后,在会话期间内不盲目相信法定人数 98 条虚假成功
将 OpenSearch 隔离检测时间纳入预算 默认值为 65 秒
官方 redis:8 + Sentinel 时,请清空模块文件夹 故障转移后的重启循环
提前写好手动提升流程并从隔离(fencing)开始 仅有 2 台时的唯一出路。各服务的耗时从 0.9 秒到 44.2 秒不等

TT 的寄语。 有了见证节点朋友,多数表决就能很好地运转了。但是帮忙搬东西的朋友依然只有两个,所以当其中一个休息时,有些服务会说:“等搭档来了我再放下行李。”还有,听到“失败了”而重新发送时,请务必贴上名牌(幂等键)。因为同样的行李送来两次会很麻烦的!

登录后即可点赞

评论

还没有评论。

登录后即可发表评论