您的位置:首页 > 运维架构 > Nginx

nginx处理post请求之数据转发

2017-09-15 06:42 459 查看
        上一篇文章分析了nginx在处理post请求时,如何启动upstream这个负载均衡模块。它是一个http框架,由它来调度具体的http模块,例如fastcgi, proxyd反向代理等,这些模块负责将来自客户端 的请求包头,请求包体转为与后端服务器通信的格式。本篇文章来分析nginx是如何将已经转换后的报文发给后端服务器。

一、转发请求数据到后端服务器流程

//与后端服务器建立连接,并注册读写事件的回调
static void ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
//发送数据给后端服务器
ngx_http_upstream_send_request(r, u)
}
        ngx_http_upstream_send_request函数负责将转换后的报文发给后端服务器,来看下这个函数的实现过程。
//发送数据给后端服务器
static void ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
c = u->peer.connection;
//向上游服务器发送请求内容,内容为request_bufs。如果一次性不能发送完,则会把
//未发送的保存到output中的busy成员。第二次被调用时,传递参数为空,因此函数内部会把上次
//没发送完的busy链表中的数据继续发给后端服务器
rc = ngx_output_chain(&u->output, u->request_sent ? NULL : u->request_bufs);

//标记为已经向上游服务器发送了请求
u->request_sent = 1;

//先移除写事件的超时事件
if (c->write->timer_set)
{
ngx_del_timer(c->write);
}

//如果一次没有发送完所有请求数据,则把重新注册写事件超时到epoll
if (rc == NGX_AGAIN)
{
ngx_add_timer(c->write, u->conf->send_timeout);
ngx_handle_write_event(c->write, u->conf->send_lowat);
return;
}

/**********执行到此,表示已经向上游服务器发送完所有请求数据**********/
/**********如果有读事件,则开始接收上游服务器的响应******************/
ngx_add_timer(c->read, u->conf->read_timeout);

//将写事件设置不做任何事情。因为nginx已经把所有请求都发给了上游服务器,不需要发送了,
u->write_event_handler = ngx_http_upstream_dummy_handler;
ngx_handle_write_event(c->write, 0);
}
        函数内部调用gx_output_chain向后端服务器发送数据,首次被调用时传递的参数是u->request_bufs,这个内容也就是经过转换后的,与后端服务器通信的报文。但如果一次发送不完, 需要再次被事件模块调度执行才能发送剩余数据时, 这个函数会再次被调用并且传递NULL空参数,这是为什么?
因为gx_output_chain函数内部会把上一次未发送完的数据报文保存到ngx_output_chain_ctx_t结构中的busy成员中。 这个函数再次被调用时传递的参数为空,就可以把busy成员中的剩余数据发送给后端服务。这个函数实现了具体的发送过程,比较复杂,文章最后面我们再来详细分析。这里先分析发送过程的整体框架,先把控整体,再来分析细节。现在分析两个场景:
        (1)如果一次调度这个函数不能发送完所有数据怎么办? nginx会重新注册写事件到epoll中,这样写事件被触发时,可以发送剩余的数据给后端服务器。来看下这个过程;

//事件模块的读写回调,事件被触发时会调用负载均衡模块对应的读写回调
static void ngx_http_upstream_handler(ngx_event_t *ev)
{
//c表示nginx与上游服务器的连接
c = ev->data;
//r表示客户端与nginx的请求
r = c->data;
//u表示nginx与上游服务器的upstream
u = r->upstream;
//这时的c表示客户端与nginx的连接
c = r->connection;

if (ev->write)
{
//向后端服务器发送数据
u->write_event_handler(r, u);
}
else
{
//接收后端服务器的响应
u->read_event_handler(r, u);
}
}
        对于负载均衡模块的写回调write_event_handler为:ngx_http_upstream_send_request_handler,用于在一次没有发送完所有的请求数据给后端服务器时,写事件再次触发时将调用这个函数发送剩余的数据。从中也可以看出,最终还是调用ngx_http_upstream_send_request这个函数发送请求数据给后端服务器。
//一次没有发送完所有的请求数据给后端服务器时,写事件再次触发时会调用这个函数发送剩余的数据
static void ngx_http_upstream_send_request_handler(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
//如果写超时,表示发送给上游服务器的请求超时了
if (c->write->timedout)
{
//重新连接服务器,会根据策略,可能连接到下一个可用的上游服务器
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT);
return;
}

//再次调用这个函数发送请求
ngx_http_upstream_send_request(r, u);
}
 
      (2)如果发送完了所有的请求数据给后端服务器,那该如何处理? 继续回到ngx_http_upstream_send_request函数进行分析。如果发送完所有的请求数据后,并且读事件已经就绪了,则立即读取来自后端服务的http响应头部;如果读事件还未就绪,则把负载均衡模块的write_event_handler写回调设置为不做任何事情的:ngx_http_upstream_dummy_handler,因为不需要再发送任何数据给后端服务器了。以此同时nginx将等待读事件被触发,当读事件被触发时,开始接收来自后端服务器的http响应头部。
二、发送函数分析

        调用ngx_output_chain函数负责把调用层传进来的数据发给后端服务器,如果调用层传进来的参数为空,则会把保存在内部隐藏层中的上一次未发送完的数据发送给后端服务器。在分析函数前,来看下这个函数维护的数据结构,分两种情况。一种是直接把调用层传进来的数据拷贝到内部隐藏层;另一种是把调用层传进来的参数先拷贝到过滤层,过滤层处理完成后,再把结果传给内部隐藏层;

        1、直接把调用层数据传给内部隐藏层;



        调用层链表,也就是调用ngx_output_chain函数传递的u->request_bufs链表或者空链表,也就是应用层要发给后端服务器的数据。通常如果调用层链表是一个空节点,或者调用层只有一个链表节点(要发给后端服务器的数据较少,一个链表节点足以存放所有数据),这两种情况下会直接把调用层的数据直接拷贝到内部隐藏层链表,也就是ngx_chain_writer_ctx_t成员out。这个内部隐藏层链表是做什么呢? 用于缓存要发送给后端服务器的报文,如果报文过大导致一次无法发送,则会保存上一次未发送完成的数据,这也是最终要发送给后端服务器的数据。另一个问题,为什么图中内部隐藏层链表节点数比调用层链表节点数更多呢?
因为内部隐藏层保留了上一次未发送完的数据,如果再次调用时,调用层又传递了新数据的话,那也会缓存这个新的数据,因此内部隐藏层数据节点肯定比调用层过多。

//向后端服务器发送数据
//参数in:调用层链表
ngx_int_t ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
{
if (ctx->in == NULL && ctx->busy == NULL)
{
if (in == NULL)
{
//ngx_chain_writer,把数据直接传递给内部隐藏层进行发送
return ctx->output_filter(ctx->filter_ctx, in);
}

//调用层链表由一个节点组成,通常这种情况是要发给后端服务器的数据较少,
//一个节点足以存放所有数据。且不需要拷贝内存操作。
if (in->next == NULL && ngx_output_chain_as_is(ctx, in->buf))
{
//ngx_chain_writer,把数据直接传递给内部隐藏层进行发送
return ctx->output_filter(ctx->filter_ctx, in);
}
}
}
        output_filter的回调为:ngx_chain_writer, 这个函数内部会把数据发给后端服务器,同时保存未发送完成的数据到内部隐藏层的out链表中,以便下一次写事件被触发时,可以发送剩余的数据给后端服务器。来看下这个函数的实现过程;
//将in链表的数据发送到后端服务器
ngx_int_t ngx_chain_writer(void *data, ngx_chain_t *in)
{
//将in的内容拷贝到输出链表ctx->last中,也就是内部隐藏层的out链表。
//之所以要保存的目的是一次不能发送完所有的数据到后端服务器时,可以在下一次把剩余的数据发给后端服务器
for (size = 0; in; in = in->next)
{
//将节点插入到out链表末尾
cl = ngx_alloc_chain_link(ctx->pool);
cl->buf = in->buf;
cl->next = NULL;
*ctx->last = cl;
ctx->last = &cl->next;
}

//发送数据给后端服务器 ngx_writev_chain,返回值为已经发送到哪个节点。
//下一次从这个节点开始发送剩余的数据给后端服务器,返回后的ctx->out指向未发送完成的链表节点
ctx->out = c->send_chain(c, ctx->out, ctx->limit);
}
        如果一次没有发送完成,则下一次写事件被调度时,ngx_output_chain传入的请求数据为空,因此ngx_chain_writer再次被调度时,将会把内部隐藏层ngx_chain_writer_ctx_t的out成员的数据发送给后端服务器。
        2、调用层的数据经过过滤后,在发给内部隐藏层

        通常调用层成传进来的数据不是一个链表节点就可以存放所有数据,或者调用层的数据还保存到文件中,则这些数据不能直接传递给内部隐藏层,而需要经过过滤层进行处理。过滤层把数据处理完后,在发给内部隐藏层。因此对于内部隐藏层来讲这是透明的操作,内部隐藏层不管数据是直接来自调用层,还是过滤层处理后的结果,它才不关心数据来自何处,只要有数据到来,内部隐藏层就接收。这是一种典型的分层设计思想。



        从图中可以看出,过滤层链表ngx_output_chain_ctx_t成员in是一个链表,缓存来自调用层的数据, 而内部隐藏层ngx_chain_writer_ctx_t成员out也是一个链表,缓存来自过滤层的处理结果或者直接来自调用层的数据(调用层只传递一个链表节点,或者传递的数据为空)。 为什么要搞两个链表呢? 岂不是多此一举,其实不然,nginx这样设计肯定有它的目的。 因为过滤层链表缓存的是来自调用层的数据,经过过滤处理 ,然后把过滤结果传递给内部隐藏层,但有可能一次操作不能把调用层传进来的数据全部过滤完成,
而需要下一次被调用时继续过滤未过虑完的数据,因此需要维护这个过滤层链表;  而内部隐藏层链表是保存未发送给后端服务器的数据,也因此需要这个内部隐藏层链表。这两个链表分工不同,作用也就不同。

ngx_int_t ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
{
//将链表in的内容拷贝到ctx->in链表的末尾,也就是保存到过滤链表
if (in)
{
ngx_output_chain_add_copy(ctx->pool, &ctx->in, in);
}

//这个循环是为了多次进行过滤操作
for ( ;; )
{
//对过滤链表ctx->in中的每一个节点内容进行过滤处理,过滤后保存到out结果链表。
//这里说的过滤只是判断是否需要为这个链表节点拷贝一份数据到内存, 为了方便理解,暂且称之为过滤。
//ctx->in链表中的数据不就是在内存吗?为什么还需要拷贝一份数据到内存。没错,如果ctx->in链表中的数据在
//内存中的话,那肯定不需要在拷贝数据到内存了,但ctx->in链表中的节点有可能指向的是文件,而文件的数据
//是没有拷贝到内存中的,因此对于文件则需要拷贝到内存中来。
while (ctx->in)
{
//计算每一个链表节点的大小
bsize = ngx_buf_size(ctx->in->buf);
//如果数据不是在文件中,则这个函数返回0,表示数据已经在内存中了,因此不需要拷贝数据到内存中。
//直接把该节点插入到out结果链表末尾,相当于这个节点就过滤完成了。否则需要申请buf空间
if (ngx_output_chain_as_is(ctx, ctx->in->buf))
{
cl = ctx->in;
ctx->in = cl->next;
*last_out = cl;
last_out = &cl->next;
cl->next = NULL;
continue;
}

if (ctx->buf == NULL)
{
//创建bsize大小的bufer缓冲区,存放到ctx->buf,通常返回的结果不等于NGX_OK
rc = ngx_output_chain_align_file_buf(ctx, bsize);
if (rc != NGX_OK)
{
if (ctx->free)
{
//从空闲链表中获取buf空间
cl = ctx->free;
ctx->buf = cl->buf;
ctx->free = cl->next;
ngx_free_chain(ctx->pool, cl);
}
else if (out || ctx->allocated == ctx->bufs.num)
{
//申请的buf空间总数超过限制,则先把缓存中的数据发送完后,在来发送剩余的数据
//发送完成后,可以重复利用这些缓存
break;

}
else if (ngx_output_chain_get_buf(ctx, bsize) != NGX_OK)
{
//重新申请一个buff空间,最多不能申请超过ctx->bufs.num个数
return NGX_ERROR;
}
}
}

//将ctx->in->buf缓冲区的内容拷贝到ctx->buf缓冲区中
rc = ngx_output_chain_copy_buf(ctx);
//已经将该过滤节点的数据移动到了out结果链表中,因此可以删除该过滤节点
if (ngx_buf_size(ctx->in->buf) == 0)
{
ctx->in = ctx->in->next;
}

//创建一个链表节点
cl = ngx_alloc_chain_link(ctx->pool);
cl->buf = ctx->buf;
cl->next = NULL;
//将链表节点插入到out链表末尾
*last_out = cl;
last_out = &cl->next;
ctx->buf = NULL;
}

//发送数据给后端服务器 ngx_chain_writer
last = ctx->output_filter(ctx->filter_ctx, out);

//将out中已经发送完的数据移动到free空闲链表,之后需要buf空间的话就可以直接从free空闲链表中获取
//而对应没有发送完的数据则保存到busy链表,以便下一次写事件触发时发送剩余的数据
ngx_chain_update_chains(&ctx->free, &ctx->busy, &out, ctx->tag);

//此时out此时一定是空,因为ngx_chain_update_chains函数内部把out未发送完的数据保存到busy后,将out设置为空
last_out = &out;
}
}

        函数有点长,不过注释得很清楚了。这里所说的过滤,是我方便理解而称之为过滤。说白了就是判断调用层传进来的数据是否需要做一个内存拷贝操作。当调用层传进来的链表节点指向的数据是保存到文件中时,则需要把文件中的数据拷贝到内存中,拷贝结束后将这个节点插入到过滤结果链表。而如果调用层传进来的链表指向的数据已经保存到了内存,则不需要在做内存拷贝操作了,直接将该节点插入到过滤链结果链表末尾。 

        需要注意的是,如果需要执行内存拷贝操作,则会有两种方式获取一个新的buf空间。一种是从空闲链表free中获取,那这个空闲链表从何而来呢? 当发送数据给后端服务器完成后,会将已经发送完的链表节点插入到空闲链表中,从ngx_chain_update_chains函数可以看出这个过程,这样就可以复用这个缓冲区。另一种方式是,如果空闲链表没有空间容纳新的数据了,则重新从内存池中申请一个空间,但申请缓冲区的总个数不能超过限制,超过限制时,需要等缓冲区中的数据发给后端服务器完成后,在复用这片空间,而不是重新开辟。

         到此为止, 对于nginx是如何发生fastcgi报文给后端服务器,以及一次没发送完所有数据时,写事件再次被触发时是如何发送剩余数据给后端服务器的,这块逻辑应该清晰了吧!  做个总结,如果调用层传进来的数据比较小,一个节点足以存放所有数据,则直接把数据发给内部隐藏层处理; 如果调用层传进来的数据很大时,需要经过过滤层过滤处理,再把过滤后的结果传递给内部隐藏层。 内部隐藏层把数据发送给后端服务器,以及保存一次未发送完的数据,以便写事件触发时再次发送。通过发送逻辑的分析,能很好的体现分层设计的思想。

        下一篇文章将分析nginx是如何接收来自后端服务器响应的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息