卷三结束时的四个未决问题
到 13.2 结束,三款产品都有了自己的容量表、成本表和合规表,也都用矩阵解释了自己的全球化选择。但这些产物仍然停在产品团队内部:同一套会话组件被复制了三份,平台 API 没有版本号,发布节奏互相踩踏,共享成本没人认领。
| 问题 | 现场症状 | 为什么产品团队单独解决不了 |
|---|---|---|
| 多产品复用 | 会话、在线状态、匹配各有一套 | 复用需要跨团队契约和长期维护承诺 |
| 平台契约 | 改一个字段,三个产品同时改代码 | 契约需要版本、兼容窗口和废弃策略 |
| 发布治理 | 产品每周发,平台每月发,回滚责任不清 | 发布机制必须由平台统一提供 |
| 成本治理 | 每个产品有自己的成本表,共享成本没人摊 | 分摊需要统一账单口径和配额执行 |
这四个问题不是卷三没讲完,而是卷三的决策层方法已经到边界。再往下走,需要组织层面的平台化与治理,那是卷四的主题。本节做三件事:判断哪些能力该升级为平台,给出交接清单和责任边界,说明怎样验收一次交接。
什么时候把能力升级为平台
平台化最大的风险不是技术,而是过早。两个产品刚有一点相似需求,就抽一个“通用中台”,结果两边都不满意,最后各留一份私有分支。
比较稳的判据是三条:
- 第二个产品开始复制:同一能力出现第二份实现,且复制的是逻辑而不是配置;
- 两个团队开始同时 on-call:同一组件的故障需要两个团队各自排查;
- 风险开始共享:账号、支付、合规、安全这类能力,一个产品出问题会波及全公司。
满足第一条,先把能力做成“产品内的共享库”,由原团队维护,不承诺跨团队 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 与治理要回答:错误预算怎么定,发布门禁怎么用,成本超支怎么处理,例外谁批准。治理不是开会,而是把“什么情况下必须停”写成可执行的规则,并让规则有后果。
交接完成的验收标准
一次可接受的交接,至少满足五条:
- 四个必需领域各至少有一个事项;
- 每个平台事项都有契约引用;
- 依赖图无环,且所有依赖都能解析;
- 复用与契约领域由平台拥有,不存在产品拥有共享契约;
- 产品与平台双方负责人在同一份清单上签字,并约定 30 天后复审。
缺口报告为空只是必要条件,不是充分条件。契约是否真的可执行、SLO 是否真的度量、回滚是否真的演练过,仍然要靠第 11 章的方法验证。
失败模式与兜底
交接写成 PPT。 没有契约引用、没有验收标准,交接只是一次汇报。兜底是每个平台事项必须有 ContractRef。
契约没有废弃策略。 平台不敢改,产品不敢升,最后契约僵死。兜底是版本策略里写清支持窗口和公告周期。
SLO 没有后果。 错误预算超了也不影响发布,SLO 就变成装饰。兜底是把预算消耗和发布门禁绑定。
“共同负责”。 没有单一 owner,故障时互相等待。兜底是每个事项一个 accountable owner,另一个角色只能是 consulted。
常见错误与边界
第一,把平台化当成组织合并。平台是契约和机制,不是把所有团队并成一个。
第三,平台契约由平台单方面定义。契约的消费者是产品,必须共同评审。
第四,把发布治理理解成统一发版日。真正要统一的是回滚、灰度和审计能力。
第五,成本治理只做报表。没有配额执行和预算责任,报表不会改变行为。
第六,用缺口报告代替判断。代码能发现缺项,不能判断契约是否合理;后者仍然需要人评审。
小结
卷三用三张表把容量、成本、合规变成可核对的记录,用决策矩阵把全球化选择变成可复现的评分。卷四要解决的是这些记录和评分如何跨产品复用:哪些能力升级为平台,哪些决策留在产品,契约怎么版本化,发布和成本怎么治理。
交接的完成标志不是文档签字,而是缺口报告为空、契约可执行、责任可定位、复审有日期。下一节回到目录,按阅读路径重新组织全卷。
阅读导航:上一节:13.2 全球化架构决策矩阵 · 下一节:目录与阅读路径 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。