nginx分片模块流程分析--分片模块bug

使用代码

https://openresty.org/download/openresty-1.21.4.1.tar.gz

使用的是openresty原生代码,编译代码时需指定分片模块,编译指令如下

 ./configure   --with-http_slice_module --with-debug && make -j23 && make install

配置说明

conf配置示例及说明

location / {
    slice 512k; #分片大小,使用512k的大小回原获取数据
    proxy_set_header Range $slice_range; #与后端建立链接时,计算Range头
    proxy_pass http://127.0.0.1:80;
}

代码‘罗列’

nginx的slice模块代码是比较清晰的,主要分4大块:配置解析、变量使用、header_filter、body_filter

配置解析
static ngx_command_t  ngx_http_slice_filter_commands[] = {

    { ngx_string("slice"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_size_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_slice_loc_conf_t, size),
      NULL },

      ngx_null_command
};
变量使用
#主要是在proxy_pass的时候 第一次实时计算本次请求的Range的范围, 后续子请求的Range范围是在body-filter中创建好的

static ngx_int_t
ngx_http_slice_range_variable(ngx_http_request_t *r,
    ngx_http_variable_value_t *v, uintptr_t data)
{
    u_char                     *p;
    ngx_http_slice_ctx_t       *ctx;
    ngx_http_slice_loc_conf_t  *slcf;

    ctx = ngx_http_get_module_ctx(r, ngx_http_slice_filter_module);

    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) {
            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;
        }

        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;
    v->valid = 1;
    v->not_found = 0;
    v->no_cacheable = 1;
    v->len = ctx->range.len;

    return NGX_OK;
}
header_filter
#header_filter阶段 通过content-range获取文件总大小,对比etag等功能
static ngx_int_t
ngx_http_slice_header_filter(ngx_http_request_t *r)
{

    //....
    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;
    }


    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;
    }
    //....
}
body_filter
#body_filter阶段,将内容响应给用户: 子请求直接响应 主请求创建子请求

static ngx_int_t
ngx_http_slice_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    //子请求直接响应
    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->sr && !ctx->sr->done) {
        return rc;
    }
    if (ngx_http_subrequest(r, &r->uri, &r->args, &ctx->sr, NULL,
                            NGX_HTTP_SUBREQUEST_CLONE)
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    ngx_http_set_ctx(ctx->sr, ctx, ngx_http_slice_filter_module);

    slcf = ngx_http_get_module_loc_conf(r, ngx_http_slice_filter_module);

    ctx->range.len = ngx_sprintf(ctx->range.data, "bytes=%O-%O", ctx->start,
                                 ctx->start + (off_t) slcf->size - 1)
                     - ctx->range.data;
}

O^^O

看完上述的代码罗列,肯定一头雾水。 这么简单直白的操作也需要你来列。。。 好的,下面咱们来一起将这个分片模块在nginx内部是怎么运转起来的好好分析下? 抛出几个问题:1分片里的子请求是每次主请求来创建,还是子请求响应完之后在创建。2分片中的子请求响应完成之后,如何触发主请求。3主请求挂起后,如果触发子请求。

(一)首先分析第一次(也就是第一片)回原获取数据的行为:

1.客户端发起请求后,在proxy_pass的回原的时候,通过‘代码罗列’中的变量使用,计算出第一片Range的大小。通过异步connect的方式与源站进行建联;

2.通过ngx_event_pipe循环获取源站数据后,通过调用ngx_http_output_filter给client端发送数据(调用header-filter和body-filter链表)

3.在body-filter判断是否是最后一块数据,在发送完成后,通过ngx_http_subrequest创建一个子请求,生成的子请求sr挂在到主请求r的postponed链表中

4.upstream方向上,通过content-lengh判断本次请求获取完毕后,会调用ngx_http_upstream_finalize_request结束upstream方向的请求,同时在该函数中会调用ngx_http_finalize_request结束client端的请求。

5.但是此时请求不会关闭, 因为3中创见的子请求已经挂在到主请求r下的postponed链表中,如果判断该字段不为空,就直接返回并设置后续唤醒主请求调用的函数 r->write_event_handler =  ngx_http_writer;

if (r->buffered || c->buffered || r->postponed) {
    if (ngx_http_set_write_handler(r) != NGX_OK) {
        ngx_http_terminate_request(r, 0);
    }
    return;
}

说明:上述步骤中所涉及的请求,全部为主请求。 (新创建的子请求还没有得到运行的机会)

(二)然后分析第二次(也就是第二片)回原获取数据的行为:

1.由于upstream方向上调用的是ngx_http_upstream_handler函数,该函数中有一个触发延时请求的函数ngx_http_run_posted_requests(c); 该函数会将延时的请求唤醒继续处理。

void
ngx_http_run_posted_requests(ngx_connection_t *c)
{
    for ( ;; ) {
        r->main->posted_requests = pr->next;
        //.......
        r->write_event_handler(r);
    }
}

2.经过上述调用,子请求成功获得到了运行的机会,重新与后端建立请求

3.(同一)通过ngx_event_pipe循环获取源站数据后,通过调用ngx_http_output_filter给client端发送数据(调用header-filter和body-filter链表)

4.经过分片模块的代码时,由于是子请求,基本上不做过多的业务逻辑,只是响应客户端数据(注意:即使本次请求判断到了最后一个last-buf,也不会马上创建子请求)

5.(同一)upstream方向上,通过content-lengh判断本次请求获取完毕后,会调用ngx_http_upstream_finalize_request结束upstream方向的请求,同时在该函数中会调用ngx_http_finalize_request结束client端的请求。

6.在ngx_http_finalize_request函数中会判断当前请求是否是子请求,如果是子请求,会将他的父请求挂到延迟请求的链表中,然后本条子请求的使命完成,结束!!!

if (r != r->main) {  
    pr = r->parent;
    if (ngx_http_post_request(pr, NULL) != NGX_OK) {
        r->main->count++;
        ngx_http_terminate_request(r, 0);
        return;
    }
}

ngx_int_t
ngx_http_post_request(ngx_http_request_t *r, ngx_http_posted_request_t *pr)
{
    ngx_http_posted_request_t  **p;

    if (pr == NULL) {
        pr = ngx_palloc(r->pool, sizeof(ngx_http_posted_request_t));
        if (pr == NULL) {
            return NGX_ERROR;
        }
    }

    pr->request = r;
    pr->next = NULL;

    for (p = &r->main->posted_requests; *p; p = &(*p)->next) { /* void */ }

    *p = pr;

    return NGX_OK;
}

7.此时要注意1中的步骤又再次触发了,那么步骤6中放到延迟链表上的请求得到的处理的机会 --- 神奇不 。 最开始看代码的时候,卡在这个地方好久,不知道主请求为啥就直接被拉起来了。

-------- 分割线 ---   到此之前涉及到的请求基本为子请求--------

8.此时主请求的回调函数为ngx_http_writer,该函数调用ngx_http_output_filter又走一遍所有的filter链表,当走到分片模块中的body-filter的时候,继续创建第二个子请求,将创建的子请求sr挂在到主请求r的postponed链表中

9.在ngx_http_writer函数中,判断postponed是否不为空,如果不为空,继续将主请求进行休眠

------到这里第二次请求完成,后续第n次 同样的分析 -----------------

总结

1分片里的子请求是每次主请求来创建,还是子请求响应完之后在创建。 ==主请求创建

2分片中的子请求响应完成之后,如何触发主请求。 == 通过ngx_http_upstream_handler触发

3主请求挂起后,如果触发子请求。 === 通过ngx_http_upstream_handler触发

问题

目前遇到一个这样的问题: 在某一分片的子请求中,该请求收到了部分数据,该片剩余数据响应异常,此时对于client端方向的nginx感知不到,继续进行下一片的处理,这样会将下下片的内容追加到之前的数据后,造成缓存异常. 

如果客户端每次都拉取新数据不会存在问题,但是有断点续传的化,就会存在问题

解决的办法:写了一个,自测没有问题。可以达到的目的是,提前中断,不要继续追加数据

### body-filter阶段,在处理子请求的时候,添加逻辑判断

227     if (ctx == NULL || r != r->main) {
228         if (r != r->main) {
229             ngx_event_pipe_t  *p;
230             if (r && r->upstream && r->upstream->pipe) {
231
232                 p = r->upstream->pipe;
233
234                 if (p->upstream_done || (p->upstream_eof && p->length == -1)) {
235                     return ngx_http_next_body_filter(r, in);
236                 } else if (p->upstream_eof) {
237                     return NGX_ERROR;
238                 }
239             }
240         }
241         return ngx_http_next_body_filter(r, in);
242     }

源站ngx配置

在bytes=55050240-55574527这边片的时候,只响应200k字节
body_filter_by_lua_block {
    -- range: 0-524287
    local chunk = ngx.arg[1]
    local range = ngx.var.http_range
    if not ngx.ctx.save_len then
        ngx.ctx.save_len = string.len(ngx.arg[1])
    else
        ngx.ctx.save_len = ngx.ctx.save_len + string.len(ngx.arg[1])
    end
    if range and range == "bytes=55050240-55574527" and ngx.ctx.save_len >  262144 then
        return ngx.ERROR
    end
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值