《游戏服务端架构·规模化、全球化与成本》13.1 容量表、成本表与合规表

三张表不是季度汇报的附件,而是三条互相咬合的控制回路:容量变化触发成本重算,合规变化触发架构复审,成本越界触发配额与拓扑调整。本文给出容量表、成本表、合规表的字段、填写人、更新频率与联动规则,并用 Go 实现缺项检测、故障域余量校验和跨表影响分析。

三张表不是一个汇总页

《群岛纪元》第四赛季扩容时,容量表从 42 行涨到 61 行:大地图分片多了 6 个,行军结算队列多了 3 个消费者组,跨区同步链路多了 2 个区域。成本表没有动。两个月后财务对账,发现月度云账单比预算高出 23%,但没有人能说清是哪一次扩容带来的。

同一季度,合规团队把欧洲区账号删除的承诺从 30 天改成 14 天。合规表更新了,架构没有复审。三个月后的一次删除演练暴露了问题:备份保留周期是 35 天,主库删了,备份里还在。合规承诺在纸面上成立,在介质上不成立。

这两件事的根因相同:三张表被当成了三个部门的季度附件,而不是三条互相咬合的控制回路。容量表回答“还能接多少”,成本表回答“每个单位花多少”,合规表回答“哪些数据必须怎么处理”。任何一张变化,另外两张都可能失效。

三张表各自的读者与节奏

先明确谁看、谁填、多久填一次。没有这一步,字段设计得再漂亮也会过期。

表主读者填写人更新频率触发更新的额外事件
容量表SRE、技术负责人服务 owner + SRE每月大版本、活动、事故、扩容
成本表产品负责人、财务技术负责人 + 财务 BP每月容量变化、配额调整、续约
合规表合规、平台、法务合规对接人 + 数据 owner每季度新法规、新区域、新数据类

“每月”不是指月底抄一次监控。容量表的月度更新要重新核对峰值窗口、扩容触发线和故障域余量;成本表要重新算单位成本;合规表即使没有法规变化,也要抽查一条删除链路是否真的跑通。

表一:容量表

容量表的最小可用字段如下。它必须能独立回答“现在多满、掉一个区会怎样、什么时候必须扩”。

字段含义口径来源
service服务稳定标识与计费、告警同名服务目录
layercpu / memory / bandwidth / connections四层分开第 1 章
peak峰值需求5 分钟窗口 P95,活动另记监控
capacity可用容量同层同单位容量模型
trigger扩容触发线0–1 的利用率扩容时长反推
failure_loss最大单故障域损失比例0–1,不含 1多活拓扑
owner对该行负责的人必须是个人或轮值组组织架构
evidence数据来源看板或压测报告链接可审计
updated最后核对日期ISO 日期流程

三个口径最容易写错。

第一,peak 不是月平均在线折算出来的。它要取业务峰值窗口的实际值;如果本月有活动,活动峰值单独占一行,不能和日常峰值混在一起。

第二,capacity 必须是可购买、可观测的绝对量,不能写“剩余 30%”。百分比无法参与故障域计算,也无法和成本表对齐。

第三,failure_loss 要写真实拓扑。三可用区各三分之一,失去一个区就是 0.333;如果分片不能跨区迁移,损失不是容量,而是那部分玩家,这一行要备注“不可迁移”,不能只用数字表达。

以三款产品为例,容量表的粒度不同:

产品主要行峰值口径故障域
《裂隙战场》房间服 CPU、网关带宽、观战出口同时在线房间数 × 单房成本单可用区
《群岛纪元》地图分片 CPU、结算队列积压、跨区链路赛季开服 30 分钟单区域
《泡泡工坊》大厅 QPS、房间创建速率、CDN 回源买量投放小时单可用区

《裂隙战场》按房间算,因为它的成本随房间数线性增长;《群岛纪元》按地图分片算,因为单分片能承载的单位数有硬上限;《泡泡工坊》按入口 QPS 算,因为大厅无状态,真正的瓶颈在房间创建和 CDN。

表二:成本表

成本表要能回答三个问题:每个单位花多少、离配额还有多远、规模到多少才开始划算。

字段含义口径
service服务稳定标识与容量表一致
monthly_usd当月总成本含计算、存储、出口、许可
unit_name业务单位room-hour、MAU、GB-month
units当月单位数与 unit_name 同口径
unit_cost单位成本monthly_usd / units
quota_usd月度配额0 表示未设
break_even_units固定成本摊平所需单位数0 表示不适用
owner预算负责人必须是能签字的人
source账单导出标识可追溯到账单行
updated最后核对日期ISO 日期

单位的选择决定这张表有没有用。同一个服务,用“每房间小时”算,和用“每 MAU”算,会得到完全不同的结论。前者适合《裂隙战场》,因为成本主要由对局时长驱动;后者适合《泡泡工坊》,因为大量玩家只登录领奖励,不开房间。

配额不是预算的同义词。预算是“最多能花多少”,配额是“超过多少必须解释”。成本表里 quota_usd 的作用是触发讨论,不是自动停机。真正要停机的是单位成本持续高于收入贡献,那是另一个决策。

拐点是成本表里最容易被忽略的一列。固定成本(专用节点、预留实例、跨区专线)在低规模时摊不平;break_even_units 告诉你“这个架构至少要跑到多少单位才比按量付费便宜”。低于拐点还坚持专用集群,通常是在为“架构看起来专业”付费。

表三:合规表

合规表不是法规摘抄。它要把一条规则落到数据类、服务、区域和两个时限上。

字段含义示例
rule规则标识eu-account-deletion
data_class数据分类account、payment、telemetry
services持有该数据的服务稳定服务标识列表
region适用区域eu、cn、global
retention_days允许保留天数180
deletion_days删除请求完成时限14
legal_basis合法性依据合同、同意、法定义务
owner数据 owner平台或产品
evidence删除演练或审计记录链接
updated最后核对日期ISO 日期

两个时钟必须分开写。retention_days 是“最多能留多久”,deletion_days 是“用户提出删除后多久必须删完”。前者约束数据生命周期,后者约束删除链路。把两者合成一个“保留 30 天”,最后一定会在备份、日志、数仓三处出现口径冲突。

services 必须写服务标识,不能写团队名。写“数据平台组”看起来责任清楚,但代码无法据此找到持有数据的服务,架构复审时也无从判断影响面。

区域字段不能只写 global。全球同服不等于全球同一套数据规则;《群岛纪元》的账号数据按区域驻留,战斗统计可以跨境,这两类数据要拆成两行。

三张表怎么联动

三张表放在同一个服务标识体系下,才能形成回路:

容量表 --(Δ≥10% 或新增服务)--> 成本表 --(超配额 10%)--> 预算/拓扑决策
   |                              ^
   |(故障域利用率 > 1)            |(单位成本变化)
   v                              |
架构复审 <--(新数据类/新区域)------ 合规表

具体规则可以写成四条:

变化触发动作时限
容量变化 ≥10% 或新增服务成本表重算单位成本与拐点5 个工作日
故障域利用率 >1架构复审补余量、改拓扑或接受风险下次发布前
新数据类、新区域、法规变化合规表 + 架构复审重画数据流与删除链路10 个工作日
删除时限短于备份保留合规表 + 容量表改备份策略或缩短保留30 天

第一条是容量到成本的单向触发。扩容 10% 不一定让成本涨 10%,因为固定成本被摊薄;但不重算就永远不知道真实单位成本。

第二条是容量到架构的反向触发。故障域利用率超过 1,说明当前拓扑已经接不住一次单区故障,这不是“再加一台机器”能解决的。

第三条是合规到架构的触发。新数据类往往意味着新的存储、新的跨境路径、新的删除链路,必须重新做数据流图。

第四条是合规内部的冲突检测。删除时限比备份保留还短,说明承诺和介质不一致;这类冲突必须在演练前发现,而不是在监管问询时发现。

可运行片段

下面的 link.go 把上述联动写成可执行规则。同包的 table.go 定义三张表的行结构、整表校验和缺项扫描;所有校验方法都围绕“未知不能写成 0”这条契约。

// file: internal/capacitytable/link.go
// tier: L1
// verify: go test ./internal/capacitytable/
package capacitytable

import "fmt"

type ChangeKind string

const (
	CapacityChanged   ChangeKind = "capacity"
	ComplianceChanged ChangeKind = "compliance"
)

type Change struct {
	Kind    ChangeKind
	Service string
	Delta   float64 // fractional capacity change; zero means "review only"
}

type Impact struct {
	Service            string
	RecomputeCost      bool
	ArchitectureReview bool
	Reasons            []string
}

// Link encodes the two rules the three tables cannot enforce by themselves:
// a capacity change invalidates the unit-economics row, and a compliance
// change on a held data class reopens the architecture decision.
func Link(t Table, c Change) (Impact, error) {
	impact := Impact{Service: c.Service}
	switch c.Kind {
	case CapacityChanged:
		found := false
		for _, r := range t.Capacity {
			if r.Service == c.Service {
				found = true
				break
			}
		}
		if !found {
			return Impact{}, fmt.Errorf("%w: no capacity row for %s", ErrTable, c.Service)
		}
		if c.Delta != 0 {
			impact.RecomputeCost = true
			impact.Reasons = append(impact.Reasons, fmt.Sprintf("capacity changed by %.0f%%", c.Delta*100))
		}
		for _, r := range t.Capacity {
			if r.Service == c.Service && r.FailureUtil() > 1 {
				impact.ArchitectureReview = true
				impact.Reasons = append(impact.Reasons, fmt.Sprintf("%s no longer survives losing %.0f%% capacity", r.Layer, r.FailureDomainLoss*100))
			}
		}
	case ComplianceChanged:
		found := false
		for _, r := range t.Compliance {
			for _, svc := range r.Services {
				if svc != c.Service {
					continue
				}
				found = true
				impact.ArchitectureReview = true
				impact.Reasons = append(impact.Reasons, fmt.Sprintf("%s applies to %s (%d-day retention)", r.Rule, r.DataClass, r.RetentionDays))
			}
		}
		if !found {
			return Impact{}, fmt.Errorf("%w: no compliance row covers %s", ErrTable, c.Service)
		}
	default:
		return Impact{}, fmt.Errorf("%w: unknown change kind %q", ErrTable, c.Kind)
	}
	return impact, nil
}

Link 只接受两类变化:容量变化和合规变化。成本变化不触发架构复审,因为成本超配额首先是一个预算决策,不是拓扑决策;它应该走配额流程,而不是自动改架构。

读代码:联动规则的三个边界

第一,Delta == 0 表示“只复审、不重算”。它用于季度评审:容量没变,但拓扑或依赖变了,仍然要看一眼成本。把 0 当成“没有变化所以什么都不做”,会让评审退化成签字。

第二,找不到对应行时返回错误,而不是静默通过。Link 对容量变化要求至少有一行 capacity,对合规变化要求至少有一行 compliance 覆盖该服务。拼错服务名、用了旧服务名、服务刚拆分但表没更新,都会在这里暴露。

第三,故障域判断遍历该服务的所有层。一个服务可能在 CPU 层很宽裕,但在连接数层已经超过故障域余量;只看 CPU 会漏掉真正的单点。FailureUtil 的分母是 capacity × (1 - loss),损失为 0 时退化成普通利用率。

合规分支会为一个服务返回多条原因:账号删除、支付留存、遥测跨境可能同时覆盖它。Reasons 是给人看的审计文本,不要拿它做程序判断;程序判断应该用 ArchitectureReview 布尔值和结构化字段。

失败模式与兜底

只写百分比。 容量表写“CPU 剩余 30%”,无法算故障域,也无法和成本对齐。兜底是强制绝对量,百分比只作为派生列。

单位混用。 成本表同一列里既有 room-hour 又有 MAU,单位成本没有可比性。兜底是每行只允许一个 unit_name,跨服务比较前先换算到统一单位。

合规表写团队名。 services 填“数据平台组”,代码找不到持有者。兜底是服务目录校验,填写的标识必须存在。

删除时限短于备份保留。 主库删了,备份还在,承诺不成立。兜底是把备份保留纳入合规表的证据字段,删除演练必须覆盖备份恢复点。

三张表更新不同步。 容量表每月更新,成本表每季度更新,联动就断了。兜底是把“容量变化 ≥10%”做成成本表的强制触发器,而不是靠人记得。

没有 owner。 行存在,但没人对数字负责。兜底是 Missing() 把空 owner 列为缺项,评审会不通过。

把所有服务塞进一张表。 行数太多,没人看得完。兜底是按产品域拆表,但保持服务标识全局唯一。

常见错误与边界

第一,把容量表当监控大盘。表是决策记录,大盘是原始数据;表要保留峰值窗口和判断,不需要秒级曲线。

第二,用容量利用率推算成本。成本由单位数和单价驱动,不由利用率驱动;闲置容量也会花钱。

第三,合规表只列法规名。法规名不能告诉工程师改哪个字段、删哪条链路。

第四,把 retention_days 和 deletion_days 当成一个数。一个是上限,一个是承诺时限。

第五,成本表不含共享平台分摊。登录、匹配、观测这些共享组件不摊到产品,单位经济会被低估。

第六,三张表用不同的服务命名。没有稳定主键,联动只能靠人肉匹配。

第七,联动规则写在文档里但不执行。规则必须进代码或进发布检查单,否则下一次扩容仍然会漏掉成本表。

小结

三张表的价值不在“汇总”,而在“互相触发”。容量表给出绝对量和故障域余量,成本表给出单位经济、配额和拐点,合规表给出数据类、区域和两个时限。三者用同一个服务标识连接,容量变化触发成本重算,合规变化触发架构复审。

这一节把表结构和联动规则做成了可执行代码。下一节换一个视角:当多个架构选项同时摆在桌上,如何用一张带权重的矩阵把争论变成可复现的评分,并识别哪些分数其实是不能妥协的硬约束。

阅读导航:上一节:12.3 《泡泡工坊》的轻量出海 · 下一节:13.2 全球化架构决策矩阵 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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