memcache是一款高性能的分布式cache系统,得到了非常广泛的应用。memcache定义了一套私有通信协议,使得不能通过HTTP请求来访问memcache。但协议本身简单高效,而且memcache使用广泛,所以大部分现代开发语言和平台都提供了memcache支持,方便开发者使用memcache。 nginx提供了ngx_http_memcached模块,提供从memcache读取数据的功能,而不提供向memcache写数据的功能。作为web服务器,这种设计是可以接受的。 下面,我们开始分析ngx_http_memcached模块,一窥upstream的奥秘。 #### Handler模块?[](http://tengine.taobao.org/book/chapter_05.html#handler "永久链接至标题") 初看memcached模块,大家可能觉得并无特别之处。如果稍微细看,甚至觉得有点像handler模块,当大家看到这段代码以后,必定疑惑为什么会跟handler模块一模一样。 [](http:// "点击提交Issue,反馈你的意见...") clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_memcached_handler; 因为upstream模块使用的就是handler模块的接入方式。同时,upstream模块的指令系统的设计也是遵循handler模块的基本规则:配置该模块才会执行该模块。 [](http:// "点击提交Issue,反馈你的意见...") { ngx_string("memcached_pass"), NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1, ngx_http_memcached_pass, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL } 所以大家觉得眼熟是好事,说明大家对Handler的写法已经很熟悉了。 [](http:// "点击提交Issue,反馈你的意见...") #### Upstream模块![](http://tengine.taobao.org/book/chapter_05.html#id2 "永久链接至标题") 那么,upstream模块的特别之处究竟在哪里呢?答案是就在模块处理函数的实现中。upstream模块的处理函数进行的操作都包含一个固定的流程。在memcached的例子中,可以观察ngx_http_memcached_handler的代码,可以发现,这个固定的操作流程是: 1. 创建upstream数据结构。 [](http:// "点击提交Issue,反馈你的意见...") if (ngx_http_upstream_create(r) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } 2. 设置模块的tag和schema。schema现在只会用于日志,tag会用于buf_chain管理。 [](http:// "点击提交Issue,反馈你的意见...") u = r->upstream; ngx_str_set(&u->schema, "memcached://"); u->output.tag = (ngx_buf_tag_t) &ngx_http_memcached_module; 3. 设置upstream的后端服务器列表数据结构。 [](http:// "点击提交Issue,反馈你的意见...") mlcf = ngx_http_get_module_loc_conf(r, ngx_http_memcached_module); u->conf = &mlcf->upstream; 4. 设置upstream回调函数。在这里列出的代码稍稍调整了代码顺序。 [](http:// "点击提交Issue,反馈你的意见...") u->create_request = ngx_http_memcached_create_request; u->reinit_request = ngx_http_memcached_reinit_request; u->process_header = ngx_http_memcached_process_header; u->abort_request = ngx_http_memcached_abort_request; u->finalize_request = ngx_http_memcached_finalize_request; u->input_filter_init = ngx_http_memcached_filter_init; u->input_filter = ngx_http_memcached_filter; 5. 创建并设置upstream环境数据结构。 [](http:// "点击提交Issue,反馈你的意见...") ctx = ngx_palloc(r->pool, sizeof(ngx_http_memcached_ctx_t)); if (ctx == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } ctx->rest = NGX_HTTP_MEMCACHED_END; ctx->request = r; ngx_http_set_ctx(r, ctx, ngx_http_memcached_module); u->input_filter_ctx = ctx; 6. 完成upstream初始化并进行收尾工作。 [](http:// "点击提交Issue,反馈你的意见...") r->main->count++; ngx_http_upstream_init(r); return NGX_DONE; 任何upstream模块,简单如memcached,复杂如proxy、fastcgi都是如此。不同的upstream模块在这6步中的最大差别会出现在第2、3、4、5上。其中第2、4两步很容易理解,不同的模块设置的标志和使用的回调函数肯定不同。第5步也不难理解,只有第3步是最为晦涩的,不同的模块在取得后端服务器列表时,策略的差异非常大,有如memcached这样简单明了的,也有如proxy那样逻辑复杂的。这个问题先记下来,等把memcached剖析清楚了,再单独讨论。 第6步是一个常态。将count加1,然后返回NGX_DONE。nginx遇到这种情况,虽然会认为当前请求的处理已经结束,但是不会释放请求使用的内存资源,也不会关闭与客户端的连接。之所以需要这样,是因为nginx建立了upstream请求和客户端请求之间一对一的关系,在后续使用ngx_event_pipe将upstream响应发送回客户端时,还要使用到这些保存着客户端信息的数据结构。这部分会在后面的原理篇做具体介绍,这里不再展开。 将upstream请求和客户端请求进行一对一绑定,这个设计有优势也有缺陷。优势就是简化模块开发,可以将精力集中在模块逻辑上,而缺陷同样明显,一对一的设计很多时候都不能满足复杂逻辑的需要。对于这一点,将会在后面的原理篇来阐述。 [](http:// "点击提交Issue,反馈你的意见...") #### 回调函数[](http://tengine.taobao.org/book/chapter_05.html#id3 "永久链接至标题") 前面剖析了memcached模块的骨架,现在开始逐个解决每个回调函数。 1. ngx_http_memcached_create_request:很简单的按照设置的内容生成一个key,接着生成一个“get $key”的请求,放在r->upstream->request_bufs里面。 2. ngx_http_memcached_reinit_request:无需初始化。 3. ngx_http_memcached_abort_request:无需额外操作。 4. ngx_http_memcached_finalize_request:无需额外操作。 5. ngx_http_memcached_process_header:模块的业务重点函数。memcache协议的头部信息被定义为第一行文本,可以找到这段代码证明: [](http:// "点击提交Issue,反馈你的意见...") for (p = u->buffer.pos; p < u->buffer.last; p++) { if ( * p == LF) { goto found; } 如果在已读入缓冲的数据中没有发现LF(‘n’)字符,函数返回NGX_AGAIN,表示头部未完全读入,需要继续读取数据。nginx在收到新的数据以后会再次调用该函数。 nginx处理后端服务器的响应头时只会使用一块缓存,所有数据都在这块缓存中,所以解析头部信息时不需要考虑头部信息跨越多块缓存的情况。而如果头部过大,不能保存在这块缓存中,nginx会返回错误信息给客户端,并记录error log,提示缓存不够大。 process_header的重要职责是将后端服务器返回的状态翻译成返回给客户端的状态。例如,在ngx_http_memcached_process_header中,有这样几段代码: [](http:// "点击提交Issue,反馈你的意见...") r->headers_out.content_length_n = ngx_atoof(len, p - len - 1); u->headers_in.status_n = 200; u->state->status = 200; u->headers_in.status_n = 404; u->state->status = 404; u->state用于计算upstream相关的变量。比如u->state->status将被用于计算变量“upstream_status”的值。u->headers_in将被作为返回给客户端的响应返回状态码。而第一行则是设置返回给客户端的响应的长度。 在这个函数中不能忘记的一件事情是处理完头部信息以后需要将读指针pos后移,否则这段数据也将被复制到返回给客户端的响应的正文中,进而导致正文内容不正确。 [](http:// "点击提交Issue,反馈你的意见...") u->buffer.pos = p + 1; process_header函数完成响应头的正确处理,应该返回NGX_OK。如果返回NGX_AGAIN,表示未读取完整数据,需要从后端服务器继续读取数据。返回NGX_DECLINED无意义,其他任何返回值都被认为是出错状态,nginx将结束upstream请求并返回错误信息。 6. ngx_http_memcached_filter_init:修正从后端服务器收到的内容长度。因为在处理header时没有加上这部分长度。 7. ngx_http_memcached_filter:memcached模块是少有的带有处理正文的回调函数的模块。因为memcached模块需要过滤正文末尾CRLF “END” CRLF,所以实现了自己的filter回调函数。处理正文的实际意义是将从后端服务器收到的正文有效内容封装成ngx_chain_t,并加在u->out_bufs末尾。nginx并不进行数据拷贝,而是建立ngx_buf_t数据结构指向这些数据内存区,然后由ngx_chain_t组织这些buf。这种实现避免了内存大量搬迁,也是nginx高效的奥秘之一。