使用 Varnish 实现 HTTP Live Streaming (HLS) 协议流媒体访问加速。 根据最近做的一个小项目的笔记整理。

做 Web 加速方案时,最核心的组件——缓存软件——的可选项一般有 NginxSquidTraffic ServerVarnish 等。 由于命令行参数和环境变量丰富,Varnish 无疑是最易于部署的一款,即使不花费太多时间修改配置文件也能快速运行起来。

Varnish 内置支持 HLS。 然而使用 Varnish 做视频缓存,绝对不仅仅是配置文件中 set beresp.do_stream = true; 这么简单。


典型的部署结构

如下图的这类典型的简化部署结构里: 摄像设备将视频推送到流媒体源, 流媒体源编码切割HLS 协议的视频流, 经由用户访问性价比高的 Varnish 节点所组成的 CDN 网络代理,最终分发给终端用户的播放器和电视设备。

Varnish HTTP Streaming Acceleration

无论是大型的视频网站,还是小型的直播项目,都努力从性能、成本和架构复杂性方面取得平衡。


缓存策略

playlist 的设置

.m3u8 是 playlist 文件,需要及时更新,默认不缓存。 然而在做大规模 CDN 时可以设置一定的缓存时间,从而提升访问性能,也减轻源站的请求量。 即使是 1 秒缓存对源端压力也能有很大缓解。 设置时的原则是:根据 playlist 中给出的 #EXT-X-TARGETDURATION 的值,将缓存时间设置低于该值的一半。

以下例子中 #EXT-X-TARGETDURATION:10,所以将 playlist 的缓存时间设置为 5 秒。 较低的缓存时间(如 1s)可降低客户端平均播放延迟。

  if (bereq.url ~ "\.m3u8$") {
    set beresp.ttl = 5s;
    set beresp.grace = 900s;
  }

grace 设置源站失效时的缓存时间为 15 分钟。 这段时间客户端访问会循环播放最后一段可用的视频,而不是不播放。

传输流(TS)的设置

.ts 是切割后的媒体文件。 因为有时效性,不需要保存太长时间。 为保证有些播放滞后的客户端,这里设置最长时间为 5 分钟。

  if (bereq.url ~ "\.ts$") {
    set beresp.ttl = 300s;
    set beresp.grace = 900s;
  }

和 playlist 一样,grace 时间也设置为 15 分钟。

容量计算

容量包含两个层次的含义:

  1. 每个缓存节点所覆盖的区域、用户数量,及源节点的所需要承担的连接数量计算。
    由于用户数量影响带宽的采购,而覆盖区域则影响性能,这一选择主要是性价比的考量。
  2. 每个 Varnish 节点的带宽、内存、磁盘等硬件资源计算。
      根据缓存策略缓存到内存和磁盘,容量可以根据公式 最大缓存容量 = 码率 x 缓存时长 x 频道数 来估算。 将 100 个 1080p 画质的直播频道在内存中缓存 5 分钟,用一台 32G 内存的主机基本够用了。 当然考虑到分散带宽压力,还需要考虑多个节点。

设置内存使用量

VARNISH_STORAGE="malloc,20G"

高可用

高可用 用以应对缓存节点不稳定、宕机等情况。

由于缓存节点无状态,可以在某个节点故障时,将相应流量自动切换到其它节点。 支持自动切换的负载均衡,可以自建,也可以使用云服务商提供的方案,不再赘述。

稳定性

稳定性 用于应对网络质量波动、突发流量、源站失效等情况。

不稳定时首先保证最小可用返回。 比如设定条件临时自动调高 playlist 的缓存时间,这些操作要能够自动无干预完成。

当无法保证最小可用时则退而求其次,保证最短恢复时间。 一种常见的情况是后端不稳定,会返回错误状态码。 为了后端能在最短时间里恢复,一小段时间之内不再进行回源尝试,从而为源站减轻压力。 当然实际配置中,对收到错误响应的请求重试一次,从而排除偶发抖动的情况。

if (beresp.status == 503) {
  if (bereq.retries < 1) {
    return (retry);
  } else {
    set beresp.ttl = 3s;
  }
}

缓存键值规范化

由于客户端支持的特性不同,可能需要请求返回不一样的文件格式。 因此,缓存软件在本地存储文件时,不是简单直接根据文件名来保存。 而是通过几个特定键值的组合来区分在本地保存的文件。

URL 是这些键值里最重要的一个。 对 URL 的选择主要是考虑是否忽略其所包含参数。

另一个约定的是 HTTP 头里的 Vary 头。 一般是 Accept-Encoding。 但是不同浏览器发来的请求头五花八门。 即使同样需要 gzip,也可能是不一样的字符串,这会导致被区分本地存储为不同的文件。

Firefox, IE: gzip, deflate
Chrome: gzip,deflate,sdch
Opera: deflate, gzip, x-gzip, identity, *;q=0

所以我们需要对请求头进行规范化。 不相信