上一篇 下一篇 分享链接 返回 返回顶部

在香港服务器跑 Windows Server 2019,我是如何把 IIS 和 CDN 搭起来,把点播视频加载速度做顺的

发布人:Minchunlin 发布时间:2025-09-10 18:11 阅读量:863


那天夜里,香港柴湾机房的监控大屏上,北京与深圳用户的“首帧时间 P95”一路飙到 5 秒多,工单像雪片一样砸到 on-call 群里:“视频点开一直转圈”。我掀开服务器前面的理线扣,看着那台跑 Windows Server 2019 的视频分发源站,心里明白——不是带宽不够,是栈没打好:IIS 没把静态视频的缓存头喂给 CDN、Range 请求被压缩拦了、.m3u8 的 MIME 也配置错了。

那一夜我把 IIS 的站点、转码脚本、CDN 规则、日志指标全梳了一遍。第二天早上,指标回落,业务群里只剩一句“流畅了”。这篇就是那次复盘+重构的完整实操。

1)环境与目标(我这台机器和当晚的指标)

硬件/系统/网络

项目 参数
机房 香港(HK, 柴湾),到华南延迟低
服务器 Dell R650(单路)
CPU Intel Xeon Silver 4310(12C/24T)
内存 64 GB
系统盘 960 GB SATA SSD(OS)
数据盘 2 × 1.92 TB NVMe(RAID1)
网卡 2 × 10GbE(LACP 到接入交换机)
OS Windows Server 2019 Datacenter (IIS 10.0)
IIS 组件 Static Content / Logging / URL Rewrite / WebSockets / Static Compression
CDN 以“某主流国际 CDN”为例(你可以替换成 CloudFront、Cloudflare、阿里云、腾讯云等,规则通用)

业务目标 & 基线

指标 变更前(基线) 目标
首帧时间 P95(北京) 5.2 s ≤ 2.0 s
rebuffer ratio(全站) 7.5% ≤ 2.5%
源站出站带宽峰值 2.8 Gbps ≤ 1.2 Gbps(更多命中 CDN)
CDN 命中率 63% ≥ 92%(VOD 可做到很高)

核心思路:

  • VOD 不要整文件直出,用 HLS/DASH 切片,CDN 才能“块级高命中”。
  • IIS 只做“干净的静态源站”:正确的 MIME/Cache-Control/ETag/Range,不给媒体做压缩。
  • CDN 规则分层:m3u8 短 TTL、分片长 TTL、忽略无关 query,保留鉴权参数。
  • 播放器配合:HLS.js/原生 HLS,合理缓冲与 Range。
  • 监控闭环:IIS 日志 + CDN 日志 + 首帧/卡顿埋点,能定位到“哪一跳”。

2)步骤一:把 Windows/IIS 基座垫平

2.1 安装 IIS 组件(PowerShell)

Install-WindowsFeature Web-Server, Web-Common-Http, Web-Static-Content, Web-Default-Doc, `
Web-Http-Errors, Web-Http-Redirect, Web-Http-Logging, Web-Stat-Compression, Web-Filtering, `
Web-Mgmt-Tools, Web-WebSockets

# URL Rewrite 需要单独安装(离线包或企业软件仓库)
# 可选:IIS Compression 扩展(若需 Brotli,注意媒体路径禁用)

Windows Server 2019 原生支持 HTTP/2(TLS 1.2),够用;TLS 1.3 不强求(2019 并非原生全量)。

2.2 目录与权限

D:\vod\
   ├─ hls\         # 切片输出(m3u8/m4s/ts/init)
   ├─ dash\        # 可选
   └─ mp4\         # 仍保留整文件做 fallback

IIS_IUSRS 对 D:\vod 读权限,禁止写入(除你确实要用应用写切片)。

关闭防病毒对 D:\vod 的实时扫描(会卡 IO),改用计划全盘扫描。

2.3 站点绑定与 HTTP/2

  • 站点绑定 :443,证书选 SAN 覆盖所有业务域名。
  • 关闭旧协议:禁用 TLS1.0/1.1(组策略或注册表)。
  • 保持 Keep-Alive,HTTP/2 默认开启即可。

3)步骤二:转码与切片(HLS 优先)

我选择 HLS (fMP4/CMAF):浏览器兼容好(Safari 原生,其他用 hls.js),分片可被 CDN 高命中,首帧更快。

3.1 碎片码率梯度(实践值)

档位 分辨率 视频码率 音频 典型场景
1080p 1920×1080 5800 kbps AAC 128 kbps 大屏/宽带
720p 1280×720 2800 kbps AAC 128 kbps 主力
480p 854×480 1200 kbps AAC 96 kbps 弱网
360p 640×360 800 kbps AAC 64 kbps 省流

GOP 设为 2 * fps(例如 24 fps 就 48),所有层对齐关键帧。

3.2 ffmpeg 切 HLS(CMAF/fMP4)

ffmpeg -i input.mp4 -preset veryfast -sc_threshold 0 -keyint_min 48 -g 48 \
  -filter:v:0 scale=w=1920:h=1080:force_original_aspect_ratio=decrease \
  -filter:v:1 scale=w=1280:h=720:force_original_aspect_ratio=decrease \
  -filter:v:2 scale=w=854:h=480:force_original_aspect_ratio=decrease \
  -filter:v:3 scale=w=640:h=360:force_original_aspect_ratio=decrease \
  -map 0:v -map 0:a -c:v h264 -profile:v main -c:a aac -ar 48000 -ac 2 \
  -b:v:0 5800k -maxrate:0 6100k -bufsize:0 11600k \
  -b:v:1 2800k -maxrate:1 3000k -bufsize:1 5600k \
  -b:v:2 1200k -maxrate:2 1300k -bufsize:2 2600k \
  -b:v:3 800k  -maxrate:3 900k  -bufsize:3 1600k \
  -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0 v:3,a:0" \
  -master_pl_name master.m3u8 \
  -hls_time 4 -hls_playlist_type vod \
  -hls_segment_type fmp4 \
  -hls_fmp4_init_filename init_$VAR$.m4s \
  -hls_segment_filename "D:\vod\hls\%v\seg_%06d.m4s" \
  -master_pl_publish_rate 1 \
  -use_template 1 -use_timeline 1 \
  -f hls "D:\vod\hls\%v\index.m3u8"

输出目录会是:

D:\vod\hls\
  ├─0\ index.m3u8 init_0.m4s seg_000001.m4s ...
  ├─1\ ...
  ├─2\ ...
  └─3\ ...
master.m3u8

为什么 fMP4:字节寻址好、跨平台一致性强;CDN 对小分片(4s)更容易命中,首帧也快。

4)步骤三:IIS 配置(MIME、缓存头、Range、不压视频)

关键点是:让 CDN 愿意且敢缓存。

4.1 MIME 类型(一定要加)

IIS 管理器 → 站点 → MIME Types:

扩展 MIME
.m3u8 application/vnd.apple.mpegurl(或 application/x-mpegURL)
.ts video/mp2t
.mpd application/dash+xml
.m4s video/iso.segment
.mp4 video/mp4

4.2 web.config(按后缀发不同缓存头)

把下面的 web.config 放在站点根(或 D:\vod):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>

    <!-- 1) 媒体路径禁用压缩(避免 Range + gzip 冲突、浪费 CPU) -->
    <urlCompression doStaticCompression="true" doDynamicCompression="true" />
    <staticContent>
      <!-- 明确禁掉这些后缀的压缩 -->
      <remove fileExtension=".mp4" />
      <mimeMap fileExtension=".mp4" mimeType="video/mp4" />
      <remove fileExtension=".m4s" />
      <mimeMap fileExtension=".m4s" mimeType="video/iso.segment" />
      <remove fileExtension=".m3u8" />
      <mimeMap fileExtension=".m3u8" mimeType="application/vnd.apple.mpegurl" />
      <remove fileExtension=".ts" />
      <mimeMap fileExtension=".ts" mimeType="video/mp2t" />
    </staticContent>

    <!-- 2) 发送长 cache(分片/整视频),短 cache(清单) -->
    <httpProtocol>
      <customHeaders>
        <!-- 全局安全头,可按需裁剪 -->
        <add name="X-Content-Type-Options" value="nosniff" />
        <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Methods" value="GET,HEAD,OPTIONS" />
        <add name="Access-Control-Allow-Headers" value="Range,Origin,Accept,Content-Type" />
      </customHeaders>
    </httpProtocol>

    <staticContent>
      <!-- 让静态文件支持 Range(IIS 对静态默认支持 206,不需额外配置,这里只是强调 MIME) -->
    </staticContent>

    <rewrite>
      <outboundRules>
        <!-- 给不同类型加不同 Cache-Control -->
        <rule name="Cache m3u8 short" preCondition="IsM3U8">
          <match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
          <action type="Rewrite" value="public, max-age=30, stale-while-revalidate=60" />
        </rule>

        <rule name="Cache segments long" preCondition="IsSegment">
          <match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
          <action type="Rewrite" value="public, max-age=2592000, immutable" />
        </rule>

        <rule name="Cache mp4 long" preCondition="IsMP4">
          <match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
          <action type="Rewrite" value="public, max-age=2592000, immutable" />
        </rule>

        <!-- 禁止对媒体启用压缩(防止某些模块介入) -->
        <rule name="Disable compression for media" preCondition="IsMedia">
          <match serverVariable="RESPONSE_Vary" pattern=".*" />
          <action type="Rewrite" value="" />
        </rule>
      </outboundRules>

      <preConditions>
        <preCondition name="IsM3U8">
          <add input="{RESPONSE_Content-Type}" pattern="(application\/vnd\.apple\.mpegurl|application\/x-mpegURL)" />
        </preCondition>
        <preCondition name="IsSegment">
          <add input="{RESPONSE_Content-Type}" pattern="(video\/iso\.segment|video\/mp2t)" />
        </preCondition>
        <preCondition name="IsMP4">
          <add input="{RESPONSE_Content-Type}" pattern="video\/mp4" />
        </preCondition>
        <preCondition name="IsMedia">
          <add input="{RESPONSE_Content-Type}" pattern="(video\/mp4|video\/iso\.segment|video\/mp2t)" />
        </preCondition>
      </preConditions>
    </rewrite>

    <!-- 3) 目录浏览关、默认文档按需设 -->
    <directoryBrowse enabled="false" />

    <!-- 4) 日志:增加关键字段 -->
    <httpLogging dontLog="false" />
  </system.webServer>

  <system.applicationHost>
    <sites>
      <siteDefaults>
        <logFile logExtFileFlags="Date, Time, ClientIP, UserName, SiteName, ComputerName, ServerIP, Method, UriStem, UriQuery, Protocol, HttpStatus, Win32Status, TimeTaken, Host, UserAgent, Referer, BytesSent, BytesRecv, Connection, HTTPSubStatus" />
      </siteDefaults>
    </sites>
  </system.applicationHost>
</configuration>

要点

  • .m3u8 只缓存 30 秒(VOD 也可调大到几分钟,但我保守些,利于版本滚动)。
  • 分片(.m4s/.ts)和整 .mp4 缓存 30 天 + immutable。
  • 不要给媒体做 gzip/br 压缩,Range 会冲突,CDN 也不缓存压缩后的视频。
  • CORS 放开 GET/HEAD/OPTIONS,播放器跨域取分片不报错。

5)步骤四:CDN 规则(命中率与回源率的生死线)

不同厂商配置界面不同,但策略通用:

源站:指到我们的 https://your-origin.example.com(HK 源站)。

缓存规则(按路径匹配):

  • *.m3u8:TTL 30~120 秒;忽略除 token/auth/exp 外的 query;不合并 Range;允许 STALE on error。
  • */[0-3]/index.m3u8(分层清单):TTL 同上或略长。
  • *.m4s|*.ts|*.mp4:TTL 7~30 天;缓存键包含文件名与必要 query(若鉴权);允许 Range 缓存(有的厂商需勾选 Large File/Range Caching)。

缓存键(Cache Key):

  • 只保留鉴权参数:如 token, expires, sign;忽略 utm_*、v、t 等无关 query。

回源头:透传 Host;回源不压缩;可加 Origin Shield(CDN 内部二级缓存,抗抖动)。

访问控制:

  • 防盗链:Referer 允许白名单、对视频用 签名 URL 更稳。
  • CORS:允许播放器域名访问。

刷新/预热:发布新版本时先 预热 master.m3u8 与首屏分片,降低冷启动延迟。

CloudFront 的“Cache policy”里可以选 CachingOptimized 并白名单 query 参数;Cloudflare 用“Cache Rules”+“Origin Rules”就能做同样的事。国内厂商也都有“忽略参数/仅保留参数、分类型TTL、Range 缓存”等配置。

6)步骤五:播放器侧(最容易被忽视的一环)

6.1 HLS.js(Chrome/Firefox/Edge)

<!doctype html>
<html>
<head><meta charset="utf-8"></head>
<body>
<video id="v" controls playsinline preload="metadata" width="960" crossorigin="anonymous"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
const url = 'https://cdn.example.com/hls/master.m3u8?token=...';
const video = document.getElementById('v');

if (video.canPlayType('application/vnd.apple.mpegurl')) {
  video.src = url; // Safari 原生
} else if (Hls.isSupported()) {
  const hls = new Hls({
    maxBufferLength: 30,
    maxMaxBufferLength: 60,
    fragLoadingTimeOut: 20000,
    enableWorker: true,
    lowLatencyMode: false
  });
  hls.loadSource(url);
  hls.attachMedia(video);
  hls.on(Hls.Events.ERROR, (evt, data) => console.log('HLS error', data));
}
</script>
</body>
</html>

6.2 MP4 回退(老终端)

<video controls playsinline preload="metadata" width="960" src="https://cdn.example.com/mp4/movie.mp4#t=0.1"></video>

小技巧:hash #t=0.1 触发部分播放器按 Range 请求起播(IIS 默认支持 206)。

7)步骤六:监控与排障(IIS 日志 + PerfMon + CDN 日志)

7.1 IIS 日志字段(已经在 web.config 里加了扩展字段)

典型日志行(裁剪示例):

2025-09-05 18:02:33 10.0.0.5 GET /hls/1/seg_000123.m4s - 443 - 203.0.113.45 Mozilla/5.0 ...
200 0 0 45 cdn.example.com 1234 567 - - ...

关键看:sc-status、time-taken、cs(Referer)、time-taken。

7.2 LogParser 迅速出报表

logparser "SELECT cs-host, cs-uri-stem, COUNT(*) AS hits, AVG(time-taken) AS avg_ms
INTO hits.csv FROM C:\inetpub\logs\LogFiles\W3SVC1\u_*.log
WHERE sc-status=200 GROUP BY cs-host, cs-uri-stem ORDER BY hits DESC" -i:w3c -o:CSV

7.3 PerfMon 关键计数器

  • Web Service Cache:File Cache Hits %(> 90%)
  • HTTP Service Request Queues:Rejected Requests(=0)
  • Network Interface:Output Queue Length(≈0)

7.4 CDN 日志/面板

命中率、回源率、状态码分布、边缘/回源 RTT、各地区首字节时间(TTFB)。

8)效果验证(跨地域测速)

地区 变更前首帧 P95 变更后首帧 P95 备注
深圳 3.1 s 1.0 s HLS 首分片命中
广州 3.4 s 1.2 s 同上
北京 5.2 s 1.8 s 北向跨网好转
上海 4.1 s 1.5 s  
成都 4.6 s 1.9 s  

全站 CDN 命中率从 63% → 94%,源站峰值带宽 2.8 → 1.1 Gbps。

播放器埋点的 rebuffer ratio 从 7.5% → 2.1%。

9)我现场踩过的坑(以及怎么填)

.m3u8 404 或下载成文本

原因:MIME 没配或被默认成 text/plain。

解决:按表格添加 MIME,清理 CDN 缓存再试。

视频 200 而非 206,或拖动失效

原因:播放器没发 Range,或 Nginx/代理在某处剥了 Accept-Ranges。

解决:确认直连源站返回有 Accept-Ranges: bytes(IIS 静态默认有),播放器启用 #t= 或 HLS。

CDN 不缓存或命中低

原因:Cache-Control: private 或 no-store;Cache Key 带了随机 query。

解决:用 outbound rules 统一 Cache-Control;在 CDN 缓存键里只保留鉴权参数。

Range + 压缩冲突,播放器报错

原因:某些模块对 .mp4/.m4s 做了 gzip/br。

解决:媒体路径禁用压缩(见 web.config),CDN 也关闭媒体压缩。

跨域(CORS)报错

原因:m3u8/分片与页面不同域,响应头没 Access-Control-Allow-Origin。

解决:IIS 或 CDN 增加 CORS 头,允许 GET,HEAD,OPTIONS;必要时允许 Range 头。

多源/多机 ETag 不一致导致 200 反复回源

原因:IIS 生成的 ETag 包含 inode/时间戳,跨机器不同。

解决:要么关 ETag(依赖 Last-Modified),要么固定版本号到 URL(推荐)。

CDN 节点合并 Range 导致卡

原因:某些“按段回源合并”策略在高并发时抖动。

解决:关闭大对象合并回源(Large File Merge),改用 HLS 分片天然规避。

10)附录:几段我在线上用的命令/脚本

10.1 appcmd 一键修改压缩和缓存(给媒体路径)

%windir%\system32\inetsrv\appcmd set config "Default Web Site" `
    -section:system.webServer/urlCompression /doStaticCompression:"True" /commit:apphost

%windir%\system32\inetsrv\appcmd set config "Default Web Site" `
    -section:system.webServer/staticContent /+"[fileExtension='.m4s',mimeType='video/iso.segment']" /commit:apphost

10.2 PowerShell 预热首屏分片(命中热点)

$urls = @(
 "https://cdn.example.com/hls/master.m3u8?token=...",
 "https://cdn.example.com/hls/1/index.m3u8?token=...",
 "https://cdn.example.com/hls/1/seg_000001.m4s?token=..."
)
$wc = New-Object System.Net.WebClient
$urls | % { try { $wc.DownloadString($_) | Out-Null } catch { Write-Host "Warm failed: $_" } }

10.3 ffmpeg 批量脚本(多文件)
Get-ChildItem "D:\ingest\*.mp4" | % {
  $name = [System.IO.Path]::GetFileNameWithoutExtension($_.FullName)
  & ffmpeg.exe -y -i $_.FullName ... -master_pl_name master.m3u8 -hls_time 4 `
    -hls_segment_type fmp4 -hls_segment_filename "D:\vod\hls\$name\%v\seg_%06d.m4s" `
    -f hls "D:\vod\hls\$name\%v\index.m3u8"
}

结尾:风从机房过,面板上只剩下绿

凌晨 4 点,我在机柜前坐了会儿,听着风从冷通道穿过去。Grafana 上,首帧曲线滑进了 2 秒以内,CDN 命中率稳在 94% 左右。
这套“IIS 做干净源站 + HLS 切片 + CDN 分层缓存”落地后,我们再也没被“视频转圈”追着跑。后来新同事问我:“为什么不用更复杂的媒体服务器?”
我说:先把基础打对。MIME、缓存头、Range、切片、Cache Key、CORS——每一个都不难,但只有都到位,播放才会“顺”。
如果你也在香港、也在 Windows 2019 上跑 VOD,照着这份清单走一遍,你的进度条,也会乖乖动起来

目录结构
全文