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、跨地域,每一层都要单独计算 “能承受几个域同时故障”。