把一个服务从一台机器部署到三台,意味着有了更多计算资源,也有了更多需要协调的事情:请求发给谁,数据存在哪里,某个节点失联后谁接管,扩容时怎样移动数据,以及客户端如何知道系统已经恢复。
集群就是围绕这些协作关系组织起来的一组节点。理解它,要同时看请求怎样分配、状态怎样保存,以及系统在故障和变化中怎样继续工作。
本文先建立集群的整体结构,再分别讲服务集群、数据副本、共识、分片和恢复,最后把这些机制放回常见系统。读者只需了解基本的进程、网络请求和数据库操作。文中的服务、节点与数字均为教学示例。
一、集群由什么组成,解决什么问题
节点、实例与共同任务
集群中的节点可以是物理机、虚拟机,也可以指承担某种角色的服务进程。讨论节点数量时,必须先说明统计对象。例如同一台机器运行三个数据库进程,确实有三个进程实例,但不能因此获得抵御整台主机故障的能力。
一组节点协作,通常为了提高处理能力、增加数据容量、提高服务连续性,或完成单节点无法独立承担的任务。
例如,三个 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 组件
理解这些系统,可以沿同一条请求依次定位:谁分发请求,谁持有状态,谁确认写入,谁决定接管,客户端怎样恢复。再把数据量、读写比例、允许陈旧程度、故障目标与维护成本放进来,才能选择适合的集群组织方式。