TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

为什么两台服务器无法实现 HA (高可用) 1: PostgreSQL·Redis·OpenSearch·ClickHouse·SeaweedFS 实测

状态: 实测 (2026-10-31)。 此数据是在 TT Lab 三台实验 VM(KubeVirt,Ubuntu 24.04)的 Docker 环境中测得的。每项测试进行了一次,且未执行 VM 断电操作(未实测)。所有数字均提取自原始数据(每次试验留下一行的写入记录以及恢复后的对比结果)。编写规则:实测数据作为基础,未能测得的标为「未实测」,原因未明的解释标为「推测」。

目录

1. 疑问:两台机器坏一台难道不行吗

在本地部署(On-Premise)中接到要求对数据库做双机热备时,脑海中最先浮现的架构通常是两台服务器。一台作为主节点(Primary)接收写入,另一台持有副本,等主节点挂了之后接管。服务器成本和机位成本两台都是最便宜的。

这个系列记录了通过这五种服务亲自验证 该架构无法自动运行的过程。先说结论。

2. 为什么多数派需要 3 个节点

2.1 无法区分是宕机还是网络中断

假设 db1 和 db2 互相监视。在 db1 看来,db2 的响应中断了。有两种可能性:

  1. db2 确实挂了。
  2. db2 完好无损,但两者之间的网络中断了(网络分区,network partition)。

db1 独自一人无法区分这两种情况。因为两者看起来都只是「无响应」。如果 db1 坚信是第 1 种情况并成为了主节点,而实际上是第 2 种情况,那么 db2 也会认为自己依然是主节点并接收写入。 从而导致双主(split brain) ,当网络恢复时,两侧的数据已经产生分歧。其中一侧的写入必须被丢弃。

2.2 因此「只有获得多数派的一方才能成为主节点」

避免这一问题的标准规则是 多数派(法定人数,quorum) 。只有获得 N 个投票成员中超过半数(⌊N/2⌋ + 1)同意的一方才能成为主节点。无论网络如何分割,拥有多数派的一方最多只有一个,因此不会产生两个主节点。

과반의 산수: 2대와 2대 + 증인 둘 사이의 선이 끊겼을 때(분할), 각 쪽이 셀 수 있는 표 데이터 노드 2대 db1 표 1 db2 표 1 멤버 2, 과반 = 2 어느 쪽도 1표뿐 양쪽 모두 쓰기 정지 주인 쪽도 스스로 내려온다(두 주인 방지) 2대 + 증인 db1 표 1 db2 표 1 증인 표 1 db1 + 증인 = 2/3 과반. 이쪽이 계속한다 멤버 수 과반 견디는 장애 1 1 0 2 2 0 3 2 1 4 3 1 5 3 2 2대는 1대와 똑같이 장애를 하나도 견디지 못한다
图:多数派的算术:两台与两台 + 见证者

问题在于只有两台机器时。如果成员数量 N 为 2,多数派就是 2。一旦线路中断,双方都只有 1 票, 因此哪一方都无法成为主节点,原本是主节点的一方也会 主动降级。因为如果不这样,它就会在对端发生什么完全未知的情况下接收写入。正如图片下方的表格所示,两台机器和 1 台机器一样,完全无法容忍任何故障。要想容忍一个故障,投票成员必须为 3 个。

2.3 第三个节点即使没有数据,只需参与投票即可

第三个成员不一定非得是存储数据的服务器。因为计算多数派所需的只有 选票。数据保存在两台机器上,第三个位置只放置一个仅用于投票的小进程。这被称为 见证者(Witness) 或决胜节点(Tie-breaker)。本次实验中的见证者 VM 为 2 vCPU·2 GiB,安装后实际使用的内存为 1.5 GiB。

3. 这五种服务各自如何选出主节点

虽然这五种服务都使用了多数派,但统计多数派的主体各不相同。因此,见证者上应该部署什么也随之改变。

服务 HA 方式 决定主节点的机制 见证者上部署的内容
PostgreSQL 17 Patroni 4.1 + 流复制, 同步复制(synchronous_mode: true, strict: false),pg_rewind etcd 3 成员(双向 TLS) etcd 成员之一
Redis 8.8 主 1 + 副本 1,AOF everysec Sentinel 3 个,quorum 2 Sentinel 之一
OpenSearch 3.9 数据节点 2,索引副本 1,开启安全插件 cluster_manager 投票节点 3 无数据的 cluster_manager 专用节点
ClickHouse 26.8 分片 1 × 副本 2, ReplicatedMergeTree ClickHouse Keeper 3 个(Raft) Keeper 之一
SeaweedFS 4.51 master 3(Raft),volume 2(复制 001),filer 2(共享 PostgreSQL),S3 网关 2 master Raft 3 master 之一
실습 구성: 데이터 노드 2대와 증인 1대 증인에는 데이터가 없다. 표를 던지는 투표 멤버만 둔다 db1 4 vCPU · 6 GiB db2 4 vCPU · 6 GiB 증인 2 vCPU · 2 GiB PostgreSQL PG 주, Patroni etcd PG 복제, Patroni etcd etcd (투표만) Redis Redis 주 Sentinel Redis 복제 Sentinel Sentinel (투표만) OpenSearch 데이터 + 투표 데이터 + 투표 투표 전용 데이터 없음 ClickHouse 복제본 Keeper 복제본 Keeper Keeper (투표만) SeaweedFS master, volume filer, s3 master, volume filer, s3 master (투표만) 증인 VM 은 앱 서버 자리도 겸했다: 시험 클라이언트와 HAProxy(주 서버를 찾아 보내는 앞단)
图:实验架构:2 个数据节点与 1 个见证者

选择各个服务的理由及其代价如下。

数据节点容器内存限制总计约为 4.6 GiB(OpenSearch 1.4g、ClickHouse 1.2g、PostgreSQL 768m、Redis 384m 等),安装后即时测得的实际用量为:数据节点 2.5 GiB,见证者 1.5 GiB。

4. 配置摘录

密码和密钥已省略(实际中仅保存在节点的 .env 文件且权限为 600 中)。地址已替换为节点名称或变量。所有服务均仅绑定到局域网地址和 127.0.0.1。

etcd(Patroni 的共识存储)。 三个节点使用相同的 compose 配置,仅修改节点名称。最后一行是仅有两台机器时的手动恢复位置。

services:
  etcd:
    image: gcr.io/etcd-development/etcd:v3.6.16
    network_mode: host
    command:
      - etcd
      - --name=${NODE_NAME}
      - --initial-cluster=${ETCD_INITIAL_CLUSTER}   # db1, db2, witness 세 멤버
      - --client-cert-auth                          # 클라이언트·피어 모두 상호 TLS
      - --peer-client-cert-auth
      - --heartbeat-interval=100
      - --election-timeout=1000
      - --force-new-cluster=${ETCD_FORCE_NEW:-false} # 살아남은 한 대를 단일 멤버로 다시 세울 때만 true
    mem_limit: 256m

Patroni。 Leader 密钥寿命 ttl 决定了「主服务器消失到完成提升」的上限。

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    synchronous_mode: true          # 커밋이 복제본에 닿은 뒤 확인
    synchronous_mode_strict: false  # 복제본이 죽으면 동기 요구를 내리고 계속
    failsafe_mode: false
    postgresql:
      use_pg_rewind: true
      use_slots: true
watchdog:
  mode: off                         # 컨테이너에 /dev/watchdog 이 없다

Redis 与 Sentinel。 min-replicas-* 这两行是本系列中付出最大代价的配置(第 2 篇)。

# redis.conf
appendonly yes
appendfsync everysec
min-replicas-to-write 1     # 10초 안에 응답한 복제본이 1개 이상일 때만 쓰기를 받는다
min-replicas-max-lag 10

# sentinel.conf
sentinel monitor mymaster <db1> 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000

OpenSearch。 最初三方集结共同创建投票配置。角色通过环境变量指定(参考第 2 篇中的陷阱)。

# opensearch.yml
cluster.initial_cluster_manager_nodes: ["db1", "db2", "witness"]
# docker-compose.yml 의 environment
node.roles: cluster_manager,data,ingest   # 증인은 cluster_manager 만

ClickHouse Keeper。 三个 Raft 成员之一作为见证者。30 秒会话超时与第 2 篇中的陷阱息息相关。

<coordination_settings>
    <operation_timeout_ms>10000</operation_timeout_ms>
    <session_timeout_ms>30000</session_timeout_ms>
</coordination_settings>
<raft_configuration>
    <server><id>1</id><hostname>db1</hostname><port>9234</port></server>
    <server><id>2</id><hostname>db2</hostname><port>9234</port></server>
    <server><id>3</id><hostname>witness</hostname><port>9234</port></server>
</raft_configuration>

SeaweedFS master。 见证者上仅运行 master。

  master:
    profiles: ["data", "witness"]
    command:
      - master
      - -peers=${DB1_IP}:9333,${DB2_IP}:9333,${WITNESS_IP}:9333
      - -defaultReplication=001
    mem_limit: 192m            # 128m 에서 89MiB 까지 오르는 것을 보고 늘렸다(실측)

HAProxy(应用侧)。 应用仅知道 127.0.0.1:5000。当主服务器发生变更时,HAProxy 通过间隔 1 秒的两次检查来跟进。

listen pg_primary
    bind 127.0.0.1:5000
    option httpchk GET /primary
    http-check expect status 200
    default-server inter 1s fall 2 rise 2 on-marked-down shutdown-sessions
    server db1 <db1>:5432 check port 8008
    server db2 <db2>:5432 check port 8008

5. 测试方法

6. 真的只有 2 台时

6.1 是如何配成 2 台的

在原本有见证节点(Witness)的配置中移除了见证节点侧。

6.2 如果将其中一台隔离 60 秒

当时 PostgreSQL 和 Redis 的主节点都是 db2。因此,隔离 db1 相当于“失去副本侧”,隔离 db2 相当于“失去主节点侧”。

2대뿐일 때 한 대를 잃으면: 최장 쓰기 중단 증인을 뺀 2대 구성, 한 대의 세션망을 60초 끊었다(한 번씩) 복제본 쪽(db1)을 60초 고립 주인 쪽(db2)을 60초 고립 0 30 60 90 120 초 PostgreSQL 108.4 107.8 Redis 50.3 61.1 OpenSearch 34.2 62.3 ClickHouse 60.5 60.9 SeaweedFS S3 108.9 107.9 점선 = 고립 60초. 막대가 점선보다 길면 복구 뒤에도 더 멈춰 있었다는 뜻
图表:当只有 2 台时失去其中一台:最长写入中断
服务 副本侧 (db1) 隔离 60 秒 主节点侧 (db2) 隔离 60 秒 原因
PostgreSQL 中断 108.4 秒. 存活的主服务器在 +23 秒时自动降级 中断 107.8 秒. 无提升 etcd 2 成员的过半数为 2。失去对方后无法更新 Leader 键,因此 主节点侧也会下线. 恢复后也需要等待键过期,额外花了 47 秒
Redis 50.3 秒。主节点虽然保留,但从 +11 秒开始 NOREPLICAS 61.1 秒。 MasterNotFound, 无主备切换 2 个 Sentinel 中的 1 个无法获取故障转移权限(过半数)
OpenSearch 34.2 秒后继续(db2 独自作为投票配置) 62.3 秒,停止直到恢复 投票配置仅剩 db2 一台。失去 db2 则集群不复存在
ClickHouse 60.5 秒(插入挂起,读取失败 0) 60.9 秒 Keeper 3 个位置中只能联系到 1 个
SeaweedFS S3 108.9 秒 107.9 秒 从 +18 秒起 master 无 leader,且复制 001

有三个值得关注的点。

7. 手动提升:由人工声明“对方已死”

要在只有 2 台的配置中让停止的服务重新运行,需要人工干预。即声明“对方已死,剩下的一台即为过半数”。 在继续隔离 db2 的同时,在 db1 上按顺序对各个服务执行了操作。操作步骤已提前编写为脚本。

服务 步骤 从步骤开始到恢复写入 从隔离到恢复写入
PostgreSQL ① 将 etcd 以 --force-new-cluster=true 重新建立为单成员集群(8.7秒时健康) ② Patroni 在等待旧 Leader 键过期(ttl 30)后提升 db1(同步副本) 44.2秒 60.2秒
Redis REPLICAOF NO ONE → CONFIG SET min-replicas-to-write 0 → CONFIG REWRITE. Sentinel 在 SENTINEL REMOVE 后将 db1 重新设为 quorum 1 SENTINEL MONITOR 0.9秒 59.9秒
OpenSearch 停止容器 → bin/opensearch-node unsafe-bootstrap(相同的数据卷) → 启动。集群状态为 red(primary 位于 db2 的索引) 27.0秒 87.3秒
ClickHouse 以单服务器配置启动 Keeper + --force-recovery → 2.5秒时为 standalone → 9秒时表格 is_readonly=0 11.1秒 98.1秒
SeaweedFS 将 master 以 -peers=none 单节点重新拉起(1.4秒时成为 leader) → fs.configure -locationPrefix=/buckets/ -replication=000 -apply 降低复制要求 24.2秒 121.2秒

整个流程在隔离发生 15.9 秒后从 PostgreSQL 开始,按服务逐个执行。因此顺序越靠后,“从隔离起”的耗时数值就越大。在实际故障中花费的时间约等于“察觉并做决定的时间 + 倒数第二列”。所有五个服务中,已确认写入的数据丢失量均为 0。

PostgreSQL 耗时最长的原因在于,即使重建 etcd 后,也必须等待旧 Leader 键过期。这 30 秒即为 ttl 。

7.1 手动提升时的注意事项

8. 总结

问题 答案(实测)
2 台机器能否实现自动 HA 不能。失去一台后,所有五个服务的写入都会停止
仅仅失去副本也会停止吗 PostgreSQL(etcd 2 成员)的主服务器自行下线,中断了 108.4 秒
是否丢失了数据 已确认写入的数据丢失量为 0。只会停止而不会丢失
若要靠 2 台机器坚持运行 需手动提升。各服务耗时 0.9 秒至 44.2 秒不等,需先进行隔离防护
若要实现自动切换 投票成员应设为 3 个。第三个节点只需是一个不存数据的小型见证节点即可

加入了见证节点的配置在各种故障下的表现,以及“数据节点仍然只有 2 台”这一条件留下的陷阱,将在 第 2 篇中探讨。

Titi 的小贴士。 两个人少数服从多数时,永远都是 1 对 1。所以双方都会想“我来决定真的可以吗?”然后就停下来了。只要再多一个能帮忙投个票的朋友就行了!那个朋友根本不需要背负任何数据包袱!

参考

登录后即可点赞

评论

还没有评论。

登录后即可发表评论