使用代码
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
}