如何复盘复杂客户端系统
自动关联目录:如何复盘复杂客户端系统
功能清单只能证明做过很多事,不能证明理解了这些事。真正有价值的复盘,要能回答:为什么当时这样设计、什么变化让它失效、怎样更早识别问题、下一版用什么契约避免重演。
从“做了什么”切换到“解决什么”
同一个技术机制,可以有完全不同的业务价值:
- 异步加载可能为了减少主线程停顿,也可能为了支持远端内容按需到达。
- DS 权威可能为了防作弊,也可能为了保证多人看到同一个玩法状态。
- 对象池可能为了降低分配,也可能为了让海量 AI 的升降级保持稳定身份。
- 二进制格式可能为了体积,也可能为了明确 schema 和跨版本兼容。
所以文章不能从“使用了某技术”开始,而要从失败时用户看到了什么开始。
flowchart LR
A[用户或创作者目标] --> B[可观察失败]
B --> C[被击穿的系统假设]
C --> D[约束与不变量]
D --> E[候选方案与取舍]
E --> F[架构契约]
F --> G[指标与验证]
G --> H[可迁移经验]业务深度:先问五个问题
1. 谁在完成什么任务
“用户”过于宽泛。创作者导入资产、策划调参数、玩家进入世界、客户端观察远端角色、DS 结算规则,是不同任务,也有不同失败成本。
一个可用的问题定义应该包含:
角色 + 任务 + 环境 + 成功结果 + 失败影响例如,不说“优化加载”,而说:
玩家进入包含远端内容的世界时,应先看到可交互的最低完整状态,次要视觉内容可以继续流式到达;网络失败不能破坏已有缓存和可玩状态。
2. 用户看到的失败是什么
技术指标不是问题本身。主线程耗时只是原因之一,用户真正感知的是黑屏、占位物长时间不消失、交互已经生效但外观还没出现,或者重试后内容状态反而更差。
先写可观察失败,才能区分:
- 正确性问题还是性能问题;
- 首次使用问题还是稳定态问题;
- 局部降级是否可接受;
- 应阻断流程还是带警告继续。
3. 哪个规模变化让问题出现
“规模”不只有对象数量,还包括:
| 维度 | 常见拐点 |
|---|---|
| 内容规模 | 依赖图、制品数量、版本跨度增加 |
| 运行规模 | 同屏实体、AI、物理体、UI 节点增加 |
| 平台规模 | 设备、输入方式、纹理格式和目标程序增加 |
| 团队规模 | 功能由多人并行扩展,隐含约定开始冲突 |
| 时间规模 | 老内容、旧缓存和新 Reader 必须长期共存 |
| 网络规模 | 延迟、丢包、重连和晚加入成为常态路径 |
优秀的复盘会指出被击穿的假设,例如“资产路径长期稳定”“异步回调返回时对象还活着”“一个物体可以独立转移物理所有权”。
4. 什么结果绝不能退化
在优化前先写不变量:
- 权威状态不能由客户端表现反推。
- 新制品安装失败时,旧的可用版本不能被破坏。
- 一组有约束关系的物理体不能只更新其中一部分。
- 降低动画频率不能漏掉玩法关键事件。
- 缺少次要视觉资产可以降级,损坏的清单不能悄悄放行。
没有不变量,优化就容易把“少做工作”变成“少做正确性”。
5. 怎样证明改善了业务结果
指标必须能支持决策。只测平均帧率,很难判断首帧加载或峰值尖刺;只测下载结束时间,也不知道内容何时真正可见。
推荐把指标写成链路分解:
输入到反馈 = 输入采集 + 本地处理 + 网络 + DS 处理 + 状态返回 + 动画/相机/UI 呈现
可见就绪 = 解析 + 排队 + 获取 + 校验 + 解码 + 实体化 + 首次提交渲染历史真实值不进入知识库;文章定义公式、采样点、维度和比较方法。
技术深度:检查六种边界
数据边界
谁是源数据,谁是派生数据?编辑模型、发布制品、运行时缓存和呈现对象是否被误当成同一份状态?
生命周期边界
谁创建、持有、取消和销毁?异步结果回来时,World、Session、Actor 或 UI 是否已经换代?
权威边界
客户端能预测什么,DS 决定什么,哪些事件只用于表现?“本地先显示”不能自动变成“本地可提交真相”。
执行边界
工作在哪个线程、进程或目标程序执行?DS 是否误加载了视觉资源?主线程是否承担了可后台完成的解析和准备?
兼容边界
Writer 和 Reader 如何演进?未知字段、未知文件、缺失字段、旧缓存和新产物分别怎样处理?
故障边界
网络失败、内容不存在、数据损坏、格式不支持、取消和超时是否被压成同一个“加载失败”?不同故障决定不同的重试、降级和告警策略。
用演进轴解释架构
复杂系统很少从一开始就需要最终形态。复盘不是嘲笑早期方案,而是找出它适用的边界。
| 阶段 | 应回答的问题 |
|---|---|
| 早期方案 | 当时的内容量、目标平台和迭代速度为什么让它合理 |
| 规模拐点 | 哪些业务变化使原有假设不再成立 |
| 症状 | 错误、卡顿、维护成本或兼容问题怎样暴露 |
| 后续演进 | 身份、数据、模块、生命周期或权威边界怎样移动 |
| 新代价 | 新系统增加了哪些状态、工具、测试和迁移成本 |
| 遗留债务 | 哪些双轨、兼容层或性能问题仍然存在 |
| 目标方向 | 如果重新设计,应该先固定哪些合同和验证门禁 |
这条轴比“旧方案不好、新方案好”更重要,因为很多目标架构在小规模下并不经济。
三种知识状态不能混淆
工程原则
由机制直接推出,例如:异步回调必须验证目标生命周期;不可信客户端不能决定权威状态。
复盘推论
由多类实际问题归纳,例如:当加载、发布、预览分别维护资产路径时,路径迟早会成为多个系统之间的隐式主键。
目标设计
面向下一阶段的推荐,例如:引入类型化资产身份、不可变制品和原子安装指针。它必须写清前提、迁移成本和不采用条件。
不保存内部证据映射
内部材料只用于确认“这个问题确实出现过”。进入文章前需要完成抽象:
内部事实
-> 去掉名字、路径、时间、数值和实现指纹
-> 保留问题、约束、状态转移和失败机制
-> 用领域无关契约重新表达
-> 检查多条信息组合后能否反推出来源不建立“结论对应哪个文件”的公开或半公开索引。这样会牺牲内部可追溯性,但能明确知识库不是项目档案。
一篇深挖文章的模板
# 一句话结论
# 业务问题
## 使用者与任务
## 可观察失败
## 规模触发器
# 架构演进
## 阶段 A:为什么合理
## 假设如何失效
## 平台化阶段:解决了什么
## 新代价和遗留问题
# 目标设计
## 数据契约
## 生命周期与权威
## 关键状态机或时序
## 伪代码
# 失败模式与决策表
# 指标和验证
# 个人反思
## 当时忽略了什么信号
## 下一次更早问什么
## 可迁移结论伪代码应该表达什么
伪代码不复刻生产 API,而应该突出合同:
RequestHandle Request(ResourceRef ref, Generation ownerGeneration)
{
Ticket ticket = Deduplicate(ref);
ticket.OnReady([ownerGeneration](PreparedResource value) {
if (!IsCurrent(ownerGeneration))
return; // 生命周期已经换代,拒绝晚到结果
CommitOnMainThread(value);
});
return ticket.CancellableHandle();
}这段代码的重点不是语言,而是四个判断:稳定引用、请求去重、Generation 校验、主线程提交。
个人反思不等于自我评价
“做得不错”或“考虑不周”都没有复用价值。个人反思应该落到决策过程:
| 低价值表达 | 高价值表达 |
|---|---|
| 没想到数据会这么多 | 没有把对象数、依赖扇出和实体化成本写成容量模型 |
| 异步加载有很多坑 | 没有给请求定义所有者、取消和晚到结果策略 |
| 后来重构了模块 | 原边界按代码目录划分,没有按变化原因和运行目标划分 |
| 测试不够 | 只测成功路径,没有覆盖旧数据、取消、损坏、重入和部分失败 |
每篇文章最后回答三个问题:
- 哪个早期信号本可以更早发现?
- 当时缺少的是数据、模型、边界还是验证手段?
- 下一次评审时,要增加哪一个必须回答的问题?
完成标准
一篇文章只有同时满足以下条件才算完成:
- 能从业务失败追到技术根因。
- 能解释方案适用范围,而不是只给最佳实践。
- 至少比较一个替代方案及其代价。
- 明确状态、生命周期、权威和错误处理。
- 性能结论有成本模型和验证方法。
- 目标设计没有冒充已完成的历史实现。
- 全文无法识别或还原具体来源。
返回总目录:客户端工程复盘。