slice模块是nginx的一个filter module,根据用户请求分割成多个子请求,该filter提供了非常有效的大请求cache,在CDN公司中广泛应用。
下面我就以CDN的场景来介绍该工作原理,网络架构是client->CDN proxy-> CDN store ->mongotv源站,很多用户想访问mongotv的一个热门视频hot.mp4,这个文件是1G。
如果不使用CDN,大量用户都去访问mongotv源站,那么mongotv很快就会被打垮;
如果使用CDN,CDN不支持slice,那么当CDN存储集群去向mongotv源站取1G文件过程中失败时,整个文件会全部重传;
如果使用CDN,CDN不支持slice,那么当CDN proxy通过url 一致性hash到存储集群时,存储集群的其中一台服务器肯定会成为一个热点,带宽、IO都会成为瓶颈,严重时会导致存储集群雪崩;
如果走CDN,CDN支持slice,则用户的请求都会被分散成多个子请求,每个url也不一样,因此当有个文件是热点时,压力会分散到store集群的每台机器,store集群和mongotv源出现网络错误,也只是重传该分片,不会传输整个文件。
总体来说,slice的好处是:
(1)资源分散:解决了热点视频带来的问题;
(2)异常处理:解决了因网络问题带来的重传;
(3)range请求支持:当用户传一个bytes=1-5的请求时,只会传输一个分片,而不是整个文件;
#./configure –prefix=/root/macro --with-http_slice_module
#make
#make install
proxy_cache_path /tmp/nginx/cache levels=1:2
keys_zone=cache:100m;
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;
}
备注:该配置不能解决热点视频带来的upstream的单台压力,需要将slice_range放在args中。
syntax:slice size;
default: 0
context: http、server、loc
指令功能:设置分片的大小。
typedef struct { size_t size; //slice指令的分片大小 } ngx_http_slice_loc_conf_t;
typedef struct { off_t start; //该分片的起始位置,是slice的整数倍 off_t end; //数据的最末尾 ngx_str_t range; //”bytes=2048-3071” ngx_str_t etag; //文件的etag标记,看分片请求时,文件是否有更新 ngx_uint_t last; //表示是该子请求的最后一块 } ngx_http_slice_ctx_t;
备注:假设文件大小是16M,slice设置的是4M,请求的range头依次为bytes=0-4194303、bytes=4194304-8388607、bytes=8388608-12582911。
typedef struct { off_t start; //该分片请求的start off_t end; //该分片请求的end+1 off_t complete_length; //文件完整的长度 } ngx_http_slice_content_range_t;de>
函数功能:添加自定义变量$slice_range,handler是ngx_http_slice_range_variable。
函数功能:create loc配置。
函数功能:merge loc配置,size默认为0。
函数功能:设置header filter为ngx_http_slice_header_filter和body filter为ngx_http_slice_body_filter。
函数功能:获取变量$slice_range的值并填充,指令slice设置为0的话,不会填充该值。
备注:假设请求的文件是16M,slice设置的是4M,则会4次进入该函数,值依次为"bytes=0-4194303"、"bytes=4194304-8388607"、"bytes=8388608-12582911"、"bytes=12582912-16777215"。
备注:假设是range请求bytes=2-256,则只会发第一块"bytes=0-4194303"。
static ngx_int_t ngx_http_slice_range_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { 。。。 ctx = ngx_http_get_module_ctx(r, ngx_http_slice_filter_module); //如果是lua发的子请求访问,r=r->main,待验证??? //猜测还是会进来,r->main表示当前的r的主请求 if (ctx == NULL) { //是主请求,第一次进来 if (r != r->main || r->headers_out.status) { v->not_found = 1; return NGX_OK; } slcf = ngx_http_get_module_loc_conf(r, ngx_http_slice_filter_module); if (slcf->size == 0) { //未设置slice size v->not_found = 1; return NGX_OK; } ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_slice_ctx_t)); if (ctx == NULL) { return NGX_ERROR; } ngx_http_set_ctx(r, ctx, ngx_http_slice_filter_module); p = ngx_pnalloc(r->pool, sizeof("bytes=-") - 1 + 2 * NGX_OFF_T_LEN); if (p == NULL) { return NGX_ERROR; } //数据块是对齐的,主要区分在哪块 //因为可能是range请求bytes=XX-XX,发送一个slice块 ctx->start = slcf->size * (ngx_http_slice_get_start(r) / slcf->size); ctx->range.data = p; ctx->range.len = ngx_sprintf(p, "bytes=%O-%O", ctx->start, ctx->start + (off_t) slcf->size - 1) - p; } v->data = ctx->range.data; //第二次直接到这里,值已经在body_filter中填充好 v->valid = 1; v->not_found = 0; v->no_cacheable = 1; v->len = ctx->range.len; return NGX_OK; }
函数功能:返回range请求起始的偏移,如bytes=2049-2059,则返回2049,格式错误或其他则返回0。
函数功能:解析upstream的响应头Content-Range(格式如bytes 12582912-16050755/16050756),填充在cr中。
备注:假设响应头Content-Range: bytes 12582912-16050755/16050756
则{start = 12582912, end = 16050756, complete_length = 16050756}
函数功能:header filter,假设range的是N个块,则整个过程会有N次进入该函数。
static ngx_int_t ngx_http_slice_header_filter(ngx_http_request_t *r) { 。。。 ctx = ngx_http_get_module_ctx(r, ngx_http_slice_filter_module); if (ctx == NULL) { return ngx_http_next_header_filter(r); } //range请求,返回206 if (r->headers_out.status != NGX_HTTP_PARTIAL_CONTENT) { if (r == r->main) { //可能出错 ngx_http_set_ctx(r, NULL, ngx_http_slice_filter_module); return ngx_http_next_header_filter(r); } ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unexpected status code %ui in slice response", r->headers_out.status); return NGX_ERROR; } h = r->headers_out.etag; //响应的etag头 if (ctx->etag.len) { //第一次为空,第二次有值,用来标记文件是否有变化 if (h == NULL || h->value.len != ctx->etag.len || ngx_strncmp(h->value.data, ctx->etag.data, ctx->etag.len) != 0) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "etag mismatch in slice response"); return NGX_ERROR; } } if (h) { ctx->etag = h->value; } //解析content-range头,格式如bytes 12582912-16050755/16050756 if (ngx_http_slice_parse_content_range(r, &cr) != NGX_OK) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid range in slice response"); return NGX_ERROR; } if (cr.complete_length == -1) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "no complete length in slice response"); return NGX_ERROR; } slcf = ngx_http_get_module_loc_conf(r, ngx_http_slice_filter_module); end = ngx_min(cr.start + (off_t) slcf->size, cr.complete_length); if (cr.start != ctx->start || cr.end != end) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "unexpected range in slice response: %O-%O", cr.start, cr.end); return NGX_ERROR; } ctx->start = end; //下一个块的开始 //主要是避免range_filter再次处理 r->headers_out.status = NGX_HTTP_OK; r->headers_out.status_line.len = 0; r->headers_out.content_length_n = cr.complete_length; r->headers_out.content_offset = cr.start; r->headers_out.content_range->hash = 0; r->headers_out.content_range = NULL; r->allow_ranges = 1; r->subrequest_ranges = 1; r->single_range = 1; rc = ngx_http_next_header_filter(r); if (r != r->main) { return rc; //子请求返回 } if (r->headers_out.status == NGX_HTTP_PARTIAL_CONTENT) { if (ctx->start + (off_t) slcf->size <= r->headers_out.content_offset) { ctx->start = slcf->size * (r->headers_out.content_offset / slcf->size); } ctx->end = r->headers_out.content_offset + r->headers_out.content_length_n; } else { ctx->end = cr.complete_length; } return rc; }
函数功能:body filter。
static ngx_int_t ngx_http_slice_body_filter(ngx_http_request_t *r, ngx_chain_t *in) { 。。。 ctx = ngx_http_get_module_ctx(r, ngx_http_slice_filter_module); if (ctx == NULL || r != r->main) { //子请求 return ngx_http_next_body_filter(r, in); } //主请求,第一个分片 for (cl = in; cl; cl = cl->next) { if (cl->buf->last_buf) { cl->buf->last_buf = 0; cl->buf->last_in_chain = 1; cl->buf->sync = 1; ctx->last = 1; //第一个分片全部接收完毕 } } rc = ngx_http_next_body_filter(r, in); if (rc == NGX_ERROR || !ctx->last) { return rc; } if (ctx->start >= ctx->end) { ngx_http_set_ctx(r, NULL, ngx_http_slice_filter_module); ngx_http_send_special(r, NGX_HTTP_LAST); return rc; } if (r->buffered) { return rc; } //创建子请求,子请求和主请求的区别就是range头不一样 //由于分片顺序需要保证,因此都是在主请求中创建子请求的,不会是子请求中再创建子请求 if (ngx_http_subrequest(r, &r->uri, &r->args, &sr, NULL, 0) != NGX_OK) { return NGX_ERROR; } ngx_http_set_ctx(sr, ctx, ngx_http_slice_filter_module); slcf = ngx_http_get_module_loc_conf(r, ngx_http_slice_filter_module); //子请求的range头变化了 ctx->range.len = ngx_sprintf(ctx->range.data, "bytes=%O-%O", ctx->start, ctx->start + (off_t) slcf->size - 1) - ctx->range.data; ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http slice subrequest: \"%V\"", &ctx->range); return rc; }
测试架构:client->slice proxy->web-server
location / {
slice 4m;
proxy_set_header range $slice_range;
proxy_pass http://localhost:8000;
}
备注:总文件是16M-。
#curl http://127.0.0.1:80/video/card.mp4 -o /dev/null
Proxy日志:
Web-server日志:
备注:从日志可以看出,slice proxy会将客户端的请求转换成4个range请求。
#curl http://127.0.0.1:80/video/card.mp4 --header 'range: bytes=500-999' -o /dev/null
Proxy日志:
Web-server日志:
备注:从日志可以看出,slice proxy会将客户端的请求转换成1个range请求,因为用户的请求只是第一个分片,proxy拿到该分片后,会利用自己的range模块来进行切割。
nginx slice wiki:
http://nginx.org/en/docs/http/ngx_http_slice_module.html
slice模块的好处:
http://pureage.info/2015/12/10/nginx-slice-module.html
range请求:
https://tools.ietf.org/html/rfc7233#section-2.1
tengine(叔度):