直播场景 http flv 流内存泄露排查小记

2,208 阅读5分钟

IMWebConf 2020 直播期间,腾讯课堂上课页出现 flv 流直播场景页面崩溃现象:此稳定性问题颇为严重,在此记录下解决过程以示警戒。

现象

IMWebConf 2020 直播期间,腾讯课堂上课页出现 flv 流直播场景页面崩溃现象:

此稳定性问题颇为严重,在此记录下解决过程以示警戒。

定位过程

定性为内存泄露

通过搜索了解到页面崩溃通常是因为内存泄露导致(非网络等其他问题情况下)。

稳定重现

音视频分队同学首先是去做实验重现这个问题,针对直播的代码做测试页面:

  • 空白测试页面:XHR 请求 flv 资源,页面内存一直增长到一定阈值后,突然回到五十来兆,然后不再增长,请求还在继续,无 crash。
  • 空白测试页面:使用 flv.js 只拉流不播放,没有加额外参数,页面内存在两百多兆的波动,无 crash。
  • 空白测试页面:使用 flv.js 拉流并播放,没有加额外参数,页面内存在两百多兆的波动,无 crash。
  • 空白测试页面:使用 flv.js 拉流并播放,使用和课堂页面同样的参数,页面内存在两百多兆的波动,无crash。
  • Sample测试页面:使用 loki-player 的 flv 模式进行拉流播放,内存激增到 crash。
  • Sample测试页面:使用 imweb-tcplayer 封装后的包在课堂页面中拉流播放,内存激增到 crash。
  • 空白测试页面:使用 tcplayer 拉流播放,内存稳定,无 crash。
  • Sample 测试页面:使用 flv.js 拉流播放,还是有问题,页面有 eruda。

稳定重现了此问题,值得一提的是这跟传统意义上的JS内存泄露不一样:

Memory 调试工具看不出来任何问题,而任务管理器则可以看出此 Tab 内存使用在飙升:

缩减代码范围

根据上述实验的 5/6/8 及非 Javascript Heap 的内存泄露现象,对比代码差异可知内存泄露极有可能与网络劫持逻辑有关:

无论是 eruda 调试工具还是播放器依赖到的内部上报工具,都有劫持网络请求的逻辑,由于普通用户不可能使用 eruda,故问题出在内部上报工具上。

在Chrome中网络请求实现可以参考:

补充参考:

Our multi-process application can be viewed in three layers. At the lowest layer is the Blink engine which renders pages. Above that are the renderer process (simplistically, one-per-tab), each of which contains one Blink instance. Managing all the renderers is the browser process, which controls all network accesses.

简单翻译如下: 多进程应用程序可以分为三层。最底层是呈现页面的 Blink 引擎。在其上方是渲染器进程(简单地说,每个选项卡一个),每个进程都包含一个 Blink 实例。浏览器进程管理所有渲染器,它控制所有网络访问。

可知Renderer进程是通过 IPC 来读取Browser进程的请求响应数据的。而 IPC (Inter-process communication)是通过共享内存来实现的。

结合 Chrome 关于内存计算的共识说明:

Define the memory footprint of a process to be the amount of memory that would become available to the system if the process were killed. More specifically, define:

  • Physical Memory Footprint: (Number of physical pages that would become available if the process were killed).
  • Swapped Memory Footprint: (Number of pre-compression pages in swap or compressed memory that would become available if the process were killed).
  • Memory Footprint: Physical + Swapped.

简单翻译如下: 将进程的内存占用量定义为:如果该进程被杀死,则系统可以使用的内存量。更具体地说,定义:

  • 物理内存占用空间:(如果进程被杀死,将变为可用的物理页面数)。
  • 交换内存占用空间:(如果进程被终止,则交换或压缩内存中的预压缩页面数)。
  • 内存占用:物理内存+交换内存。

所以共享内存作为 Swapped Memory Footprint 被计算进任务管理器中的 Memory 列了,故而才能发现问题所在。 接着手动注释掉内部上报工具的劫持逻辑,果然不复现了。Review 劫持代码后发现:

origFetch.apply(this, args).then((response)=>{
  try {
      loggerFetch.status = response.status;
      loggerFetch.cost = Date.now()- loggerFetch.start;

      // 这里clone().then()调用导致对flv流 Blob数据的引用计数
      loggerFetch.resContentType = response.headers.get('Content-Type');
      const cloned = response.clone();
      
      // 由于直播http flv流一直不断响应数据,实际上这个Promise会直到直播结束后才触发
      // 大部分情况早就因为内存问题崩溃了
      cloned.text().then((text)=>{
        loggerFetch.response = text;
        Network.trigger('fetch', loggerFetch);
      });
  } catch (e){
    Network.trigger('fetch', loggerFetch);
  }
  return response;
});

由于 Response 实例的 clone().then() 调用对http flv流 Blob 响应数据的等待,使得此部分 Blob 数据一直被引用计数(详见 Javascript GC介绍 ),直到直播结束后才会释放此部分内存。大部分情况页面早就因为内存问题而崩溃。

问题修复

修改方式是采用白名单过滤的机制来获取响应内容,仅获取 application/json, text/plain, text/xml 的响应内容:

const defaultOpts = {
    // 是否记录response内容黑白名单
    resContentTypePatterns: {
      includes: [/application\/json/, /text\/xml/, /text\/plain/],
      excludes: [/video/],
    },
}
if (multiMatch(loggerFetch.resContentType, defaultOpts.resContentTypePatterns)) {
  // ...
}

其他补充

内部上报工具为什么要劫持 window.fetch

因为内部上报工具会自动抓取页面的 Ajax 请求的耗时,返回码等数据,来监控和统计 CGI 的性能、成功率。

总结

此次经历又一次告诫我们,一定要对自己所写的代码抱有敬畏之心,像此类劫持全局方法的实现更是要小心、严谨、做好容灾逻辑。只有这样才能更大程度地保障业务的可用性、稳定性,对客户负责。

另外,如何做好音视频场景的核心页面内存泄露的监控以暴露问题是一个非常有挑战的事情,团队在尝试中,后续补充此部分内容!






扫码关注 IMWeb前端社区公众号,获取最新前端好文

微博、掘金、Github、知乎可搜索 IMWeb或 IMWeb团队关注我们。

扫码关注我们吧~