← 返回文章列表

Article / 文章

集群基础

把一个服务从一台机器部署到三台,意味着有了更多计算资源,也有了更多需要协调的事情:请求发给谁,数据存在哪里,某个节点失联后谁接管,扩容时怎样移动数据,以及客户端如何知道系统已经恢复。

集群就是围绕这些协作关系组织起来的一组节点。理解它,要同时看请求怎样分配、状态怎样保存,以及系统在故障和变化中怎样继续工作。

本文先建立集群的整体结构,再分别讲服务集群、数据副本、共识、分片和恢复,最后把这些机制放回常见系统。读者只需了解基本的进程、网络请求和数据库操作。文中的服务、节点与数字均为教学示例。

一、集群由什么组成,解决什么问题

节点、实例与共同任务

集群中的节点可以是物理机、虚拟机,也可以指承担某种角色的服务进程。讨论节点数量时,必须先说明统计对象。例如同一台机器运行三个数据库进程,确实有三个进程实例,但不能因此获得抵御整台主机故障的能力。

一组节点协作,通常为了提高处理能力、增加数据容量、提高服务连续性,或完成单节点无法独立承担的任务。

例如,三个 Web 实例可以分摊请求;三个数据副本可以为故障接管提供基础;三个数据分片可以分别保存不同的数据;三个计算 Worker 可以处理不同的作业。这些组织都可能被称为集群,但协作机制不同。

集群与分布式系统

“集群”强调一组节点如何作为一个整体提供能力;“分布式系统”强调计算或状态分布在多个可独立发生故障的组成部分之间,并通过通信协作。

两者有很大交集,但不宜理解成两个互斥分类。一个数据库集群本身就是分布式系统的一部分。一个电商系统包含订单、库存、支付等服务,每个服务又可以有自己的实例集群和数据集群。

“多台机器运行相同程序”也不能完整定义所有集群。一个系统可以让不同节点承担路由、协调、存储或计算角色;同一种节点也可能因当前角色不同而处理不同请求。

几种组织方式可以叠加

组织方式 节点之间主要怎样分工 直接解决什么问题
服务多实例 多个实例处理同类请求 分摊负载,为实例替换提供基础
数据复制 多个节点保存同一份逻辑数据 冗余、接管以及特定条件下的读扩展
数据分片 不同组保存不同的数据 扩大独立数据容量与并行处理能力
任务分工 Worker 领取不同任务或数据块 扩展批处理与计算吞吐
集群控制 维护成员、配置、领导权或调度状态 协调节点的加入、分工与变化

这些方式可以同时存在。例如一个搜索系统先把索引分成多个分片,每个分片再建立副本,前面由查询节点分发请求;后台任务负责生成和迁移索引。

因此,判断一个集群的能力,要把“增加了什么节点”和“节点之间如何协作”放在一起看。

二、沿一次请求看整体结构

假设一个订单系统有统一入口、三个应用实例和一个存储订单的数据集群。请求先到入口,入口选择应用实例;应用再定位数据节点,执行读写,最后把结果返回给用户。

flowchart TD
  accTitle: 一个应用系统中的集群与请求路径
  accDescr: 用户经入口访问应用实例,应用访问数据分片及其副本;成员、健康和路由信息由管理机制提供。
  U[客户端] --> L[入口与负载均衡]
  L --> A[应用实例 A]
  L --> B[应用实例 B]
  A --> R[数据路由]
  B --> R
  R --> S1[分片 1 及其副本组]
  R --> S2[分片 2 及其副本组]
  M[成员、健康与路由管理] -.-> L
  M -.-> R

图中的实线是业务请求路径,虚线表示管理信息影响请求选择。可以把直接处理请求和数据的部分称为数据平面,把成员管理、配置和调度等部分称为控制平面。具体系统如何划分,需要看它的职责设计。

一次业务调用不一定经过所有管理组件。例如应用先从注册中心取得地址,再直接访问服务实例;控制组件临时不可用时,已有地址可能仍可使用,但新实例加入和故障信息更新会受到影响。

理解整体结构后,可以分别追问:入口是否还有单点,应用状态能否在实例间共享,数据更新满足什么提交条件,路由变更怎样传播。增加某一层的实例,只能直接改变这一层的能力。

三、服务集群怎样分发请求

负载均衡选择哪个实例

负载均衡器维护可用后端,并按策略为请求选择目标。它可以位于反向代理、网关、客户端库或其他网络组件中。

常见策略包括轮询、按权重分配、按连接数量选择,以及按某个请求特征散列。以 NGINX 的 HTTP 负载均衡为例,官方文档展示了轮询、最少连接和 IP 散列等方式。NGINX 负载均衡

轮询容易理解:依次把请求发给 A、B、C。但请求代价可能不同,一次导出需要几十秒,一次查询只需要几毫秒。轮询使请求数量接近,不一定使资源占用接近。连接数也只是负载的一个近似指标,长连接与短请求需要分别考虑。

如果某个请求必须访问存有特定数据的节点,选择范围还会受到数据路由约束。负载均衡通常在合适的候选集合里选目标,不能任意把所有请求发给所有数据节点。

发现、存活与就绪

服务发现解决“实例地址有哪些”。静态配置适合变化少的环境,DNS、注册中心或平台提供的服务发现适合动态实例。

健康检查则回答不同层次的问题:进程是否还活着,是否完成初始化,当前能否承担请求。一个刚启动的进程可能已经监听端口,却仍在加载索引或预热连接池;此时把流量送进去,会产生启动期错误。

Kubernetes 将相关检查区分为存活、就绪与启动探针:存活失败可触发重启,就绪状态影响是否接收常规服务流量,启动探针用于保护较慢的启动过程。它们应根据实际故障语义配置。Kubernetes 探针

例如,数据库短暂抖动时,把所有应用实例判定为“需要重启”,可能引发同时重连和更大压力。健康检查应区分暂时无法完成某类请求与进程自身无法恢复。

无状态实例与外部状态

无状态服务通常指:完成请求所需的持久业务状态,不只保存在某个应用实例的私有内存中。它仍可能有缓存、连接池和临时数据。

假设登录会话仅保存在 A 的内存里,下一次请求被分给 B,B 就无法识别会话。可以把会话放到共享存储,也可以使用经过正确签名和校验的客户端令牌;具体方案取决于撤销、过期和权限变更需求。

粘性会话把同一客户端尽量分到原实例,可以减少跨实例状态问题,但实例故障后,原来的私有状态仍可能消失。它改变了路由方式,没有复制那份状态。

文件上传也有类似问题:文件写进 A 的本地磁盘,B 未必能够读取。多实例化时要明确哪些文件是临时产物,哪些需要进入共享或对象存储。

扩容后为什么不一定等比例变快

如果所有应用请求都要访问同一个数据库,应用增加到十个实例后,数据库可能成为瓶颈。实例增多还可能增加总连接数,让数据库更早到达限制。

假设只有 CPU 处理能并行扩展,数据库部分固定占用请求时间,应用节点增加只能缩短可并行的部分。还要观察排队、锁竞争、网络和缓存命中率。

容错也需要容量余量。三个实例刚好承担全部峰值负载,失去一个后,剩下两个可能一起过载。设计时要计算故障后的可用容量,并配置限流、降级或背压,让系统在超载时保留核心处理能力。

四、做一个本地请求分发实验

下面用 Node.js 24 的内置模块,启动两个独立后端进程和一个入口进程,观察轮询、节点停止与本地状态。保存为 cluster-demo.mjs:

import { createServer } from 'node:http';

const [role, label = 'node', portText = '18100'] = process.argv.slice(2);
const port = Number(portText);

if (role === 'worker') {
  let count = 0;
  createServer((request, response) => {
    count += 1;
    response.writeHead(200, { 'content-type': 'application/json' });
    response.end(JSON.stringify({ node: label, count }));
  }).listen(port, '127.0.0.1');
} else if (role === 'proxy') {
  const targets = [18101, 18102];
  let next = 0;
  createServer(async (request, response) => {
    if (request.method !== 'GET' || request.url !== '/') {
      response.writeHead(404);
      response.end();
      return;
    }
    const start = next++ % targets.length;
    for (let i = 0; i < targets.length; i += 1) {
      const target = targets[(start + i) % targets.length];
      try {
        const upstream = await fetch(`http://127.0.0.1:${target}/`, {
          signal: AbortSignal.timeout(500),
        });
        if (!upstream.ok) {
          await upstream.body?.cancel();
          continue;
        }
        const body = await upstream.text();
        response.writeHead(200, { 'content-type': 'application/json' });
        response.end(body);
        return;
      } catch {
        // 本例仅对无业务副作用的 GET 尝试下一个节点。
      }
    }
    response.writeHead(503, { 'content-type': 'application/json' });
    response.end(JSON.stringify({ error: 'no healthy backend' }));
  }).listen(port, '127.0.0.1');
} else {
  throw new Error('用法:worker <名称> <端口>,或 proxy entry 18100');
}

在三个终端分别启动,确保这些端口空闲:

node cluster-demo.mjs worker A 18101
node cluster-demo.mjs worker B 18102
node cluster-demo.mjs proxy entry 18100

等三个进程启动后,在另一个终端连续执行四次:

curl -s http://127.0.0.1:18100/

没有其他请求时,预期依次得到 A 的计数 1、B 的计数 1、A 的计数 2、B 的计数 2。这说明入口分配了请求,各进程的计数独立存在。

在 A 的终端按 Ctrl+C,再访问入口,请求仍可由 B 完成。入口每次都会尝试后端,遇到连接错误或超时再尝试另一个节点;它没有维护长期健康状态。两个后端都停止时,入口返回 503。

重新启动 A 后,它的计数从零开始。这个现象说明多进程处理请求与状态复制是两项工作。入口自身仍只有一个进程,全部进程又在同一台机器上,因此这个实验用于观察路由和进程局部状态,不具备完整的高可用部署条件。

实验完成后,在各终端停止进程。实际代理还需要连接与请求限制、故障节点摘除、就绪管理等机制;写请求能否换节点重试,必须结合业务副作用判断。

五、保存数据的集群:复制与持久化

同一份数据放在多个节点上

A、B、C 都保存配置 requestTimeoutMs = 1000,这叫复制。主节点接收更新,其他节点通过日志或其他复制机制取得变化。

副本提供冗余基础,也可以在允许的读取模式下分担读请求。三个完整副本并不会让独立数据容量变成三倍;每个节点仍要保存同样的数据。

复制与持久化也有区别。复制关注有多少节点保存数据,持久化关注进程或机器重启后,数据能否从稳定存储恢复。写入内存、写入操作系统缓存和完成持久存储所需步骤,提供的故障保证可能不同。

异步与同步确认

假设 A 把配置改成 2000,立即向客户端返回成功,再异步复制给 B、C:

时刻 A B、C 客户端
修改前 1000 1000 —
A 本地更新 2000 1000 收到成功
复制前 A 故障 不可用 1000 等待恢复
B 接管 不可用 1000 又读到旧值

这是一种已确认写入在故障切换中丢失的过程。异步确认减少等待复制的延迟,但成功响应后的保护程度较弱。

若要求副本确认后才返回,就需要说明等待哪些副本、确认到内存还是持久存储,以及接管时如何选择节点。等待全部副本会让最慢或失联的节点阻塞写入;等待一部分副本则需要相应协议,保证未来接管保留已确认结果。

Redis 默认采用异步复制。WAIT 可以等待一定数量的副本确认复制进度,但官方明确指出它不会把 Redis 变成强一致存储,故障切换仍有相关丢失风险。Redis 复制与确认语义

复制拓扑影响协调方式

单主复制让一个主节点确定写入顺序,其他节点跟随;多主允许多个节点接收写入,需要协调或合并冲突;无主式设计可以让客户端或协调节点联系多个副本,以读写确认集合和版本处理分歧。

不同拓扑影响写入口、冲突、延迟和故障行为。不能只看到“有三个副本”,就推导出相同的一致性与容错能力。

六、共识如何保护已提交结果

从数据副本到有顺序的日志

共识协议让参与者在一定故障条件下,就一系列决定形成共同结果。Raft 把这件事组织为复制日志:Leader 接收命令,复制给 Follower,确认提交后,各节点按相同顺序应用到状态机。

例如,日志位置 12 记录:

index = 12
term = 8
command = set requestTimeoutMs 2000

index 表示顺序,term 是选举任期编号,命令描述具体修改。保存日志、提交日志和应用命令分别表示记录已经存在、协议已确定它必须保留,以及它已执行到本地状态。

对任期 8 的当前任期日志,正常过程可以是:

sequenceDiagram
  accTitle: Raft 复制组确认一次修改
  accDescr: A 与 B 按协议持久化当前任期日志形成多数,A 提交并应用后响应,C 可以随后追赶。
  participant U as 客户端
  participant A as A:Leader
  participant B as B:Follower
  participant C as C:Follower
  U->>A: 设置超时为 2000
  A->>A: 持久化日志 12,任期 8
  A->>B: 复制日志
  A->>C: 复制日志
  B->>B: 按协议持久化
  B-->>A: 确认
  A->>A: 提交并应用日志
  A-->>U: 成功
  A->>B: 告知提交位置
  C-->>A: 稍后追赶

这里 A、B 构成三投票节点组的多数。C 暂时落后不妨碍本次提交。Raft 还有日志匹配、持久化与当前任期提交限制,旧任期日志不能简单按副本计数直接提交。Raft 论文,第 5 节

心跳、任期与选举

Raft 节点通常处于 Leader、Follower 或 Candidate 三种角色。Leader 定期发送心跳或日志,让 Follower 知道当前领导者仍可联系。

Follower 在选举超时内没有收到所需联系后,可以进入 Candidate,增加任期、给自己投票并请求其他成员投票。同一任期内,一个成员至多投给一个候选者;候选者还必须满足日志条件,取得多数票才能成为 Leader。

多个候选者同时参选可能分散票数。随机化选举超时使后续轮次更有机会由某个候选者先取得多数;收到更新任期时,节点按协议更新任期并退回 Follower。任期识别领导权的新旧,超时提供活性所需的故障怀疑,持久化与投票限制维护安全性。

选主为什么必须检查历史

假设日志 12 已提交,A 随后停止,B 有这条日志,C 还没有。在这个简单场景中,C 的日志较旧,得不到 B 的票;B 则可以在满足选举条件后成为新 Leader,再让 C 追赶。

多数集合存在交集:先前的 A、B 与后来的 B、C 共享 B。交集提供携带历史信息的节点,协议的日志新旧比较与投票规则再限制谁能接管。只取“多数派有交集”这一条,还不足以构成安全选举。

合法 Leader 保留已提交历史,未提交的分歧日志则可以在恢复通信后修复。已经向客户端确认的结果与尚未完成的尝试,需要区别对待。

多数、容错与节点数量

固定投票成员组的多数票数为:

Q = floor(N / 2) + 1

在剩余节点能够通信并满足其他协议条件时:

投票成员数 N 所需多数 Q 最多失去多少成员后仍保留多数
1 1 0
2 2 0
3 2 1
4 3 1
5 3 2

从三增到四,没有多容忍一个成员故障。这里统计同一个投票组,不能把其他分片、只读副本和监控进程一起加入公式。具体产品还可能有非投票成员,用于追赶数据或承担特定工作。etcd 成员数量与容错

多数派协议主要处理节点停止、重启、延迟与网络分区等故障,不能直接套用到节点任意伪造消息的拜占庭故障模型。多个持久副本永久丢失时,也无法靠选举重新创造原始数据。

共识的范围通常是一个复制组

一个分片内部通过 Raft 确定日志顺序,不意味着所有分片共享同一个全局顺序。两个账户分属两个组时,转账仍可能需要跨组事务或其他业务协调。

同样,数据库已提交与外部邮件已发送也是两类状态。共识能保护复制组内的决定,如何把它与组外副作用连接起来,需要事务消息、幂等处理或其他明确机制。

七、读取一致性、持久性与事务

已提交不代表每个副本都已应用

A 已提交并应用配置 2000,C 仍停留在 1000。此时读 C 的本地状态,可能取得旧值;这与已提交写入永久丢失是不同的问题。

线性一致性要求操作能解释为在各自调用与响应之间某一时刻生效,并形成符合真实先后关系的统一顺序。在没有后续写入时,成功写入 2000 之后才发起的读取,应看到 2000。

如果读取与写入并发,则按合法顺序解释,不能简单定义成“读取响应发出那一刻的最新值”。

不同读取方式提供不同承诺

安全的线性一致读取,可能需要确认领导权、确定提交位置并等待本地应用到位。直接读一个仍自称 Leader 的旧节点内存,可能在网络隔离后返回陈旧状态。

客户端也可能只要求读到自己已完成的写入。系统可以通过版本位置、会话约束或合适的路由提供相应保证,但“读自己的写”仍弱于所有客户端共享的线性一致顺序。

最终一致性则关注更新停止并满足传播条件后,副本能否收敛。它需要实际传播和冲突处理机制,也没有天然给出固定的收敛时限。

etcd 3.6 的普通键值操作默认提供线性一致性,显式的 serializable 读取可能取得旧数据;Watch 又有独立保证,不能把普通读取语义直接套给 Watch。etcd API 保证

三个不同维度

持久性关注已确认数据在相应故障后能否保留;一致性模型关注不同操作观察到怎样的顺序与值;事务隔离关注多步事务并发时允许哪些相互影响。

例如,从一个持久副本读取旧值,数据未必丢失;一次线性一致的单 key 读取,也不会自动让随后的两个独立写入成为原子事务。

设计业务时,要明确保护的对象是一条读写、一组字段更新,还是跨服务工作流,再选择相应机制。

八、网络分区、CAP 与多写冲突

超时说明无法确认对方

假设 A 与 B、C 之间的网络断开,但三个进程都正常运行。心跳超时能触发怀疑与接管流程,却不能证明 A 已经停止。

如果每侧都自行宣布唯一领导权并接受互相冲突的写入,就可能形成脑裂。多数派协议限制只有具备相应多数的一侧才能完成提交;旧主即使暂时不知道新任期,也无法独自提交新修改。

B、C 选出新主以后,只有能访问这一侧的客户端才可能恢复操作。仍连接 A 的客户端还需要发现新地址与重连。

CAP 的冲突发生在哪里

假设允许隔离的 A 把配置改成 2000 并返回成功。随后一个请求到达 B,而 B 仍只有 1000,也联系不上 A。

如果 B 立即返回旧值,就可能违背成功写入后的线性一致读取要求;如果 B 等待确认,操作就可能无法在分区期间完成。实际系统会设置超时,但返回错误没有完成用户要求的数据操作。

CAP 表述的是:在允许网络分区的模型中,无法同时保证线性一致性和每个到达未故障节点的操作都能完成。这里的可用性不是机器在线率,也不直接等同于业务定义的响应时限。Gilbert 与 Lynch:CAP 论文

CP 取向会让不能安全完成的操作等待或失败;AP 取向允许分区各侧继续处理,并接受需要恢复后解决的分歧。具体保证可以按接口和数据类型不同,不能给整个产品贴标签后就跳过读写路径。

两边都写成功,恢复后怎样合并

A 把超时改成 2000,B 把它改成 3000,两侧都返回成功。恢复通信后,需要一种确定规则处理两个版本。

例如一个教学规则使用逻辑序号与节点编号,先比较序号,序号相同再比较编号:

A 的记录:(8, A, 2000)
B 的记录:(8, B, 3000)
约定同序号时 B 较大,最终选择 B 的记录。

这个规则可以让副本在取得相同记录后收敛,但 A 的赋值意图被覆盖。它不是按真实时间判断谁最后写入,也不能直接推广成完整多主协议。

计数增加可以设计成合并独立增量,购物车可以保存并合并并发操作;具体规则取决于删除、重复和并发的业务含义。Dynamo 的论文展示了版本信息、读写副本选择与冲突处理如何共同参与可用性设计。Dynamo 原始论文

对于唯一座位出售,两侧各自向不同用户承诺成功,恢复后选一个值并不能兑现两个承诺。这类约束通常需要在接受操作前协调。

锁与租约还需要下游配合

假设 Worker A 持有租约后长时间暂停,租约过期,B 获得新租约开始写入。A 恢复时,仍可能尝试执行旧工作。

一种保护方式是使用单调递增的 fencing token:下游记录已接受的最高 token,拒绝较旧 token 的写入。这样新的持有者进入以后,恢复的旧持有者不能继续覆盖其结果。

这要求 token 分配具备所需保证,并且真正保存资源的系统执行检查。只在客户端检查锁是否过期,无法覆盖检查后再次暂停的窗口。租约与 fencing 的具体过程

九、分片如何扩展容量与吞吐

复制和分片是两个方向

复制让同一份数据有多个副本;分片让不同组承担不同数据。例如用户 1 至 10000 属于分片 1,10001 至 20000 属于分片 2,每个分片又有自己的副本组。

flowchart TD
  accTitle: 分片与复制的组合
  accDescr: 请求先按数据标识定位分片,每个分片内部再通过副本组提供冗余。
  U[用户 15000 的请求] --> R[按标识定位分片]
  R --> S1[分片 1]
  R --> S2[分片 2]
  S1 --> A[副本组 A1、A2、A3]
  S2 --> B[副本组 B1、B2、B3]

图中六个数据节点并不是一个六投票节点组。每个组怎样确认和接管,需要分别分析。

范围、哈希与路由表

范围分片按连续区间划分,便于范围查询,但按时间递增写入时,最新区间可能成为热点。哈希分片把 key 映射到空间中,通常更容易分散 key,但范围查询可能要访问多个分片。

简单的 hash(key) % N 在 N 改变时可能让大量 key 改变位置。固定槽、虚拟节点或显式分区映射可以把逻辑分区与实际节点分开,扩容时移动一部分分区,并更新映射。

路由可以由客户端缓存、代理维护,也可以由接收节点转发或重定向。路由过期时,需要有版本或重定向机制,把请求带到正确位置。

迁移期间仍然有请求

把分片 S 从 A 移到 B,通常包含复制已有数据、追赶迁移期间新增变化、确认接管条件、切换路由和清理旧副本等步骤。实际产品顺序与控制方式不同。

如果只复制某一时刻的快照就直接切流,复制期间发生的写入可能遗漏。如果新旧节点同时把自己当作唯一负责人,又可能产生冲突。因此,迁移协议需要定义写入归属与切换时刻。

迁移本身也消耗磁盘、网络和 CPU。扩容期间新节点还未接管负载,旧节点却要额外传输数据,系统可能暂时更忙。

热点与跨分片操作

数据量均匀不意味着请求量均匀。一个热门商品 key 的写入仍可能集中在一个组;增加其他分片未必解决这个热点。

可以根据语义采用缓存、读副本、预聚合或拆分写入,但同一个必须串行判断的状态仍会限制并行度。

跨分片查询通常需要分发与合并,跨分片原子修改还需要额外事务协调。分片提高容量的同时,也提高了路由、迁移和跨组操作成本。

十、从节点恢复到客户端恢复

超时后的结果可能不确定

客户端请求写入,服务端已经提交,却在返回响应前断开连接。客户端收到超时,无法仅凭这个现象判断写入是否发生。

对于查询,可以依据策略重试;对于“计数加一”或“创建任务”,重复执行可能产生额外效果。服务端可以使用请求标识与业务结果进行去重,但去重记录需要与业务修改保持相应原子性。

设置同一个值也要考虑并发。一次设置 2000 的迟到重试,可能覆盖另一客户端刚更新的 3000。携带预期版本的条件更新,可以拒绝覆盖后续修改。

恢复时间包含多个阶段

一次故障后的恢复可能经历故障发现、选主、日志追赶、路由更新、连接重建和客户端重试。

这些阶段有时重叠,有时互相等待。节点选主完成,不代表所有客户端已刷新地址;进程启动,也不代表数据已追赶到可以安全服务的位置。

重试还需要预算、退避与抖动。所有客户端同时立即重试,可能把刚恢复的节点再次压垮。已有超时的请求也应停止无意义的后续工作,防止后台堆积。

RPO 与 RTO 描述不同目标

RPO 关注可接受的数据恢复点落后程度;RTO 关注恢复业务服务所需的时间目标。它们需要绑定明确故障场景。

例如“单节点故障不丢已提交写入”与“整个存储区域损坏后从每日备份恢复”,对应不同数据保护路径。一次选主演练无法证明区域灾难下仍满足同样结果。

评估时应记录具体故障、已确认操作、恢复后数据,以及客户端实际恢复时间,而不能只记录剩余节点的进程是否在线。

十一、成员变化、部署与日常运维

加节点与换节点

无状态应用加入新实例,通常需要启动、初始化、通过就绪检查,再加入流量。退出时先停止接收新请求,等待或终止在途工作,再结束进程。

数据节点加入还要追赶数据。多数派组改变投票成员时,必须使用产品支持的成员变更流程,不能分别编辑各节点配置,让不同节点自行认为存在不同的投票集合。

etcd 的 learner 设计让新成员先作为非投票成员追赶,再在条件满足时提升,减少数据尚未追上就改变多数条件的风险。etcd learner

滚动升级同样要确认版本兼容、可同时停止的成员数量,以及新旧代码的数据格式。仅保证一台一台重启,还不足以保证业务兼容。

故障域与剩余容量

节点部署应考虑共同故障来源:主机、电源、机架、存储、网络与可用区。三个投票进程放在同一台物理机上,主机停止就会同时失去三票。

跨可用区能减少某些共同故障,但远端确认可能增加写入延迟。放置策略需要结合故障域、网络时延和剩余容量,而不是只追求地理距离。

副本不能代替历史备份

复制通常让副本持续跟随当前状态。误删除也可能迅速传播到全部副本。备份保存历史恢复点,恢复演练检查它是否真的可用。

恢复一个旧快照后,还要处理后续日志、成员信息、客户端缓存和与外部系统的状态差异。数据恢复与服务重新接流是相互关联的步骤。

日常观测至少应能关联请求成功率与延迟、复制落后、领导权变化、队列与资源占用。只看平均 CPU,可能漏掉慢磁盘导致的提交延迟,或某个热点分片持续排队。

十二、放回具体系统看这些机制

注册中心与配置中心

注册中心维护服务到实例地址的映射,消费者取得地址后通常直接调用实例。旧列表仍可提供部分调用能力,但失效地址需要超时与健康处理;新实例也需要持续刷新才能被发现。

消费者使用缓存,只说明业务侧能够暂时使用旧信息,不能直接证明注册中心的每个读写接口都具备 AP 保证。

配置中心还多一层传播过程:存储确认新配置,应用收到通知或主动拉取,应用使用新版本处理请求。存储内部线性一致,也不意味着所有应用在同一瞬间完成配置切换。

若允许短暂不同,各应用可以保留旧值并重试刷新;若需要协调切换,就要有版本、就绪确认和切换条件。这是存储保证之外的发布设计。

Redis 复制、Sentinel 与 Cluster

Redis 主从复制保存数据副本。Sentinel 负责监控、故障判断与协调副本提升,并为支持它的客户端提供新主发现;它不保存业务 key,也不改变数据复制的异步性质。

Sentinel 的 quorum 用于形成客观下线判断,故障切换还需多数 Sentinel 授权。例如五个 Sentinel、quorum 为二,两票可以形成下线判断,实际切换仍需至少三票授权。这些票也不是数据副本的持久化确认。Redis Sentinel

Redis Cluster 增加分片,把 key 映射到 16384 个槽,由不同主节点负责,再按部署配备副本。客户端根据槽路由,处理 MOVED 等重定向;迁移中的临时路由还有相应机制。Redis Cluster 规范

Cluster 仍采用异步复制。网络分区后,少数派一侧无法联系多数主节点达到相应超时条件时,会停止接受写入;多数派侧恢复失效分片,也需要合适副本等条件。分析时应分别看槽覆盖、接管条件和写入丢失窗口。

etcd 与 Kubernetes

etcd 适合保存需要明确协调语义的较小规模关键状态,如成员、配置与领导权相关信息。它的复制组保护这些状态,并不代替应用所有业务数据的存储设计。etcd 的定位

Kubernetes 则管理工作负载:控制平面维护期望状态,调度与节点组件让容器在合适位置运行。它可以重新创建失败的应用实例,但应用内部的数据复制、事务和幂等仍需要相应组件实现。Kubernetes 组件

理解这些系统,可以沿同一条请求依次定位:谁分发请求,谁持有状态,谁确认写入,谁决定接管,客户端怎样恢复。再把数据量、读写比例、允许陈旧程度、故障目标与维护成本放进来,才能选择适合的集群组织方式。

查看全部文章 →