《游戏服务端架构·规模化、全球化与成本》13.3 进入卷四:平台化与治理

卷三把容量、成本、合规和全球化决策做成了表与矩阵,但多产品复用、平台契约、发布治理和成本治理仍然停在产品团队各自为战的状态。本文给出能力升级为平台的判据、四类交接事项、产品与平台的责任边界、缺口报告与验收标准,并用 Go 实现交接清单的结构校验、依赖环检测和责任归属检查。

卷三结束时的四个未决问题

到 13.2 结束,三款产品都有了自己的容量表、成本表和合规表,也都用矩阵解释了自己的全球化选择。但这些产物仍然停在产品团队内部:同一套会话组件被复制了三份,平台 API 没有版本号,发布节奏互相踩踏,共享成本没人认领。

问题现场症状为什么产品团队单独解决不了
多产品复用会话、在线状态、匹配各有一套复用需要跨团队契约和长期维护承诺
平台契约改一个字段,三个产品同时改代码契约需要版本、兼容窗口和废弃策略
发布治理产品每周发,平台每月发,回滚责任不清发布机制必须由平台统一提供
成本治理每个产品有自己的成本表,共享成本没人摊分摊需要统一账单口径和配额执行

这四个问题不是卷三没讲完,而是卷三的决策层方法已经到边界。再往下走,需要组织层面的平台化与治理,那是卷四的主题。本节做三件事:判断哪些能力该升级为平台,给出交接清单和责任边界,说明怎样验收一次交接。

什么时候把能力升级为平台

平台化最大的风险不是技术,而是过早。两个产品刚有一点相似需求,就抽一个“通用中台”,结果两边都不满意,最后各留一份私有分支。

比较稳的判据是三条:

  1. 第二个产品开始复制:同一能力出现第二份实现,且复制的是逻辑而不是配置;
  2. 两个团队开始同时 on-call:同一组件的故障需要两个团队各自排查;
  3. 风险开始共享:账号、支付、合规、安全这类能力,一个产品出问题会波及全公司。

满足第一条,先把能力做成“产品内的共享库”,由原团队维护,不承诺跨团队 SLA。满足第二条或第三条,才升级为平台,需要契约、SLO、版本策略和独立预算。

能力归属理由
账号与身份平台合规与安全风险共享,重复实现代价最高
会话与在线状态平台三款产品逻辑一致,网关层强依赖
匹配核心平台队列、分段、扩圈算法可复用
匹配规则产品分段边界和玩法偏好是产品差异
战斗结算产品与玩法强耦合,平台化收益低
支付与发货平台对账、退款、合规链路必须统一
活动系统产品节奏和玩法差异大,复用会拖慢上线
可观测性平台指标、日志、追踪需要统一口径
成本分摊平台账单和标签体系必须全局一致

这张表的关键不是“平台越多越好”,而是把“机制”和“策略”分开。平台拥有机制:会话怎么保活、契约怎么版本化、成本怎么打标签;产品拥有策略:匹配分段怎么划、活动什么时候开、预算怎么花。

交接清单:四类事项

交接不是把代码挪个仓库。每一类事项都要有负责人、契约引用和验收标准。

类别事项负责人契约验收
多产品复用共享会话与在线状态平台platform.reuse.v1两个产品连续运行 30 天
平台契约API、错误码、SLO、废弃策略平台platform.api.v1三个产品完成接入
发布治理发布列车、灰度、回滚开关平台 + 产品platform.release.v1完成一次联合回滚演练
成本治理预算归属、单位经济、配额产品 + 财务platform.cost.v1月度分摊可对账

多产品复用

复用的是能力和契约,不是产品代码。共享会话组件要定义清楚:会话生命周期、踢下线语义、跨区迁移时的状态归属、故障时的降级行为。这些语义如果由某个产品私自扩展,复用就会退化成三份分叉。

验收标准要可执行:“两个产品连续运行 30 天”比“完成接入”更难造假。30 天里要覆盖一次发布、一次扩容、一次故障演练。

平台契约

契约至少包含四样东西:接口与错误码、SLO、版本策略、废弃窗口。没有错误码,产品只能靠字符串匹配;没有 SLO,平台无法承诺;没有版本策略,每次变更都是突袭;没有废弃窗口,产品不敢升级。

平台契约的读者是产品工程师,不是平台内部。契约文档要能回答“我怎么在本地跑通”“失败时我会看到什么”“下一个版本什么时候必须升”。

发布治理

发布治理的核心不是“统一发版日”,而是“变更可回滚、责任可定位”。平台提供机制:发布列车、灰度比例、回滚开关、变更审计;产品决定内容:这次发什么、灰度给谁、什么时候全量。

联合回滚演练是验收的关键。只写在文档里的回滚流程,在真实故障中通常跑不通;演练要覆盖数据库迁移、缓存兼容和客户端协议三个层面。

成本治理

成本治理要把“谁花钱”变成“谁负责”。平台提供分摊管道:标签规范、账单导出、单位经济报表;产品拥有预算:每个月的配额、超支解释、优化计划。

验收标准是“月度分摊可对账”:财务的账单总额、平台的分摊报表、产品的成本表三者能对上。对不上时,先修口径,不要先追责。

哪些决策留在产品团队

平台化最常见的组织错误,是把产品决策也收走。平台不应该决定《裂隙战场》的匹配分段,也不应该决定《群岛纪元》的赛季长度。

决策负责人平台角色
内容与活动节奏产品提供容量与发布机制
活动期容量预留产品 + SRE提供扩容与配额能力
区域上线顺序产品提供区域目录与合规结论
玩法匹配规则产品提供匹配核心与可配置参数
预算与成本目标产品 + 财务提供分摊数据与配额执行
API 版本与废弃窗口平台决定并公告
共享组件 SLO平台定义、度量、对外承诺
配额执行机制平台实现,不替产品定额度

一句话:平台拥有机制和契约,产品拥有策略和预算。 平台不做产品决策,产品不改平台契约。跨越这条线的需求,应该走契约变更,而不是私下 fork。

三种责任错配

代码只能检查结构,但结构错配往往先于技术故障出现。交接清单要能识别三种典型错配。

第一,平台事项依赖产品私有事项。平台承诺了一个 SLA,但它的实现依赖某个产品团队下个月才做的迁移。这个 SLA 不可兑现,因为平台无法控制依赖方的优先级。

第二,产品拥有共享契约。共享组件的接口由某个产品团队维护,其他产品只能提需求。这个团队没有跨团队授权,也没有动力为别人排期,最后契约会退化成“谁声音大谁改”。

第三,“所有人负责”等于没人负责。清单里写“平台 + 产品共同负责”,但没有单一 accountable owner,出问题时两边都能说“对方没做”。

缺口报告长什么样

一份不完整的交接清单,跑完检查后应该得到类似下面的报告(示例输出,不是本机运行结果):

area release-governance: no item
item H1: platform item depends on product item H3
item H1: platform item has no contract

三条信息分别对应三类问题:某个必需领域完全没有交接事项;平台事项没有契约引用;平台事项依赖了产品私有事项。报告要稳定排序,才能进代码评审和发布门禁。

可运行片段

下面的 handoff.go 把交接清单做成可校验结构。Validate 检查结构完整性,Gaps 检查责任与覆盖是否符合平台化策略。

// file: internal/volfourhandoff/handoff.go
// tier: L1
// verify: go test ./internal/volfourhandoff/
package volfourhandoff

import (
	"errors"
	"fmt"
	"sort"
)

var ErrHandoff = errors.New("volfourhandoff: invalid checklist")

type Area string

const (
	Reuse    Area = "multi-product-reuse"
	Contract Area = "platform-contract"
	Release  Area = "release-governance"
	Cost     Area = "cost-governance"
)

var RequiredAreas = []Area{Reuse, Contract, Release, Cost}

type Owner string

const (
	Platform Owner = "platform"
	Product  Owner = "product"
)

// Item is one decision leaving volume three. ContractRef is the platform
// interface or SLA the item commits to; without it a "handoff" is only a wish.
type Item struct {
	ID          string
	Title       string
	Area        Area
	Owner       Owner
	ContractRef string
	DependsOn   []string
}

type Checklist struct {
	Items []Item
}

func (c Checklist) Validate() error {
	byID := make(map[string]Item, len(c.Items))
	for i, it := range c.Items {
		switch {
		case it.ID == "":
			return fmt.Errorf("%w: item[%d] has no id", ErrHandoff, i)
		case it.Title == "":
			return fmt.Errorf("%w: item %s has no title", ErrHandoff, it.ID)
		case !validArea(it.Area):
			return fmt.Errorf("%w: item %s has unknown area %q", ErrHandoff, it.ID, it.Area)
		case !validOwner(it.Owner):
			return fmt.Errorf("%w: item %s has unknown owner %q", ErrHandoff, it.ID, it.Owner)
		}
		if _, dup := byID[it.ID]; dup {
			return fmt.Errorf("%w: duplicate id %s", ErrHandoff, it.ID)
		}
		byID[it.ID] = it
	}
	for _, it := range c.Items {
		for _, dep := range it.DependsOn {
			if dep == it.ID {
				return fmt.Errorf("%w: item %s depends on itself", ErrHandoff, it.ID)
			}
			if _, ok := byID[dep]; !ok {
				return fmt.Errorf("%w: item %s depends on missing %s", ErrHandoff, it.ID, dep)
			}
		}
	}
	if cycle := findCycle(c.Items, byID); len(cycle) > 0 {
		return fmt.Errorf("%w: dependency cycle %v", ErrHandoff, cycle)
	}
	return nil
}

// Gaps reports policy problems that Validate deliberately allows: missing
// areas, platform items without a contract, ownership that contradicts the
// area, and platform items that depend on a single product.
func (c Checklist) Gaps() []string {
	var out []string
	seen := make(map[Area]int)
	byID := make(map[string]Item, len(c.Items))
	for _, it := range c.Items {
		seen[it.Area]++
		byID[it.ID] = it
	}
	for _, area := range RequiredAreas {
		if seen[area] == 0 {
			out = append(out, fmt.Sprintf("area %s: no item", area))
		}
	}
	for _, it := range c.Items {
		if it.Owner == Platform && it.ContractRef == "" {
			out = append(out, fmt.Sprintf("item %s: platform item has no contract", it.ID))
		}
		if (it.Area == Reuse || it.Area == Contract) && it.Owner != Platform {
			out = append(out, fmt.Sprintf("item %s: area %s must be owned by platform", it.ID, it.Area))
		}
		if it.Owner != Platform {
			continue
		}
		for _, dep := range it.DependsOn {
			if byID[dep].Owner == Product {
				out = append(out, fmt.Sprintf("item %s: platform item depends on product item %s", it.ID, dep))
			}
		}
	}
	sort.Strings(out)
	return out
}

func (c Checklist) ByOwner(o Owner) []Item {
	var out []Item
	for _, it := range c.Items {
		if it.Owner == o {
			out = append(out, it)
		}
	}
	return out
}

func (c Checklist) Coverage() map[Area]int {
	out := make(map[Area]int, len(RequiredAreas))
	for _, area := range RequiredAreas {
		out[area] = 0
	}
	for _, it := range c.Items {
		out[it.Area]++
	}
	return out
}

func findCycle(items []Item, byID map[string]Item) []string {
	const (
		white = 0
		grey  = 1
		black = 2
	)
	state := make(map[string]int, len(items))
	var stack []string
	var visit func(string) []string
	visit = func(id string) []string {
		switch state[id] {
		case grey:
			return append(append([]string{}, stack...), id)
		case black:
			return nil
		}
		state[id] = grey
		stack = append(stack, id)
		for _, dep := range byID[id].DependsOn {
			if cycle := visit(dep); len(cycle) > 0 {
				return cycle
			}
		}
		stack = stack[:len(stack)-1]
		state[id] = black
		return nil
	}
	for _, it := range items {
		if cycle := visit(it.ID); len(cycle) > 0 {
			return cycle
		}
	}
	return nil
}

func validArea(a Area) bool {
	switch a {
	case Reuse, Contract, Release, Cost:
		return true
	}
	return false
}

func validOwner(o Owner) bool {
	switch o {
	case Platform, Product:
		return true
	}
	return false
}

两个方法分开是有意的:一份空清单在结构上是合法的,但它有四个领域缺口;一份结构合法的清单,也可能因为平台事项没写契约而不可交接。

读代码:结构校验与责任校验

Validate 只做四件事:ID 非空且唯一、标题非空、领域与负责人合法、依赖存在且无环。它不判断“这个事项该不该由平台做”,那是 Gaps 的职责。

Gaps 检查四条策略:四个必需领域是否都有事项;平台事项是否有契约引用;复用与契约领域是否由平台拥有;平台事项是否依赖产品事项。四条都来自本节前面的组织规则,不是通用最佳实践。

findCycle 用三色标记找环,返回的是环上的路径,例如 [A B A]。只返回布尔值不够,评审时需要知道是哪几个事项互相依赖。

Coverage 会先把四个必需领域初始化为 0,再累加。这样“某个领域完全没有事项”会以 0 出现,而不是从 map 里消失。

调用顺序是先 Validate 再 Gaps。Gaps 假设依赖 ID 都存在;如果清单结构不合法,先修结构,再看缺口报告。

卷四入口:平台工程、版本与兼容、SLO 与治理

平台工程

平台工程要回答:如何让产品团队自助接入,而不是排队等平台排期。铺好的主路径(paved road)、租户隔离、配额与自助扩缩容,都是这个方向的具体问题。判断平台工程是否成功,看产品接入时间,不看平台功能数量。

版本与兼容

版本与兼容要回答:契约怎么版本化,支持几个版本,废弃窗口多长,数据迁移怎么和契约变更配合。第 11 章的兼容窗口和回滚在这里从单产品问题升级为多产品问题:一个平台契约的破坏性变更,会同时影响三款产品的发布计划。

SLO 与治理

SLO 与治理要回答:错误预算怎么定,发布门禁怎么用,成本超支怎么处理,例外谁批准。治理不是开会,而是把“什么情况下必须停”写成可执行的规则,并让规则有后果。

交接完成的验收标准

一次可接受的交接,至少满足五条:

  1. 四个必需领域各至少有一个事项;
  2. 每个平台事项都有契约引用;
  3. 依赖图无环,且所有依赖都能解析;
  4. 复用与契约领域由平台拥有,不存在产品拥有共享契约;
  5. 产品与平台双方负责人在同一份清单上签字,并约定 30 天后复审。

缺口报告为空只是必要条件,不是充分条件。契约是否真的可执行、SLO 是否真的度量、回滚是否真的演练过,仍然要靠第 11 章的方法验证。

失败模式与兜底

交接写成 PPT。 没有契约引用、没有验收标准,交接只是一次汇报。兜底是每个平台事项必须有 ContractRef。

契约没有废弃策略。 平台不敢改,产品不敢升,最后契约僵死。兜底是版本策略里写清支持窗口和公告周期。

SLO 没有后果。 错误预算超了也不影响发布,SLO 就变成装饰。兜底是把预算消耗和发布门禁绑定。

“共同负责”。 没有单一 owner,故障时互相等待。兜底是每个事项一个 accountable owner,另一个角色只能是 consulted。

常见错误与边界

第一,把平台化当成组织合并。平台是契约和机制,不是把所有团队并成一个。

第三,平台契约由平台单方面定义。契约的消费者是产品,必须共同评审。

第四,把发布治理理解成统一发版日。真正要统一的是回滚、灰度和审计能力。

第五,成本治理只做报表。没有配额执行和预算责任,报表不会改变行为。

第六,用缺口报告代替判断。代码能发现缺项,不能判断契约是否合理;后者仍然需要人评审。

小结

卷三用三张表把容量、成本、合规变成可核对的记录,用决策矩阵把全球化选择变成可复现的评分。卷四要解决的是这些记录和评分如何跨产品复用:哪些能力升级为平台,哪些决策留在产品,契约怎么版本化,发布和成本怎么治理。

交接的完成标志不是文档签字,而是缺口报告为空、契约可执行、责任可定位、复审有日期。下一节回到目录,按阅读路径重新组织全卷。

阅读导航:上一节:13.2 全球化架构决策矩阵 · 下一节:目录与阅读路径 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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