同一个平台团队,三种完全不同的拓扑
北辰工作室的平台组曾经想做一个“统一游戏服务”:账号、匹配、房间、战斗、结算全部复用一套代码,三款产品只改配置。
这个想法在第一年就失败了。原因不是代码写得不好,而是三款产品的拓扑需求根本不同:
- 《裂隙战场》需要大量短生命周期房间,每个房间独立 tick,故障不能互相影响;
- 《群岛纪元》需要一个持续存在的全球世界,玩家分布在不同地域,但必须看到同一张地图;
- 《泡泡工坊》需要快速开房、快速结束,单局只有 3–8 分钟,后端越轻越好。
用一套拓扑硬套三种产品,最后得到的是一个同时具备三者缺点的系统:房间不够隔离、世界不够一致、登录不够轻。
品类不是市场标签,它直接决定服务端的拓扑。
拓扑谱系
在讨论“选哪个”之前,先把常见拓扑列清楚。游戏服务端大致有五种形态。
模块化单体
一个可部署单元,内部按模块划分边界。适合:
- 实时性要求不高;
- 团队小,发布频率低;
- 玩法还在快速试错,边界不稳定。
优点是部署简单、调试方便、没有网络故障。缺点是有状态部分难以独立扩容,故障域大。
大厅加房间
大厅无状态,负责登录、匹配、组队;房间有状态,负责一局游戏的完整生命周期。适合:
- 离散对局;
- 强实时;
- 每局玩家数量有限。
这是竞技游戏最常见的形态。关键是房间的创建、分配、回收和故障恢复。
分区世界
把持续世界按地图、服务器或玩家群体切成多个分片,每个分片有独立的状态和进程。适合:
- 有持久世界;
- 玩家数量超过单进程承载;
- 可以接受分片之间的有限交互。
难点在分片边界、跨分片交互和再均衡。
全球同服
所有玩家在逻辑上共享一个世界,物理上通过地域路由、分片和多副本实现。适合:
- SLG、开放世界;
- 玩家社交关系跨地域;
- 商业上需要“全球一个服”的卖点。
难点在全球延迟、数据一致性、跨地域结算和合规。
专用服池
预先准备一批专用服务器,按需分配给对局或会话,结束后回收。适合:
- FPS、竞技、房间制;
- 需要独占 CPU/网络资源;
- 对延迟和公平性要求高。
难点在池化、冷启动、扩容速度和成本。
决定拓扑的六个产品事实
拓扑不是由“技术先进程度”决定的,而是由产品事实决定的。下面六个问题,基本能区分大多数情况:
| 事实 | 问题 | 影响 |
|---|---|---|
| 并发玩家数 | 同时在线多少人? | 是否需要分片/多地域 |
| 会话时长 | 一局或一次在线多久? | 房间生命周期与状态持久化 |
| 实时性 | 玩家是否感知帧/操作延迟? | 是否需要专用 tick 与房间隔离 |
| 共享世界 | 玩家是否共享一个持久世界? | 分区 vs 房间 |
| 匹配制 | 玩法是否以离散对局为单位? | 大厅+房间 vs 世界服 |
| 地域敏感 | 跨地域延迟是否影响公平? | 是否需要地域路由/赛区 |
把这六个事实写成结构体,就可以做第一版选型:
// file: internal/topology/choose.go
// tier: L1
// verify: go test ./internal/topology/
package topology
// Kind is a first-pass server topology for one product.
type Kind string
const (
ModularMonolith Kind = "modular-monolith"
LobbyRoom Kind = "lobby-room"
ShardedWorld Kind = "sharded-world"
GlobalShard Kind = "global-single-shard"
DedicatedPool Kind = "dedicated-server-pool"
)
// Profile is the handful of product facts that actually change topology.
type Profile struct {
Name string
ConcurrentPlayers int
SessionMinutes int
RealTime bool // frame or operation budget is player-visible
SharedWorld bool // players share one persistent world
MatchBased bool // play happens in discrete matches
RegionSensitive bool // cross-region latency changes fairness
}
type Decision struct {
Kind Kind
Reasons []string
}
// Choose is a decision aid, not an architecture review. It makes the first
// fork explicit; the ADR still has to justify the cost.
func Choose(p Profile) Decision {
switch {
case p.MatchBased && p.RealTime:
return Decision{
Kind: LobbyRoom,
Reasons: []string{
"matches need isolated failure domains",
"the lobby can scale statelessly",
},
}
case p.SharedWorld && p.ConcurrentPlayers > 10000:
return Decision{
Kind: GlobalShard,
Reasons: []string{
"one persistent world with a large population",
"region routing must be decided before sharding",
},
}
case p.SharedWorld:
return Decision{
Kind: ShardedWorld,
Reasons: []string{
"persistent world, but population fits a small number of shards",
"shard by map region or server group first",
},
}
case p.RealTime:
return Decision{
Kind: DedicatedPool,
Reasons: []string{
"real-time sessions need dedicated capacity",
"no shared world means instances can be recycled",
},
}
default:
return Decision{
Kind: ModularMonolith,
Reasons: []string{
"no player-visible real-time pressure",
"start with one deployable and split only when the boundary hurts",
},
}
}
}
这个函数故意很粗。它不是架构评审的替代品,而是把“第一刀切在哪”显式化。真正的决策还要写 ADR,说明代价和退出条件。
对应的测试用三款产品作为固定样例:
// file: internal/topology/choose_test.go
// tier: L1
// verify: go test ./internal/topology/
package topology
import "testing"
func TestNorthstarProducts(t *testing.T) {
cases := []struct {
name string
p Profile
want Kind
}{
{
name: "裂隙战场",
p: Profile{
Name: "裂隙战场", ConcurrentPlayers: 200000,
SessionMinutes: 25, RealTime: true, MatchBased: true, RegionSensitive: true,
},
want: LobbyRoom,
},
{
name: "群岛纪元",
p: Profile{
Name: "群岛纪元", ConcurrentPlayers: 80000,
SessionMinutes: 60, SharedWorld: true, RegionSensitive: true,
},
want: GlobalShard,
},
{
name: "泡泡工坊",
p: Profile{
Name: "泡泡工坊", ConcurrentPlayers: 30000,
SessionMinutes: 8, MatchBased: true,
},
want: ModularMonolith,
},
}
for _, c := range cases {
got := Choose(c.p)
if got.Kind != c.want {
t.Fatalf("%s: got %s, want %s", c.name, got.Kind, c.want)
}
if len(got.Reasons) == 0 {
t.Fatalf("%s: decision has no reasons", c.name)
}
}
}
func TestSharedWorldBelowThreshold(t *testing.T) {
got := Choose(Profile{SharedWorld: true, ConcurrentPlayers: 5000})
if got.Kind != ShardedWorld {
t.Fatalf("got %s, want %s", got.Kind, ShardedWorld)
}
}
本机验证:
$ cd .local/checks/gsarch-lab
$ go test ./internal/topology/ -v
=== RUN TestNorthstarProducts
--- PASS: TestNorthstarProducts (0.00s)
=== RUN TestSharedWorldBelowThreshold
--- PASS: TestSharedWorldBelowThreshold (0.00s)
PASS
ok gsarchlab/internal/topology 0.4s
三款产品的选择与理由
《裂隙战场》:大厅加房间
输入:20 万在线、单局 25 分钟、强实时、匹配制、地域敏感。
选择:大厅无状态,房间有状态,按赛区部署。
理由:
- 一局比赛是一个天然故障域,房间进程崩溃只影响 10 个人,不影响其他房间;
- 大厅只做登录、匹配、组队,可以水平扩容;
- 赛区隔离让跨地域延迟不进入对局公平性。
代价:
- 房间是有状态服务,发布需要排空;
- 大厅与房间之间多了一次网络调用;
- 需要房间分配器和暖池,冷启动要优化。
《群岛纪元》:全球同服
输入:8 万在线、长时间在线、非实时、共享世界、地域敏感。
选择:逻辑上全球一个世界,物理上按地域路由、按地图分片。
理由:
- SLG 的核心体验是“所有人看到同一张地图”,分服会破坏社交和战争叙事;
- 8 万在线可以放在少量分片里,不需要按玩家数无限切分;
- 地域路由解决延迟,分片解决单进程容量。
代价:
- 跨分片交互复杂;
- 全球结算需要统一时间线;
- 数据合规和跨境传输必须提前设计。
《泡泡工坊》:模块化单体起步
输入:3 万在线、单局 8 分钟、有房间但实时性弱、团队只有 3 人。
选择:先做一个模块化单体,把房间作为内部模块;达到规模阈值再拆。
理由:
- 团队只有 3 个人,拆成多个服务会先把运维成本压垮;
- 休闲玩法对帧不敏感,单进程可以承载;
- 房间模块边界清晰,未来拆分成本可控。
代价:
- 单体故障域大;
- 有状态房间和登录在同一进程,发布需要排空;
- 规模上来后必须拆分,不能一直将就。
注意:Choose 函数对《泡泡工坊》返回 ModularMonolith,即使它是匹配制。这是有意的:匹配制只说明需要房间,不说明房间必须独立部署。第一版决策函数看的是“实时性 + 匹配制”的组合,而不是单个布尔值。
三份 ADR 的核心结论
把上面的选择写成 ADR,每份只需要回答四个问题:
| ADR | 决定 | 主要备选 | 退出条件 |
|---|---|---|---|
| ADR-002 | 《裂隙战场》采用大厅+房间 | 单体;全球同服 | 单赛区房间数超过调度器上限,或跨赛区匹配成为主玩法 |
| ADR-003 | 《群岛纪元》采用全球同服+地域分片 | 分区分服;按联盟分服 | 跨地域延迟导致战争结果不可接受,或合规要求数据本地化 |
| ADR-004 | 《泡泡工坊》先做模块化单体 | 直接上大厅+房间;Serverless | 单进程房间数接近承载上限,或团队扩到能独立运维 |
“退出条件”是 ADR 里最容易被忽略、却最重要的一栏。没有退出条件,架构就会变成信仰;有了退出条件,团队知道什么时候该重新评估。
拓扑不是永久决定
拓扑会随着产品阶段变化:
立项期:模块化单体,先验证玩法
│
├── 玩法成立、在线增长 → 拆大厅与房间
│
├── 全球化发行 → 加地域路由与分片
│
└── 多产品复用 → 平台服务化
北辰三款产品也走过这条路:《泡泡工坊》在第二年拆出了房间服务,《裂隙战场》在第三年加了跨赛区匹配,《群岛纪元》在出海时重画了地域路由。
架构文档如果不记录“当前阶段”和“下一阶段触发条件”,就会被当成永久真理。
常见错误与边界
第一,按技术潮流选拓扑。微服务、Serverless、Service Mesh 都不是品类需求的答案。
第二,把“在线人数”当成唯一指标。会话时长、实时性、共享世界同样决定拓扑。
第三,忽略团队规模。三个人的团队运维不了十个服务。
第四,把第一版决策函数当最终结论。它只负责切第一刀,ADR 负责解释代价。
第五,不写退出条件。没有退出条件,架构就无法演进。
第六,三款产品强行共用一套拓扑。共用平台服务可以,共用拓扑通常不行。
小结
品类通过六个产品事实影响拓扑:并发、会话时长、实时性、共享世界、匹配制、地域敏感。北辰三款产品分别落到大厅+房间、全球同服、模块化单体三种形态,各自的代价和退出条件都不同。
这一节把“品类如何决定拓扑”写成了一个可测试的决策函数,但它只是第一版。下一章进入架构决策方法:如何用质量属性、ADR 和评审,把第一版决策变成团队可执行的共识。
阅读导航:上一节:1.2 三类延迟预算:帧、操作、结算 · 下一节:2.1 质量属性不是形容词,而是可测量的场景 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。