IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    nginx模块开发(48)—slice模块分析

    cjhust发表于 2016-03-18 11:22:23
    love 0

    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都会成为瓶颈,严重时会导致存储集群雪崩;

    nginx模块开发(48)—slice模块分析 - cjhust - 我一直在努力

           如果走CDN,CDN支持slice,则用户的请求都会被分散成多个子请求,每个url也不一样,因此当有个文件是热点时,压力会分散到store集群的每台机器,store集群和mongotv源出现网络错误,也只是重传该分片,不会传输整个文件。

    总体来说,slice的好处是:

    (1)资源分散:解决了热点视频带来的问题;

    (2)异常处理:解决了因网络问题带来的重传;

    (3)range请求支持:当用户传一个bytes=1-5的请求时,只会传输一个分片,而不是整个文件;

    2、示例配置

    #./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中。

    slice

    syntax:slice size;

    default: 0

    context: http、server、loc

    指令功能:设置分片的大小。

    3、数据结构

    ngx_http_slice_loc_conf_t

    typedef struct {
        size_t      size;            //slice指令的分片大小
    } ngx_http_slice_loc_conf_t;


    ngx_http_slice_ctx_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。

    image

    ngx_http_slice_content_range_t

    typedef struct {
        off_t       start;               //该分片请求的start
        off_t       end;                 //该分片请求的end+1
        off_t       complete_length;     //文件完整的长度
    } ngx_http_slice_content_range_t;de>

    4、源码分析

    ngx_http_slice_add_variables(ngx_conf_t *cf)

    函数功能:添加自定义变量$slice_range,handler是ngx_http_slice_range_variable。

    ngx_http_slice_create_loc_conf(ngx_conf_t *cf)

    函数功能:create loc配置。

    ngx_http_slice_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)

    函数功能:merge loc配置,size默认为0。

    ngx_http_slice_init(ngx_conf_t *cf)

    函数功能:设置header filter为ngx_http_slice_header_filter和body filter为ngx_http_slice_body_filter。


    ngx_http_slice_range_variable(ngx_http_request_t *r,ngx_http_variable_value_t *v, uintptr_t data)

    函数功能:获取变量$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;
    }

    ngx_http_slice_get_start(ngx_http_request_t *r)

    函数功能:返回range请求起始的偏移,如bytes=2049-2059,则返回2049,格式错误或其他则返回0。

    ngx_http_slice_parse_content_range(ngx_http_request_t *r,ngx_http_slice_content_range_t *cr)

    函数功能:解析upstream的响应头Content-Range(格式如bytes 12582912-16050755/16050756),填充在cr中。

    备注:假设响应头Content-Range: bytes 12582912-16050755/16050756

    则{start = 12582912, end = 16050756, complete_length = 16050756}

     

    ngx_http_slice_header_filter(ngx_http_request_t *r)

    函数功能: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;
    }

    image

    ngx_http_slice_body_filter(ngx_http_request_t *r, ngx_chain_t *in)

    函数功能: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;
    }

    5、测试验证

    测试架构: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日志:

    image

    Web-server日志:

    image

    备注:从日志可以看出,slice proxy会将客户端的请求转换成4个range请求。

     

    #curl http://127.0.0.1:80/video/card.mp4 --header 'range: bytes=500-999' -o /dev/null

    Proxy日志:

    image

    Web-server日志:

    image

    备注:从日志可以看出,slice proxy会将客户端的请求转换成1个range请求,因为用户的请求只是第一个分片,proxy拿到该分片后,会利用自己的range模块来进行切割。

    6、参考资料

    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(叔度):

    https://github.com/alibaba/nginx-http-slice



沪ICP备19023445号-2号
友情链接