学而实习之 不亦乐乎

Nginx 中 slice 模块的使用

2023-10-24 07:58:19

Nginx的slice模块可以将一个请求分解成多个子请求,每个子请求返回响应内容的一个片段,让大文件的缓存更有效率。

 一、HTTP Range请求

HTTP 客户端下载文件时,如果发生了网络中断,必须重新向服务器发起 HTTP 请求,这时客户端已经有了文件的一部分,只需要请求剩余的内容,而不需要传输整个文件,Range 请求就可以用来处理这种问题。如果 HTTP 请求的头部有 Range 字段,如下:

Range: bytes=1024-2047

表示客户端请求文件的第 1025 到第 2048 个字节,这时服务器只会响应文件的这部分内容,响应的状态码为206,表示返回的是响应的一部分。如果服务器不支持 Range 请求,仍然会返回整个文件,这时状态码仍是 200。

 
二、Nginx 启用 slice 模块

ngx_http_slice_filter_module 模块默认没有编译到 Nginx 中,需要编译时添加 --with-http_slice_module 选项。编译完成后, 需要在Nginx配置文件中开启,配置如下:

location / {
    slice             1m;
    proxy_cache       cache;
    proxy_cache_key   $uri$is_args$args$slice_range;
    proxy_set_header  Range $slice_range;
    proxy_cache_valid 200 206 1h;
    proxy_pass        http://localhost:8000;
}

slice 指令设置分片的大小为 1m。 这里使用了 proxy_set_header 指令,在取源时的 HTTP 请求中添加了 Range 头部,向源服务器请求文件的一部分,而不是全部内容。在 proxy_cache_key 中添加 slice_range 变量这样可以分片缓存。

三、slice_range 变量

slice_range 这个变量作用非常特殊,这个变量的值是当前需要向源服务器请求的分片,如果分片的大小为1m,那么最开始变量的值为 bytes=0-1048575,通过配置文件中的 proxy_set_header Range $slice_range;可以知道取源时请求的 Range 头部为Range:bytes=0-1048575,源服务器如果支持Range请求,便会返回响应的前1m字节,得到这个响应后slice_range变量的值变为bytes=1048576-2097171 ,再次取源时便会取后1m字节,依次直到取得全部响应内容。

四、Nginx分片的实现

Nginx 的 slice 模块是通过挂载 filter 模块来起作用的,处理流程如下所示

  1. 每次取源时都会携带Range头部,
  2. 第一次取源请求前1m内容,如果响应在1m以内,或者源服务器不支持 Range 请求,返回状态码为 200,这时会直接跳过 slice 模块。
  3. 在 body_filter 中向客户端发送得到的当前的分片,然后检查是否到达文件末尾,如果没有则生成一个子请求,子请求会向源服务器请求下一个分片,依次循环。

五、slice模块

当我们使用nginx作为反向代理,并且响应上游的响应的时候,如果上游的文件特别大。那么 nginx 去处理这么大的响应的时候,它的效率就比较低下了,特别是有多个请求打到暂时没有缓存的大文件的时候。

这个时候 nginx 官方提供了 slice 的模块可以通过 range 协议将一个很大的响应分解为很多小的响应来提升我们的服务性能。

语法:slice size;
默认值:slice 0;
上下文:http,server,location
功能:通过 range 协议将大文件分解成多个小文件,更好的用缓存为客户端的 range 协议服务。
模块:http_slice_module,通过 --with-http_slice_module 启用功能,

为 0 的时候表示禁用这个功能,后面跟上一个 size 表示通过 range 协议将多个大文件分解为多个小文件独立的缓存,当客户端发来的请求中已经含有 range 协议时可以更好的服务。

客户端请求100个字节,起始于150,请求内容的范围是150-249。发到 nginx 之后根据 slice 配置,比如配置为 100,那么就是 0-100,100-200,200-300,这样就分为了3块,但是最终这个文件有多大就切分为多少块。之后nginx就会构造两个请求,第一个请求时 100-199,然后第二个请求时 200-300 的。这两个请求返回之后会生成两个文件,第一个100-199,200-299。然后将其组合起来生成客户端要的 150-249 这样一个响应。

六、过程总结

客户端向 nginx 请求一个10M文件,nginx 进行 4m 的切片,整个过程如下:

  1. 客户端向 nginx 请求 10M
  2. nginx 发起第一个切片(主请求)请求 range:0-4194303
  3. 第一个切片(主请求)请求的内容全部发给客户端后,在 slice 模块的 body_filter 发起第二个切片(子请求),请求range: 4194304-8388607
  4. 第二个切片(子请求)请求的内容完全发完給客户端后,切回主请求
  5. 主请求在 slice 模块的 body_filter 发起第三个切片(子请求),请求 range: 8388608-12582911
  6. 第三个切片(子请求)请求的内容(8388608-10485759)完全发完給客户端后,切回主请求
  7. 主请求在 slice 模块的 body_filter 判断已经将 10M 的文件发給客户端,不再进行 slice 的模块处理

七、代理服务器不使用slice模块

#192.168.179.99是代理服务器  192.168.179.100为上游服务器
#192.168.179.99配置

proxy_cache_path /data/nginx/tmpcache3 levels=2:2 keys_zone=nginx_cache:10m loader_threshold=300 loader_files=200 max_size=200m inactive=1m;

server {
    server_name test.net;

    error_log logs/cacherr.log debug;

    location /{
        proxy_cache nginx_cache;
        #slice             1m;
        #proxy_cache_key   $uri$is_args$args$slice_range;
        #proxy_set_header  Range $slice_range;

        proxy_cache_valid 200 206 1m;
        add_header X-Cache-Status $upstream_cache_status;

        proxy_pass http://192.168.179.100;
    }
}

让客户端使用 range 协议,-r 表示使用 range 协议会构造 range 的头部,我只访问 3000000-3000009 即 3M 里面的 10 个字节。可以看到返回也确实只有10个字节

# curl -r 3000000-3000009 192.168.179.99/test.mp4 -I
HTTP/1.1 206 Partial Content
Server: nginx/1.16.1
Date: Sun, 07 Jun 2020 03:33:15 GMT
Content-Type: video/mp4
Content-Length: 10   #返回10字节
Connection: keep-alive
Last-Modified: Sun, 15 Mar 2020 06:29:56 GMT
ETag: "5e6dcb64-2746cad"
X-Cache-Status: MISS   #缓存没有命中,访问到上游服务器了
Content-Range: bytes 3000000-3000009/41184429

访问到上游但是只返回了10个字节,上游究竟发生了什么,192.168.179.100为上游服务器日志

# tail -f /usr/local/nginx/logs/access.log 
192.168.179.99 - - [07/Jun/2020:11:28:46 +0800] "GET /test.mp4 HTTP/1.0" 200 41184429 "-" "curl/7.29.0" "-"

可以看到上游直接返回了41M完整的响应(这里我只访问10个字节却给我返回了41M),这是nginx做的一个优化,你只是访问了大文件range其中一小部分,但是 nginx 考虑到一次性向上游取到整个响应内容,后续再访问到其他字节就可以直接使用我的缓存了。

但是如果我们的服务是并发的,同时有多个客户去访问大文件的某一块的话就会引发很严重的问题。很多请求都会去访问一个巨大的文件,这个时候slice模块就有了用武之地

八、代理服务器使用 slice 模块

使用slice该模块需要配置3个地方,slice后面要有一个单位,即分为多大的大小进行切分,如果分的特别小会造成很多文件,如果分的特别大效果就不会特别明显。这里分为1M。
proxy_cache_key   $uri$is_args$args$slice_range;  $slice_range,这样才能知道客户端请求的内容是100-199字节。
proxy_set_header  Range $slice_range;必须保证我们的range这个头部是发到上游的,发送到上游的单位是1M,即每次1M发往上游。

#192.168.179.99代理服务配置
location /{
        proxy_cache nginx_cache;
        slice             1m;
        proxy_cache_key   $uri$is_args$args$slice_range;
            proxy_set_header  Range $slice_range;

        proxy_cache_valid 200 206 1m;
        add_header X-Cache-Status $upstream_cache_status;

        proxy_pass http://localhost:192.168.179.100;
}
# curl -r 3000000-3000009 192.168.179.99/test.mp4 -I
HTTP/1.1 206 Partial Content
Server: nginx/1.16.1
Date: Sun, 07 Jun 2020 03:42:54 GMT
Content-Type: video/mp4
Content-Length: 10  #返回的还是10个字节
Connection: keep-alive
Last-Modified: Sun, 15 Mar 2020 06:29:56 GMT
ETag: "5e6dcb64-2746cad"
X-Cache-Status: MISS  #可以看到响应还是发往上游了
Content-Range: bytes 3000000-3000009/41184429

#上游服务192.168.179.100日志
# tail -f /usr/local/nginx/logs/access.log 
192.168.179.99 - - [07/Jun/2020:11:42:54 +0800] "GET /test.mp4 HTTP/1.0" 206 1048576 "-" "curl/7.29.0" "-"

可以看到请求现在变为了 1M,而不是 41M。
当上游返回巨大文件的时候,使用 slice 可以针对如果客户端使用断点续传,多线程下载等等含有 range 场景。那么 slice 模块是非常有用的。

九、Range的范围

请求中的Range范围可能会超过文件的大小,如第一次取源时,Nginx并不知道实际文件的大小,所以Nginx请求时总是按照分片的大小设置Range范围,如slice设置为1m,那么第一次取bytes=0-1048575,如果文件不足1m,响应状态吗为200,表示不需要分片。如果超过1m,第二次取源时Range字段为bytes=1048576-2097171,即使这时可以知道文件实际大小。

线上使用时就遇到过一次源服务器对 Range 请求支持不完善的问题,文件大小为 1.5m,第一次取源状态码为 206,返回 1m 内容,第二次取源使 Range 字段为 bytes=1048576-2097171,但是文件不足 2m,源服务器发现这个范围超过了文件大小,所以返回了整个文件,状态码为200,这时Nginx就不能理解了,直接报错中断了响应。

开始以为是Nginx的问题,然后查看了下RFC文档,发现有解释这种情况
A client can limit the number of bytes requested without knowing the size of the selected representation. If the last-byte-pos value is absent, or if the value is greater than or equal to the current length of the representation data, the byte range is interpreted as the remainder of the representation (i.e., the server replaces the value of last-byte-pos with a value that is one less than the current length of the selected representation).

大致意思是说,如果请求的分片的后一个偏移超过了文件的实际大小,服务器应该返回剩余的部分内容。这个问题应该是源服务器的实现并没有按照RFC文档的要求。