用 NGINX 高效缓存字节范围请求

用 NGINX 高效缓存字节范围请求

大文件点播常以 HTTP Range 请求读取局部字节。缓存未命中时,NGINX 默认仍可能从源站拉取整个文件;当请求在填充完成前不断到来,源站就会重复承担完整回源。本篇沿用 Owen Garrett 的可复现实验,比较默认行为、缓存锁与切片缓存,并说明各自的取舍。

三种字节范围缓存路径:默认策略拉取整个文件,缓存锁合并填充,切片缓存按需获取并缓存小片段。
图 1:默认整文件填充、缓存锁和切片缓存的请求路径(自绘示意图)。

实验如何模拟慢速源站

原文用一个约 10 MB 的测试文件模拟视频对象。文件每隔 10 个字节写入对应偏移量,因而可以通过局部读取结果检查 Range 处理是否正确。作者先用 curl 请求字节 500000 至 500009,返回的十字节内容对应偏移 500000。随后在隔离实验网络里将源站到缓存代理的链路限制到约 1 MB/s,完整下载耗时 10.993 秒(原文报告约 11 秒)。这个数字来自原文当年的实验环境,只用于解释填充过程,不是当前环境的性能保证。

请只在可丢弃的实验主机与临时配置中复现带宽整形。原文中的 tc qdisc add 会修改网卡队列规则,不能在共享或生产主机上照抄。测试源站、缓存和客户端都应使用隔离网络及专用目录。

默认行为:局部响应,整文件回源

对象已经完整进入缓存后,NGINX 可直接从磁盘缓存返回所需范围。若对象尚未缓存,默认行为是向源站请求整个文件,并把响应写入临时文件;一旦已收到客户端所需范围,就先向客户端返回那部分数据,随后继续在后台接收完整文件,传输完成后再将其移入缓存。

在作者的 10 MB、1 MB/s 实验中,首次请求文件中间的十个字节耗时 5.352 秒后收到。虽然客户端已得到所需片段,完整缓存总计约 10.993 秒,再等待约五秒才完整。更麻烦的是,在首次填充完成前重复请求相同位置,NGINX 可能重新启动整文件填充。若新请求间隔短于完整填充时间,缓存就难以稳定下来,源站会重复发送整个对象。

proxy_cache_path /var/cache/nginx/range-test
    keys_zone=range_test:10m
    max_size=200m
    inactive=10m;

server {
    listen 127.0.0.1:8080;
    server_name localhost;

    location / {
        proxy_cache range_test;
        proxy_pass http://127.0.0.1:8081;
    }
}

上例假设本机专用测试源站监听 127.0.0.1:8081,并将缓存路径、区域和代理监听端口限定在实验配置中。正式部署时要依据当前 NGINX 版本、磁盘容量、缓存键、失效策略及源站身份验证重新设计;示例并不是可直接上线的完整虚拟主机配置。

方案一:缓存锁控制并发填充

proxy_cache_lock on 让同一个缓存键的填充由一个请求主导。其他请求等待填充完成,或等待锁超时。下面是与前一段基础配置分开的完整回环测试方案。它将锁超时设为零,使等待请求立即以原有字节范围请求转发到源站,而不会启动另一轮整文件缓存填充;锁龄设为 200 秒,长于该实验预计的填充时间。proxy_cache_use_stale updating 则在资源更新过程中允许使用已有的旧缓存响应。

http {
    proxy_cache_path /var/cache/nginx/range-test
        keys_zone=range_test:10m max_size=200m inactive=10m;

    upstream origin {
        server 127.0.0.1:8081;
    }

    server {
        listen 127.0.0.1:8080;
        server_name localhost;

        location / {
            proxy_cache range_test;
            proxy_cache_valid 200 10m;
            proxy_cache_lock on;
            proxy_cache_lock_timeout 0s;
            proxy_cache_lock_age 200s;
            proxy_cache_use_stale updating;
            proxy_pass http://origin;
        }
    }
}

原文记录的首次请求耗时 5.422 秒;在整文件填充期间,后续范围请求耗时 0.042 秒,由源站直接响应,原文访问日志以 206 状态码确认其保留了 Range 请求。完整填充结束后,缓存命中耗时 0.012 秒;原文日志中初次填充为 200 整文件响应。以上均为作者在 2015 年测试环境中的结果。缓存锁减少重复的完整填充,但填充窗口内的后续流量仍会到达源站。原文还指出,锁超时默认值为五秒;超时后排队请求会保留 Range 请求头转发给源站,且该响应不会被缓存。默认值与行为应以正在使用的版本官方代理模块文档为准。

方案二:把对象拆成可缓存切片

Cache Slice 模块把文件分成多个范围,只按需向源站读取相关片段。客户端请求某一段时,NGINX 获取覆盖该范围的一个或多个切片,缓存这些片段,再组合出响应。对于原文的中间十字节请求,1 MiB 切片覆盖了字节 5000000 至 5000009,实际回源范围为 4194304 至 5242879。原文记录客户端请求耗时 0.977 秒,而不是等整份文件到齐。

原文将 slice 和切片缓存描述为 NGINX Plus R8 与 NGINX Open Source 1.9.8 引入的能力。下例给出完整的 http 配置上下文,限制为回环接口上的测试代理;本机源站应监听 127.0.0.1:8081,缓存目录须由 NGINX 工作进程写入。它保留关键关系:缓存键基于 NGINX 默认键变量并包含 $slice_range,向源站传递该切片的 Range,启用 HTTP/1.1,并缓存 200 与 206。示例仍不是生产配置。

http {
    proxy_cache_path /var/cache/nginx/range-test
        keys_zone=range_test:10m max_size=200m inactive=10m;

    upstream origin {
        server 127.0.0.1:8081;
    }

    server {
        listen 127.0.0.1:8080;
        server_name localhost;

        location / {
            slice               1m;
            proxy_cache         range_test;
            proxy_cache_key     $scheme$proxy_host$request_uri$slice_range;
            proxy_set_header    Range $slice_range;
            proxy_http_version  1.1;
            proxy_cache_valid   200 206 1h;
            proxy_pass          http://origin;
        }
    }
}

当前官方模块文档说明,Cache Slice 模块并非默认构建,需以 --with-http_slice_module 启用;文档还警告它与后台缓存更新子请求的组合目前不能按预期工作。切片缓存适用于体积大、发布后不变的对象,例如静态视频文件。NGINX 会检查源站 ETag;原文说明若 ETag 变化,正在进行的交易会被中止,因为已缓存片段可能拼成不一致的版本。上线前还应确认源站正确支持范围响应、代理缓存键不会让不同租户或权限上下文共享内容,并验证对象更新与清理策略。

如何选择切片大小

切片越小,单片传输越快,缓存逐步可用的机会越高;但整文件请求可能同时触发成千上万个子请求,增加内存、文件描述符、磁盘访问和请求调度开销。切片太大则可能再次接近整文件填充,削弱按需读取的优势。原文建议以“单片约一两秒内可传完”作为初步思路,最终仍需根据网络带宽、对象尺寸、并发度与源站承载能力做负载验证。

结论与限制

  • 对象填充较快,且能接受填充期间额外回源时,缓存锁可以避免重复整文件填充。
  • 填充慢、对象发布后稳定时,切片缓存可缩短首次局部响应的等待时间,也让缓存按请求逐步积累。
  • 切片越小并非越好;必须评估子请求数量、资源消耗、源站的范围请求实现和缓存失效策略。
  • 原文使用 2015 年的测试数据及 NGINX Plus R8 / 开源版 1.9.8 的版本背景。配置和默认值不能直接当作当前版本的规范;本文没有运行命令或验证实际吞吐。

原文作者:Owen Garrett。原文发表于 2016 年 1 月 21 日,刊载于 NGINX Community Blog。来源:Smart and Efficient Byte-Range Caching with NGINX。本文为中文整理译稿;原页面未标明适用于全文的开放内容许可证,原文版权归其权利人。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容