同一张矩阵,三种读法
第 12 章的三款产品给出了三个不同答案:《裂隙战场》按赛区部署,房间留在玩家附近;《群岛纪元》全球同服但按区域分片,跨区走异步结算;《泡泡工坊》只用一个托管区域,靠 CDN 和就近入口解决大部分问题。
如果只看结论,会以为这是三个品味问题。放到同一张矩阵上,它们是同一组选项在不同权重下的排序结果。矩阵的价值不是替人做决定,而是把“为什么选它”变成可复现的输入:维度、权重、分数、硬约束、结论。
这一节不讨论某个具体地域怎么部署,那些在第 3 章和第 12 章。这里讨论的是决策方法本身,以及它在三种常见场景下怎么用。
五个维度,统一方向
评分最怕方向不一致:延迟 5 分是最好,成本 5 分却成了最贵。矩阵统一约定:所有维度都是 5 分最好。因此成本维度叫“成本友好度”,运维维度叫“运维简单度”。
| 维度 | 回答的问题 | 5 分锚点 | 1 分锚点 | 证据 |
|---|---|---|---|---|
| 延迟 | 目标玩家能否在预算内到达 | 80% 玩家 p99 ≤ 80ms | 跨洋,p99 > 250ms | 路由探测、压测 |
| 合规 | 数据能否合法落地与删除 | 全区域本地驻留,删除链可演练 | 关键区域无合法路径 | 法务意见、演练 |
| 成本 | 单位经济是否可接受 | 单位成本低于预算 30% | 超预算 2 倍 | 账单、容量模型 |
| 运维复杂度 | 团队能否长期值守 | 单区域、单套发布 | 多区域手工切换 | on-call 记录 |
| 发布速度 | 变更能否快速且安全上线 | 每日可发,回滚 <10 分钟 | 每周窗口,回滚靠人 | 发布记录 |
锚点必须写进评分表。没有锚点,评分会退化成“我觉得这个方案更稳”,不同人打出的 4 分含义完全不同。
延迟维度的主体是玩家,不是机房。一个方案在法兰克福到阿姆斯特丹之间 p99 只有 12ms,但如果 60% 玩家在东南亚,它的延迟分数不应该高。
合规维度的主体是数据类,不是公司注册地。账号数据、支付数据、遥测数据的跨境规则可能不同,一个方案对账号合规、对遥测不合规,应该按最严的那类打分,并在备注里写清。
成本维度比较的是单位成本,不是月账单。一个月花 20 万美元但服务 100 万 MAU,可能比一个月花 8 万美元但只服务 20 万 MAU 更便宜。
运维复杂度要算长期值守,不是上线当天的工程量。多区域双活的上线可能只多两周,但它会永久增加值班面、故障定位路径和发布协调成本。
发布速度要算“安全变更”的速度。一个每天都能发但回滚要手工切 DNS 的方案,不应该拿满分。
权重是产品策略,不是平均值
权重不是把五个维度平均分。它表达的是:这个产品愿意在哪个维度上多付代价。
| 产品/场景 | 延迟 | 合规 | 成本 | 运维 | 发布 |
|---|---|---|---|---|---|
| 《裂隙战场》 | 0.35 | 0.10 | 0.15 | 0.15 | 0.25 |
| 《群岛纪元》 | 0.15 | 0.30 | 0.25 | 0.20 | 0.10 |
| 《泡泡工坊》 | 0.10 | 0.15 | 0.30 | 0.20 | 0.25 |
| 新项目立项 | 0.25 | 0.20 | 0.20 | 0.20 | 0.15 |
| 区域扩张 | 0.25 | 0.30 | 0.20 | 0.15 | 0.10 |
| 成本压缩 | 0.20 | 0.15 | 0.35 | 0.15 | 0.15 |
《裂隙战场》把 35% 给延迟,因为 5v5 对局的技能命中和走位反馈直接受 RTT 影响;发布速度给 25%,因为赛季平衡补丁需要快速上线。合规只给 10%,不是不重要,而是它的底线由硬约束保证,不需要在权重里重复计价。
《群岛纪元》把 30% 给合规,因为全球同服的数据驻留是最难改的约束;成本 25%,因为大地图长期占用内存和存储;发布只给 10%,因为 SLG 的版本节奏本来就慢。
《泡泡工坊》把 30% 给成本、25% 给发布速度。它的竞争力来自买量转化和快速试错,架构越轻越好;延迟只给 10%,因为休闲玩法对 100ms 级别的差异不敏感。
权重之和必须是 1。缺失的维度不能当成 0,因为 0 权重和“没填”在语义上完全不同:前者是“明确不关心”,后者是“还没想清楚”。
硬约束:不能妥协的分数
权重可以折中,硬约束不能。硬约束的含义是:低于这条线,选项不是“差一点”,而是“不可选”。
三款产品各有一条:
| 产品 | 硬约束 | 原因 | 谁有权改 |
|---|---|---|---|
| 《裂隙战场》 | 延迟 ≥4 | 低于 4 分的方案 p99 超过对局预算 | 技术负责人 + 产品 |
| 《群岛纪元》 | 合规 ≥4 | 关键区域无合法数据路径 | 法务 + 平台 |
| 《泡泡工坊》 | 成本 ≥4 | 单位成本击穿买量回收模型 | 产品 + 财务 |
硬约束和“高权重”是两件事。把合规权重调到 0.5,仍然可能被一个合规 2 分、其他维度满分的方案用加权总分压过去;硬约束则在加权之前就把它剔除。
硬约束也不能太多。五条维度全设成 ≥4,通常会得到“没有可选方案”的结果。这时候正确的动作是改架构或改范围,而不是悄悄把门槛降到 3。降门槛必须留下书面例外,写清谁批准、有效期多久、补偿措施是什么。
硬约束要有证据和所有者。没有证据的硬约束只是偏好;没有所有者的硬约束没人敢改,最后会变成永久教条。
三款产品在同一张矩阵上的得分
下面是四个候选方案。分数是评审示例值,不是实测值;真实评分要用本节后面的方法重新打。
| 方案 | 延迟 | 合规 | 成本 | 运维 | 发布 |
|---|---|---|---|---|---|
| A 单地域托管 | 2 | 3 | 5 | 5 | 5 |
| B 多地域双活 | 5 | 4 | 2 | 2 | 3 |
| C 区域集群 + 异步跨区 | 4 | 5 | 3 | 3 | 4 |
| D 全球单服 | 3 | 2 | 4 | 3 | 4 |
按产品权重计算:
| 产品 | A | B | C | D | 硬约束淘汰 | 结论 |
|---|---|---|---|---|---|---|
| 《裂隙战场》 | 3.75 | 3.50 | 3.80 | 3.30 | A、D(延迟 <4) | C |
| 《群岛纪元》 | 3.95 | 3.15 | 3.85 | 3.05 | A、D(合规 <4) | C |
| 《泡泡工坊》 | 4.40 | 2.85 | 3.65 | 3.40 | B、C(成本 <4) | A |
三个结论值得注意。
第一,《群岛纪元》的 A 方案加权分 3.95,其实高于 C 的 3.85,但 A 的合规只有 3,被硬约束淘汰。如果没有硬约束,矩阵会推荐一个不合规的方案;这正是硬约束存在的意义。
第二,《裂隙战场》的 B 方案延迟满分,但成本、运维、发布三项太低,加权后输给 C。多地域双活不是“更全球”,它对竞技产品意味着更高的发布协调成本和更复杂的故障定位。
第三,《泡泡工坊》的 A 方案赢得不惊险:4.40 对 3.40。它的产品阶段不需要为全球低延迟付双活的钱,单地域托管加 CDN 已经够用。
这些分数会随阶段变化。《泡泡工坊》如果进入东南亚买量期,延迟权重可能从 0.10 升到 0.20,A 的领先会缩小;如果监管要求账号本地化,合规硬约束会直接把 A 淘汰。矩阵要跟着阶段重算,而不是一次定终身。
矩阵怎么用:三种场景
新项目立项
用 new-project 预设:五个维度都保留权重,暂时不设硬约束,除了法律明确要求的区域合规。
第一步,列出 3–5 个候选,不要只列“做”和“不做”。单地域、区域集群、全球单服、托管服务,都可以进矩阵。
第二步,先定评分锚点,再打分。锚点可以粗,但必须一致;不同方案用不同锚点,分数没有可比性。
第三步,看排序,也看分差。分差小于 0.2 时,不要假装矩阵给出了明确结论;这时候应该补证据,或者承认两个方案等价,用组织能力做 tie-breaker。
第四步,记录权重、分数、证据和日期。新项目两个月后就会变,没有记录就无法解释“为什么当时选了这个”。
区域扩张
用 region-expansion 预设:在原有产品权重上,把合规权重提到 0.30,并加两条硬约束——合规 ≥4、延迟 ≥3。
区域扩张不是重新选一遍全球架构,而是在现有架构上增加一个区域。矩阵要回答的是:新区域用现有拓扑的延伸,还是独立一套?
这时要把“现有架构改造成本”作为一个候选方案纳入评分,而不是只评绿地方案。改造成本包括数据迁移、契约兼容、值班培训,它们会拉低运维和发布分数。
区域扩张还必须单独评目标区域,不能拿全球平均分代替。一个方案在欧洲合规 5 分,在东南亚可能只有 2 分;按最严区域打分,或者按区域分别建矩阵。
成本压缩
用 cost-squeeze 预设:成本权重 0.35,硬约束是成本 ≥3、合规 ≥3。
成本压缩最容易出现的错误,是把合规和容灾当成可削减项。矩阵的硬约束在这里起护栏作用:合规低于 3 的方案直接淘汰,不管它多便宜。
第二容易出现的错误,是只看单价不看单位经济。把专用节点换成按量实例,单价可能更低,但冷启动和运维复杂度会把发布速度拉下来;矩阵会把这项代价显式化。
成本压缩的结论通常不是“换一个全新架构”,而是“在现有架构上关掉低价值能力”:降低非核心日志保留、合并低负载区域、把离线计算移到spot。矩阵帮你看清每次削减在哪一维上付代价。
数据模型与接口契约
代码把矩阵拆成四个概念:
Dimension:五个固定维度,方向统一为“5 分最好”;Weights:每个维度一个权重,必须齐全且和为 1;Option:候选方案,每个维度一个 0–5 分;Constraint:硬约束,低于Min直接淘汰。
关键契约有三条。第一,权重缺失返回错误,不默认 0;第二,选项缺维度返回错误,不默认 3;第三,评分范围固定 0–5,超出范围直接拒绝。三条都是为了防“悄悄补一个中间值”。
可运行片段
下面的 matrix.go 实现加权评分、硬约束过滤和确定性排序。同包的 presets.go 提供三款产品和三种场景的权重预设。
// file: internal/globalmatrix/matrix.go
// tier: L1
// verify: go test ./internal/globalmatrix/
package globalmatrix
import (
"errors"
"fmt"
"sort"
)
var (
ErrMatrix = errors.New("globalmatrix: invalid matrix")
ErrNoOption = errors.New("globalmatrix: no option satisfies hard constraints")
)
// Dimension is scored 0..5, always in the same direction: 5 is better. Cost is
// therefore "cheapness", and ops is "simplicity", not raw spend or headcount.
type Dimension string
const (
Latency Dimension = "latency"
Compliance Dimension = "compliance"
Cost Dimension = "cost"
Ops Dimension = "ops-complexity"
Release Dimension = "release-speed"
)
var Dimensions = []Dimension{Latency, Compliance, Cost, Ops, Release}
type Weights map[Dimension]float64
func (w Weights) Validate() error {
total := 0.0
for _, d := range Dimensions {
v, ok := w[d]
if !ok {
return fmt.Errorf("%w: missing weight for %s", ErrMatrix, d)
}
if v < 0 || v > 1 {
return fmt.Errorf("%w: weight for %s is %g, want [0,1]", ErrMatrix, d, v)
}
total += v
}
if total < 0.999 || total > 1.001 {
return fmt.Errorf("%w: weights sum to %g, want 1", ErrMatrix, total)
}
return nil
}
type Option struct {
Name string
Scores map[Dimension]float64
}
func (o Option) Validate() error {
if o.Name == "" {
return fmt.Errorf("%w: option needs a name", ErrMatrix)
}
for _, d := range Dimensions {
v, ok := o.Scores[d]
if !ok {
return fmt.Errorf("%w: option %s has no score for %s", ErrMatrix, o.Name, d)
}
if v < 0 || v > 5 {
return fmt.Errorf("%w: option %s score for %s is %g, want [0,5]", ErrMatrix, o.Name, d, v)
}
}
return nil
}
// Constraint removes an option before weighting. It exists because some
// dimensions are not tradeable: a region without a lawful data path is not a
// slightly worse region, it is not an option.
type Constraint struct {
Dimension Dimension
Min float64
}
func (c Constraint) Validate() error {
valid := false
for _, d := range Dimensions {
if c.Dimension == d {
valid = true
break
}
}
if !valid {
return fmt.Errorf("%w: unknown constraint dimension %q", ErrMatrix, c.Dimension)
}
if c.Min < 0 || c.Min > 5 {
return fmt.Errorf("%w: constraint %s min is %g, want [0,5]", ErrMatrix, c.Dimension, c.Min)
}
return nil
}
type Result struct {
Name string
Score float64
Pass bool
Failed []Dimension
}
// Evaluate returns every option, sorted with passing options first, then by
// weighted score, then by name so the output is stable across runs.
func Evaluate(w Weights, cs []Constraint, opts []Option) ([]Result, error) {
if err := w.Validate(); err != nil {
return nil, err
}
for _, c := range cs {
if err := c.Validate(); err != nil {
return nil, err
}
}
if len(opts) == 0 {
return nil, fmt.Errorf("%w: no options", ErrMatrix)
}
results := make([]Result, 0, len(opts))
for _, o := range opts {
if err := o.Validate(); err != nil {
return nil, err
}
r := Result{Name: o.Name, Pass: true}
for _, c := range cs {
if o.Scores[c.Dimension] < c.Min {
r.Pass = false
r.Failed = append(r.Failed, c.Dimension)
}
}
for _, d := range Dimensions {
r.Score += w[d] * o.Scores[d]
}
results = append(results, r)
}
sort.SliceStable(results, func(i, j int) bool {
if results[i].Pass != results[j].Pass {
return results[i].Pass
}
if results[i].Score != results[j].Score {
return results[i].Score > results[j].Score
}
return results[i].Name < results[j].Name
})
return results, nil
}
// Choose returns the best passing option. A tied score is broken by name in
// Evaluate, so the caller never has to invent a second tie-breaker.
func Choose(w Weights, cs []Constraint, opts []Option) (Result, error) {
results, err := Evaluate(w, cs, opts)
if err != nil {
return Result{}, err
}
if !results[0].Pass {
return Result{}, ErrNoOption
}
return results[0], nil
}
Evaluate 返回所有选项,而不只是赢家。这个设计是有意的:评审时需要看到落选方案差在哪一维,而不是只看到一个名字。
读代码:排序与淘汰的细节
第一,排序先看 Pass,再看 Score。一个被硬约束淘汰的方案,即使加权分最高,也排在所有通过方案之后。这保证了“高分但不合规”不会出现在推荐位。
第二,同分按名称排序。矩阵经常出现两个方案同分的情况,尤其是新项目阶段;用名称做确定性 tie-breaker,至少保证同样的输入每次得到同样的输出。真正的 tie-breaker 应该由人补,但不能让程序随机。
第三,Failed 记录被哪条硬约束淘汰,而不是只返回布尔值。评审时最有用的信息往往是“它差在合规,不在成本”。
第四,Choose 在没有方案通过时返回 ErrNoOption,而不是返回分数最高的失败方案。后者会让调用方误以为还有选择,实际上需要改架构或改范围。
第五,权重允许 0,但必须显式写 0。这和缺失不同:显式 0 表示“这一维不参与本次决策”,缺失表示“矩阵不完整”。
失败模式与兜底
评分锚点不一致。 同一个 4 分,有人理解为“行业平均”,有人理解为“接近最好”。兜底是把锚点写进表,评审前先对齐。
看完结果再调权重。 这是最常见的作弊。兜底是权重和硬约束在打分前冻结,变更要记录理由和批准人。
硬约束过多导致无解。 五维全设高门槛,结果没有方案通过。兜底是接受 ErrNoOption,回到架构或范围讨论,不静默降门槛。
合规分由工程单独打。 工程师可以评估技术可行性,但不能替法务判断合法性。兜底是合规分需要法务签字。
运维分只看上线工程量。 忽略长期值班、故障定位和发布协调。兜底是用过去 6 个月的 on-call 记录做证据。
成本分混用月账单和单位成本。 规模不同的方案不可比。兜底是统一换算到单位成本,再映射到 0–5。
用全球平均分掩盖区域差异。 一个方案在欧洲 5 分、东南亚 2 分,平均 3.5 分看起来还行。兜底是按区域分别评分,取最差区域进入矩阵。
把软偏好写成硬约束。 “我们希望运维简单”不是硬约束,“团队没有 24 小时值班能力”才是。兜底是每条硬约束都要有证据和所有者。
常见错误与边界
第一,把矩阵当成客观真理。分数是评审输入,不是测量结果;它的价值在于让分歧可见。
第二,维度太多。超过七个维度,评审会失去焦点;五个维度覆盖了全球化决策的主要矛盾。
第三,权重平均分配。平均权重等于没有策略,最后总是选到“哪一维都不突出”的方案。
第四,忽略阶段变化。新项目、扩张期、成熟期的权重不同,矩阵要重算。
第五,只评绿地方案。改造现有架构往往才是真实选项,必须纳入。
第六,用硬约束淘汰所有创新方案。硬约束应该少而硬,通常一到两条。
第七,不记录落选原因。半年后复盘时,只会记得“当时选了 C”,不记得为什么。
小结
全球化架构决策矩阵把“选哪个”拆成五个统一方向的维度、一组显式权重、若干条硬约束和一次确定性排序。三款产品在同一张矩阵上得到不同结论,不是因为品味不同,而是因为权重和硬约束不同。
矩阵的输出不是答案,而是一份可复现的决策记录:谁在什么阶段、用什么证据、把哪一维看得很重、哪条线不能碰。下一节收口全卷,把这些决策和三张表交给平台团队,并说明哪些责任必须留在产品团队。
阅读导航:上一节:13.1 容量表、成本表与合规表 · 下一节:13.3 进入卷四:平台化与治理 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。