《游戏服务端架构·品类、状态与拓扑》13.2 品类与拓扑决策矩阵

把实时性、共享世界、房间制、状态规模、跨区需求五个品类特征,与五种拓扑做成带理由和代价的决策矩阵;每个格子标注推荐、可选或不推荐,再用北辰三款产品回填验证。配套 Go 包实现特征向量打分、成本平局裁决与 25 格完整性测试,并说明矩阵在什么条件下失效。

矩阵不是投票器

一次选型会上,两个人为了《泡泡工坊》要不要拆房间服务争论了四十分钟。一方说“匹配制就该大厅加房间”,另一方说“团队这么小,拆了没人运维”。吵到最后才发现,双方对“实时性”的赋值不同:一个人把“有网络延迟”算成高,另一个人按“玩家不感知帧”算成低。

矩阵的第一个作用不是给答案,而是把这类分歧变成可讨论的输入。特征怎么定义、强度怎么打分、代价写在哪一列,必须先在桌上对齐;对齐之后,结论往往自己就出来了。

第 1.3 节 用六个产品事实做了第一版选型函数,第 3.3 节 比较了分区、分服与全球同服。这一节把前十二章的判断收成一张矩阵:行是品类特征,列是拓扑选项,每个格子写清推荐程度、理由和代价。

五个特征,四种强度

特征不能只填布尔值。“有实时性”和“帧级实时性”是两种完全不同的约束;“有共享世界”和“全球共享一张大地图”也不是一个量级。矩阵给每个特征四档:

特征要回答的问题0 无1 弱2 中3 强
实时性玩家是否感知帧或操作延迟无连续操作秒级反馈可接受100–300ms帧级,50ms 以内
共享世界玩家是否共享持久世界每局独立单局内共享小型持久世界全球大地图
房间制玩法是否以离散对局为单位无对局弱对局边界部分玩法分房一局即一个故障域
状态规模每 tick 需要裁决多少状态很小单进程可容纳需要分片全球多分片
跨区需求跨地域交互是否影响公平或体验无弱社交有跨区玩法强对抗或强协作

四档不是精确测量,而是评审时必须给出证据的判断。实时性看操作预算,共享世界看地图是否持久,房间制看一局是否有明确开始和结束,状态规模看峰值 tick 成本而不是注册人数,跨区需求看跨地域交互是否进入同一场战斗。没有证据的档位,应该在评审记录里标成“待验证”,而不是当作事实。

1.3 节的六个产品事实在这里合并成五维:并发玩家数与状态规模合并,因为大并发最终体现为峰值 tick 成本;会话时长通过房间制体现,短会话通常对应离散对局;实时性、共享世界、跨区需求保持独立。合并不是丢信息,而是把“人数”和“状态量”这两个经常被混用的指标分开。

25 个格子

行是特征,列是拓扑。每个格子给出推荐程度、一句理由和代价。推荐表示这个拓扑对该特征有原生支持;可选表示能做,但要额外工程;不推荐表示会直接破坏该特征的核心体验。

特征模块化单体大厅 + 房间分区世界全球同服专用服池
实时性可选·单进程可跑 tick;代价:没有房间级隔离推荐·每房独占 tick 与输入窗口;代价:分配器与暖池可选·世界 tick 可以很粗;代价:操作反馈不跟手不推荐·跨地域 RTT 进入帧预算;代价:公平无法承诺推荐·独占 CPU 与网络;代价:池化与冷启动
共享世界可选·小世界可以放单进程;代价:只能纵向扩容不推荐·房间结束即销毁;代价:世界状态无处安放推荐·按地图范围切持久世界;代价:跨片迁移协议推荐·逻辑一张图、物理多套数据;代价:地域路由与合规不推荐·实例按局回收;代价:世界不连续
房间制推荐·房间可作为模块先内聚;代价:发布仍绑在一起推荐·大厅无状态、房间有状态;代价:多一层分配调用可选·房间可以挂在分片里;代价:生命周期受分片影响可选·全球匹配、房间就近落地;代价:匹配与开房跨层推荐·每局独占实例;代价:调度与空闲成本
状态规模不推荐·单库单进程很快碰到上限;代价:拆分边界被代码绑死可选·单房状态小但总量线性增长;代价:需要独立归档推荐·状态跟着地图范围走;代价:热区与再均衡推荐·按赛区与地图分摊状态;代价:全球索引与事件同步不推荐·实例状态随对局回收;代价:持久化仍要另建
跨区需求不推荐·单部署单元只能选一个地域;代价:跨区靠数据库复制可选·房间按赛区就近创建;代价:跨区组队要另设计可选·地图分片可按地域部署;代价:跨区行军需要显式协议推荐·地域路由是全球同服的前置条件;代价:数据驻留与结算时间线可选·各地区分别建池;代价:容量规划翻倍

这张表最重要的不是“推荐”两个字,而是“代价”那一半。推荐只说明方向对,不说明便宜。大厅加房间对所有实时对局都推荐,但它要求分配器、暖池、租约和排空发布;全球同服对共享世界推荐,但它要求地域路由、数据驻留和跨区结算。只看推荐列做选型,会把代价留到上线后才发现。

打分不是投票

把特征强度和格子推荐合成一个分数:

推荐 = 2 分,可选 = 1 分,不推荐 = 0 分
总分 = Σ(特征强度 × 格子分数)

强度取 0 到 3,所以单个特征最高贡献 6 分,五个特征满分 30 分。分数区间对应的只是“值不值得进入下一轮评审”:

总分判定含义
14 分及以上推荐值得写 ADR,进入方案设计
6 到 13 分可选能工作,但需要明确代价和补偿措施
5 分及以下不推荐除非有外部约束,否则不应作为首选

平局按运维成本裁决:模块化单体最低,大厅加房间次之,分区世界与专用服池再次,全球同服最高。同样的分数下,先用更简单的形态验证玩法;等触发条件出现,再付复杂的代价。

打分有三个刻意保留的粗糙之处。第一,五个特征默认等权,但真实项目里实时性和共享世界往往比跨区需求更重要;需要时可以在 Profile 里用强度表达,而不是改权重。第二,格子分数只有 0/1/2,不能表达“推荐但很贵”和“可选但很便宜”的细微差别,这部分交给代价列和 ADR。第三,总分不包含团队规模、预算和合规,这些是矩阵之外的硬约束,必须单独写。

代码:特征向量与拓扑打分

internal/matrix 把上面的表写成可测试的结构:capability 是 5×5 的能力矩阵,Profile 是产品特征向量,Evaluate 负责打分、排序和平局裁决。下面是完整实现。

// file: internal/matrix/matrix.go
// tier: L1
// verify: go test ./internal/matrix/
package matrix

import "sort"

type Feature string

const (
	Realtime    Feature = "realtime"
	SharedWorld Feature = "shared-world"
	RoomBased   Feature = "room-based"
	LargeState  Feature = "large-state"
	CrossRegion Feature = "cross-region"
)

var Features = []Feature{Realtime, SharedWorld, RoomBased, LargeState, CrossRegion}

type Topology string

const (
	ModularMonolith Topology = "modular-monolith"
	LobbyRoom       Topology = "lobby-room"
	ShardedWorld    Topology = "sharded-world"
	GlobalShard     Topology = "global-shard"
	DedicatedPool   Topology = "dedicated-server-pool"
)

var Topologies = []Topology{ModularMonolith, LobbyRoom, ShardedWorld, GlobalShard, DedicatedPool}

type Fit int

const (
	NotRecommended Fit = iota
	Optional
	Recommended
)

func (f Fit) String() string {
	return [...]string{"不推荐", "可选", "推荐"}[f]
}

// Cell is one feature/topology intersection. Reason says why the topology does
// or does not support the feature; Cost is paid even when the fit is good.
type Cell struct {
	Fit    Fit
	Reason string
	Cost   string
}

// capability is the reviewed matrix. It is a capability view, not a final
// recommendation: a product combines several rows, and cost breaks ties.
var capability = map[Feature]map[Topology]Cell{
	Realtime: {
		ModularMonolith: {Optional, "单进程也能跑 tick,但没有房间级隔离", "一次停顿影响全部对局"},
		LobbyRoom:       {Recommended, "每间房独占 tick 与输入窗口", "分配器、暖池与排空发布"},
		ShardedWorld:    {Optional, "世界 tick 可以很粗,操作反馈不跟手", "边界迁移与热区扩容"},
		GlobalShard:     {NotRecommended, "跨地域 RTT 直接进入帧预算", "无法承诺对局公平"},
		DedicatedPool:   {Recommended, "独占 CPU 与网络,延迟最稳", "池化、冷启动与空闲成本"},
	},
	SharedWorld: {
		ModularMonolith: {Optional, "小世界可以放在单进程", "只能纵向扩容,故障域大"},
		LobbyRoom:       {NotRecommended, "房间结束即销毁,没有持久世界", "世界状态无处安放"},
		ShardedWorld:    {Recommended, "按地图范围切分持久世界", "跨片交互与迁移协议"},
		GlobalShard:     {Recommended, "逻辑一张图,物理多套数据", "地域路由与数据驻留"},
		DedicatedPool:   {NotRecommended, "实例按局回收,世界不连续", "无法承载长期世界"},
	},
	RoomBased: {
		ModularMonolith: {Recommended, "房间可以作为清晰模块先内聚", "扩容与发布仍绑在一起"},
		LobbyRoom:       {Recommended, "大厅无状态、房间有状态是原生分工", "多一层分配与调用"},
		ShardedWorld:    {Optional, "房间可以挂在世界分片里", "房间生命周期受分片影响"},
		GlobalShard:     {Optional, "全球匹配,房间仍要就近落地", "匹配与开房跨层"},
		DedicatedPool:   {Recommended, "每局独占实例,天然隔离", "实例利用率与调度成本"},
	},
	LargeState: {
		ModularMonolith: {NotRecommended, "单库单进程很快碰到容量上限", "拆分时边界已被代码绑死"},
		LobbyRoom:       {Optional, "单房间状态小,总量随房间数增长", "需要独立持久化与归档"},
		ShardedWorld:    {Recommended, "状态跟着地图范围走", "热区与再均衡"},
		GlobalShard:     {Recommended, "按赛区与地图分片分摊状态", "全球索引与事件同步"},
		DedicatedPool:   {NotRecommended, "实例状态随对局回收,不能当长期存储", "持久化仍要另建"},
	},
	CrossRegion: {
		ModularMonolith: {NotRecommended, "单部署单元只能选一个地域", "跨区只能靠数据库复制"},
		LobbyRoom:       {Optional, "房间按赛区就近创建", "跨区匹配与组队要额外设计"},
		ShardedWorld:    {Optional, "地图分片可以按地域部署", "跨区行军需要显式协议"},
		GlobalShard:     {Recommended, "地域路由是全球同服的前置条件", "数据驻留与结算时间线"},
		DedicatedPool:   {Optional, "各地区分别建池", "池容量规划翻倍"},
	},
}

type Level int

const (
	Absent Level = iota
	Low
	Medium
	High
)

type Profile map[Feature]Level

type Verdict struct {
	Topology Topology
	Score    int
	Fit      Fit
	Reason   string
	Cost     string
}

var opsCost = map[Topology]int{
	ModularMonolith: 0,
	LobbyRoom:       1,
	ShardedWorld:    2,
	DedicatedPool:   2,
	GlobalShard:     3,
}

func CellFor(f Feature, t Topology) (Cell, bool) {
	row, ok := capability[f]
	if !ok {
		return Cell{}, false
	}
	cell, ok := row[t]
	return cell, ok
}

func dominant(p Profile) Feature {
	best := Feature("")
	level := Absent
	for _, f := range Features {
		if p[f] > level {
			best, level = f, p[f]
		}
	}
	return best
}

func classify(score int) Fit {
	switch {
	case score >= 14:
		return Recommended
	case score >= 6:
		return Optional
	default:
		return NotRecommended
	}
}

// Evaluate scores every topology against a product profile. Score is the sum
// of level times cell fit; ties break on operations cost, then on name, so the
// result is deterministic and explainable.
func Evaluate(p Profile) []Verdict {
	out := make([]Verdict, 0, len(Topologies))
	for _, t := range Topologies {
		score := 0
		for _, f := range Features {
			score += int(p[f]) * int(capability[f][t].Fit)
		}
		v := Verdict{Topology: t, Score: score, Fit: classify(score)}
		if f := dominant(p); f != "" {
			cell := capability[f][t]
			v.Reason, v.Cost = cell.Reason, cell.Cost
		} else {
			v.Reason, v.Cost = "没有强特征,按最低运维成本起步", "功能增长后仍要复审"
		}
		out = append(out, v)
	}
	sort.SliceStable(out, func(i, j int) bool {
		if out[i].Score != out[j].Score {
			return out[i].Score > out[j].Score
		}
		if opsCost[out[i].Topology] != opsCost[out[j].Topology] {
			return opsCost[out[i].Topology] < opsCost[out[j].Topology]
		}
		return out[i].Topology < out[j].Topology
	})
	return out
}

// Top returns the first verdict after scoring and tie-breaking.
func Top(p Profile) Verdict {
	return Evaluate(p)[0]
}

几个实现选择值得说明:

  • 能力矩阵是单一事实来源。 文章里的 25 格和代码里的 capability 必须一致;测试逐格检查理由和代价非空,防止只填了推荐程度却忘了写代价。
  • 打分是确定性的。 排序先比分数,再比运维成本,最后比拓扑名。没有随机、没有 map 遍历顺序,评审记录可以逐字复现。
  • 平局裁决显式化。 opsCost 是一张独立的表,不是藏在排序函数里的魔法数字。想调整“同等分数下先选谁”,改这一张表并补测试。
  • Top 只返回第一名。 它不替评审做决定,只把“最值得进入下一轮”的候选挑出来;完整排序用 Evaluate。

三款产品回填

用第 1.3 节的产品事实给三款产品填特征向量,再跑矩阵:

产品实时性共享世界房间制状态规模跨区需求第一名第二名结论
《裂隙战场》30322大厅 + 房间 16 分专用服池 14 分与 3.2 的基线一致
《群岛纪元》03033全球同服 18 分分区世界 15 分与 8.1 的基线一致
《泡泡工坊》00300模块化单体 6 分大厅 + 房间、专用服池同分按运维成本裁决,与 1.3 一致

《裂隙战场》的分数差距不大:大厅加房间 16 分,专用服池 14 分。这不奇怪,两者都原生支持实时和房间;决定差异的是跨区需求,大厅加房间可以按赛区就近开房,专用服池则要为每个地区分别建池。第 3.2 节最终选大厅加房间,代价是分配器和暖池;如果团队已经有成熟的专用服池运维能力,14 分的方案也不是错误答案。

《群岛纪元》的 18 分几乎没有悬念:共享世界、状态规模、跨区需求三项都是强,只有全球同服同时拿到三个推荐。分区世界 15 分,差距在跨区需求上——它可以按地域部署,但跨区行军要显式协议,全球事件也要另建。

最值得看的是《泡泡工坊》。它的最高分只有 6 分,判定是“可选”,不是“推荐”。矩阵没有团队规模这一维,所以它无法解释“三到四个后端不该维护多个服务”;这个理由由第 9.3 节的成本分析和 ADR 补上。矩阵能做的是把三个同分方案摆出来:模块化单体、大厅加房间、专用服池都是 6 分,按运维成本选单体。当矩阵输出“可选”而团队仍然选择它时,必须把外部约束写进 ADR,而不是假装矩阵给了推荐。

测试守住了什么

internal/matrix 有 5 个测试函数、39 个运行项(含子测试),覆盖三款产品回填、特征主导、25 格完整性、平局裁决和确定性。

测试覆盖的失败模式
TestNorthstarBackfill三款产品回填后第一名与卷一基线一致
TestFeatureDominance共享世界加跨区、实时加房间、小世界、休闲房间四类组合
TestEveryCellHasReasonAndCost25 个格子逐格检查推荐程度、理由和代价
TestTieBreaksOnOpsCost同分时按运维成本选择,并确认确实是平局
TestEvaluateIsDeterministic同一向量连续 20 次排序完全一致

本机实际运行结果:

$ cd .local/checks/gsarch-lab
$ gofmt -l internal/matrix
$ go vet ./internal/matrix/
$ go test -count=1 ./internal/matrix/
ok  	gsarchlab/internal/matrix	0.256s

gofmt -l 与 go vet 均无输出,退出码为 0。耗时随机器变化,测试数量与通过状态不随耗时变化。

矩阵的适用边界与失效条件

矩阵回答的是“在给定特征下,哪种拓扑值得优先评审”,不是“哪种拓扑正确”。它有四条明确边界。

第一,不含组织约束。团队规模、运维能力、预算、招聘计划都不在矩阵里。《泡泡工坊》的 6 分是拓扑分数,不是组织建议;把一个 6 分方案做成基线,需要 ADR 解释“为什么愿意接受这个分数”。

第二,不含合规与数据驻留。全球同服在矩阵里是推荐,但如果合规要求账号数据本地化,全球单库仍然不可行。第 8.1 节已经把“逻辑一张图、物理多套数据”写成前提,矩阵不能替代这条前提。

第三,特征是判断,不是测量。同一个产品,运营和架构师可能给出不同的强度。矩阵要求每个档位附证据:并发、会话时长、操作预算、峰值 tick 成本、跨区 RTT。没有证据的档位,结论只能当假设。

第四,矩阵会随产品阶段失效。立项期的《泡泡工坊》是房间制 3、实时性 0,矩阵给单体;如果第二年加入实时对战,实时性变成 2 到 3,矩阵会立刻把大厅加房间推到前面。基线要复审,矩阵也要复审。

还有一类矩阵处理不了的情况:两个强特征指向不同拓扑。例如一个产品既要帧级实时,又要全球共享大地图,矩阵会给全球同服或分区世界一个折中分,但真实答案往往是拆成两套子系统——竞技战场按房间就近部署,世界地图按地域分片,中间用事件同步。矩阵能暴露冲突,不能替你拆系统。

常见错误与边界

第一,把特征布尔化。“有实时性”填 1,“有共享世界”填 1,五个 1 相加得到 5 分,矩阵退化成投票器。强度必须分档,否则无法区分“偶尔同步”和“每帧同步”。

第二,五个特征等权却不说明。等权是默认值,不是真理;如果实时性明显更重要,应该在评审记录里写清楚,而不是偷偷改分数。

第三,只看推荐列,不看代价列。推荐只说明方向,代价才决定能不能落地。大厅加房间的分配器、全球同服的地域路由,都是推荐背后的硬成本。

第四,用在线人数代替状态规模。状态规模看的是峰值 tick 成本和活跃实体数,不是注册玩家数;SLG 的王城战可以把四万人压进一个矩形,在线人数却可能很平稳。

第五,跨产品复用矩阵但不重填特征。《裂隙战场》的向量不能直接套到《泡泡工坊》,房间制相同不代表实时性和状态规模相同。

第六,矩阵不版本化。格子含义、权重、平局规则变化后,旧结论无法复现;矩阵应该和 ADR 一样带版本号。

小结

品类与拓扑决策矩阵把五个特征和五种拓扑放进同一张表,每个格子写清推荐程度、理由和代价;打分只用来排序,平局按运维成本裁决,组织与合规约束留给 ADR。三款产品回填后,矩阵与卷一前面的结论一致:《裂隙战场》落在大厅加房间,《群岛纪元》落在全球同服,《泡泡工坊》落在模块化单体,而且最后一个是“可选”而非“推荐”,必须由成本理由补足。

下一节收口:把基线、风险、未决决策和待压测假设装进一份可判定的检查清单,说明哪些问题可以带条件进入卷二,哪些必须在一卷内解决。

阅读导航:上一节:13.1 三款产品的架构基线 · 下一节:13.3 进入卷二:把系统装进边界 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查