您的位置:首页 > 理论基础 > 计算机网络

nginx处理post请求(http响应包体收发之上游网速优先策略)

2017-10-02 07:11 921 查看
        上一篇文章分析了在下游网速优先策略下,nginx是如何接收来自后端服务器的响应包体,以及如何把来自后端服务器的响应包体转发给客户端的。本篇文章接着分析另一种http响应包体收发策略----上游网速优先策略。

        上游网速优先,即nginx接收后端服务器的响应包体要比下游客户端接收来自nginx的响应包体更快。在这种情况下,nginx需要开辟多个缓冲区,尽可能多的缓存来自后端服务器的响应。如果这多个缓冲区都满了,还会把来自后端服务器的响应包体写入临时文件(有可能一次操作并没有把接收到的响应包体全部写入到文件, 因为每次写入文件的大小是有限制的。不需要等待全部响应包体都写入文件后,才继续接收来自后端服务的响应。写入部分响应包体后,接收缓冲区就有剩余空间了,可以继续接收来自后端服务器的响应。这样就实现了边接收后端服务器的响应,边把接收缓冲区的响应包体写入到文件中保存)。当然了临时文件的大小也是有限制的,不可能无限大。当缓冲区与临时文件都满了时,nginx会停止接收来自后端服务器的响应,等待下游客户端接收完部分数据后(不需要接收完缓冲区或者临时文件中的全部数据),
缓冲区中又有剩余的空间了,此时可以继续接收来自后端服务器的响应包体。



        从图中可以看出,如果接收缓冲区中的数据太多,超过每次写入文件数据大小的限制,则只会把接收缓冲区中的部分数据写入到文件中。这部分数据写入到文件后,接收缓冲区就有剩余空间了,这部分空间可以继续接收来自后端服务器的响应。因此在接收缓冲区满了,会把部分缓冲区中的数据写入到文件,不需要等所有数据都写入到文件后,才开始继续接收来自后端服务器的响应包体,而是边接收边写入文件。



        同样的,每次发给客户端浏览器的http响应包体也是有大小限制的。如果接收缓冲区数据太多的话,则每次只会发送部分响应包体给客户端。这部分响应包体发送给客户端后,接收缓冲区就有剩余空间了,这部分空间可以继续接收来自后端服务器的响应。因此接收缓冲区满后,不需等待接收缓冲区中的数据全部发送给客户端后,才开始继续接收来自后端服务器的响应包体,而是边接收边发送。

一、数据收发准备工作

        nginx要能接收来自后端服务器的响应,以及把响应转发给下游客户端,肯定先要注册读事件,写事件的回调;需要开辟多少个接收后端服务器响应的缓冲区,每个缓冲区占用多大的空间; 读取来自后端服务器响应的超时时间等等都需要进行指定。这些都是数据收发的准备工作需要完成的事情,来看下这个过程。

//接收来自后端服务器的http响应包体后,把响应包体发送给客户端
static void ngx_http_upstream_send_response(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
//执行到这里,表示上游网络优先,buffering为1,以下为向pipe成员初始化过程。使用多个缓冲区或者文件存放响应包体
p = u->pipe;
//接收完响应包体后对包体处理完后,最终向下游客户端发送响应包体的方法
p->output_filter = (ngx_event_pipe_output_filter_pt) ngx_http_output_filter;
p->output_ctx = r;

//设置接收来自后端服务器响应缓冲区的大小以及个数
p->bufs = u->conf->bufs;
p->busy_size = u->conf->busy_buffers_size;
//指定nginx与后端服务器的连接以及nginx与下游客户端的连接
p->upstream = u->peer.connection;
p->downstream = c;

//创建临时文件,用于在接收缓冲区满了,把响应包体临时存放到文件中保存。这样接收缓冲区就有剩余空间
//继续接收来自后端服务器的响应。同时指定每次可以写入多少数据到文件,以及文件的总大小
p->temp_file = ngx_pcalloc(r->pool, sizeof(ngx_temp_file_t));
p->max_temp_file_size = u->conf->max_temp_file_size;
p->temp_file_write_size = u->conf->temp_file_write_size;
//存放在接收上游服务器响应头部时,已经接收到的响应包体。
p->preread_bufs = ngx_alloc_chain_link(r->pool);
//读后端服务响应的超时时间,已经发送给后端服务器的超时时间
p->read_timeout = u->conf->read_timeout;
p->send_timeout = clcf->send_timeout;
p->send_lowat = clcf->send_lowat;

//设置读取来自上游服务器响应包体的回调
u->read_event_handler = ngx_http_upstream_process_upstream;
//设置向下游客户端写入来自上游服务响应包体的回调
r->write_event_handler = ngx_http_upstream_process_downstream;
//读取上游服务器的响应包体
ngx_http_upstream_process_upstream(r, u);
}
        ngx_http_upstream_send_response函数的后半部分就是对上游网速优先策略的数据收发进行处理。那区分上游网速优先与下游网速优先的条件是什么呢? 根据ngx_http_upstream_s结构中的buffering进行区分,如果buffering为0表示下游网速优先,开辟一个固定的缓冲区空间,接收后端服务器的响应包体; 如果buffering为1,表示上游网速优先,开辟多个缓冲区空间,接收后端服务器的响应包体,在这些缓冲区都满后,还会把接收缓冲区中的响应包体写入到文件进行存储,这样接收缓冲区就有剩余空间,继续接收后端服务器的响应。上游网速优先策略基本上都是在处理ngx_event_pipe_t结构,不管是接收后端服务器的响应,还是把响应包体转发给客户端,都是在围绕这个结构进行处理,这里就不详细分析这个结构中的字段,在下面进行代码分析时会贯穿这个结构中的主要字段;
        数据收发准备工作大概做了以下几件事情;(1)注册下游客户端发送响应包体的方法,这个方法其实就是前面文章分析的过滤器模块;(2)设置每个接收缓冲区的大小以及接收缓冲区的个数;(3)打开临时文件,用于在接收缓冲区满后,把缓冲区中的数据保存到文件中进行存放; (4)注册负载均衡模块中的读后端服务器响应包体的回调为:ngx_http_upstream_process_upstream,以及向下游客户端发送响应包体的回调为:ngx_http_upstream_process_downstream

二、读写事件的调度

        nginx要接收来自后端服务器的响应包体,以及把响应包体发给下游客户端,这些都需要事件模块的介入调度执行。在ngx_http_upstream_connect函数中已经把读写事件的回调都设置为:ngx_http_upstream_handler。看下这个函数的实现; 在接收后端服务器的读事件触发后,会调用负载均衡模块的读回调:ngx_http_upstream_process_upstream,在向下游客户端发送响应的写事件被触发后,进而调用负载均衡模块的写回调:ngx_http_upstream_process_downstream

//事件模块的读写回调,事件被触发时会调用负载均衡模块对应的读写回调
static void ngx_http_upstream_handler(ngx_event_t *ev)
{
if (ev->write)
{
//向后端服务器发送数据
u->write_event_handler(r, u);
}
else
{
//接收后端服务器的响应
u->read_event_handler(r, u);
}
}
        而不管是负载均衡模块的读回调ngx_http_upstream_process_upstream,还是写回调ngx_http_upstream_process_downstream,最终都是调用ngx_event_pipe来读取后端服务器的响应包体,或者把来自后端服务器的响应包体,经过处理后转发给下游客户端。
//读取上游服务器的响应包体
static void ngx_http_upstream_process_upstream(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
ngx_event_pipe(u->pipe, 0);
}

//向下游客户端转发来自上游服务器的响应包体
static void ngx_http_upstream_process_downstream(ngx_http_request_t *r)
{
ngx_event_pipe(p, wev->write);
}
        来看下ngx_event_pipe的实现过程,这个函数负载读取来自后端服务器的响应数据,保存到ngx_event_pipe_t的成员free_raw_bufs接收缓冲区中,同时把接收缓冲区中的数据经过处理后保存到ngx_event_pipe_t的成员in待发送链表中;然后把in待发送链表中的响应包体发送给下游客户端浏览器;如果一次操作并不能接收完来自后端服务器的响应包体,或者把接收到的所有响应包体发给下游客户端浏览器,则会重新注册读写事件到epoll事件模块中,以便下次读写事件触发时,能够继续完成剩余的操作。ngx_event_pipe函数只是一个框架而已,具体读后端服务器的响应包体,由ngx_event_pipe_read_upstream函数完成,而向下游客户端发送响应包体则由ngx_event_pipe_write_to_downstream函数完成。
//参数: do_write 值为0表示接受上游服务器的响应包体,1表示向下游客户端发送响应包体
ngx_int_t ngx_event_pipe(ngx_event_pipe_t *p, ngx_int_t do_write)
{
//边接收来自后端服务器的响应包体,边把处理后的响应包体发给下游客户端
for ( ;; )
{
//向下游客户端发送来自上游服务器的响应包体
if (do_write)
{
rc = ngx_event_pipe_write_to_downstream(p);
}

//读取来自上游服务器的响应包体
if (ngx_event_pipe_read_upstream(p) == NGX_ABORT)
{
return NGX_ABORT;
}
//两个标记同时为0,则说明没有读取到来自上游服务器的响应包体,则重新注册事件到epoll
if (!p->read && !p->upstream_blocked)
{
break;
}
//执行到这里,表示已经读取到来自上游服务器的响应包体,需要把这部分响应包体发给下游客户端
do_write = 1;
}

//重新把读上游响应包体事件加入epoll,同时将读事件加入到超时红黑树中
rev = p->upstream->read;
ngx_handle_read_event(rev, flags);
//重新把向下游服务器写响应包体事件加入epoll,同时将写事件加入到超时红黑树中
wev = p->downstream->write;
ngx_handle_write_event(wev, p->send_lowat);
}
三、接收后端服务器的响应包体

        ngx_event_pipe_read_upstream这个函数用于接收来自后端服务器的响应包体,保存到ngx_event_pipe_t成员的free_raw_bufs接收缓冲区链表中, 并把响应包体经过转换后保存到ngx_event_pipe_t的成员in链表中,这个in链表的数据是要发送给下游客户端的数据。为什么需要转换呢? 因为来自后端服务器的响应数据fastcgi格式的数据,或者menche, proxyd反向代理的数据, 这些数据需要转为nginx与下游客户端熟知的格式。在分析这个函数之前,先来看下nginx是如何维护接收缓冲区链表这个数据结构的,以及如何把接收缓冲区的数据处理后交给待发送链表的。



        

        从图中可以看出free_raw_bufs是接收缓冲区链表,每一个链表节点都代表一个接收缓冲区,链表节点的个数以及每个接收缓冲区的大小在前面准备工作时已经指定。在这里以第一个链表节点为例进行说明,这个链表节点指向一个连续的buf接收缓冲区空间,该空间存放了多个fastcgi格式的响应包体,每一个响应包体都会有一个fastcgi头部。每当这个接收缓冲区空间满后;或者接受完所有来自后端服务器的响应包体后,此时没有剩余的包体了,此时有可能接收缓冲区还未满; 这两种情况下都会对这个缓冲区空间的响应包体进行处理,处理后的响应包体保存到待发送链表in中。我们也可以看出,待发送链表in中的每一个节点并没有重新开辟buf空间,而是指向接收缓冲区中的每一个fastcgi格式的响应包体位置,
一个fastcgi格式的响应包体,都有一个in链表节点与之一一对应。

        图中可以看出,还有一个影子单项循环链表,这个是什么鬼?  所谓的影子单项循环链表,其实是由in链表中每一个链表节点都指向同一个连续的buf空间中不同位置,这些指向同一个缓冲区组成的一个单向循环链表,其中free_raw_bufs接收缓冲区链表节点指向的buf空间的shadow是这个缓冲区的影子链表头部。每一个接收缓冲区链表都有独自的一个影子单项循环链表,因为每一个接收缓冲区链表节点都有一个接收缓冲区,这个缓冲区可以被in链表中多个节点指向这片空间。需要注意的是每一个free_raw_bufs链表节点都有一个独立的影子缓冲区链表头,但却只有一个in成员链表,这个in成员链表存放所有处理后的响应包体,只不过in链表中的某些链表节点指向free_raw_bufs链表中的一个节点,
in链表中的另一些链表节点指向free_raw_bufs链表中的另一个节点。例如, 图in链表的前两个节点指向free_raw_bufs接收缓冲区链表第一个节点指向的空间, in链表的后两个节点指向free_raw_bufs接收缓冲区第二个节点指向的空间。

        那这个影子单项循环链表有什么作用呢? 说白了就是nginx处于性能考虑,为了节约内存,一份数据绝不会拷贝两次。使用影子链表目的是为了知道in链表中有哪些节点指向free_raw_bufs接收缓冲区链表中的某一个节点指向的空间。只要还有一个in链表节点指向这个空间,这个空间就还不能被回收利用。一旦这个接收缓冲区空间可以被回收利用,则会删除这个影子单项循环链表, 使得这些指向同一个缓冲区空间的in链表节点与free_raw_bufs链表节点指向的空间脱离。这样free_raw_bufs链表节点指向的空间就可以被回收利用,继续接收剩余的来自后端服务器的响应包体,
而in链表中这些指向这个接收缓冲区的节点,也可以被回收利用,继续存放转换后需要发给客户端的响应包体。

        说了这么多,可能都懵逼了。看不懂没关系,分析下代码在回头看下这张图就清楚了。ngx_event_pipe_read_upstream函数就是维护了这样的一个数据结构,现在可以分析下这个函数的实现过程,由于这个函数的代码太长了,因此分三部分进行分析;

        1、获取接收缓冲区

//读取后端服务器的响应包体
static ngx_int_t ngx_event_pipe_read_upstream(ngx_event_pipe_t *p)
{
for ( ;; )
{
//已经接收完所有来自后端服务的响应,或者与后端服务器交互出现错误时,退出循环
if (p->upstream_eof || p->upstream_error || p->upstream_done)
{
break;
}
//从free_raw_bufs接收缓冲区链表中获取空闲的接收缓冲区。
//如果single_buf为1表示一次只能从后端服务器接收一个链表节点的缓冲区数据,即便接收缓冲区链表有多个链表节点空闲,
//也只会接收一个节点的数据。如果single_buf为0表示一次可以从后端服务器接收多个链表节点的缓冲区数据。
if (p->free_raw_bufs)
{
chain = p->free_raw_bufs;
if (p->single_buf)
{
p->free_raw_bufs = p->free_raw_bufs->next;
chain->next = NULL;
}
}
else if (p->allocated < p->bufs.num)
{
//如果接收缓冲区链表free_raw_bufs没有空闲的缓冲区了,则分配一个接收缓冲区
b = ngx_create_temp_buf(p->pool, p->bufs.size);
//记录已经分配了多少个接收缓冲区
p->allocated++;
}
else if (!p->cacheable && p->downstream->data == p->output_ctx
&& p->downstream->write->ready && !p->downstream->write->delayed)
{
//在不需要缓存情况下,写事件就绪了,返回后发送响应给客户端
p->upstream_blocked = 1;
break;
}
else if (p->cacheable || p->temp_file->offset < p->max_temp_file_size)
{
//在开启了文件缓存的情况下,如果接收缓冲区链表free_raw_bufs满了,则把这个链表中的数据写入到文件中保存。
//需要注意的是,一次操作可能不能写入全部的响应包体,也不需要等全部响应包体都写入到文件后才开始接收后端服务器的响应
//nginx边接收边写入文件,一旦free_raw_bufs有空闲的空间,就会立即接收后端服务器的响应
rc = ngx_event_pipe_write_chain_to_temp_file(p);
//写入文件后,free_raw_bufs接收缓冲区就有空闲的空间了,获取一个接收缓冲区
chain = p->free_raw_bufs;
if (p->single_buf)
{
p->free_raw_bufs = p->free_raw_bufs->next;
chain->next = NULL;
}
}
else
{
//缓冲区满了,暂时不接收后端服务器的响应,等发送完部分响应给下游客户端后,接收缓冲区
//有剩余空间时,立即接收后端的响应。因此nginx也是边接收边发送,不需要等接收缓冲区发送完后才接收后端的响应,
//而是接收缓冲区一有空间就立即接收后端的响应
break;
}
}
}

        这一大段代码就是为了获取一个接收缓冲区,用于接收后端服务器的响应包体。(1)如果接收缓冲区链表free_raw_bufs有空闲的空间,则从这个链表获取一个缓冲区; (2)那如果接收缓冲区链表free_raw_bufs满了,没有剩余的空间了,则会重新开辟一个接收缓冲区,开辟缓冲区的大小以及缓冲区的个数在前面准备工作那里已经指定。(3)如果接收缓冲区链表free_raw_bufs满了,且开辟缓冲区个数超过了限制,该怎么办呢? nginx会把接收缓冲区链表free_raw_bufs中的数据临时写入到文件中。当然了每次写入文件的大小以及整个文件的大小都是有限制的。如果文件也写满了,则暂时不接收后端响应,
待发送部分数据给下游客户端后,接收缓冲区有剩余空间了则继续接收后端响应。需要注意的是一次操作可能并不能把接收缓冲区链表free_raw_bufs中的所有数据写入到文件,也不需要等接收缓冲区链表中的所有数据写入到文件后才开始接收到后端服务器的响应,只要接收缓冲区链表中有空间了,就可以继续接收后端响应。nginx采用边写文件边接收后端响应的方式。

        ngx_event_pipe_write_chain_to_temp_file用于将接收缓冲区中的数据写入到文件,函数返回后p->in保存的是剩余未写入文件的数据,p->out保存的是已经写入到文件中的数据。写入文件的数据可以被回收,重新插入到接收缓冲区链表free_raw_bufs中,同时删除指向该缓冲区的影子链表。

//将p->in链表中的数据写入到文件,如果一次不能写完, p->in链表中会保存没有写完的节点。
//返回返回后p->out保存了写入文件的响应包体
static ngx_int_t ngx_event_pipe_write_chain_to_temp_file(ngx_event_pipe_t *p)
{
out = p->in;
if (!p->cacheable)
{
//p->cacheable为0表示只把p->in链表中部分数据写入文件。这里统计一次性写入到文件的数据总长度
do
{
bsize = cl->buf->last - cl->buf->pos;
if ((size + bsize > p->temp_file_write_size)
|| (p->temp_file->offset + size + bsize > p->max_temp_file_size))
{
break;
}
size += bsize;
ll = &cl->next;
cl = cl->next;
} while (cl);
//保存本次还不能写入到文件的剩余数据
p->in = cl;
*ll = NULL;
}
else
{
//p->cacheable为1,表示一次性需要把p->in中所有数据写入到文件
p->in = NULL;
p->last_in = &p->in;
}
//将out链表中的所有数据都写入到文件
ngx_write_chain_to_temp_file(p->temp_file, out);
//将所有已经写入到文件的链表节点,插入到输入到p->out链表中
for (cl = out; cl; cl = next)
{
b->file_pos = p->temp_file->offset;
p->temp_file->offset += b->last - b->pos;
b->file_last = p->temp_file->offset;
if (p->out)
{
*p->last_out = cl;
}
else
{
p->out = cl;
}
//如果是最后一个影子节点, 则清空这个buf
if (b->last_shadow)
{
//因为节点已经使用完成了,可以重新被使用了。
//因此将这个链表节点重新插入到接收缓冲区链表p->free_raw_bufs中
*last_free = tl;
last_free = &tl->next;
b->shadow->pos = b->shadow->start;
b->shadow->last = b->shadow->start;
//删除影子节点链表(为什么可以删除, 因为节点的数据已经写入到了文件)
ngx_event_pipe_remove_shadow_links(b->shadow);
}
}
}
        2、接收后端服务器的响应包体

//读取后端服务器的响应包体
static ngx_int_t ngx_event_pipe_read_upstream(ngx_event_pipe_t *p)
{
//读取数据到缓冲区链表,ngx_readv_chain
n = p->upstream->recv_chain(p->upstream, chain);
//将接收到的内容插入到接收缓冲区链表头,为什么要这么做? 因为一次接收后端响应后,这个chain可能还有空间,因此chain
//变成接收缓冲区链表头后,下次获取接收缓冲区时,将会获取到chain,从该节点开始继续接收后端响应
if (p->free_raw_bufs)
{
chain->next = p->free_raw_bufs;
}
p->free_raw_bufs = chain;

//更新读取到的数据长度
p->read_length += n;
cl = chain;
p->free_raw_bufs = NULL;
}
        调用ngx_readv_chain接收后端服务器的响应包体,函数内部就是从内核中接收响应包体到应用层的chain链表中。这个函数的实现比较简单,为了不影响主流程的分析,这里就不会这个函数的实现进行分析了。
        3、对接收到的响应包体进行处理

//读取后端服务器的响应包体
static ngx_int_t ngx_event_pipe_read_upstream(ngx_event_pipe_t *p)
{
while (cl && n > 0)
{
//清除影子节点链表(该链表所有节点的缓冲区都指向同一个空间)
ngx_event_pipe_remove_shadow_links(cl->buf);
size = cl->buf->end - cl->buf->last;
if (n >= size)
{
cl->buf->last = cl->buf->end;
//只有在接收完一个缓冲区大小的数据后,开始解析缓冲区中的数据。ngx_http_fastcgi_input_filter
p->input_filter(p, cl->buf);
n -= size;
}
else
{
//未填满一个缓冲区大小,则继续接收并写入到这个缓冲区
cl->buf->last += n;
}
}

if (cl)
{
//找到最后一个节点
for (ln = cl; ln->next; ln = ln->next)
{}
//最后一个节点next指向接收缓冲区链表的头部,构成一个单项循环接收缓冲区链表
ln->next = p->free_raw_bufs;
//更新表头
p->free_raw_bufs = cl;
}
}
       如果一个接收缓冲区节点还没有满,则下次可以继续使用这个缓冲区,接收来自后端服务器的响应包体,接收后的响应包体保存到这个缓冲区的最后面。那如果一个缓冲区节点满了,则会调用fastcgi模块解析接收缓冲区中响应包体。由此也可以出nginx框架与模块分离的原则,负载均衡是一个框架,提供一些抽象接口, 而具体的http模块例如fastcgi模块,proxyd反向代理模块,memche模块实现这些抽象接口。在这里负载均衡提供了解析响应包体的抽象接口,具体如何解析则由这些具体的http模块来实现,很好的体现了分层的设计思想。来看下fastcgi模块是如何解析响应包体的;



//解析来自后端服务器发来的fastcgi格式的响应数据,解析后的内容放到p->in链表
static ngx_int_t ngx_http_fastcgi_input_filter(ngx_event_pipe_t *p, ngx_buf_t *buf)
{
//循环解析每一个fastcgi格式的响应包体
for ( ;; )
{
if (f->state < ngx_http_fastcgi_st_data)
{
//使用状态机解析fastcgi的头部
rc = ngx_http_fastcgi_process_record(r, f);
//后端服务器发来的数据类型为响应包体,但响应包体的数据为空。
//则当前fastcgi层解析完成, 继续解析剩余的fastcgi层
if (f->type == NGX_HTTP_FASTCGI_STDOUT && f->length == 0)
{
f->state = ngx_http_fastcgi_st_version;
p->upstream_done = 1;
continue;
}
//收到后端服务器发来的结束请求,表示所有的响应包体都接收完成
if (f->type == NGX_HTTP_FASTCGI_END_REQUEST)
{
f->state = ngx_http_fastcgi_st_version;
p->upstream_done = 1;
break;
}
}
//从空闲链表free获取一个节点,用于接收转换后的响应包体
if (p->free)
{
cl = p->free;
b = cl->buf;
p->free = cl->next;
ngx_free_chain(p->pool, cl);

}
//保存响应包体
b->pos = f->pos;
b->start = buf->start;
b->end = buf->end;
//插入到影子链表末尾
*prev = b;
prev = &b->shadow;
cl = ngx_alloc_chain_link(p->pool);
cl->buf = b;
//将这个fastcgi层解析出来的响应包体插入到in链表末尾
if (p->in)
{
*p->last_in = cl;
}
else
{
p->in = cl;
}
p->last_in = &cl->next;
//缓冲区可以容纳多个这样的fastcgi包体空间,则继续解析剩余的fastcgi包体
if (f->pos + f->length < f->last)
{
f->pos += f->length;
b->last = f->pos;
continue;
}
}
}

       接收缓冲区是由多个fastcgi组成的响应包体,每一个响应包体都有一个fastcgi头部。函数内部使用扫描法,扫描每一个fastcgi头部,同时对于每一个fastcgi头部,使用状态机解析fastcgi头部的各个字段。获取到这个fastcgi的响应包体后,将响应包体插入到ngx_event_pipe_s成员in待发送链表中。同时会将in链表中指向同一个接收缓冲区的链表节点构成一个影子单项循环链表,也就是前面那张图维护的数据结构。需要注意的是这里有一个free链表, 如果这个链表有剩余的节点,则从free链表获取一个节点,用于接收转换后的响应包体,插入到in链表中。

四、转发http响应包体给客户端

        ngx_event_pipe_write_to_downstream函数负载把经过解析后的响应包体发给客户端浏览器。在分析这个函数的具体实现时,先来看下这个函数为了处理发送响应包体给客户端而维护的数据结构。



                                                                               
 图: ngx_event_pipe_t结构中维护的各种队列

        free_raw_bufs接收缓冲区链表,用于接收来自后端服务器的响应包体,是nginx与后端服务器通信所维护的数据结构。而底层发送链表,也就是http请求结构ngx_http_request_s的out链表,用于存放需要发给客户端的响应包体数据,如果一次没能全部发送完成,则底层发送链表会保存未发送完成的数据,以便下一次再发送给客户端。正常情况下只需要这两个链表就可以了,为什么还要上图中间部分的的这几个链表(in待发送链表, out写文件链表, 局部变量out待发送链表)呢?一一来分析吧?

        为什么要存在in待发送链表? 因为nginx在发送响应包体给客户端时,并不是有多少数据就发送多少,而是有一个阈值, 在没有达到这个阈值时是不会发送的。如果没有达到发送的阈值,则需要把接收缓冲区free_raw_bufs链表中的数据缓存到in待发送链表中, 等in待发送链表中的数据达到阈值时,才把in待发送链表中的这部分数据交给底层发送链表去发送。

         in待发送链表又会拆分为out写文件链表, 还有剩余的in待发送链表,为什么要这么做? 这是因为在上游网速优先策略下,如果接收缓冲区链表满后,会把接收缓冲区链表的数据写入到文件中。因为in链表中的数据是指向接收缓冲区链表的,因此相当于把in链表中的数据写入到文件。为了区分数据是保存在内存中还是保存到文件中,nginx把in待发送链表拆分为out写文件链表, 这个链表存放的是指向文件的响应包体;另一个为剩余的in待发送链表,存放保存在内存中的响应包体。

        那为什么要有一个局部变量out待发送链表呢? 因为不管是保存到文件的out写文件链表,还是保存到内存的in待发送链表,这些数据都是要发给客户端的。而底层发送链表才不管要发送的数据时来自内存还是来自于文件, 因此用一个局部变量out链表把out写文件链表,in待发送链表中的数据插入到局部链表out链表末尾,最终把局部变量out链表中的数据交给底层发送链表去发送。

       nginx为了业务的需要,也为了减少复杂度,使用分层的思想,引入了上图中间部分的链表(in待发送链表, out写文件链表等)。 虽然引入了上图中间部分的链表,占用了内存资源,但把复杂度降低了。 如果只维护free_raw_bufs接收缓冲区链表, 底层发送链表,那要完成区分文件保存到内存还是文件, 达不得发送阈值则先不发送这些功能,则必然导致这两个链表异常庞大, 复杂度也随着提高。

       知道了ngx_event_pipe_write_to_downstream维护的数据结构,在来分析函数的实现就简单了。分两部分来分析这个函数

        1、nginx接收完所有的后端服务器响应包体后,发送响应包体给客户端

//将来自后端服务器的响应包体发送给客户端
static ngx_int_t ngx_event_pipe_write_to_downstream(ngx_event_pipe_t *p)
{
//循环的把来自后端服务器的响应包体发给客户端
for ( ;; )
{
//如果与后端服务器交互完成,则把来自后端服务器的响应包体发给客户端就退出循环
if (p->upstream_eof || p->upstream_error || p->upstream_done)
{
//先发送out缓冲区数据给客户端,这部分数据保存在了文件
if (p->out)
{
//使用过滤器模块向客户端发送响应包体:ngx_http_output_filter
rc = p->output_filter(p->output_ctx, p->out);
p->out = NULL;
}

//在发送in缓冲区数据给客户端,这部分数据保存到内存
if (p->in)
{
//使用过滤器模块向客户端发送响应包体:ngx_http_output_filter
rc = p->output_filter(p->output_ctx, p->in);
p->in = NULL;
}
}
}
}        这个逻辑是在nginx接收完后端服务器的所有响应包体后,需要把接收到的这些响应包体发给客户端。先发送out写文件链表中的数据,在发送in链表内存中的响应包体,为什么呢? 因为每次要把in链表中的数据写入到文件时,都是从in链表的头部写入到文件。因此out就是写入文件的这部分数据构成的链表, 而in就是剩余未写入文件的链表。这种场景下并没有限制每次最多可以往底层发送链表写入多少数据,是发送响应包体给客户端的最后一个流程。
       2、发送部分响应包体给客户端

        发送部分响应包体给客户端,这种场景下会计算每次需要发送多少数据给客户端,达到阈值才发送。先统计底层发送链表中上一次未发送完的剩余数据,如果这部分数据达到了发送阈值,则先发送底层发送链表中的响应包体,而不管in待发送链表,out写文件链表中的数据。如果底层发送链表中剩余的数据达不到发送阈值,才会统计out写文件链表,in待发送链表中的数据,当总大小达到阈值后才发送这部分数据。需要注意的是,当发送完一个缓冲区大小的的响应包体后,会回收这个接收缓冲区空间,加入到free_raw_bufs接收缓冲区链表中,同时也会删除指向该缓冲区的影子链表。接收缓冲区中就有剩余空间了,可以继续接收后端服务器的响应,而不需要等发送缓冲区中的数据发送完成后才接收。nginx采用边接收边发送的方式,一旦接收缓冲区中有空间,就可以继续接收来自后端服务器的响应包体。

//将来自后端服务器的响应包体发送给客户端
static ngx_int_t ngx_event_pipe_write_to_downstream(ngx_event_pipe_t *p)
{
//循环的把来自后端服务器的响应包体发给客户端
for ( ;; )
{
//统计busy链表缓冲区的总大小(也就是http请求结构中的out缓冲区,存放的底层发送链表是上一次没有发送往的数据)
for (cl = p->busy; cl; cl = cl->next)
{
bsize += cl->buf->end - cl->buf->start;
prev = cl->buf->start;
}
//缓冲区大小达到触发值,则立即发送给客户端
if (bsize >= (size_t) p->busy_size)
{
flush = 1;
goto flush;
}

//循环统计out,in缓冲区数据的长度
for ( ;; )
{
if (p->out)
{
//加上out缓冲区的大小后,是否达到发送给客户端的阈值。out中的数据保存到了文件
cl = p->out;
if (cl->buf->recycled && bsize + cl->buf->last - cl->buf->pos > p->busy_size)
{
flush = 1;
break;
}
p->out = p->out->next;
}
else if (!p->cacheable && p->in)
{
//加上in缓冲区的大小后,是否达到发送给客户端的阈值。in中的数据保存到了内存
cl = p->in;
if (cl->buf->recycled && cl->buf->last_shadow && bsize + cl->buf->last - cl->buf->pos > p->busy_size)
{
flush = 1;
break;
}
}
else
{
break;
}

//统计缓冲区中数据大大小
if (cl->buf->recycled)
{
bsize += cl->buf->last - cl->buf->pos;
}
cl->next = NULL;
//将节点插入到局部变量out链表末尾,这个out链表就是要发送给客户端的响应包体
if (out)
{
*ll = cl;
}
else
{
out = cl;
}
ll = &cl->next;
}

//使用过滤器模块向客户端发送响应包体:ngx_http_output_filter
rc = p->output_filter(p->output_ctx, out);
//发送完成后,将out链表返回到空闲链表中
ngx_chain_update_chains(&p->free, &p->busy, &out, p->tag);
for (cl = p->free; cl; cl = cl->next)
{
//影子单项链表中最后一个节点,则将b这个buf缓冲区添加到free_raw_bufs空闲链表中。以便留出剩余空间,接收后端服务的响应
if (cl->buf->last_shadow)
{
ngx_event_pipe_add_free_buf(p, cl->buf->shadow);
cl->buf->last_shadow = 0;
}
cl->buf->shadow = NULL;
}
}
}        代码上注释已经很清楚了,可以结合上面的图进行分析。 需要注意的是ngx_chain_update_chains这个函数。如果某个节点的数据发送完了,则会被回收到free链表中,以便可以存放解析完接收缓冲区free_raw_bufs后的数据。如果free链表中有剩余的节点,则会从free链表中获取一个节点,插入到待发送链表in中。这一点可以从ngx_http_fastcgi_input_filter中看出。
       到此为止,上游网速优先策略下,nginx接收来自后端服务器的响应包体,以及把转换后的响应包体发给下游客户端已经分析完成了。下一篇文章来分析负载均衡策略的选择。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  nginx处理post请求