设想你是连锁餐饮集团的老板,要在外地连开几家店,最怕的就是后厨“掉链子”——煤气爆了、主厨请假、水管裂了,一停业顾客全跑光。高可用架构和安排后厨团队是同一个道理:主备架构是一家店配一位主厨加一位待命副厨,主厨倒下副厨顶上去,中间要挂“暂停营业”牌子;主主架构是把大厨房改成两个灶台同时炒菜,两位主厨都能出菜,但共用烤箱就可能“抢”;多活架构则是干脆多开几家分店,各自接客、互相支援,一家停电其他分店照常营业。三种架构在不同场景下各有取舍,本篇文章带你从“开餐厅”的视角,把高可用架构的指标、复制拓扑、冲突处理、切换方案和选型决策一次讲透。

岗位关联

高可用架构是架构师绕不开的必修课,掌握主备/主主/多活的取舍逻辑,能帮你在系统设计、故障抢修与灾备选型中做出正确决策,是通往高级技术专家的关键能力。

目录
  1. 可视化演示
  2. 高可用核心指标:先把评星标准定下来
  3. 主备架构:一位主厨,一位待命副厨
  4. 主主架构:两个灶台同时炒菜
  5. 多活架构:直接开连锁分店
  6. 架构选型决策:看客单价选模式
  7. 工程实践复盘:消防演习不能停
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

对比主备、主主与多活三种高可用架构的复制拓扑、故障切换与RPO/RTO取舍,理解不同场景下的选型思路,可切换三种模式。

本节配有交互动效(进入视口后自动加载,约 3KB)。

主备架构:单主复制 主节点承载写入,备节点异步复制待命 主故障时心跳检测并切换,备提升为新主 RTO≈秒级 简单可靠,但备节点利用率低。

高可用核心指标:先把评星标准定下来

做高可用之前,先要回答一个基础问题:“高到什么程度才算够用?”这就像餐厅想评米其林星级,评审员看两条硬指标:一是遇到火灾后要停业多久才能重新开张,二是重新开张后,已经点过但还没上桌的菜单还能不能保住。落在技术上,第一条叫 RTO(恢复时间目标),衡量从故障发生到服务恢复所花的时间;第二条叫 RPO(恢复点目标),衡量故障发生后最多允许丢失多少已写入的数据。

这两个指标像一把尺子,量出了后厨团队的“救场能力”。RTO=0 意味着永不中断,RPO=0 意味着一个字节都不丢。真实世界里它们都做不到绝对零,所以要允许“可容忍的损失”。数据丢失量可以用一个简单公式估算:

可用性等级则是另一个通行标尺,它描述系统在一年中正常服务的时间占比:

这几个数字让“评星标准”变得可量化:4 个 9 是大型互联网公司的及格线,5 个 9 就需要多活架构和自动切换支撑了。

丢失数据量=λ写入×t窗口\displaystyle \text{丢失数据量} = \lambda_{\text{写入}} \times t_{\text{窗口}}
99.9% 可用意味着每年约 8.76 小时停机,99.99% 是 52.56 分钟,99.999% 只有 5.26 分钟。
  1. 写入速率 λ单位时间内系统接收的写入请求量,比如每秒 1000 条订单。
    λ写入=写入总数时间\lambda_{\text{写入}} = \frac{\text{写入总数}}{\text{时间}}
  2. 时间窗口 t从最近一次数据备份/复制完成到故障发生之间的间隔。
    t窗口=t故障−t上次备份t_{\text{窗口}} = t_{\text{故障}} - t_{\text{上次备份}}
  3. 乘积作用两者相乘得到故障窗口内累计写入的数据总量。
    可用性=正常运行时间正常运行时间+故障时间×100%\displaystyle \text{可用性} = \frac{\text{正常运行时间}}{\text{正常运行时间} + \text{故障时间}} \times 100\%

主备架构:一位主厨,一位待命副厨

主备架构像一家餐厅配了两位厨师:主厨掌勺出菜,副厨不碰炒锅,只在一旁备菜、洗料、熟悉菜谱,保证随时能接班。主厨手一滑切了手指,副厨需要几分钟穿上围裙上场——这“几分钟”就是 RTO,而副厨之前备好的那批菜料就是 RPO,从副厨上一次备料到主厨倒下的这段时间里没来得及准备的菜,就损失掉了。

工程上,主节点接收全部读写流量,备节点通过单向复制持续同步主节点的数据。两者之间靠心跳检测维持联系:主节点每隔几秒向备节点发一个“我还活着”的信号,备节点连续几次收不到,就判定主节点故障。此时进入 DNS 切换流程——把域名解析从主节点 IP 换成备节点 IP,引导新流量到备机接管。整个切换时间可拆解为三段:

主备架构的最大优点是简单可靠、设计直观,最大缺点是备机平时不承接流量,服务器资源白白闲置;同时主备切换存在秒级到分钟级的停机窗口。它适合业务对成本敏感、能容忍短暂中断的场景,比如内部管理系统、中小网站的后端数据库。

t切换=t心跳超时+t健康检查+tDNS生效\displaystyle t_{\text{切换}} = t_{\text{心跳超时}} + t_{\text{健康检查}} + t_{\text{DNS生效}}
心跳超时是等待确认故障的时间,健康检查是二次确认,DNS 生效是让新流量找到新主厨。
  1. 心跳超时监测方连续 N 次未收到心跳信号,被动等待的时间。
    t心跳超时=N×心跳间隔t_{\text{心跳超时}} = N \times \text{心跳间隔}
  2. 健康检查对疑似故障节点做主动探测,确认是否真的宕机,避免误判。
    t健康检查=探测次数×探测间隔t_{\text{健康检查}} = \text{探测次数} \times \text{探测间隔}
  3. DNS 生效修改域名解析记录后,等待客户端缓存过期并解析到新地址。
    tDNS生效≈TTL 过期时间t_{\text{DNS生效}} \approx \text{TTL 过期时间}

主主架构:两个灶台同时炒菜

要不要试试让两位主厨同时掌勺?这就是主主架构的设想:两家灶台都通电,都能独立出菜,后厨效率理论上翻倍。但问题来了——当两位主厨同时要做“红烧肉”,一个放糖一个放盐时,到底听谁的?在技术世界这叫写冲突:两个节点同时对同一条数据做修改,双向复制时互相覆盖,最终数据变成“谁同步晚谁赢”的混乱状态。

为了解决冲突,常见策略有时间戳后写覆盖(最后写入者胜)、CRDT 结构化合并(把修改合并成无冲突的数据结构),以及更工程化的用户分片——按用户 ID 哈希取模,让同一个用户的写请求永远落到同一个节点。分片让“两个主厨各管一摊菜”,从根源上避免抢烤箱:

主主架构适合写流量可以被明确拆分的场景,比如按地域、租户或用户维度分片,能有效利用两个节点的资源。但如果业务存在大量无法分片的全局数据,写冲突会变成频繁事故,让系统复杂度飙升。

目标节点=hash(用户ID) mod N\displaystyle \text{目标节点} = \text{hash}(\text{用户ID}) \bmod N
N 是主节点数量,hash 把用户均匀散开,取模后每个用户只由一个节点负责写入。
  1. 哈希散列把用户 ID 映射成一个整数,同一用户每次结果相同。
    h=hash(用户ID)h = \text{hash}(\text{用户ID})
  2. 取模分配哈希值对节点数 N 取余,得到 0 到 N-1 的节点序号。
    r=h mod Nr = h \bmod N
  3. 路由写入所有对该用户的读写都路由到第 r 号节点,避免双写冲突。
    该用户的请求→Noder\text{该用户的请求} \rightarrow \text{Node}_r

多活架构:直接开连锁分店

多活架构是把后厨从一家店变成多家连锁分店:每家店都能独立接客、独立做菜,顾客就近进店,不用等远程调度。分店之间通过“总店菜谱”(数据异步复制)保持最终一致,某一分店停电了,顾客走两步去隔壁分店即可,服务不中断。

多活架构的核心理念是多副本同时对外服务,配合全局流量调度(如 GSLB)把用户按地域或负载分发到不同节点。由于副本间是异步复制,各节点的数据存在短暂不一致:

当节点数量多、业务单元化时,还会引入单元化部署设计:把用户按 ID 划分成若干个“单元”,每个单元的数据和流量在自身闭环,跨单元交互降到最低。多活架构提供最高的可用性,但代价是最终一致性、复杂的流量调度和较高的网络成本。

Δt可见=t本地写入+t异步复制\displaystyle \Delta t_{\text{可见}} = t_{\text{本地写入}} + t_{\text{异步复制}}
一行数据写入 A 节点后,要经过本地落盘和异步复制传播,B 节点才能读到它。
  1. 本地写入请求在所在节点完成磁盘落盘的时间。
    t本地写入=写入确认时间t_{\text{本地写入}} = \text{写入确认时间}
  2. 异步复制数据从源节点传输到目标节点并应用的时间。
    t异步复制=传输延迟+目标节点回放时间t_{\text{异步复制}} = \text{传输延迟} + \text{目标节点回放时间}
  3. 两者相加写入在另一个节点可见的总延迟,即不一致窗口。
    Δt可见=用户感知到的同步延迟\Delta t_{\text{可见}} = \text{用户感知到的同步延迟}

架构选型决策:看客单价选模式

选主备、主主还是多活,本质上和餐厅选址一样:开在旅游景区的网红店,停业一小时就损失上万,必须上多活;社区楼下的早餐铺,停业半天顾客也能改天再来,主备足够。选型的核心是两个轴:业务对 RTO/RPO 的容忍度,以及愿意为高可用支付的成本。

高可用没有免费午餐。主备简单但浪费一台备机,主主提升效率但引入冲突处理,多活可用性最高但要求数据最终一致。复制流量是决定成本的关键因素之一:

普遍的选型规律是:RPO=0 且 RTO 秒级 → 同城双活 + 同步复制;RPO 分钟级且 RTO 分钟级 → 主备 + 异步复制;跨地域用户就近访问 → 多活 + 最终一致。没有最好的架构,只有最匹配业务目标的组合。

复制流量=QPS×(N−1)×日志大小\displaystyle \text{复制流量} = \text{QPS} \times (N-1) \times \text{日志大小}
每秒写入量越大、副本数越多、单条数据越大,节点间的网络复制压力越大,成本越高。
  1. QPS 写入量每秒产生的写请求数,是复制流量的基数。
    QPS=每日写入总量86400\text{QPS} = \frac{\text{每日写入总量}}{86400}
  2. 副本广播系数一个写入需要复制到其余 N-1 个节点。
    系数=N−1\text{系数} = N - 1
  3. 日志大小每条写入产生的 binlog/redo log 数据量。
    复制流量=QPS×(N−1)×日志大小\text{复制流量} = \text{QPS} \times (N-1) \times \text{日志大小}

工程实践复盘:消防演习不能停

再完美的架构设计,如果没有持续演练,故障来临时仍会手忙脚乱——就像餐厅消防通道画得再漂亮,不实际跑几次火灾演习,真着火时员工照样找不到灭火器。高可用的工程实践因此强调“用演练验证设计”。

混沌工程是主动注入故障(杀进程、断网、断电)来验证系统韧性;监控告警围绕可用性、延迟、同步延迟等指标实时预警;切换自动化把人工操作脚本化,避免故障时“手抖点错按钮”。多副本冗余的价值可以用并联可用性公式量化:

典型的案例复盘是某大型电商在双 11 大促前,对核心订单单元执行了随机的机房断电演练:第一次演练发现 DNS 缓存 TTL 设置过长导致切换花了 10 分钟,紧急调短 TTL 并预推 DNS 记录后,第二次演练把切换时间压缩到 40 秒。这正是“演练 → 发现问题 → 改进 → 再演练”循环的价值——高可用不是装修出来的,是反复“逼”出来的。

可用性集群=1−(1−A1)(1−A2)\displaystyle \text{可用性}_{\text{集群}} = 1 - (1 - A_1)(1 - A_2)
两个独立节点同时故障系统才不可用,所以单节点可用性 99% 的两个节点组合可用性可达 99.99%。
  1. 单点故障率节点 1 的故障概率是 1 减去它的可用性。
    p1=1−A1p_1 = 1 - A_1
  2. 同时故障率两个节点同时发生故障的概率是两者乘积。
    p双故障=p1×p2=(1−A1)(1−A2)p_{\text{双故障}} = p_1 \times p_2 = (1 - A_1)(1 - A_2)
  3. 补集即可用性只要不同时故障,集群就可用,用 1 减去双故障概率。
    可用性集群=1−(1−A1)(1−A2)\text{可用性}_{\text{集群}} = 1 - (1 - A_1)(1 - A_2)

AI 应用场景

AI 大模型推理服务的全球多活

阶段:模型推理服务部署与容灾

解决的问题:单一 GPU 集群一旦机房断电,所有 AI 应用直接不可用;多活部署让用户就近接入不同地域的推理集群,单点故障自动切流。

具体案例:某 AI 公司在全国部署三个推理节点,用 GSLB 按用户来源分发请求,模型参数定期异步同步。一个地域网络故障时,调度系统在 30 秒内把流量全部切到另外两个节点,用户无感知。

金融支付系统的同城主主双活

阶段:在线交易系统的数据层高可用

解决的问题:支付场景要求 RPO=0(一分钱都不能丢)且 RTO 秒级,用同城双机房的主主同步复制,保证切换时不丢账务数据。

具体案例:某支付平台两个机房间建设专线,数据库双向同步。一次机房空调故障导致服务器批量宕机,另一个机房在 1 分钟内接管全部交易,账务零丢失,事后对账完全一致。

电商大促的单元化多活

阶段:智能流量调度与用户分片

解决的问题:千人千面推荐系统要求每个用户的请求都带着完整的上下文数据;单元化多活按用户 ID 分片,让每个单元的推荐数据自闭环,避免跨单元调用。

具体案例:某电商平台把用户哈希到 100 个单元,分布在三个机房。大促中某个单元所在机柜断电,系统只把该单元的流量平滑切到同城备份单元,整体可用性保持在 99.99% 以上。

Python 演示

python74 行
# -*- coding: utf-8 -*-"""高可用架构故障切换模拟器:对比主备、主主、多活在节点故障时的表现"""import random  class Node:    def __init__(self, name):        self.name = name        self.alive = True     def handle_request(self, req_id):        return self.alive  class FailoverSimulator:    def __init__(self, arch_type):        self.arch_type = arch_type        self.nodes = [Node("A"), Node("B")]        if arch_type == "multi":            self.nodes.append(Node("C"))        self.request_count = 0        self.success_count = 0        self.switch_count = 0        self.pending_switch = 0     def send_request(self, req_id):        self.request_count += 1        # 主备在切换窗口内的请求会失败        if self.arch_type == "active_standby" and self.pending_switch > 0:            self.pending_switch -= 1            return False        if self.arch_type == "active_standby":            candidates = [0] if self.nodes[0].alive else [1]        else:            candidates = [i for i, n in enumerate(self.nodes) if n.alive]        if not candidates:            return False        ok = self.nodes[random.choice(candidates)].handle_request(req_id)        if ok:            self.success_count += 1        return ok     def fail_node(self, index):        self.nodes[index].alive = False        self.switch_count += 1        # 主备需要额外切换窗口,主主/多活即时转移        if self.arch_type == "active_standby":            self.pending_switch = 3     def recover_node(self, index):        self.nodes[index].alive = True  def run_experiment(arch_type, total=200):    sim = FailoverSimulator(arch_type)    for req_id in range(total):        if req_id == 50:            sim.fail_node(0)        elif req_id == 100:            sim.recover_node(0)        elif req_id == 150 and arch_type == "multi":            sim.fail_node(2)        sim.send_request(req_id)    loss = sim.request_count - sim.success_count    print("架构: %s | 请求: %d | 成功: %d | 失败: %d | 切换: %d"          % (arch_type, sim.request_count, sim.success_count, loss, sim.switch_count))    return loss  if __name__ == "__main__":    print("=== 高可用故障切换模拟 ===\n")    for arch in ["active_standby", "active_active", "multi"]:        run_experiment(arch)    print("\n主备在切换窗口会丢请求;主主/多活故障转移更平滑。")
目的:模拟三种架构在节点故障时的请求成功率差异,直观展示主备切换窗口的 RTO 代价与主主/多活的平滑转移能力。
  1. candidates = [0] if self.nodes[0].alive else [1]主备架构下,只有主节点活着才接收请求,主挂了才轮到备机。
  2. if self.arch_type == "active_standby" and self.pending_switch > 0:主备切换期间请求会失败,模拟 RTO 窗口。
  3. candidates = [i for i, n in enumerate(self.nodes) if n.alive]主主/多活从所有存活节点中选一个,故障转移即时生效。
  4. sim.fail_node(0)在第 50 个请求时让 0 号节点宕机,触发高可用切换。

常见误区

误区主备架构里的备机不就摆着看吗?等主节点挂了,拿备份文件恢复到备机上就行,反正有专人运维。

正解主备的备机是热备,它通过单向复制实时同步主节点的数据,心跳检测和健康检查随时待命。切换时只需把流量 DNS 指过去,几分钟就能接管;如果真是冷备份,恢复需要重新拷贝数据、回放日志,RTO 往往以小时计。副厨必须时刻站在灶台边才叫“待命”,回老家了再喊回来就晚了。

为何常见:很多人把“备份”和“备机”混为一谈,以为高可用就是定期做数据备份,忽略了在线热备和自动切换才是高可用的关键。

误区两个节点都能写,吞吐量一定是单机的两倍,那主主架构不就完美了吗?

正解主主双写会引入双向复制开销和写冲突处理成本。两个节点互相复制日志、冲突后要合并或丢弃,这些都在消耗 CPU、网络和 IO。如果系统不能按用户/租户分片规避冲突,主主的写性能甚至不如单主节点。就像两个主厨抢一个烤箱,菜没出得更快,人先吵起来了。

为何常见:只看到“双节点同时工作”的表象,忽略了分布式系统中协调与冲突的代价。

误区既然叫“多活”,那每个节点都应该是实时同步的,我在 A 节点刚修改的数据,立刻去 B 节点查询也应该是最新的。

正解多活普遍采用最终一致性,节点之间是异步复制,存在几毫秒到几秒的可见延迟窗口。如果业务要求强一致(如账户余额),必须让同一个用户的读写都路由到同一节点,或者使用同步复制方案。分店之间菜谱同步需要时间,顾客在甲店点的新菜,乙店菜单可能还没更新。

为何常见:把“高可用”与“强一致”绑定,忽略了 CAP 定理中可用性和一致性之间的天然矛盾。

误区高可用系统应该永远不宕机,任何切换过程只要有暂停,就说明架构不合格。

正解高可用是在可接受范围内快速恢复,不是物理上的永不故障。RTO 是业务目标,由产品形态决定:游戏登录可以接受 1 分钟切换,支付交易则需要秒级甚至零感知。99.99% 可用即达标,关键是把 RTO 承诺变成可度量的工程指标并不断演练逼近。

为何常见:对“高可用”抱有绝对化期待,忽略了投入产出比——从 4 个 9 到 5 个 9 的成本可能翻数倍。

面试题

Q1. 主备架构中,备用节点的首要职责是什么?

单选题 · easy

  • A.独立承担全部业务流量
  • B.实时同步主节点数据并随时接管← 正确答案
  • C.只做定期冷备份的数据仓库
  • D.分担主节点读请求的压力

答案:B。备节点通过单向复制持续同步主节点数据,主节点故障后立即接管,这是主备的核心。A 是主主/多活职责;C 是冷备份,恢复速度慢;D 是读写分离场景,不是主备的首要职责。

Q2. RPO 为 5 分钟,说明系统允许什么?

单选题 · easy

  • A.故障后 5 分钟内必须恢复服务
  • B.丢失故障前 5 分钟内写入的数据← 正确答案
  • C.系统每 5 分钟自动重启一次
  • D.每次数据备份需要耗时 5 分钟

答案:B。RPO 是恢复点目标,衡量数据损失容忍度,“5 分钟”代表最多可丢失最近 5 分钟写入的数据。A 描述的是 RTO;C 和 D 是对概念的曲解。

Q3. 可用性达到 99.99% 时,一年内累计停机时间约为多少?

单选题 · mid

  • A.52.56 分钟← 正确答案
  • B.8.76 小时
  • C.5.26 分钟
  • D.31.54 秒

答案:A。365×24×60×(1-0.9999)=52.56 分钟。8.76 小时是 99.9% 的停机时间,5.26 分钟是 99.999%,31.54 秒是 99.9999%。记住 4 个 9 对应约 1 年 52 分钟即可。

Q4. 两个主节点同时修改同一条用户记录,最合理的处理方式是?

单选题 · mid

  • A.停机后人工合并两条记录
  • B.时间戳后写覆盖或按用户分片← 正确答案
  • C.直接关闭其中一个节点
  • D.加大专线带宽提升同步速度

答案:B。写冲突的常规解法是时间戳后写覆盖(LWW)或用哈希分片让同一用户固定写同一节点,从源头避免冲突。A 人工合并不适用于在线系统;C 违背高可用初衷;D 只解决网络延迟,不解决逻辑冲突。

Q5. 单元化多活架构中,“单元”的核心设计目标是什么?

单选题 · hard

  • A.按机柜物理位置划分彼此隔离
  • B.用户维度数据闭环且可独立完成业务← 正确答案
  • C.所有数据在所有单元强一致实时同步
  • D.每个单元只承担只读缓存流量

答案:B。单元化按用户维度(如 ID 哈希)划分,每个单元的数据和请求在内部闭环,任意单元故障可整体切换。A 只是物理分布;C 强一致与多活矛盾;D 只读缓存无法支撑完整业务。

Q6. 主备切换中,DNS 承担的关键作用是什么?

单选题 · easy

  • A.加速主备之间的数据复制
  • B.把客户端流量解析到备用节点← 正确答案
  • C.检测主节点是否存活
  • D.解决主备双写的冲突

答案:B。DNS 把域名映射到 IP,切换后修改解析记录让新流量指向备节点,实现服务转移。数据复制靠复制通道,存活检测靠心跳,主备本来就不存在双写,因此 A/C/D 均不准确。

Q7. 多活架构采用最终一致性的现实含义是?

单选题 · mid

  • A.节点间的数据永远不会一致
  • B.数据短暂延迟后各节点趋于一致← 正确答案
  • C.数据只保证在本地节点一致
  • D.数据一致性依赖人工运维协调

答案:B。多活节点通过异步复制传播更新,写入后其他节点短暂落后,复制完成后数据收敛一致。A 过于绝对;C 忽略了复制;D 是人工方案不是架构设计。

Q8. 下列哪些是处理主主写冲突的常见策略?

多选题 · mid

  • A.时间戳后写覆盖← 正确答案
  • B.用户哈希固定写入分片← 正确答案
  • C.增加备用节点等待切换
  • D.CRDT 结构化冲突合并← 正确答案

答案:ABD。LWW 让最后写入者胜出,哈希分片让同一用户集中写一个节点,CRDT 通过无冲突数据结构合并修改,三者都是主主冲突的解法。C 是主备架构的手段,不解决双写冲突。

Q9. 相比主备架构,多活架构引入了哪些新的风险和挑战?

多选题 · hard

  • A.数据同步延迟导致用户看到旧数据← 正确答案
  • B.跨地域网络抖动影响复制稳定性← 正确答案
  • C.局部故障可能经复制链路放大扩散← 正确答案
  • D.单节点故障概率显著上升

答案:ABC。多活的异步复制带来可见延迟(A),跨地域专线抖动会影响数据复制(B),复制风暴可能把局部问题扩散到全局(C)。多活通过冗余降低单点风险,D 说法与事实相反。

Q10. 架构选型时业务要求 RTO < 1 分钟且 RPO = 0,下列方案哪个最合适?

单选题 · mid

  • A.单机部署加每日冷备
  • B.主备异步复制加人工切换
  • C.多活同步复制加自动切换← 正确答案
  • D.本地磁盘定期快照恢复

答案:C。RPO=0 要求同步复制(数据不丢),RTO<1 分钟要求自动切换,多活或同城同步主备能满足。A/D 冷备恢复小时级,B 异步复制会丢数据,均不符合要求。