1 背景
在一个春暖花开的午后,团队内部接连出现了几个诡异的现象:
- 网络运维同事反馈:“哪个域名流量这么大,太占用公司办公网资源了!”
- 业务方询问:“想问下这个问题需要怎么解决呢,我们已经遇到好几例了,上次有个店员一天消耗了60G,这个店员130G。”
- 与此同时,我收到了CDN带宽告警,心想:“哪个CDN域名用了这么多带宽啊!”
2 现象定位
2.1 问题流量定位
通过对CDN流量日志的分析,定位到消耗流量最高的几个资源均是MP4视频文件。点击流量最高的视频链接进行分析,发现这是一个大小约 1G、时长约 30分钟 的视频文件。
与业务同学确认后得知,这是一个学习培训视频,推送给了2500人学习观看。然而,CDN监控显示的流量消耗高达120TB。如果按2500人完整观看一次(1GB/人)计算,正常流量消耗应为2.5TB左右。多出的近50倍流量从何而来?
2.2 流量使用验证
为了验证问题,我使用谷歌浏览器访问了业务上传的另一个视频(大小约500MB,时长40分钟),在观看的同时开启开发者工具的Network面板进行分析。
在Network面板中,可以看到播放过程中产生了大量网络请求。状态栏显示:共发送了3696/3709个请求,已传输约8.75GB数据,而视频资源总大小约为8.89GB。这意味着播放一个500MB的视频,实际消耗了接近8.8GB的流量。
进一步观察请求详情,所有请求的状态码均为206(部分内容)。其中一张记录了时间与字节范围的表格数据如下所示:
| 时间 (Time) |
字节范围 (Range) |
| 363 ms |
bytes=0- |
| 314 ms |
bytes=360022016- |
| 40.53 s |
bytes=65536- |
| 515 ms |
bytes=5046272- |
| 408 ms |
bytes=24838144- |
| 266 ms |
bytes=9863168- |
| 397 ms |
bytes=26509312- |
| 532 ms |
bytes=10223616- |
| 262 ms |
bytes=27918336- |
| 665 ms |
bytes=10059776- |
| 300 ms |
bytes=28409856- |
| 667 ms |
bytes=8945664- |
| 265 ms |
bytes=30212096- |
问题现象总结:
- 视频开始播放后,会立即出现 3个206请求。
- 之后,浏览器会随时间发起 数千个206请求。
- 请求的
Range 范围在视频文件的不同位置 来回跳动,例如在相近的时间点(如408ms和397ms),请求的字节范围跳跃很大(从约24MB跳到约26MB)。
经过搜索,发现可以通过一条简单的 ffmpeg 命令处理视频来解决此问题:
ffmpeg -i bad.mp4 -c copy -movflags faststart good.mp4
将处理后的新视频上传并分析,发现206请求数量大幅减少至正常水平(通常为1个),多206请求现象消失,播放视频消耗的流量等于视频原始大小。
在处理长视频时,更优的方案是将其转为流媒体格式(如HLS)。推动业务进行相应改进后,从监控图表可以看到,流量消耗的曲线在最初的峰值后迅速下降并趋于平稳。
3 问题探究
要分析这个奇异现象,需要理解整个播放流程。我们主要探究以下四个问题:
- 为什么一开始会出现3个206请求?
- 3个206请求后,为什么会发起多个206请求?
- 为什么请求头
Range 会来回跳跃?
- 为什么使用
ffmpeg 处理视频后能解决这个问题?
3.1 MP4 文件结构
MP4 文件由多个 Box(盒子)组成,其基本结构如下:
MP4 文件结构:
┌─────────────────────────────────────┐
│ ftyp (文件类型) │ ← 文件开头
├─────────────────────────────────────┤
│ mdat (媒体数据) │
├─────────────────────────────────────┤
│ moov (元数据) │ ← 可能在开头或结尾
│ ├─ mvhd (文件头信息) │
│ ├─ trak (视频轨道) │
│ │ ├─ stco (Chunk偏移表) │ ← 记录数据位置
│ │ ├─ stsz (样本大小表) │ ← 记录每帧大小
│ │ └─ stsc (样本到Chunk映射) │ ← 记录帧的分布
│ └─ trak (音频轨道) │
│ ├─ stco (Chunk偏移表) │
│ ├─ stsz (样本大小表) │
│ └─ stsc (样本到Chunk映射) │
└─────────────────────────────────────┘
关键点:
moov box 包含了所有的索引信息(类似目录),播放器必须先读取它才能知道数据在哪里。
mdat box 包含了实际的音视频数据。
3.2 浏览器播放流程
通过查看谷歌浏览器开发者工具中“Media”标签页下的日志信息,可以得知Chrome浏览器使用 FFmpegDemuxer 来解析读取 MP4 文件。参考源码,其播放时的数据读取流程可以概括为以下步骤:
- HTML5
<video> 标签 发起播放。
- FFmpegDemuxer 接管,它封装了 FFmpeg 的
libavformat 库。
- 调用
mov_find_next_sample() 函数查找下一个音/视频样本(sample)的位置(offset)。
- 调用
avio_seek() 检查 FFmpeg 的内部缓冲区。
- 判断跳跃距离:
- 如果跳跃距离 大于 32KB,则未命中FFmpeg缓冲区。
- 进入下一级缓存检查(如谷歌的2MB缓冲区)。
- 若仍未命中,则最终发起一个新的HTTP 206范围请求。
3.3 源码分析
在 mov_find_next_sample 函数中,其核心逻辑是遍历所有流(音频、视频),找到“下一个”应该读取的样本。其中一个关键策略是:
- 当不同轨道的样本解码时间戳(DTS)差值小于等于1秒(
AV_TIME_BASE)时,优先选择文件位置(pos)更靠前的样本,目的是减少寻址(seek)操作。
这个函数受一个名为 interleaved_read 的开关影响。其注释说明:“在解复用器层对多个轨道的包进行交错。对于交错不良的文件,这可以防止由于不同轨道之间包的大间隙而引起的播放问题……然而,对于非常交错不良的文件,这可能会导致过多的寻道操作。”
什么是理想的交错?
一个交错良好的 MP4 文件会将音频和视频数据包按照时间顺序交替排列:
[视频包1] [音频包1] [视频包2] [音频包2] [视频包3] [音频包3] ...
这种存储方式能显著减少磁盘/网络访问次数,降低播放缓冲区需求,并支持高效的渐进式下载。
什么是不良交错?
如果音视频数据是集中存储的,例如:
[视频包1] [视频包2] [视频包3] ... [音频包1] [音频包2] [音频包3] ...
就会导致播放器需要在视频和音频数据块之间频繁跳转,从而引发大量 HTTP Range 请求,导致播放卡顿和带宽浪费。
3.4 测试验证
由于谷歌浏览器使用 FFmpegDemuxer,我们可以直接用 FFmpeg 命令行工具模拟其行为,分析问题。
测试视频: longbad.mp4(一个存在严重音视频交错问题的视频)
1. 模拟浏览器默认(交错读取)行为:
ffmpeg -i https://xxxx/longbad.mp4 -ss 60 -t 5.0 -y output.mp4 -loglevel trace 2>&1 | grep “Range”
统计 grep 输出的行数,发现为了播放5秒钟的视频,竟然发起了 5078次 Range 请求!
结合CDN请求日志和视频轨道分析数据(部分关键记录如下),可以清楚地看到问题:
Range 来回剧烈跳动。例如,在相近时间点,请求从字节位置 5967030 跳到了 46622680。
- 相同时间范围内的音频 offset 和视频 offset 在文件中的物理距离非常远。
| request_range |
request_time |
... |
| 46641572- |
181 |
... |
| 5967030- |
96 |
... |
| 46622680- |
83 |
... |
| 46654153- |
186 |
... |
2. 模拟关闭交错读取的行为:
ffmpeg -interleaved_read 0 -i https://xxxx/longbad.mp4 -ss 60 -t 5.0 -y output.mp4 -loglevel trace 2>&1 | grep “Range”
统计结果显示,同样的5秒视频,仅发起了 3次 Range 请求。这证明了问题的根源正是 interleaved_read 模式在遇到交错不良文件时的低效行为。
4 分析原因
4.1 为什么一开始会出现3个206请求?
原因:moov box 位于文件末尾。
对于正常的MP4文件(moov在头部),播放流程是:读取 ftyp + moov → 开始读取 mdat 数据(通常只需1个206请求)。
问题视频的 moov 元数据盒子位于文件尾部,其文件结构树形视图类似于:
ftyp (FileTypeBox)
free (FreeSpaceBox)
mdat (MediaDataBox)
moov (MovieBox)
这导致播放流程变为:
- 请求文件开头,试图寻找
moov,未果(第一个206)。
- 需要到文件尾部获取
moov(第二个206)。
- 拿到索引后,回到文件开始位置播放数据(第三个206)。
4.2 为什么会发起多个206请求,且Range范围来回跳动?
核心原因:视频文件音视频交错不良 + FFmpeg 的 interleaved_read 读取策略。
交错不良的文件布局:
通过分析文件生成的数据包交错模式图可以直观看到问题。图中,X轴代表数据包在文件中的物理位置索引,Y轴代表流类型(红色为视频包,青色为音频包)。在不良交错的文件中,会呈现大块的单一颜色区域,例如文件前半部分全是音频包,后半部分全是视频包。
[音频包1] [音频包2] [音频包3] ... ... [视频包1] [视频包2] [视频包3]
FFmpeg的读取策略导致来回跳跃:
在 interleaved_read 模式下,mov_find_next_sample() 函数会尝试交替读取音视频包。当它读完一个视频包,需要找下一个音频包时,由于上述不良布局,下一个音频包可能远在文件开头(距离数十MB)。根据“DTS差≤1秒时选择位置靠前样本”的逻辑,FFmpeg会发起一个到文件开头的Range请求去获取那个音频包。紧接着,读完这个音频包后,下一个视频包又远在文件末尾,于是又发起一个跳回文件尾部的Range请求。如此循环,形成了“来回跳动”的大量206请求。
4.3 为什么使用ffmpeg处理视频后能解决这个问题?
原因:ffmpeg 命令在复制流(-c copy)并添加 -movflags faststart 时,会对音视频数据包的物理存储位置进行重新编排(交织)。
处理后的文件,其数据包交错模式图会呈现出红青两色紧密交替的细条纹状,这表明音视频包已经按照时间顺序良好地交错排列。
ffmpeg -i bad.mp4 -c copy -movflags faststart good.mp4 这条命令的作用是:
-i:指定输入文件。
-c copy:进行流拷贝,不重新编码。
-movflags faststart:将 moov 盒子移动到文件开头,并在移动过程中优化音视频包的交错存储。
其底层通过 av_interleaved_write_frame() 和 ff_interleave_packet_per_dts() 等函数,依据数据包的解码时间戳(DTS)和音频预加载(audio_preload)等设置,对包进行排序和交错写入,从而生成一个适合流式播放、无需频繁远程寻址的文件。
5 总结
流媒体格式(如HLS、DASH)在带宽使用效率和播放流畅度上更具优势,但存在一定的改造成本。MP4格式因兼容性极强和部署简单,仍是许多场景下的主流选择。
综合成本与收益,我们建议:
- 对于短视频:统一使用
ffmpeg -i input.mp4 -c copy -movflags faststart output.mp4 命令进行处理。这能确保 moov 盒子位于文件头部,并优化音视频交错,从根本上避免因播放器频繁寻址而引发的CDN带宽激增问题。
- 对于长视频:采用 HLS 等流媒体格式进行分发,实现按需加载视频分片,从而提升播放体验并优化带宽利用率。
参考资料
[1] 一条命令让CDN视频带宽成本降低90%!!!, 微信公众号:mp.weixin.qq.com/s/EniHocKC5x4eAV9MDgOwOQ
版权声明:本文由 云栈社区 整理发布,版权归原作者所有。