《游戏服务端架构·平台化与架构治理》9.3 组织变化如何反映到架构

独立工作室从总部拆出后,组织图改了,服务仍共用数据库、计费账号和发布流水线,三个月后权限审计与成本归属同时失败。本节给出合并、拆分、外包、独立四种变更的架构影响矩阵,定义双轨、切换、退出与稳定四个阶段,并用可运行的 Go 夹具检查过渡计划、临时接口拆除日期与不可逆数据迁移。

组织图改了,服务边界没改

北辰把《泡泡工坊》的研发团队拆成一个独立工作室,组织图上已经分开:负责人、预算、招聘都独立。三个月后做权限审计,发现工作室仍在用总部的数据库账号、总部的计费密钥和总部的发布流水线;成本报表也无法区分两个实体的支出,因为资源标签还是旧的。

组织变更本身不会自动改变架构。组织图决定谁向谁汇报,架构决定服务如何部署、数据存在哪里、谁能访问、成本记在谁头上。两者之间需要一份过渡计划,否则拆分只发生在通讯录里,事故和账单仍沿着旧边界流动。

这一节按四个问题展开:变更类型决定哪些架构面必须动;过渡期如何分阶段并设定退出条件;临时接口如何避免变成永久补丁;数据、权限、计费、流水线各自的最低动作是什么。

四种变更,四种架构影响

变更服务边界数据权限计费流水线值班
合并两个 owner 合成一个,去重合并库或建同步统一角色,收回旧账号合并成本中心可共用一条合并值班表
拆分按故障域与发布节奏切开拆库或明确只读边界新团队独立身份成本按新边界归集必须隔离两边都要覆盖
外包合同定义接口,内部保留核心导出最小数据集供应商账号有期限合同计价,内部可核算独立或受控内部保留首响
独立完整边界,能独立发布全量迁移并可回滚独立身份体系独立账单独立流水线独立值班

四种变更里,合并最容易低估数据去重,拆分最容易低估流水线,外包最容易低估权限回收,独立最容易低估计费归属。低估的共同原因是:组织变更的验收标准通常是“人到位、预算到位”,而不是“服务能独立运行”。

一个实用的判断是:变更完成后,新实体能否在不呼叫旧实体的情况下完成一次发布、一次回滚、一次数据恢复和一次账单核对。四件事都能独立完成,才算架构真的跟上了组织。

先定退出条件,再排迁移步骤

本节的 26 周上限、双轨时长与检查阈值均为示例值,需按数据量、回滚信心与组织规模调整。

过渡期不是一段模糊的“先这样跑着”,而是一个有状态、有退出条件的流程:

阶段进入条件允许做的事退出证据
planned计划批准设计边界、准备环境计划与退出日期
dual-run新旧路径同时可用双写、对账、影子流量数据一致性与错误率达标
cutover切换窗口开始切流、回滚演练切换完成且可回滚
exiting新路径承载主要流量清理旧路径、回收权限旧路径无流量
stable退出完成只保留新路径账单、权限、值班均已归属
overdue超过退出周仍未退出只允许收尾,不允许加新功能需要重新决策

状态机的价值在于把“还在过渡”变成可检查的事实。超过退出周仍未退出,不应该继续叫“过渡期”,而应该标记为 overdue,并升级到 steering 决策:要么给出新的退出日期和资源,要么回滚。没有这个标记,过渡期会自然延长,直到所有人都忘记它曾经是临时的。

退出日期要写在计划里,而不是写在会议纪要里。计划至少要包含:切换周、退出周、每个步骤的 owner、哪些步骤可逆、哪些临时产物必须在退出前拆除。示例上限可以把切换至退出控制在 26 周以内;超过这个跨度,组织变更的收益通常已经被双轨成本吃掉。

双轨运行的成本与时限

双轨不是免费的。它至少带来四类成本:双写与对账、两套值班、配置漂移、以及“到底以哪边为准”的决策成本。双轨时间越长,临时补丁越多,最后清理旧路径的难度越大。

成本具体表现控制手段
双写与对账两边数据不一致,需要定时修复明确主库,写路径单向,定时对账
两套值班同一故障两边都响应,责任不清指定主值班,另一边只做备份
配置漂移两边参数逐渐不同,回滚失效配置版本化,差异清单化
决策成本每个变更都要判断走哪边设定切流比例与退出阈值

双轨的退出不能只看时间,还要看证据:数据对账差异在可接受范围内、新路径的错误率与延迟不劣于旧路径、回滚演练成功、旧路径流量降到零。四类证据里缺一项,就不应该宣布退出;但也不能因为追求完美而无限期延长,超过示例上限 26 周要重新决策。

短双轨和长双轨各有风险。短双轨节省成本,但数据迁移与回滚验证不充分;长双轨验证充分,但补丁累积、组织疲劳。选择依据是数据量与回滚信心,不是项目组的乐观程度。数据量大、回滚路径没演练过,宁可多留两周;数据量小、回滚已演练,尽快退出。

临时接口必须带拆除日期

组织变更一定会产生临时接口:共享登录令牌、跨工作室的 GM 命令、计费代理、临时数据库视图。它们本身不是问题,问题是没有人负责拆除。

规则可以写得很硬:任何标记为临时的步骤,必须同时写拆除周,且拆除周不得晚于退出周。没有拆除周的临时步骤,在计划评审时直接拒绝;拆除周晚于退出周的,视为永久补丁,必须重新设计。

临时产物典型用途owner拆除条件
共享认证令牌过渡期单点登录安全团队新身份体系全量切换
跨实体 GM 命令客服临时处理两边数据运营平台工单系统完成隔离
计费代理旧账单系统过渡财务系统新账单对账通过
只读数据库视图报表临时取数数据团队数据仓库同步完成

“永久补丁”的识别不能靠人肉记忆。每季度扫一次计划里的临时标记:没有拆除周的、拆除周已过但仍存在的、owner 已离职的,全部生成整改单。临时接口的存在本身不是债务,没有拆除日期的临时接口才是。

数据、权限、计费、流水线四件事

数据迁移。 切换前的数据复制必须可逆,或者至少有可验证的备份与恢复步骤。不可逆的复制不能发生在切换点之前,否则一次失败的切换会同时失去新旧两条路径。双写阶段要明确主库,避免两边都能写。

权限归属。 新实体要有独立身份,旧账号要在退出前回收。外包场景还要给供应商账号设置到期时间,到期自动失效,而不是靠人记得删。权限边界不清楚,数据边界一定不清楚。

计费归属。 每个实体只能有一个账单 owner。共享成本可以分摊,但分摊规则要公开、可核对;资源标签要在切换前改好,否则第一个月的账单就无法归属。计费归属失败往往比技术故障更晚被发现,也更难追溯。

流水线隔离。 拆分和独立必须在切换前拥有独立流水线;合并可以在切换后共用,但需要先统一构建与发布权限。流水线不隔离,所谓独立团队仍然要被旧团队的发布窗口卡住,组织变更就没有完成。

变更必须提前完成的步骤可以延后的步骤
合并计费 owner、权限统一流水线合并、数据去重
拆分流水线隔离、值班覆盖数据物理拆分
外包权限回收、计费合同、访问到期内部工具替换
独立流水线、权限、计费、数据回滚历史报表迁移

可运行片段:计划模型与状态机校验

internal/orgevolution 把过渡计划建模成可校验的结构。Plan 记录变更类型、切换周、退出周、实际退出周和每一步的 owner;Validate 检查时间线、必需步骤、临时步骤的拆除周、不可逆数据复制的位置,以及步骤是否越过退出周。

// file: internal/orgevolution/plan.go
// tier: L1
// verify: go test ./internal/orgevolution/
package orgevolution

import (
	"sort"
)

// ChangeKind is the org move that forces an architecture change.
type ChangeKind string

const (
	Merge       ChangeKind = "merge"
	Split       ChangeKind = "split"
	Outsource   ChangeKind = "outsource"
	Independent ChangeKind = "independent"
)

// StepKind is one unit of transition work. The names are deliberately about
// state, not about meetings: dual-run, data-copy, cutover, exit.
type StepKind string

const (
	StepDualRun  StepKind = "dual-run"
	StepDataCopy StepKind = "data-copy"
	StepCutover  StepKind = "cutover"
	StepBilling  StepKind = "billing-owner"
	StepAccess   StepKind = "access-review"
	StepPipeline StepKind = "pipeline-isolation"
	StepExit     StepKind = "exit"
	StepCleanup  StepKind = "cleanup"
)

// Step is scheduled in relative weeks from the start of the change. Temporary
// steps must carry a removal week; that is what stops a bridge interface from
// becoming permanent.
type Step struct {
	Kind        StepKind
	Week        int
	Owner       string
	Reversible  bool
	Temporary   bool
	RemovalWeek int
}

// Plan is one organization change with its architecture work. ExitedWeek is
// the actual completion week, zero while the change is still open.
type Plan struct {
	ID          string
	Kind        ChangeKind
	From        []string
	To          []string
	CutoverWeek int
	ExitWeek    int
	ExitedWeek  int
	Steps       []Step
	Services    []string
	DataStores  []string
}

// Issue is one plan defect. Subject is the plan ID or a step kind so the
// output can be attached to a ticket without translation.
type Issue struct {
	Subject string
	Rule    string
	Detail  string
}

func sortIssues(is []Issue) {
	sort.SliceStable(is, func(i, j int) bool {
		if is[i].Subject != is[j].Subject {
			return is[i].Subject < is[j].Subject
		}
		if is[i].Rule != is[j].Rule {
			return is[i].Rule < is[j].Rule
		}
		return is[i].Detail < is[j].Detail
	})
}

func hasStep(steps []Step, kind StepKind) bool {
	for _, s := range steps {
		if s.Kind == kind {
			return true
		}
	}
	return false
}

func countStep(steps []Step, kind StepKind) int {
	n := 0
	for _, s := range steps {
		if s.Kind == kind {
			n++
		}
	}
	return n
}

var knownKind = map[ChangeKind]bool{
	Merge: true, Split: true, Outsource: true, Independent: true,
}

Validate 的核心不是“步骤是否齐全”,而是“退出是否可验证”。它要求存在双轨、唯一一次切换、退出步骤,以及不同变更类型各自的必需步骤:拆分要有流水线隔离,合并要有计费 owner,外包要有权限评审,独立要有流水线、权限和计费。

// file: internal/orgevolution/validate.go
// tier: L1
// verify: go test ./internal/orgevolution/
package orgevolution

import (
	"fmt"
)

// Validate checks the transition plan as a state machine: there is one
// cutover, a dated exit, no temporary artifact without a removal week, and
// every step has an owner. It also requires the steps that the change kind
// cannot safely skip.
func Validate(p Plan) []Issue {
	subject := p.ID
	if subject == "" {
		subject = "<plan>"
	}
	var out []Issue
	add := func(rule, detail string) {
		out = append(out, Issue{Subject: subject, Rule: rule, Detail: detail})
	}

	if p.ID == "" {
		add("plan-without-id", "plan has no stable identifier")
	}
	if len(p.From) == 0 {
		add("no-source-team", "change names no team on the leaving side")
	}
	if len(p.To) == 0 {
		add("no-target-team", "change names no team on the receiving side")
	}
	if !knownKind[p.Kind] {
		add("unknown-change-kind", fmt.Sprintf("kind %q is not recognized", p.Kind))
	}
	if p.CutoverWeek <= 0 {
		add("invalid-cutover-week", fmt.Sprintf("cutover week=%d, want > 0", p.CutoverWeek))
	}
	switch {
	case p.ExitWeek == 0:
		add("missing-exit-deadline", "transition has no exit week")
	case p.ExitWeek <= p.CutoverWeek:
		add("exit-not-after-cutover", fmt.Sprintf("exit week=%d must be after cutover week=%d",
			p.ExitWeek, p.CutoverWeek))
	case p.ExitWeek-p.CutoverWeek > 26:
		add("transition-too-long", fmt.Sprintf("cutover to exit spans %d weeks; cap is 26",
			p.ExitWeek-p.CutoverWeek))
	}

	for _, s := range p.Steps {
		step := string(s.Kind)
		if s.Owner == "" {
			out = append(out, Issue{Subject: subject + "/" + step, Rule: "step-without-owner",
				Detail: "every transition step needs one accountable team"})
		}
		if s.Temporary {
			switch {
			case s.RemovalWeek == 0:
				out = append(out, Issue{Subject: subject + "/" + step, Rule: "temporary-without-removal",
					Detail: "temporary step has no removal week"})
			case p.ExitWeek > 0 && s.RemovalWeek > p.ExitWeek:
				out = append(out, Issue{Subject: subject + "/" + step, Rule: "temporary-outlives-exit",
					Detail: fmt.Sprintf("removal week=%d is after exit week=%d", s.RemovalWeek, p.ExitWeek)})
			}
		}
		if s.Kind == StepDataCopy && !s.Reversible && p.CutoverWeek > 0 && s.Week < p.CutoverWeek {
			out = append(out, Issue{Subject: subject + "/" + string(s.Kind),
				Rule:   "irreversible-data-copy-before-cutover",
				Detail: "data copy cannot be irreversible before the cutover point"})
		}
		if s.Kind != StepCleanup && p.ExitWeek > 0 && s.Week > p.ExitWeek {
			out = append(out, Issue{Subject: subject + "/" + step, Rule: "step-after-exit",
				Detail: fmt.Sprintf("week=%d is after exit week=%d", s.Week, p.ExitWeek)})
		}
	}

	switch {
	case !hasStep(p.Steps, StepDualRun):
		add("missing-dual-run", "no dual-run period; the old and new paths never overlap")
	case countStep(p.Steps, StepCutover) == 0:
		add("missing-cutover", "no cutover step")
	case countStep(p.Steps, StepCutover) > 1:
		add("duplicate-cutover", "more than one cutover step")
	}
	if !hasStep(p.Steps, StepExit) {
		add("missing-exit-step", "exit deadline has no corresponding step")
	}

	switch p.Kind {
	case Split:
		if !hasStep(p.Steps, StepPipeline) {
			add("missing-pipeline-isolation", "split teams need separate build and release pipelines")
		}
	case Merge:
		if !hasStep(p.Steps, StepBilling) {
			add("missing-billing-owner", "merged services need one billing owner")
		}
	case Outsource:
		if !hasStep(p.Steps, StepAccess) {
			add("missing-access-review", "outsourcing needs an access review before cutover")
		}
		if !hasStep(p.Steps, StepBilling) {
			add("missing-billing-owner", "outsourcing needs a named billing owner")
		}
	case Independent:
		if !hasStep(p.Steps, StepAccess) {
			add("missing-access-review", "independent studio needs its own access boundary")
		}
		if !hasStep(p.Steps, StepBilling) {
			add("missing-billing-owner", "independent studio needs a billing owner")
		}
		if !hasStep(p.Steps, StepPipeline) {
			add("missing-pipeline-isolation", "independent studio needs its own release pipeline")
		}
	}

	sortIssues(out)
	return out
}

本机验证命令与结果:

cd .local/checks/gsarch-lab
go test -count=1 ./internal/orgevolution/
ok  	gsarchlab/internal/orgevolution	0.250s

该包共有 5 个表驱动测试,覆盖时间线、必需步骤、临时步骤、影响风险与阶段判定;gofmt -l 无输出。同包的 Risks 检查值班、回滚、流水线、计费与权限缺口,Status 根据当前周给出 planned、dual-run、cutover、exiting、stable 或 overdue。

四个失败路径

失败路径识别信号后果修复
只改组织图服务 owner、权限、账单仍是旧的事故仍找旧团队,成本无法归属用四件事验收:发布、回滚、恢复、对账
双轨无限期超过退出周仍无退出证据成本翻倍,补丁累积标记 overdue,升级决策或回滚
不可逆迁移无回滚切换前已做不可逆复制切换失败时两条路径同时丢失切换前只做可逆复制,保留备份
计费与权限仍共享资源标签未改、账号未回收审计失败,责任无法划分切换前完成标签与账号归属

四个路径里,“只改组织图”最隐蔽,因为它在组织层面看起来已经完成;“双轨无限期”最昂贵,因为每天都在付两套成本;“不可逆迁移”最危险,因为它把一次可回滚的变更变成单程票。

检查表

  1. 变更类型是否明确为合并、拆分、外包或独立?
  2. 是否写明了切换周与退出周,且退出晚于切换?
  3. 切换至退出是否在示例上限 26 周以内;超出是否有重新决策机制?
  4. 是否有双轨阶段,且双轨有数据对账与主库约定?
  5. 每个临时步骤是否有 owner 和拆除周,拆除周不晚于退出周?
  6. 切换前的数据复制是否可逆,或是否有可验证的备份恢复?
  7. 新实体是否有独立身份,旧账号是否在退出前回收?
  8. 每个实体是否只有一个账单 owner,资源标签是否已改?
  9. 拆分或独立是否在切换前拥有独立发布流水线?
  10. 事故值班在过渡期是否有明确的主值班与备份?

第 2、5、6 条是硬门槛:没有退出日期、没有拆除日期、没有回滚路径的计划,不应该进入切换窗口。

小结

组织变更要落到四个架构面:服务边界、数据、权限与计费、发布流水线。合并、拆分、外包、独立四种变更的影响不同,验收标准是同一组:新实体能否独立完成发布、回滚、数据恢复和账单核对。过渡期用 planned、dual-run、cutover、exiting、stable 五个状态管理,超过退出周标记 overdue 并重新决策,示例上限 26 周。临时接口必须带拆除日期,不可逆数据复制不能发生在切换点之前。只改组织图、双轨无限期、不可逆迁移、计费权限共享,是四个需要提前拦住的失败路径。

下一篇进入第 10 章,讨论架构如何随时间演进:绞杀者模式与增量迁移,如何在不停止业务的情况下替换旧系统。

阅读导航:上一节:9.2 平台团队、产品团队与 SRE · 下一节:10.1 绞杀者与增量迁移 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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