4.5 请求的上下文

在Nginx中,上下文有很多种含义,然而本节描述的上下文是指HTTP框架为每个HTTP请求所准备的结构体。HTTP框架定义的这个上下文是针对于HTTP请求的,而且一个HTTP请求对应于每一个HTTP模块都可以有一个独立的上下文结构体(并不是一个请求的上下文由所有HTTP模块共用)。

4.5.1 上下文与全异步Web服务器的关系

上下文是什么?简单地讲,就是在一个请求的处理过程中,用类似struct这样的结构体把一些关键的信息都保存下来,这个结构体可以称为请求的上下文。每个HTTP模块都可以有自己的上下文结构体,一般都是在刚开始处理请求时在内存池上分配它,之后当经由epoll、HTTP框架再次调用到HTTP模块的处理方法时,这个HTTP模块可以由请求的上下文结构体中获取信息。请求结束时就会销毁该请求的内存池,自然也就销毁了上下文结构体。以上就是HTTP请求上下文的使用场景,由于1个上下文结构体是仅对1个请求1个模块而言的,所以它是低耦合的。如果这个模块不需要使用上下文,也可以完全不理会HTTP上下文这个概念。

那么,为什么要定义HTTP上下文这个概念呢?因为Nginx是个强大的全异步处理的Web服务器,意味着1个请求并不会在epoll的1次调度中处理完成,甚至可能成千上万次的调度各个HTTP模块后才能完成请求的处理。

怎么理解上面这句话呢?以Apache服务器为例,Apache就像某些高档餐厅,每位客人(HTTP请求)都有1位服务员(一个Apache进程)全程服务,每位服务员只有从头至尾服务完这位客人后,才能去为下一个客人提供服务。因此餐厅的并发处理数量受制于服务员的数量,但服务员的数量也不是越多越好,因为餐厅的固定设施(CPU)是有限的,它的管理成本(Linux内核的进程切换成本)也会随着服务员数量的增加而提高,最终影响服务质量。Nginx则不同,它就像Playfirst公司在2005年发布的休闲游戏《美女餐厅》一样,1位服务员同时处理所有客人的需求。当1位客人进入餐厅后,服务员首先给它安排好桌子并把菜单给客人后就离开了,继续服务于其他客人。当这位客人决定点哪些菜后,就试图去叫服务员过来处理点菜需求,当然,服务员可能正在忙于其他客人,但只要一有空闲就会过来拿菜单并交给厨房,再去服务于其他客人。直到厨房通知这位客人的菜已烹饪完毕,服务员再取来菜主动地传递给客人,请他用餐,之后服务员又去寻找是否有其他客人在等待服务。

可以注意到,当1位客人进入Nginx“餐厅”时,首先是由客人来“激活”Nginx“服务员”的。Nginx“服务员”再次来处理这位客人的请求时,有可能是因为这位客人点完菜后大声地叫Nginx“服务员”,等候她来服务,也有可能是因为厨房做好菜后厨师“激活”了这位客人的服务,也就是说“激活”Nginx“服务员”的对象是不固定的。餐厅的流程是先点菜,再上菜,最后收账单以及撤碗盘,但客人是不想了解这个流程的,所以Nginx“服务员”需要为每位客人建立上下文结构体来表示客人进行到哪个步骤,即他点了哪些菜、目前已经上了哪些菜,这些信息都需要独立的保存。“服务员”不会去记住所有客人的“上下文信息”,因为要同时服务的客人可能很多,只有在服务到某位客人时才会去查对应的“上下文信息”。

上面说的Nginx“服务员”就像Nginx worker进程,客人就是一个个请求,一个Nginx进程同时可以处理百万级别的并发HTTP请求。厨房这些设施可能是网卡、硬盘等硬件。因此,如果我们开发的HTTP模块会多次反复处理同1个请求,那么必须定义上下文结构体来保存处理过程的中间状态,因为谁也不知道下一次是由网卡还是硬盘等服务来激活Nginx进程继续处理这个请求。Nginx框架不会维护这个上下文,只能由这个请求自己保存着上下文结构体。

再把这个例子对应到HTTP框架中。点菜可能是一件非常复杂的事,因为可能涉及凉菜、热菜、汤、甜品等。假如HTTP模块A负责凉菜、HTTP模块B负责热菜、HTTP模块C负责汤。当一位新客人到来后,他招呼着服务员(worker进程)和HTTP框架处理他的点菜需求时(假设他想点2个凉菜、5个热菜、1个汤),HTTP模块A刚处理了1个凉菜,又有其他客人将服务员叫走了,那么,这个客人处必须有一张纸记录着关于凉菜刚点了一个,另一张纸记录着热菜一个没点,由于HTTP模块C知道,当前的餐厅汤已经卖完,业务实在是太简单了(回顾一下第3章的helloword例子),所以不需要再有一张纸记录着汤有没有点。这两张纸只从属于这个客人,对于其他客人没有意义,这就是上面所说的,上下文只是对于一个请求而言。同时,每个HTTP模块都可以拥有记录客人(请求)状态的纸,这张纸就其实就是上下文结构体。当这个客人叫来服务员时,各个HTTP模块可以查看客人身前的两张纸,了解到点了哪些菜,这才可以继续处理下去。

在第3章中的例子中虽然没有使用到上下文,但也完成了许多功能,这是因为第3章中的mytest模块对同1个请求只处理了一次(发送响应包时虽然有许多次调用,但这些调用是由HTTP框架帮助我们完成的,并没有再次回调mytest模块中的方法),它的功能非常简单。在第5章中可以看到,无论是subrequest还是upstream,都必须有上下文结构体来支持异步地访问第三方服务。