57.3 冗余系数

一句话

冗余系数 = “为故障多备的容量比例”:N+1 是最低配置(多一台),N/(N-1) 是它的容量代价;可用区的冗余系数是 1/(1-1/AZ数)——这两个数决定了“故障时还能不能扛住峰值”。

三种冗余配置

配置 含义 容量代价 能扛什么
N 刚好够用 1.0 什么都扛不住,一台故障即过载
N+1 多一台 (N+1)/N 单节点故障
N+2 多两台 (N+2)/N 同时两台故障 / 滚动发布 + 一台故障
跨 3 AZ 每个 AZ 至少 1 组 1/(1-1/3)=1.5 单 AZ 故障

🔑 注意两种冗余是叠加的:既要 N+1 又要跨 AZ,容量 = 峰值 × 1.5 × (N+1)/N。只算其中一项,故障时就会差那一块。

代码:冗余系数与成本

package main

import (
	"fmt"
	"math"
)

func main() {
	const (
		peakQPS  = 9000.0
		perNode  = 1200.0 // 单节点“安全容量”(已按 70% 折算)
		azs      = 3
	)

	fmt.Printf("峰值 %.0f QPS,单节点安全容量 %.0f QPS\n\n", peakQPS, perNode)

	fmt.Printf("%-16s %-12s %-14s %-12s %s\n", "配置", "容量系数", "需要节点", "成本倍数", "能扛")
	for _, cfg := range []struct {
		name     string
		azFactor bool
		extra    int
		note     string
	}{
		{"N(无冗余)", false, 0, "单节点故障即过载"},
		{"N+1", false, 1, "单节点故障"},
		{"N+2", false, 2, "两台同时故障"},
		{"3AZ + N+1", true, 1, "单 AZ 故障 + 单节点故障"},
	} {
		factor := 1.0
		if cfg.azFactor {
			factor = 1 / (1 - 1.0/float64(azs))
		}
		need := math.Ceil(peakQPS * factor / perNode)
		total := int(need) + cfg.extra
		// 3AZ 时节点数不少于 AZ 数
		if cfg.azFactor && total < azs {
			total = azs
		}
		costMul := float64(total) / math.Ceil(peakQPS/perNode)
		fmt.Printf("%-16s %-12.2f %-14d %-12.2f %s\n", cfg.name, factor, total, costMul, cfg.note)
	}

	fmt.Println("\n=== 可用性视角:多一个 9 要多少冗余 ===")
	fmt.Printf("%-12s %-14s %-16s %s\n", "目标可用性", "年停机", "需要的冗余", "说明")
	for _, a := range []struct {
		name       string
		downtimeHr float64
		redundancy string
		note       string
	}{
		{"99%", 87.6, "N(可接受)", "单点即可"},
		{"99.9%", 8.76, "N+1", "单节点故障自动切换"},
		{"99.99%", 0.876, "跨 AZ + N+1", "区域级冗余"},
		{"99.999%", 0.0876, "多地域 + 自动切换", "成本极高"},
	} {
		fmt.Printf("%-12s %-14s %-16s %s\n", a.name,
			fmt.Sprintf("%.2f 小时", a.downtimeHr), a.redundancy, a.note)
	}
}

本机实测(macOS,Apple M1,Go 1.27):

峰值 9000 QPS,单节点安全容量 1200 QPS

配置               容量系数         需要节点           成本倍数         能扛
N(无冗余)           1.00         8              1.00         单节点故障即过载
N+1              1.00         9              1.12         单节点故障
N+2              1.00         10             1.25         两台同时故障
3AZ + N+1        1.50         13             1.62         单 AZ 故障 + 单节点故障

=== 可用性视角:多一个 9 要多少冗余 ===
目标可用性        年停机            需要的冗余            说明
99%          87.60 小时       N(可接受)           单点即可
99.9%        8.76 小时        N+1              单节点故障自动切换
99.99%       0.88 小时        跨 AZ + N+1       区域级冗余
99.999%      0.09 小时        多地域 + 自动切换       成本极高

读数:

配置 节点 成本倍数 能扛
N 8 1.00 什么都扛不住
N+1 9 1.13 单节点故障
N+2 10 1.25 两台同时故障
3AZ + N+1 13 1.63 单 AZ + 单节点

🔑 从 99.9% 到 99.99%,停机从 8.76 小时降到 0.876 小时,但成本倍数从 1.13 涨到 1.63——每多一个 9,成本是指数级上升的。所以可用性目标要先由业务定(SLO),再倒推冗余,而不是“越稳越好”。

冗余的四个隐藏成本

成本 说明
资源闲置 冗余节点平时不承载流量(或只承载部分)
切换复杂度 主备切换要能自动、要演练,否则冗余等于没有
数据一致性 多副本要处理复制延迟与脑裂(44.13)
容量规划 冗余后每 AZ 的容量要重新算(45.3 的分片分布同理)

反模式

反模式 后果
只做 N,宣称“高可用” 单节点故障即全站过载
冗余了但不演练切换 真故障时切换失败
只做 N+1,不做 AZ 打散 一个 AZ 挂掉,N+1 也救不了
把冗余节点当“平时也能用” 故障时容量不够
可用性目标定 99.99% 但只肯付 N+1 的钱 SLO 永远达不成,团队疲于救火
冗余系数不随业务增长更新 半年前够用,现在不够

自检

Q1 为什么“N+1”的容量代价是 1/N,而跨 3 AZ 是 50%?

▸ 答案:N+1 只多一台,代价 1/N,N 越大越便宜(100 台多 1 台是 1%)。跨 AZ 的代价来自“一个 AZ 故障后,剩余 2/3 的容量必须扛住全部峰值”,所以总容量要是峰值的 3/2 = 1.5 倍,即多备 50%。这两者不能互相替代:N+1 防单机,跨 AZ 防机房。

Q2 为什么“99.99% 需要跨 AZ + N+1”,而 99.9% 只要 N+1?

▸ 答案:99.9%(年停机 8.76 小时)允许“单节点故障 + 切换时间”,N+1 + 自动切换足够;99.99%(年停机 52.6 分钟)不允许机房级故障,因为一个 AZ 故障的恢复时间通常是小时级,必须让流量自动切到其他 AZ,且剩余 AZ 要能扛住峰值(容量系数 1.5)。

Q3 冗余节点平时完全不承载流量,是不是浪费?

▸ 答案:不是浪费,是保险。但可以用两种方式降低成本:① 让冗余节点平时承载部分流量(如 N+1 的 1 台也参与服务,故障时其余节点接管),即“共享冗余”;② 用弹性伸缩在故障时快速补节点(需要冷启动足够快)。前者要求剩余节点有余量,后者要求扩容速度 < 故障影响时间。

Q4 N+1 与 N+2 怎么选?

▸ 答案:按故障域与恢复时间选:单 AZ 内 N+1 通常够(坏一台补一台);跨 AZ 要求 “挂一个 AZ 仍能承载”,就需要 N+2(或按 AZ 数做冗余)。如果恢复要人工介入且耗时长,冗余要更高;如果自动扩缩容很快,N+1 也可以接受。

Q5 为什么冗余系数要看 “故障域” 而不是 “机器台数”?

▸ 答案:因为相关性决定冗余是否有效:3 台机器在同一个机架、同一个 AZ,机架断电就全灭,台数再多也没用。有效冗余是 “跨故障域” 的:跨机架、跨 AZ、跨地域,每一层都要单独计算 “能承受几个域同时故障”。

关联