Nginx upstream原理分析【3】-带buffering给客户端返回数据
上一篇Nginx upstream原理分析【2】-带buffering读取upstream数据 我们介绍了nginx在带buffering的情况下是如何读取FCGI数据的,这里我们介绍他是怎么给客户端返回数据的。
从上篇文章我们知道ngx_event_pipe这个函数一句参数不同,既可以读取upstream数据,也可以给客户端返回数据。其调用的功能函数分别为:ngx_event_pipe_read_upstream 和ngx_event_pipe_write_to_downstream。nginx读取upstream的数据后,会把数据放在p->in链表里面,当然如果缓存不够等原因,写入了磁盘的话,nginx总是将p->in的前面部分写入磁盘,因此会记录在p->out链表上面。在发送时就围绕这2个成员进行了。
发送数据时,客户端的连接结构存放在p->downstream。先来看一下缓存结构:
struct ngx_buf_s {//内存的描述结构/管理结构,其start/end指向一块真正存放数据的内存。 u_char *pos;//当前数据读到了这里 u_char *last;//所有数据的末尾 off_t file_pos;//如果在文件中,那就表示为偏移 off_t file_last;///如果在文件中,那就表示为偏移 u_char *start; /* start of buffer */ u_char *end; /* end of buffer */ ngx_buf_tag_t tag; ngx_file_t *file; ngx_buf_t *shadow;//shadow会将buf组成一条链表。用last_shadow标记表明是否是某个大裸FCGI数据块中的最后一个。通过p->in等指向头部。 //这里容易混淆,以为last_shadow指的是整个链表的最后一个,其实不是,这个链表中可能是属于几个大FCGI数据块,就有几个为1的。具体参考ngx_event_pipe_drain_chains /* the buf's content could be changed */ unsigned temporary:1; /* * the buf's content is in a memory cache or in a read only memory * and must not be changed */ unsigned memory:1; /* the buf's content is mmap()ed and must not be changed */ unsigned mmap:1; unsigned recycled:1;//这个域表示我们当前的buf是需要被回收的。调用output_filter之前会设置为0.代表我这个buf是否需要回收重复利用。 unsigned in_file:1;//这个BUFFER存放在文件中。 unsigned flush:1;//这块内存是不是需要尽快flush给客户端,也就是是否需要尽快发送出去,ngx_http_write_filter会利用这个标志做判断。 unsigned sync:1; unsigned last_buf:1;//是否是最后一块内存。 unsigned last_in_chain:1; unsigned last_shadow:1;//这个代表的是,我这个data数据块是不是属于裸FCGI数据块的最后一个,如果是,那我的shadow指向这个大数据块。否则指向下一个data节点。 unsigned temp_file:1; /* STUB */ int num; };
上面有个标志:recycled,这个标志是用来控制内存回收的,如果标志位1,标志其他地方关注这块内存的回收,希望回收它。如果设置为0,表示不需要回收。在nginx读取完毕upstream的数据,FCGI返回FIN包后,我们由于不需要分配其他内存了,因此不需要回收这块内存,因此recycled会设置为0.
这里插入一下,在ngx_http_fastcgi_input_filter函数解析到一块有小数据后,会带哦用如下代码,设置recycled为1 !!!!为什么??那这样不就会导致nginx尽快的将数据发送给客户端吗?
//用这个新的缓存描述结构,指向buf这块内存里面的标准输出数据部分,注意这里并没有拷贝数据,而是用b指向了f->pos也就是buf的某个数据地方。 ngx_memzero(b, sizeof(ngx_buf_t)); b->pos = f->pos;//从pos到end b->start = buf->start;//b 跟buf共享一块客户端发送过来的数据。这就是shadow的地方, 类似影子? b->end = buf->end; b->tag = p->tag; b->temporary = 1; b->recycled = 1;//设置为需要回收的标志,这样在发送数据时,会考虑回收这块内存的。但是,请看ngx_event_pipe_remove_shadow_links这里会设置为1的。
比如我gdb了一个nginx工作进程,其p->in->buf的内容是这样的:
$18 = {
pos = 0x1755f10 "h</td><td class=\"v\">/usr/sbin/sendmail -t -i </td><td class=\"v\">/usr/sbin/sendmail -t&n
bsp;-i </td></tr>\n<tr><td class=\"e\">serialize_precision</td><td class=\"v\">100</td><td cl"..., last = 0x1756f10 "@@",
file_pos = 0, file_last = 0,
start = 0x1755f10 "h</td><td class=\"v\">/usr/sbin/sendmail -t -i </td><td class=\"v\">/usr/sbin/sendmail -t
-i </td></tr>\n<tr><td class=\"e\">serialize_precision</td><td class=\"v\">100</td><td cl"..., end = 0x1756f10 "@@",
tag = 0x693940, file = 0x0, shadow = 0x17529f8, temporary = 1, memory = 0, mmap = 0, recycled = 1, in_file = 0, flush = 0,
sync = 0, last_buf = 0, last_in_chain = 0, last_shadow = 1, temp_file = 0, num = 3}
从上面可以看到,一般情况下recycled是设置为1了的,也就是说希望尽快回收这块内存,一般fastcgi_buffers设置的缓存也就几块,默认为:fastcgi_buffers 8 4k|8k ; 所以回收是肯定的。另外,可以看到 last_shadow为1,那就是说明这个内存覆盖了整块从upstream读取过来的数据。这样其shadow指针肯定指向这块数据的头部了。
下面回到ngx_event_pipe_write_to_downstream,不然跑题了。这个函数是个大的循环,不断的遍历p->out,p->in的数据。在循环开头,如果FCGI发送完了所有数据,发送了FIN包,那么会进入特殊的处理:直接清楚recycled标志,调用output_filter发送数据,这个函数就是ngx_http_output_filter函数,这部分比较简单,看一下代码:
static ngx_int_t ngx_event_pipe_write_to_downstream(ngx_event_pipe_t *p) {//ngx_event_pipe调用这里进行数据发送给客户端,数据已经准备在p->out,p->in里面了。 u_char *prev; size_t bsize; ngx_int_t rc; ngx_uint_t flush, flushed, prev_last_shadow; ngx_chain_t *out, **ll, *cl, file; ngx_connection_t *downstream; downstream = p->downstream; ngx_log_debug1(NGX_LOG_DEBUG_EVENT, p->log, 0,"pipe write downstream: %d", downstream->write->ready); flushed = 0; for ( ;; ) { if (p->downstream_error) {//如果客户端连接出错了。drain=排水;流干, //清空upstream发过来的,解析过格式后的HTML数据。将其放入free_raw_bufs里面。 return ngx_event_pipe_drain_chains(p); } if (p->upstream_eof || p->upstream_error || p->upstream_done) { //如果upstream的连接已经关闭了,或出问题了,或者发送完毕了,那就可以发送了。 /* pass the p->out and p->in chains to the output filter */ for (cl = p->busy; cl; cl = cl->next) { cl->buf->recycled = 0; } if (p->out) {//数据写到磁盘了 ngx_log_debug0(NGX_LOG_DEBUG_EVENT, p->log, 0, "pipe write downstream flush out"); for (cl = p->out; cl; cl = cl->next) { cl->buf->recycled = 0;//不需要回收重复利用了,因为upstream_done了,不会再给我发送数据了。 } //下面,因为p->out的链表里面一块块都是解析后的HTML数据,所以直接调用ngx_http_output_filter进行HTML数据发送就行了。 rc = p->output_filter(p->output_ctx, p->out); if (rc == NGX_ERROR) { p->downstream_error = 1; return ngx_event_pipe_drain_chains(p); } p->out = NULL; } if (p->in) {//跟out同理。简单调用ngx_http_output_filter进入各个filter发送过程中。 ngx_log_debug0(NGX_LOG_DEBUG_EVENT, p->log, 0, "pipe write downstream flush in"); for (cl = p->in; cl; cl = cl->next) { cl->buf->recycled = 0;//已经是最后的了,不需要回收了 } //注意下面的发送不是真的writev了,得看具体情况比如是否需要recycled,是否是最后一块等。ngx_http_write_filter会判断这个的。 rc = p->output_filter(p->output_ctx, p->in);//调用ngx_http_output_filter发送,最后一个是ngx_http_write_filter if (rc == NGX_ERROR) { p->downstream_error = 1; return ngx_event_pipe_drain_chains(p); } p->in = NULL; } //如果要缓存,那就写入到文件里面去。 if (p->cacheable && p->buf_to_file) { file.buf = p->buf_to_file; file.next = NULL; if (ngx_write_chain_to_temp_file(p->temp_file, &file) == NGX_ERROR){ return NGX_ABORT; } } ngx_log_debug0(NGX_LOG_DEBUG_EVENT, p->log, 0, "pipe write downstream done"); /* TODO: free unused bufs */ p->downstream_done = 1; break; }
上面这部分代码如果进入,最终就会退出整个函数从而结束请求的,如果FCGI没有关闭,数据读取没有完成,那么就可以进行数据发送了。
Nginx有个配置参数:fastcgi_busy_buffers_size,我们看一下其定义:
syntax: fastcgi_busy_buffers_size
size
;default: fastcgi_busy_buffers_size 8k|16k; context: http
,server
,location
Limits the total
size
of buffers that can be busy sending a response to the client while the response is not yet fully read. In the mean time, the rest of the buffers can be used for reading a response and, if needed, buffering part of a response to a temporary file. By default,size
is limited by two buffers set by the fastcgi_buffer_size and fastcgi_buffers directives.
简单说,busy代表经过了output_filter过的,从out移动过来的缓存,其里面可能有已经发送完成了,因为ngx_http_write_filter会更新这写buf的。因此nginx会在这个时候遍历计算一下busy buf的大小,看看是不是超过了大小,如果是,就会开始发送,也就是设置flush=1,直接跳过去到后面的goto语句goto flush;去调用output_filter,然后ngx_chain_update_chains更新busy,free指针,这个我们后面再介绍。
//否则upstream数据还没有发送完毕。 if (downstream->data != p->output_ctx || !downstream->write->ready || downstream->write->delayed) { break; } /* bsize is the size of the busy recycled bufs */ prev = NULL; bsize = 0; //这里遍历需要busy这个正在发送,已经调用过output_filter的buf链表,计算一下那些可以回收重复利用的buf //计算这些buf的总容量,注意这里不是计算busy中还有多少数据没有真正writev出去,而是他们总共的最大容量 for (cl = p->busy; cl; cl = cl->next) { if (cl->buf->recycled) { if (prev == cl->buf->start) { continue; } bsize += cl->buf->end - cl->buf->start; prev = cl->buf->start; } } ngx_log_debug1(NGX_LOG_DEBUG_EVENT, p->log, 0, "pipe write busy: %uz", bsize); out = NULL; //busy_size为fastcgi_busy_buffers_size 指令设置的大小,指最大待发送的busy状态的内存总大小。 //如果大于这个大小,nginx会尝试去发送新的数据并回收这些busy状态的buf。 if (bsize >= (size_t) p->busy_size) { flush = 1;//如果busy链表里面的数据很多了,超过fastcgi_busy_buffers_size 指令,那就赶紧去发送,回收吧,不然free_raw_bufs里面没可用缓存了。 goto flush; }
然后,就需要老老实实的看看p->out,p->in里面的数据了,后面的代码是个循环,不断遍历p->out,p->in里面的未发送数据,将他们放到out链表后面,注意这里发送的数据不超过busy_size因为配置限制了。这里有个比较有意思的地方就是,nginx会尽量一次发送一整块数据,具体方法看下面的代码中的注释,prev_last_shadow用来表示遍历过程之中,上一块内存的last_shadow标志值。综合起来就是一句话:必须要发送数据的时候,如果上一块数据正好是一块大裸FCGI内存的最后一个数据节点,那当前这个节点下次再进行发送,尽量一次一个FCGI数据块的发送。
循环执行完后,我们就得到了一个HTML节点链表,由out指针指向,待会就可以发送这个链表的数据了。代码如下:
flush = 0; ll = NULL; prev_last_shadow = 1;//标记上一个节点是不是正好是一块FCGI buffer的最后一个数据节点。 //遍历p->out,p->in里面的未发送数据,将他们放到out链表后面,注意这里发送的数据不超过busy_size因为配置限制了。 for ( ;; ) { //循环,这个循环的终止后,我们就能获得几块HTML数据节点,并且他们跨越了1个以上的FCGI数据块的并以最后一块带有last_shadow结束。 if (p->out) {//buf到tempfile的数据会放到out里面。 cl = p->out; if (cl->buf->recycled && bsize + cl->buf->last - cl->buf->pos > p->busy_size) { flush = 1;//判断是否超过busy_size break; } p->out = p->out->next; ngx_event_pipe_free_shadow_raw_buf(&p->free_raw_bufs, cl->buf); } else if (!p->cacheable && p->in) { cl = p->in; ngx_log_debug3(NGX_LOG_DEBUG_EVENT, p->log, 0, "pipe write buf ls:%d %p %z", cl->buf->last_shadow, cl->buf->pos, cl->buf->last - cl->buf->pos); // if (cl->buf->recycled && cl->buf->last_shadow && bsize + cl->buf->last - cl->buf->pos > p->busy_size) { //1.对于在in里面的数据,如果其需要回收; //2.并且又是某一块大FCGI buf的最后一个有效html数据节点; //3.而且当前的没法送的大小大于busy_size, 那就需要回收一下了,因为我们有buffer机制 if (!prev_last_shadow) { //如果前面的一块不是某个大FCGI buffer的最后一个数据块,那就将当前这块放入out的后面,然后退出循环去flash //什么意思呢,就是说,如果当前的这块不会导致out链表多加了一个节点,而倒数第二个节点正好是一块FCGI大内存的结尾。 //其实是i做了个优化,让nginx尽量一块块的发送。 p->in = p->in->next; cl->next = NULL; if (out) { *ll = cl; } else { out = cl; } } flush = 1;//超过了大小,标记一下待会是需要真正发送的。不过这个好像没发挥多少作用,因为后面不怎么判断、 break;//停止处理后面的内存块,因为这里已经大于busy_size了。 } prev_last_shadow = cl->buf->last_shadow; p->in = p->in->next; } else { break;//后面没有数据了,那没办法了,发吧。不过一般情况肯定有last_shadow为1的。这里很难进来的。 } //cl指向当前需要处理的数据,比如cl = p->out或者cl = p->in; //下面就将这块内存放入out指向的链表的最后,ll指向最后一块的next指针地址。 if (cl->buf->recycled) {//如果这块buf是需要回收利用的,就统计其大小 bsize += cl->buf->last - cl->buf->pos; } cl->next = NULL; if (out) { *ll = cl; } else { out = cl;//指向第一块数据 } ll = &cl->next; }
找到要发送的数据后,下面进入flush的过程,out指针指向一个链表,其里面的数据是从p->out,p->in来的要发送的数据。后面将out指针指向的内存调用output_filter,进入filter过程。
0.首先调用output_filter()将这块数据进行filter,最后尝试发送出去。最终将r->out里面的数据,和参数里面的数据一并以writev的机制发送给客户端,如果没有发送完所有的,则将剩下的放在r->out。下次发送的时候从out开始继续发送。
1.如果输出失败,就需要释放相关的结构,调用的是ngx_event_pipe_drain_chains函数,这个我们待会介绍一下;
2.然后ngx_chain_update_chains()将各个链表整理一下,主要是将out刚刚发送完的buf挂入busy,busy里面已经确定调用了writev的内存放入free链表,以便后续进行重复利用。
3.之后,遍历free里面的数据,如果其中某个节点cl->buf->last_shadow等于1的话,说明我们碰到了一块FCGI裸数据的buf的最后一个数据节点,那么久调用ngx_event_pipe_add_free_buf将这个FCGI裸数据buf返回放到free_raw_bufs里面去。
看一下ngx_event_pipe_write_to_downstream最后一部分,也就是数据发送filter输出,整理链表的代码:
//到这里后,out指针指向一个链表,其里面的数据是从p->out,p->in来的要发送的数据。 flush: //下面将out指针指向的内存调用output_filter,进入filter过程。 ngx_log_debug2(NGX_LOG_DEBUG_EVENT, p->log, 0, "pipe write: out:%p, f:%d", out, flush); if (out == NULL) { if (!flush) { break; } /* a workaround for AIO */ if (flushed++ > 10) { return NGX_BUSY; } } rc = p->output_filter(p->output_ctx, out);//简单调用ngx_http_output_filter进入各个filter发送过程中。 if (rc == NGX_ERROR) { p->downstream_error = 1; return ngx_event_pipe_drain_chains(p); } //将out的数据移动到busy,busy中发送完成的移动到free ngx_chain_update_chains(&p->free, &p->busy, &out, p->tag); for (cl = p->free; cl; cl = cl->next) { if (cl->buf->temp_file) { if (p->cacheable || !p->cyclic_temp_file) { continue; } /* reset p->temp_offset if all bufs had been sent */ if (cl->buf->file_last == p->temp_file->offset) { p->temp_file->offset = 0; } } /* TODO: free buf if p->free_bufs && upstream done */ /* add the free shadow raw buf to p->free_raw_bufs */ if (cl->buf->last_shadow) { //前面说过了,如果这块内存正好是整个大FCGI裸内存的最后一个data节点,则释放这块大FCGI buffer。 //当last_shadow为1的时候,buf->shadow实际上指向了这块大的FCGI裸buf的。也就是原始buf,其他buf都是个影子,他们指向某块原始的buf. if (ngx_event_pipe_add_free_buf(p, cl->buf->shadow) != NGX_OK) { return NGX_ABORT; } cl->buf->last_shadow = 0; } cl->buf->shadow = NULL; } } return NGX_OK; }
到这里,ngx_event_pipe_write_to_downstream处理完毕。下面我们附带介绍一下几个函数:
ngx_http_output_filter函数:
这个函数在很多地方会被调用,用来把第二个参数的链表的数据发送到r参数所指的客户端连接。一般是挂在某个结构的output_filter成员上,比如:rc = p->output_filter(p->output_ctx, out);
ngx_http_output_filter函数会调用一个叫ngx_http_top_body_filter的函数,这个函数会形成一个输出链,也就是函数链接,一个调用一个,负责将数据发送出去,这里有个有意思的地方,就是这个函数是个安全局函数指针,其到底指向什么地方呢,看编译的顺序,比如我安装的,这个函数指向ngx_http_range_body_filter,最后一个是:ngx_http_write_filter用来发送数据的,ngx_http_write_filter函数会调用writev真正的发送数据。下面看一下这个函数就知道这里的原理了,也就是通过static变量保存上一个ngx_http_top_body_filter,这样类似实现了一个函数调用栈:
</span> static ngx_int_t ngx_http_range_body_filter_init(ngx_conf_t *cf) { ngx_http_next_body_filter = ngx_http_top_body_filter;//本文件的局部变量。 ngx_http_top_body_filter = ngx_http_range_body_filter;//覆盖全局变量 return NGX_OK; }
ngx_event_pipe_drain_chains函数: 这个函数可以看出last_shadow机制的作用,具体看下面的注释吧:
static ngx_int_t ngx_event_pipe_drain_chains(ngx_event_pipe_t *p) {//遍历p->in/out/busy,将其链表所属的裸FCGI数据块释放,放入到free_raw_bufs中间去。也就是,清空upstream发过来的,解析过格式后的HTML数据。 ngx_chain_t *cl, *tl; for ( ;; ) { //···· //找到对应的链表 while (cl) {/*要知道,这里cl里面,比如p->in里面的这些ngx_buf_t结构所指向的数据内存实际上是在 ngx_event_pipe_read_upstream里面的input_filter进行协议解析的时候设置为跟从客户端读取数据时的buf公用的,也就是所谓的影子。 然后,虽然p->in指向的链表里面有很多很多个节点,每个节点代表一块HTML代码,但是他们并不是独占一块内存的,而是可能共享的, 比如一块大的buffer,里面有3个FCGI的STDOUT数据包,都有data部分,那么将存在3个b的节点链接到p->in的末尾,他们的shadow成员 分别指向下一个节点,最后一个节点就指向其所属的大内存结构。具体在ngx_http_fastcgi_input_filter实现。 */ if (cl->buf->last_shadow) {//碰到了某个大FCGI数据块的最后一个节点,释放只,然后进入下一个大块里面的某个小html 数据块。 if (ngx_event_pipe_add_free_buf(p, cl->buf->shadow) != NGX_OK) { return NGX_ABORT; } cl->buf->last_shadow = 0; } cl->buf->shadow = NULL; tl = cl->next; cl->next = p->free;//把cl这个小buf节点放入p->free,供ngx_http_fastcgi_input_filter进行重复使用。 p->free = cl; cl = tl; } } }
ngx_chain_update_chains函数: 这个函数用来更新busy,free链表。
void ngx_chain_update_chains(ngx_chain_t **free, ngx_chain_t **busy, ngx_chain_t **out, ngx_buf_tag_t tag) {//调用方式 &u->free_bufs, &u->busy_bufs, &u->out_bufs, u->output.tag //函数完成2个功能: 1. 将out_bufs的缓冲区放入busy_bufs链表的尾部,注意顺序; //2.如果busy_bufs里面的数据没有了,发送完毕了,那就将这块buffer缓冲区移动到free_bufs链表里面。 ngx_chain_t *cl; if (*busy == NULL) {//这2个循环,将&u->out_bufs 移动到u->busy_bufs链表尾部。 *busy = *out; } else { for (cl = *busy; cl->next; cl = cl->next) { /* void */ } cl->next = *out; } *out = NULL; while (*busy) {//将已经发送完毕的buf更新,放入free链表里面。 if (ngx_buf_size((*busy)->buf) != 0) {//ngx_http_write_filter函数发送的时候会更新buf的。 break;//由于这些buffer的顺序性,如果碰到大小不等于0的,也就是数据发送到这里之后的都没有发送出去,不能释放。 } if ((*busy)->buf->tag != tag) { *busy = (*busy)->next; continue; } (*busy)->buf->pos = (*busy)->buf->start;//清空busy->buf结构里面的数据 (*busy)->buf->last = (*busy)->buf->start; //下面四行,将busy指向的指针的指向往后移动,然后将当前节点放入free_bufs的头部 cl = *busy; *busy = cl->next; cl->next = *free; *free = cl; } }
ngx_event_pipe_add_free_buf函数: ngx_event_pipe_add_free_buf将参数的b代表的数据块挂入free_raw_bufs的开头或者第二个位置。
ngx_int_t ngx_event_pipe_add_free_buf(ngx_event_pipe_t *p, ngx_buf_t *b) {//将参数的b代表的数据块挂入free_raw_bufs的开头或者第二个位置。b为上层觉得没用了的数据块。 ngx_chain_t *cl; //这里不会出现b就等于free_raw_bufs->buf的情况吗 cl = ngx_alloc_chain_link(p->pool); if (cl == NULL) { return NGX_ERROR; } b->pos = b->start;//置空这坨数据 b->last = b->start; b->shadow = NULL; cl->buf = b; if (p->free_raw_bufs == NULL) { p->free_raw_bufs = cl; cl->next = NULL; return NGX_OK; } //看下面的注释,意思是,如果最前面的free_raw_bufs中没有数据,那就吧当前这块数据放入头部就行。 //否则如果当前free_raw_bufs有数据,那就得放到其后面了。为什么会有数据呢?比如,读取一些数据后,还剩下一个尾巴存放在free_raw_bufs,然后开始往客户端写数据 //写完后,自然要把没用的buffer放入到这里面来。这个是在ngx_event_pipe_write_to_downstream里面做的。或者干脆在ngx_event_pipe_drain_chains里面做。 //因为这个函数在inpupt_filter里面调用是从数据块开始处理,然后到后面的, //并且在调用input_filter之前是会将free_raw_bufs置空的。应该是其他地方也有调用。 if (p->free_raw_bufs->buf->pos == p->free_raw_bufs->buf->last) { /* add the free buf to the list start */ cl->next = p->free_raw_bufs; p->free_raw_bufs = cl; return NGX_OK; } /* the first free buf is partialy filled, thus add the free buf after it */ cl->next = p->free_raw_bufs->next; p->free_raw_bufs->next = cl; return NGX_OK; }
ngx_event_pipe_write_to_downstream函数到这里算介绍完了,相对ngx_event_pipe_read_upstream要简单一些。这里总结一下:
ngx_event_pipe_write_to_downstream负责将p->out, p->in链表里面的data数据节点的数据按照FCGI裸数据buffer为单位,也就是以块为单位调用ngx_http_output_filter,将数据链接到r->out成员上,随时准备用writev的方式发送出去。然后更新free,busy等结构,将已经发送出去的buffer挂到free_raw_bufs链表上以备后续循环利用。
前面2篇文章:Nginx upstream原理分析【1】-无缓冲模式发送数据
Nginx upstream原理分析【2】-带buffering读取upstream数据
近期评论