《互联网产品的客户端架构·内容、社交与实时互动》附录 B 内容与实时问题排查手册

按用户症状组织的内容与实时排查手册:重复卡片、位置跳动、详情白屏、图片灰块、点赞回退、评论失败、上传卡住、消息丢失、未读数不准、审核态闪现、联想串结果。每条给出可观察信号、常见根因、验证步骤与修复方向,最后附一张总表,便于值班时按症状快速定位。

怎么用这份手册

这份手册按「用户看到的症状」组织,而不是按模块组织。线上排查时,你通常先拿到一句模糊的反馈——「列表里有重复」「消息不更新了」「角标一直是 3」——然后才需要定位到模块。

每条症状给出四段:

  1. 可观察信号:不依赖用户描述,能自己复现或从日志里看到的证据。
  2. 常见根因:按出现频率排序,不是穷举。
  3. 验证步骤:用最小成本确认是哪一个根因。
  4. 修复方向:修哪里,以及修完要补什么测试。

手册假设你已经读过正文对应章节。它不重复原理,只给排查路径。

症状:信息流出现重复卡片

可观察信号:同一条内容在相邻两屏出现两次;下拉刷新后重复消失;接口返回的 id 在两次分页中有交集。

常见根因:

  1. 游标只用了排序字段的一部分,例如只用 created_at,同一毫秒的多条内容被重复返回。
  2. 客户端没有已见集合,直接追加分页结果。
  3. 刷新与加载更多并发,刷新返回的旧数据覆盖了分页的新位置。
  4. 容器淘汰了条目,但已见集合仍保留 id,导致重新拉取时被误判为重复(这是「越滑越少」,与重复相反)。

验证步骤:

  • 抓两次分页的响应,比较返回 id 的交集。
  • 在端上打印 appendPage 的 { added, duplicates, dropped }。
  • 检查游标编码是否包含唯一字段。

修复方向:服务端用 (created_at, id) 复合游标;客户端保留已见集合并与淘汰同步清理;刷新与分页串行化或按版本号丢弃过期响应。

症状:信息流位置跳动

可观察信号:用户滑到第 20 条,刷新后回到第 1 条;或插入新内容后视觉位置突然下移。

常见根因:

  1. 刷新替换了整个数组,而不是把新条目插到顶部。
  2. 列表没有稳定的 key,重新渲染时复用了错误的节点。
  3. 新内容插入到顶部但滚动位置没有补偿。
  4. 图片加载完成后高度变化,把下方内容顶走。

验证步骤:

  • 记录刷新前后的第一条可见 id。
  • 检查刷新逻辑是 replacePage 还是重建数组。
  • 检查列表 key 是否使用内容 id。

修复方向:刷新只插入新条目,已有条目保持原位;用 id 作为 key;插入到顶部时按插入高度补偿滚动;媒体预留宽高比。

症状:详情页白屏

可观察信号:进入详情页后长时间空白;控制台有请求失败;从分享链接进入必现,从列表进入偶现。

常见根因:

  1. 详情页依赖列表传入的完整对象,深链进入时对象不存在。
  2. 正文渲染等待评论接口,评论失败导致整页失败。
  3. 内容处于审核态,端上直接返回空。
  4. 路由参数解析失败,id 为空。

验证步骤:

  • 直接用分享链接打开,看是否复现。
  • 查看网络面板,确认是哪个请求失败。
  • 检查页面 loading 状态是否覆盖了正文与评论两块。

修复方向:详情只接收 id,自己拉数据;正文与评论使用独立 loading 与错误边界;审核态渲染占位而不是空白;对无效 id 给出明确错误页。

症状:图片裂图或一直灰块

可观察信号:部分图片显示占位不消失;弱网下更明显;切换页面后恢复。

常见根因:

  1. 原图请求超时,但没有降级到缩略图。
  2. 图片 URL 带过期签名,缓存命中旧链接。
  3. 懒加载的可见性判断失效,图片从未进入视口。
  4. 加载失败后没有重试,占位状态被永久保留。

验证步骤:

  • 查看图片请求的状态码与耗时。
  • 检查占位组件是否有 onError 处理。
  • 检查 URL 是否带时间戳参数导致缓存失效。

修复方向:按「缩略图 → 原图」降级;失败重试一次后显示稳定占位;懒加载用 IntersectionObserver 并设置根边距;签名 URL 的过期时间要覆盖缓存周期。

症状:点赞数回退

可观察信号:点完赞数字 +1,几百毫秒后跳回原值;或刷新后与自己看到的不一致。

常见根因:

  1. 乐观更新后,服务端返回的旧计数覆盖了本地状态。
  2. 请求失败触发了回滚,但用户没有看到失败提示。
  3. 同一内容在列表与详情各有一份状态,只更新了其中一份。
  4. 计数更新用了绝对值而不是增量。

验证步骤:

  • 打印请求的发出与返回时间,确认覆盖顺序。
  • 检查回滚逻辑是否只改对应条目。
  • 检查列表与详情是否共享同一个计数来源。

修复方向:服务端返回新计数而不是旧快照;用增量更新;以内容 id 为键维护单一计数源;失败时给出可恢复提示。

症状:评论发不出去

可观察信号:点击发送后按钮一直转圈;或提示失败但草稿消失。

常见根因:

  1. 发送期间没有禁用按钮,重复提交被服务端拒绝。
  2. 失败后清空了输入框,用户需要重打。
  3. 评论列表按时间倒序,新评论插入位置错误,用户以为没发出去。
  4. 审核拦截返回了通用错误,端上无法给出具体原因。

验证步骤:

  • 检查请求是否重复发送。
  • 检查失败分支是否保留草稿。
  • 检查新评论的插入位置与滚动。

修复方向:发送中禁用并显示状态;失败保留草稿并提供重试;本地先插入「发送中」占位,成功后替换;审核失败给出具体文案。

症状:上传卡在 99%

可观察信号:进度条接近完成但永不结束;刷新后可续传;服务端收到全部分片但没有完成记录。

常见根因:

  1. 进度按「已发送字节」计算,最后一个分片的确认还没回来就显示 99%。
  2. 完成请求(合并分片)失败,端上没有重试。
  3. 某个分片被服务端判定失败,但客户端没有收到。
  4. 并发分片乱序完成,完成判定按顺序假设。

验证步骤:

  • 打印每个分片的发送与确认时间。
  • 检查完成请求是否发出、返回什么。
  • 检查服务端的分片清单与客户端的已确认清单是否一致。

修复方向:进度按「已确认分片」计算;完成请求失败要重试;用上传会话 id 对账;完成判定等待所有分片确认。

症状:实时消息延迟

可观察信号:对方已发送,自己几秒后才看到;刷新页面能立刻看到。

常见根因:

  1. 连接已断,但端上仍显示在线,消息走的是轮询兜底。
  2. 心跳间隔过长,半开连接未被发现。
  3. 服务端推送队列积压。
  4. 端上收到消息后进入了一个低优先级队列,渲染被推迟。

验证步骤:

  • 查看连接状态与最后一次收到心跳的时间。
  • 对比推送到达时间与渲染时间。
  • 检查是否有大量消息在同一时刻到达。

修复方向:心跳 + pong 超时检测;断线后立即降级到轮询或提示;消息到达即写入状态,渲染异步化但不延迟状态更新。

症状:重连后消息丢失

可观察信号:断网几十秒后恢复,中间的几条消息永久缺失;重新进入会话才补上。

常见根因:

  1. 重连时没有携带上次收到的序列号。
  2. 服务端忽略补拉参数,只推送新消息。
  3. 客户端把重连当成「全新会话」,清空了本地消息。
  4. 补拉只取「最新一页」,没有覆盖断线区间。

验证步骤:

  • 抓重连请求,确认是否带 lastSeq。
  • 对比服务端返回的第一条与本地最后一条。
  • 检查重连逻辑是否清空状态。

修复方向:重连携带序列号;服务端从序列号之后补发;客户端保留已有消息并检测空洞;补拉失败时提示用户手动刷新。

症状:未读数不准

可观察信号:角标显示 3,点进去只有 1 条;或已读后角标不消失;多端数字不一致。

常见根因:

  1. 多个来源(点赞、评论、关注)各自维护计数,合并时重复累加。
  2. 已读回执只发给了其中一个端。
  3. 端上用本地累加而不是服务端返回的总数。
  4. 推送到达但应用未打开,角标没有更新。

验证步骤:

  • 对比服务端返回的未读总数与端上显示。
  • 检查已读回执的发送时机与目标。
  • 检查合并逻辑是否去重。

修复方向:以服务端总数为准,本地只做增量;已读回执带设备 id;多端以最后已读位置为准;应用启动时强制同步一次。

症状:审核中的内容闪现

可观察信号:打开详情页先看到正文,几百毫秒后被替换成「审核中」。

常见根因:

  1. 列表返回的条目没有带审核状态,详情请求才知道。
  2. 端上先渲染缓存快照,未校验状态。
  3. 审核状态与正文在同一个请求里,但端上先渲染了部分字段。

验证步骤:

  • 检查列表响应是否包含 state。
  • 检查缓存是否记录了状态与时间。
  • 检查渲染是否在状态确认前发生。

修复方向:列表与详情都返回审核状态;缓存快照带状态;状态未知时先渲染占位,确认后再渲染正文。

症状:搜索联想串结果

可观察信号:输入「abc」后显示的是「ab」的结果;快速输入时结果闪烁;清空输入后仍有旧结果。

常见根因:

  1. 请求并发返回,旧请求后到覆盖了新结果。
  2. 防抖只做了延迟,没有取消在途请求。
  3. 结果没有按查询词做键,缓存串用。
  4. 输入法组合期间触发了查询。

验证步骤:

  • 打印每次请求的查询词与返回顺序。
  • 检查是否使用 AbortController。
  • 检查缓存 key 是否包含完整查询词。

修复方向:每次输入取消上一次请求;结果按查询词与版本号匹配后才渲染;组合输入期间不触发;缓存 key 包含查询词与筛选条件。

一张总表

症状第一检查点高频根因
重复卡片分页 id 交集游标不唯一 / 无已见集合
位置跳动刷新前后首条 id整表替换 / 无稳定 key
详情白屏深链是否复现依赖列表对象 / 错误边界过大
图片灰块图片请求耗时无缩略图降级 / 无 onError
点赞回退请求返回顺序旧快照覆盖 / 绝对值更新
评论失败草稿是否保留失败清空 / 重复提交
上传卡 99%分片确认清单进度语义错 / 完成请求未重试
消息延迟连接状态与心跳半开连接 / 推送积压
重连丢消息重连是否带序列号无补拉 / 清空会话
未读数不准服务端总数对比多来源累加 / 已读未同步
审核态闪现列表是否带状态状态后到 / 缓存无状态
联想串结果请求返回顺序无取消 / 缓存 key 不全

排查纪律

这份手册最容易误用的方式,是把它当成「按症状改代码」的清单。症状相同,根因可能相反。例如「重复」既可能是去重缺失,也可能是去重过度;「不更新」既可能是连接断了,也可能是端上把消息写进了错误的对象。

三条纪律:

  1. 先拿证据,再改代码。 至少有一条可观察信号能区分两个根因,否则不要动手。
  2. 一次只改一个变量。 同时改游标、去重与刷新顺序,修好了也不知道是哪一条起作用。
  3. 修完补用例。 没有用例的修复会在下一次重构时复发,这是本卷反复强调测试的原因。

常见错误与边界

  • 把用户描述当成根因。「列表重复」可能实际是「刷新位置错了」。
  • 只看端上日志,不看服务端返回。两边的时间戳与序列号要对照。
  • 用「重装应用」验证。重装会清空缓存,掩盖缓存相关的问题。
  • 在弱网下不复现就认为修好了。内容类问题大多有网络相关性。
  • 把降级当成修复。降级让用户可用,但没有消除根因。
  • 修完不补监控。下一次同类问题仍然只能靠用户反馈发现。
  • 忽略多端差异。同一账号在两端表现不同,往往说明状态同步有问题。
  • 用「偶发」结束排查。偶发通常意味着时序或并发,值得继续定位。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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