2016年对于网易杭州研究院(以下简称“杭研”)而言是重要的 – 成立十周年之际,杭研正式推出了网易云。“十年•杭研技术秀”系列文章,由杭研研发团队倾情奉献,为您展示杭研那些有用、有趣的技术实践经验。正是这些宝贵的经验,造就了今天高品质的网易云产品。
引言
基于epoll的Nginx背靠异常高效的异步事件通知机制,其并发处理能力独步武林,在许多业务场景下,开发者都会把Nginx当做一个优先考虑的Web引擎,通过打造符合业务需求的第三方模块,即可应对非常极端的业务访问量。在实际的生产环境中,模块通常还需要和上游的后端服务器通信,获取数据以构造最终的响应,此时就不得不提Nginx的subrequest机制 – 如果能够善用subrequest机制,不仅能够减小对于Nginx的侵入性,同时还能使开发者专注于业务逻辑。目前网上关于subrequest机制的博文寥寥无几,而且大多晦涩难懂,甚至已经过时。出于业务的需要,本文作者不得不从源码层面去把握subrequest,并在实际项目中有所体会。在这里通过更加形象的图说方式将自己的心得分享给大家,希望能帮助大家快速上手subrequest机制,也欢迎大家一起来交流。
业务场景
一种常见的业务场景是:一个客户C的请求到达Nginx,被业务模块捕捉到,业务模块需要同时向多个上游的服务器(S1至Sn)请求数据,在拿到所有这些数据之后,再在Nginx本地进行聚合,最后才将生成的响应返回给客户C。
为了简化分析的难度,这里取n等于3,即假设上游服务器的数量为3台。
数据结构
核心数据结构为两条链表,这两条链表在每个请求对象中均有挂载。第一条链表为posted_requests链表,该链表中的元素类型为ngx_http_posted_request_t,通过该链表,Nginx能够控制子请求按照先后创建的顺序来按序执行;第二条链表为postponed链表,该链表中的元素类型为ngx_http_postponed_request_t,通过该链表,辅助以关键变量c->data,Nginx能够控制子请求按序输出响应。下面将本节涉及到的概念进行图形化:
执行序列
在主请求的content handler部分,连续调用下面的接口来创建三个子请求:
ngx_int_t ngx_http_subrequest(ngx_http_request_t *r, ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **sr, ngx_http_post_subrequest_t *psr, ngx_uint_t flags);
本文需要对子请求的响应做进一步的聚合处理,并不希望子请求的响应直接转发给客户,为了达到这个目的,本文指定flags参数为NGX_HTTP_SUBREQUEST_IN_MEMORY。
三个子请求成功创建后的状态如下图所示:
此时主请求的PR链表中挂载了3个三角形,分别指向先后创建的子请求1-3,另一方面,PP链表中也挂载了3个菱形,分别指向子请求1-3,子请求1则被打上了红色边框,表明其拥有向客户C发送响应的权利,子请求创建完成之后,Nginx会扫描PR链表,将子请求逐个拉出来执行,子请求1向S1发送http请求之后异步返回,接着轮到子请求2向S2发送http请求,子请求3向S3发送http请求,此时的状态如下图所示:
此时PR链表已经被清空,Nginx异步处理其它请求的同时,也在等待子请求的响应返回,Nginx能够保证子请求按序执行,但是却不能保证子请求按序返回,如果本地服务器到S1的往返延时最高,那么子请求1虽然第一个执行,但却很可能是最后一个返回的,这就是subrequest乱序返回的现象。
假设子请求2最先返回,那么会触发业务模块为其注册的子请求回调接口,此时可以对响应做初步的处理,比如复制到指定的内存中,并且注册主请求的写事件回调接口,回调结束后Nginx会试图去关闭子请求2,但是会发现子请求2并没有发送响应的权利,此时发送数据的权利被红色边框的子请求1占据着,此时Nginx无法释放子请求2,只能将主请求挂载到PR链表中,让主请求获得执行的机会,状态如下:
接下来Nginx会继续扫描PR链表,将主请求拉出来执行,执行其写事件回调接口,而这个接口是业务模块定制的,在这个定制的接口内部,开发者需要判断子请求是否已经完全结束,如果没有结束直接返回继续等待其它子请求。此时PR链表再次被清空。
接下来子请求3返回,其执行序列与子请求2类似,都是乱序返回,并没有发送响应的权利,Nginx会继续等待子请求1,直到子请求1返回,子请求1返回之后,在其回调接口内部处理响应,并且设置主请求的写事件回调接口,Nginx同样会试图关闭该子请求,发现子请求1有红色边框,也就是有发送响应的权利,此时会将子请求1从PP链表中剔除掉,并且将红色边框让位给主请求,最后将主请求挂载到PR链表中,此时的状态如下图所示:
此时红色边框已经被主请求占据,Nginx继续扫描PR链表,将主请求拉出来执行,在主请求的写事件回调接口内部,开发者需要调用接口,将红色边框让位给子请求2,并且将子请求2挂载到PR链表中,让其得到执行机会,此时的状态如下所示:
Nginx开始扫描PR链表,将其中的子请求2拉出来执行,此时子请求的回调接口会被再次触发,这就要求开发者需要在子请求回调接口中避免对响应的重复处理,开一个变量来打标记即可,一旦发现回调接口被触发过,则挂载主请求的写时间回调接口,直接返回,接下来Nginx会发现子请求2拥有红色边框,具有发送响应的权利,会和子请求1的处理类似,将子请求2从PP链表中剔除出去,释放子请求2,并将红色边框再次让位给主请求,最后将主请求挂载到PR链表中,此时的状态如下图所示:
按照惯例,Nginx扫描PR链表将主请求拉出来执行,主请求会将红色边框下放给子请求3,并且将子请求3挂载到PR链表中,此时状态如下图所示:
最后子请求3被Nginx从PR链表中拉出来执行,发现自己占据红色边框,能够向客户C发送响应,则将自己从PP链表中剔除,并且将红色边框还给主请求,释放自己,最后将主请求挂载到PR链表中,让主请求获取执行机会,此时状态如下图所示:
终点来临,Nginx将主请求拉出来执行,发现主请求下面的PP链表已经空了,所有子请求均已经处理完毕,此时可以对子请求的响应进行聚合处理,并且生成最终的响应,发送给客户C。
注意事项
在注册子请求的时候,将flags参数设置成NGX_HTTP_SUBREQUEST_IN_MEMORY,防止upstream模块直接将子请求响应转发给客户C。
在子请求回调接口内部,要避免对响应进行重复处理,因为乱序返回的情况下,子请求回调接口会被二次调用。
在定制的主请求写事件回调接口内部,要判断子请求是否已经完全释放,只要还有子请求未被释放,则将红色边框下放给乱序返回的子请求,只有PP链表已经清空的情况下,才能做最后的聚合处理,将响应返回给客户C。
在content handler内部,成功注册子请求之后,需要返回NGX_OK给上层调用,防止Nginx减小主请求引用,引发错误。
再多的文字也比不上一个活生生的例子,本文作者已经将subrequest并发使用的案例推送到github上,详见https://github.com/BYBLuShiLiang/concurrent-subrequests-in-Nginx,读者只需要适当修改其中的业务逻辑,即可享受到subrequest所带来的便利。
小结
Subrequest机制不太容易理解,但如果使用得当,就能够减小对Nginx的侵入性,同时让开发者专注于业务逻辑,鉴于目前网上关于subrequest机制的博客过于陈旧,而且不易理解,本文作者基于源码分析以及实际项目应用经验,通过图说的方式,结合具体的案例分析,对subrequest的并发使用原理进行了阐述,希望对读者有所帮助,欢迎一起交流。
——陆世亮
网易杭州研究院云计算平台产品部SRE组成员