如何复盘复杂客户端系统

自动关联目录:如何复盘复杂客户端系统

功能清单只能证明做过很多事,不能证明理解了这些事。真正有价值的复盘,要能回答:为什么当时这样设计、什么变化让它失效、怎样更早识别问题、下一版用什么契约避免重演。

从“做了什么”切换到“解决什么”

同一个技术机制,可以有完全不同的业务价值:

  • 异步加载可能为了减少主线程停顿,也可能为了支持远端内容按需到达。
  • 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 校验、主线程提交。

个人反思不等于自我评价

“做得不错”或“考虑不周”都没有复用价值。个人反思应该落到决策过程:

低价值表达高价值表达
没想到数据会这么多没有把对象数、依赖扇出和实体化成本写成容量模型
异步加载有很多坑没有给请求定义所有者、取消和晚到结果策略
后来重构了模块原边界按代码目录划分,没有按变化原因和运行目标划分
测试不够只测成功路径,没有覆盖旧数据、取消、损坏、重入和部分失败

每篇文章最后回答三个问题:

  1. 哪个早期信号本可以更早发现?
  2. 当时缺少的是数据、模型、边界还是验证手段?
  3. 下一次评审时,要增加哪一个必须回答的问题?

完成标准

一篇文章只有同时满足以下条件才算完成:

  • 能从业务失败追到技术根因。
  • 能解释方案适用范围,而不是只给最佳实践。
  • 至少比较一个替代方案及其代价。
  • 明确状态、生命周期、权威和错误处理。
  • 性能结论有成本模型和验证方法。
  • 目标设计没有冒充已完成的历史实现。
  • 全文无法识别或还原具体来源。

返回总目录:客户端工程复盘。