一、背景
二、现象定位
三、问题探究
四、分析原因
五、总结
一、背景
一个春暖花开的午后,发生了一个诡异的现象:
IT同学:哪个域名流量这么大,太占用公司办公网资源了!
某业务:想问下这个问题需要怎么解决呢,我们已经遇到好几例了,上次有个店员一天消耗了60G,这个店员130G 。
我:收到CDN带宽告警,哪个cdn域名用了这么多带宽啊!
二、现象定位
通过CDN流量分析,定位到top流量消耗都是几个MP4视频。 点击流量最高的视频链接分析了下,是一个大小1G,视频时长约30分钟的MP4视频。
于是找业务同学确认:
我:这个视频是做什么用的啊?
业务同学:这是一个学习培训视频,推送给了2500人学习观看。
我:嗯?不对啊,CDN监控显示花费流量120TB,如果2500人完播看完一个视频,正常应该是 2500x1g =2.5TB,这多出的50倍流量怎么来的?
我通过谷歌浏览器访问了业务上传的另一个视频(视频大小500M时长40分钟),观看视频并开启F12分析下流量使用。


完播居然花了8G,发送了3700个206请求!!!!
现象:
开始播放后,会出现3个206请求;
第三个请求发完后,浏览器随时间发起多个206请求;
请求来来回回Range。
我:这不行啊,我赶紧ai、百度、google各种搜索。
发现通过命令ffmpeg处理视频可以解决这个问题:
ffmpeg -i bad.mp4 -c copy -movflags faststart good.mp4
将新视频上传,f12分析视频;
206请求只有一个了;
且多206请求现象消失;
完播视频流量消耗=原视频大小。
ffmpeg能解决该问题,但长视频还是将其转为流媒体格式,推动业务改进后流量监控如下图:

三、问题探究
四个问题:
为什么一开始会出现3个206请求?
3个206请求后,为什么会发起多个206请求?
为什么请求头Range会来回跳跃?
为什么使用ffmpeg处理视频后能解决这个问题?
要分析该奇异现象发生原因就得知道整个播放流程是怎么运作的。
MP4 文件由多个 Box(盒子)组成,主要包括:
MP4 文件结构:┌─────────────────────────────────────┐│ ftyp (文件类型) │ ← 文件开头├─────────────────────────────────────┤│ mdat─────────────────────────────────────┤│ moov (元数据) │ ← 可能在开头或结尾│ ├─ mvhd (文件头信息) ││ ├─ trak (视频轨道) ││ │ ├─ stco (Chunk偏移表) │ ← 记录数据位置│ │ ├─ stsz (样本大小表) │ ← 记录每帧大小│ │ └─ stsc (样本到Chunk映射) │ ← 记录帧的分布│ └─ trak (音频轨道) ││ ├─ stco (Chunk偏移表) ││ ├─ stsz (样本大小表) ││ └─ stsc (样本到Chunk映射) │└─────────────────────────────────────┘
关键点:
moov box 包含了所有的索引信息(类似目录);
mdat box 包含了实际的音视频数据;
播放器必须先读取 moov 才能知道数据在哪里。

从Media信息得知Chrome 浏览器使用 FFmpegDemuxer 来解析读取 MP4 文件,参考谷歌浏览器源码,播放流程如下:

mov_find_next_sample(这一个函数返回的是下一个样本位置):
static AVIndexEntry *mov_find_next_sample(AVFormatContext *s, AVStream **st){....................// 遍历所有流,找到最佳样本for (i = 0; i < s->nb_streams; i++) {AVIndexEntry *current_sample = &index_entries[current_index];int64_t dts = current_sample->timestamp;//if (dtsdiff <= AV_TIME_BASE && current_sample->pos < sample->pos) {// DTS 差 ≤ 1 秒,选择位置靠前的(减少 seek)sample = current_sample;} elseif (dtsdiff > AV_TIME_BASE && dts < best_dts) {// DTS 差 > 1 秒,选择 DTS 最小的(保证顺序)sample = current_sample;....................}}return sample;}}
在mov_find_next_sample函数中还有一个开关interleaved_read:
在解复用器层对多个轨道的包进行交错。对于交错不良的文件,这可以防止由于不同轨道之间包的大间隙而引起的播放问题,因为MOV/MP4文件对包的放置没有要求。然而,对于非常交错不良的文件,这可能会导致过多的寻道操作,因为需要在轨道之间寻道,所以禁用此功能可能会防止I/O问题,但代价是可能影响播放。
理想情况下,一个交错良好的 MP4 文件 会将音频和视频数据包按照时间顺序交替排列:
[视频包1] [音频包1] [视频包2] [音频包2] [视频包3] [音频包3] ...
这种交错存储方式的主要优点包括:
减少磁盘访问次数:播放器能连续读取音视频数据,提高读取效率;
降低播放缓冲区需求:播放时能够平滑加载音视频数据;
支持渐进式下载和播放:支持按需下载,提升流媒体性能。
然而,如果音视频数据没有交错,而是集中存储,比如:
[视频包1] [视频包2] [视频包3] ... [音频包1] [音频包2] [音频包3] ...
会带来以下问题:
频繁寻址:播放器需要在视频和音频数据之间跳转,增加磁盘访问次数;
大量 HTTP Range 请求:每次跳转都需要发起新的 HTTP 请求,降低性能;
播放不稳定:频繁请求数据会导致卡顿和延迟,影响用户体验。
由于谷歌浏览器使用的是 FFmpegDemuxer 解码并读取视频信息,其底层依赖于 FFmpeg 代码实现,只是在上层进行了浏览器的缓冲/预读封装。
测试视频:longbad.mp4(该视频存在较差的音视频交错问题)
测试工具:FFmpeg(模拟谷歌浏览器的解码处理过程)
本次测试旨在评估 interleaved_read 参数对视频播放的影响,并借助ffmpeg工具分析视频多206的问题。
交错读取(Interleaved Read):默认情况下,FFmpeg 会将音视频数据交错存储,Interleaved为1;ffmpeg -i https://xxxx/longbad.mp4 -ss 60 -t 5.0 -y output.mp4 -loglevel trace 2>&1 | grep "Range"-i https://...:从远程服务器读取输入文件-ss 60:从视频的第60秒开始-t 5.0:截取5秒的片段-y:覆盖输出文件output.mp4:输出文件名通过wc -l统计发现请求了5078次!,看5s的视频请求了5078次。
结合cdn日志和longbad视频的音视频轨道分析:

Range来回跳动,在60s时刻从5967030跳到46622680
相同时间范围,音频offset和视频offset距离很远
非交错读取(Non-interleaved Read):通过设置 -interleaved_read 0,FFmpeg 会将音频和视频数据分开读取。ffmpeg -interleaved_read 0 -i https://xxxx/longbad.mp4 -ss 60 -t 5.0 -y output.mp4 -loglevel trace 2>&1 | grep "Range"通过wc -l统计发现请求了3只请求了三次
四、分析原因
原因:moov box 在文件末尾。
moov存着视频的索引信息,详细看MP4 文件结构。
正常的 MP4 文件播放流程:
读取 ftyp+moov → 在头部直接读mdat ----一个206
但问题视频的 moov 在文件末尾:

导致:
1. 读取 ftyp和moov → 不在头部啊去尾部查 -----第一个2062. 读取尾部moov → -----第二个2063. 开始播放视频 → -----第三个206
原因:视频文件交错不良 + FFmpeg 的读取策略。
1)交错不良的文件布局
通过获取视频文件offset的信息制作了一个图:
音视频物理位置交错图情况:X轴 = Packet在文件中的物理顺序(按pos(offset)排序后的索引)Y轴 = Stream类型(0=视频,1=音频)每个条形 = 一个packet颜色 = 红色=视频packet,青色=音频packet可以看到有一大片音频在前边

[音频包1] [音频包2] [音频包3] ... ... [视频包1] [视频包2] [视频包3]
2)FFmpeg 的读取策略导致来回跳跃
mov_find_next_sample() 的选择逻辑:
时间差 ≤ 1 秒时,优先选择位置靠前的帧;
但交错不良导致"位置靠前"的帧距离offset很远。
举例音视频两个offsetA1 下一个V0选择策略时间轴(微秒):0 10000 20000 30000 40000├─────────┼──────────┼──────────┼──────────┤│ │ ││ A1 V0│ (23219) (33333)│ │ ││ └──10114──┘│ (差值 < 1秒)│A0 (0)V0 (0)当视频帧之间时间<1秒 会选择最小的offset( pos )这就导致交错不良的文件,DTS 差小但文件位置差大,然后来回请求
原因:fmpeg对视频处理后,会对音视频的位置进行重新编排(流媒体除外),底层默认处理。
处理视频文件后的音视频交错图:

ffmpeg -i bad.mp4 -c copy -movflags faststart good.mp4-i 输入指定文件-c -copy 流拷贝- movflags faststart 将视频moov提提前ffmpeg会对其物理位置进行重新编排!!!av_interleaved_write_frame() (libavformat/mux.c)↓ff_interleave_packet_per_dts() (libavformat/mux.c)↓static int interleave_compare_dts(AVFormatContext *s,const AVPacket *next,const AVPacket *pkt){AVStream *st = s->streams[pkt->stream_index];AVStream *st2 = s->streams[next->stream_index];// 1. 比较 DTS(转换到统一时间基)int comp = av_compare_ts(next->dts, st2->time_base,pkt->dts, st->time_base);// 2. 处理音频预加载(audio_preload)if (s->audio_preload) {int preload = st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;int preload2 = st2->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;if (preload != preload2) {// 音频包提前一点时间int64_t ts = av_rescale_q(pkt->dts, st->time_base, AV_TIME_BASE_Q)- preload * s->audio_preload;int64_t ts2 = av_rescale_q(next->dts, st2->time_base, AV_TIME_BASE_Q)- preload2 * s->audio_preload;comp = (ts2 > ts) - (ts2 < ts);}}// 3. DTS 相同时,按流索引排序if (comp == 0)return pkt->stream_index < next->stream_index;return comp > 0;}
五、总结
流媒体格式(如HLS)在带宽使用和播放流畅度上更优,但改造成本高。MP4格式因兼容性强和成本较低,仍是主流选择。综合考虑使用场景和成本,我们选择:
短视频统一使用 FFmpeg 处理,确保 moov box 在文件开头,优化音视频交错,避免频繁的 Range 请求。
长视频采用 HLS 等流媒体格式,按需加载视频片段,提升播放流畅度和带宽利用率。
参考资料
The investigation of excessive FFmpeg requests: https://blog.dreamfever.me/posts/2024-06-09-poor-performance-of-ffmpeg-i-url/#reference
endless canceled http requests when playing an mp4 in a <video> tag: https://issues.chromium.org/issues/40292515
ChromiumSrouce: https://source.chromium.org/chromium/chromium/src/+/main:third_party/ffmpeg/libavformat/mov.c;l=11048;bpv=1;bpt=1
FFmpegSoure: https://github.com/FFmpeg/FFmpeg/blob/94f2274a8b61438572f0873ccf430e55ce0e0e2b/libavformat/mov.c#L9767-L9795
作者介绍
邹泉安,转转运维部。
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721