location_on 首页 keyboard_arrow_right 片段剪辑 keyboard_arrow_right 正文

我忍了三天,蘑菇视频的缓存管理问题我终于定位到原因了

片段剪辑 access_alarms2026-02-19 visibility27 text_decrease title text_increase

我忍了三天,蘑菇视频的缓存管理问题我终于定位到原因了

我忍了三天,蘑菇视频的缓存管理问题我终于定位到原因了

前言 三天的折腾不是浪费时间,而是把一个表面看起来“随机增长”的磁盘占用问题拆成了可复现的故障流程。本文把我排查蘑菇视频缓存问题的全过程、定位到的根因、修复措施和后续预防建议都写清楚了,给遇到类似问题的人一份可直接落地的参考。

症状回顾

  • 应用启动后,磁盘占用持续增长,短时间内缓存目录从几十 MB 跳到几个 GB。
  • 重启应用或清理缓存后短期内又会恢复增长,增长速度与播放视频数量/频率有关。
  • 日志没有明显的 OOM 或异常崩溃信息,只有零散的下载失败、IO 报错和部分文件名带临时后缀(例如 .tmp、.part)。

排查思路与步骤(按时间线) 1) 快速定位大文件

  • 在设备或服务器上用 du/find 快速找出占用大的文件和目录:
  • Linux/Android shell:du -h --max-depth=1 | sort -h
  • 找到疑似缓存目录后,用 find 查找大文件:find . -type f -size +100M -ls
  • 结果:大量零散文件占用了空间,很多文件后缀异常(.tmp、.downloaded、.partial)。

2) 对比请求与缓存键

  • 抓包并查看缓存的 URL 与应用中用于生成缓存文件名/Key 的逻辑,确认是否存在不同请求生成了同一 Key 或相反同一资源被多个 Key 存储。
  • 结果:存在多种请求参数(如时间戳、token)没有被归一化,导致同一视频/分片被多次缓存为不同文件。

3) 并发与写入原子性检查

  • 在线程模型与文件写入流程上加日志,复现时观察并发下载是否会同时写同一缓存文件。
  • 检查写入是否使用临时文件 + 重命名的原子写入语义,还是直接覆盖写入。
  • 结果:并发下载会直接写同一目标路径,且没有采用先写 tmp 再重命名的策略,部分下载中断导致残留临时文件,随后的下载又以不同文件名保存,文件数暴增。

4) 缓存容量和淘汰策略核对

  • 查看缓存控制代码(LRU/大小计数逻辑),是否以文件数量而非磁盘实际占用做限制,或统计方式使用了错误的单位/统计口径。
  • 结果:用于计数的 metadata 仅在写入成功时更新,但某些中断路径没有回滚或清理,导致统计与实际不一致,从而避开了容量限制触发的淘汰流程。

5) 服务端/HTTP 层异常导致的分片残留

  • 分析 206/Range 请求、断点续传逻辑,确认服务端是否对断点请求返回异常头或没有 Content-Length,客户端因此保存为不完整文件。
  • 结果:服务端有时返回不完整的 Content-Length 或 HEAD 中缺少必要字段,客户端把下载当成“成功”并保存为独立文件。

根因总结(核心问题)

  • 并发写入 + 非原子写入流程:多个线程/任务同时写缓存文件,且没有采取临时文件写入再原子重命名的保护,导致残留不完整/重复文件。
  • 缓存 Key 管理不严:请求参数没有统一归一化(例如去掉无意义的 query 参数、token、时间戳),同一内容被多次缓存为不同文件。
  • 元数据/统计不一致:缓存空间计数依赖于写入成功的回调,但中断、异常路径没有做回滚与清理,缓存淘汰条件未能可靠触发。
  • 服务端不稳定/HTTP 标头问题:断点/分片下载与服务端的配合不稳定,导致客户端保存了大量不完整分片作为独立文件。

我采取的修复措施(可直接落地的改动) 1) 写入流程改为“先写临时文件 -> 校验完整性 -> 原子重命名”

  • 流程:
  • 写入时把文件写到 .tmp 或 .part 后缀。
  • 写完后校验 checksum(或 Content-Length 比对),校验通过再将临时文件重命名到目标文件名(rename 在同一文件系统通常是原子操作)。
  • 出错或下载中断时删除临时文件。
  • 好处:不会有残留不完整文件被误认为缓存,避免并发写入污染。

示例(伪代码): File tmp = new File(cacheDir, key + ".tmp"); try (OutputStream os = new FileOutputStream(tmp)) { // 写入数据 os.write(…); } if (checksumMatches(tmp)) { tmp.renameTo(new File(cacheDir, key)); // 原子替换 } else { tmp.delete(); }

2) 统一并规范缓存 Key 生成

  • 只用真正影响内容的参数生成 key:例如媒体 id、分辨率、清晰度、片段索引,不包含 token、时间戳、用户非内容相关的参数。
  • 对 URL 做规范化(去掉无关 query、排序参数、统一域名变体)。
  • 使用稳定的哈希(如 sha256)做文件名避免路径长度/编码问题。

3) 引入/修正容量限制与淘汰策略

  • 用基于磁盘实际占用的 LRU(按字节数)而非文件数量做限制。
  • 写入成功后更新元数据;在异常或启动时进行扫描校验,修复元数据与磁盘实际状态不一致的情况。
  • 增加启动时的完整性检查:扫描缓存目录、清理无后缀或不合法的临时文件、重建 metadata 索引。

4) 下载层健壮化

  • 对 HTTP 下载增加 Content-Length 校验,若返回不准确或缺失则按分片/流式处理,不直接作为完整文件入库。
  • 对 206/Range 请求做更严格的校验,并在多次断点续传路径上保护临时文件名不会被当成最终文件。

5) 监控与巡检(防止复发)

  • 增加周期性任务(例如每日/每小时)扫描缓存目录,报告超过阈值的大文件、临时后缀文件数量、磁盘使用趋势。
  • 在日志中增加关键事件埋点:写入开始/完成/失败、重命名成功/失败、淘汰触发等信息,便于回溯。
  • 把缓存使用量、命中率、淘汰频率暴露到监控面板(Prometheus/Grafana 或内部埋点)。

验证结果

  • 部署修复后 48 小时内,缓存目录不再异常增长:临时文件数急剧下降,磁盘占用稳住并在合理淘汰策略下波动。
  • 日志中并发写冲突的错误明显减少,重命名与校验成功率增加。
  • 用户端播放体验未受影响,缓存命中率略微提升(因为避免了重复缓存同一资源)。

给开发/运维的可操作建议(速查清单)

  • 若发现磁盘突然上涨,先找大文件,再看是否有大量临时后缀文件(.tmp/.part/.download)。
  • 检查写入流程是否遵循“临时写入 + 校验 + 原子移动”模式。
  • 确认缓存 key 生成逻辑是否包含无关参数(token、时间戳、session id)。
  • 淘汰策略要基于字节而不是仅文件数量,元数据与磁盘要能自洽(启动时可重建索引)。
  • 对外部不可信的 HTTP 响应做防护,不要盲目把不完整/没有 Content-Length 的响应当作完整资源入库。
  • 增加定时巡检任务并对异常情况报警(如临时文件增长率超过阈值)。

report_problem 举报
如果你在蘑菇视频ios上卡在画质与流量,先别慌:这样做就对了
« 上一篇 2026-02-18
看懂91大事件只需要抓住一点:临近上映才补拍,补拍的恰好是核心
下一篇 » 2026-02-19