《深入剖析NGINX》学习记录

xiaoxiao2021-09-20  98

1.HTTP服务基本特性

  处理静态页面请求;  处理index首页请求  对请求目录进行列表显示;  支持多进程间的负载均衡;  对打开文件描述符进行缓存(提高性能);  对反向代理进行缓存(加速);  支持gzip、ranges、chunked、XSLT、SSI以及图像缩放;  支持SSL、TLS SNI

2.HTTP服务高级特性

    基于名称的虚拟主机;    基于IP的虚拟主机;    支持Keep-alive和pipelined连接;    灵活和方便的配置;    在更新配置和升级执行程序时提供不间断服务;    可自定义客户端访问的日志格式;    带缓存的日志写操作(提高性能);    支持快速的日志文件切换;    支持对3xx-5xx错误代码进行重定向;    URI重写支持正则表达式;    根据客户端地址执行不同的功能;    支持基于客户端IP地址的访问控制;    支持基于HTTP基本认证机制的访问控制;    支持HTTP referer验证;    支持HTTP协议的PUT、DELETE、MKCOL、COPY以及MOVE方法;    支持FLV流和MP4流;    支持限速机制;    支持单客户端的并发控制;    支持Perl脚本嵌入;

3.邮件代理服务特性

    使用外部HTTP认证服务器将用户重定向到IMAP/POP3服务器;    使用外部HTTP认证服务器将用户重定向到内部SMTP服务器;    支持的认证方式;     

            POP3: USER/PASS、APOP、AUTHLOGIN/PLAIN/CRAM-MD5.

            IMAP:sLOGIN、AUTHLOGIN/PLAIN/CRAM-MD5.

            SMTP: AUTHLOGIN/PLAIN/CRAM-MD5.

    支持SSL;    支持STARTTLS和STLS.

4.架构和扩展性

    一个主进程和多个工作进程配合服务的工作模型;    工作进程以非特权用户运行(安全性考虑);    支持的事件机制有:kqueue(FreeBSD 4.1+)、epoll(Line 2.6+)、rt signals(Linux 2.2.19+)、/dev/poll(Solaris 7 11/99+)、event ports(Solaris 10)、select和poll;    支持kqueue的众多特性,包括EV_CLEAR、EV_DISABLE(临时禁止事件)、NOTE_LOWAT、EV_EOF等;    支持异步文件IO(FreeBSD4.3+、Linux2.6.22+);    支持DIRECTIO(FreeBSD 4.4+、Linux 2.4+、Solaris 2.6+、Mac OS X);    支持Accept-filters(FreeBSD 4.1+、NetBSD 5.0+)和TCP_DEFER_ACCEPT(Linux 2.4+);    10000个非活跃HTTP keep-alive连接仅占用约2.5MB内存;

5.已测试过的操作系统和平台

    FreeBSD 3~10/i386、FreeBSD 5~10/amd64;    Linux 2.2~3/i386、Linux 2.6~3/amd64;    Solaris 9/i386、sun4u、Solaris 10/i386、amd64、sun4v;    AIX 7.1/powerpc;    HP-UX 11.31/ia64;    Max OS X/ppc、i386;    Windows XP、Windows Server 2003.

6.由于strace能够提供Nginx执行过程中的这些内部信息,所以在出现一些奇怪现象时,比如Nginx启动失败、响应的文件数据和预期不一致、莫名其妙的Segment action Fault段错误、存在性能瓶颈(利用-T选项跟踪各个函数的消耗时间),利用strace也许能够提供一些相关帮助,最后,要退出strace跟踪,按Ctrl+C即可。

     pstack的使用非常简单,后面跟进程ID即可。比如在无客户端请求的情况下,Nginx阻塞在epoll_wait系统调用处,此时利用pstack查看到的Nginx函数调用堆栈关系。

 7.利用addr2line工具可以将这些函数地址转换回可读的函数名。

8.整体架构

    正常执行起来后的Nginx会有多个进程,最基本的有master_process(即监控进程,也叫主进程)和worker_process(即工作进程),也可能会有Cache相关进程。这些进程之间会相互通信,以传递一些信息(主要是监控进程往工作进程传递)。除了自身进程之间的相互通信,Nginx还凭借强悍的功能模块与外界四通八达,比如通过upstream与后端Web服务器通信、依靠fastcgi与后端应用服务器通信等。一个较为完整的整体框架结构体如图所示:

    

 9.分析Nginx多进程模型的入口为主进程的ngx_master_process_cycle()函数,在该函数做完信号处理设置等之后就会调用一个名为ngx_start_worker_processes()的函数用于fork()产生出子进程(子进程数目通过函数调用的第二个实参指定),子进程作为一个新的实体开始充当工作进程的角色执行ngx_worker_process_cycle()函数,该函数主体为一个无限for(;;)循环,持续不断地处理客户端的服务请求,而主进程继续执行ngx_master_process_cycle()函数,也就是作为监控进程执行主体for(;;)循环,这自然也是一个无限循环,直到进程终止才退出。服务进程基本都是这种写法,所以不用详述。

   

下图表现的很清晰,监控进程和每个工作进程各有一个无限for(;;)循环,以便进程持续的等待和处理自己负责的事务,直到进程退出。

    

10.监控进程

    监控进程的无限for(;;)循环内有一个关键的sigsuspend()函数调用,该函数的调用使得监控进程的大部分时间都处于挂起等待状态,直到监控进程接收到信号为止。当监控进程接收到信号时,信号处理函数ngx_signal_handler()就会被执行。我们知道信号处理函数一般都要求足够简单,所以在该函数内执行的动作主要也就是根据当前信号值对相应的旗标变量做设置,而实际的处理逻辑必须放在主体代码里来进行,所以该for(;;)循环接下来的代码就是判断有哪些旗标变量被设置而需要处理的,比如ngx_reap(有子进程退出?)、ngx_quit或ngx_terminate(进行要退出或终止?值得注意的是,虽然两个旗标都是表示结束Nginx,不过ngx_quit的结束更优雅,它会让Nginx监控进程做一些清理工作且等待子进程也完全清理并退出之后才终止,而ngx_terminate更为粗暴,不过它通过使用SIGKILL信号能保证在一段时间后必定被结束掉)、ngx_reconfigure(重新加载配置)等。当所有信号都处理完时又挂起在函数sigsuspend()调用处继续等待新的信号,如此反复,构成监控进程的主要执行体。

11.工作进程

    工作进程的主要关注点就是与客户端或后端真实服务器(此时Nginx作为中即代理)之间的数据可读/可写等I/O交互事件,而不是进程信号,所以工作进程的阻塞点是在像select()、epoll_wait()等这样的I/O多路复用函数调用处,以等待发生数据可读/可写事件,当然,也可能被新收到的进程信号中断。

12.cache manager process(Cache管理进程)与cache loader process(Cache加载进程)则是与Cache缓存机制相关的进程。它们也是由主进程创建,对应的模型框图如下所示:

    

    Cache进程不处理客户端请求,也就没有监控的I/O事件,而其处理的是超时事件,在ngx_process_events_and_timers()函数内执行的事件处理函数只有ngx_event_expire_timers()函数。

13.Cache管理进程的任务就是清理超时缓存文件,限制缓存文件总大小,这个过程反反复复,直到Nginx整个进程退出为止。

14.采用socketpair()函数创造一对未命名的UNIX域套接字来进行Linux下具有亲缘关系的进程之间的双向通信是一个非常不错的解决方案。Nginx就是这么做的,先看fork()生成新工作进程的ngx_spawn_process()函数以及相关代码。

代码片段3.4-1,文件名: ngx_process.h typedef struct { ngx_pid_t pid; int status; ngx_socket_t channel[2]; ... } ngx_process_t; ... #define NGX_MAX_PROCESSES 1024 代码片段3.4-2, 文件名: ngx_process.c ngx_process_t ngx_processes[NGX_MAX_PROCESSES]; ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data, char *name, ngx_int_t respawn) { ... if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1) ... pid = fork(); ... }

  15.共享内存是Linux下进程之间进行数据通信的最有效方式之一,而Nginx就为我们提供了统一的操作接口来使用共享内存。

       在Nginx里,一块完整的共享内存以结构体Ngx_shm_zone_t来封装表示,其中包括的字段有共享内存的名称(shm_zone[i].shm_name)、大小(shm_zone[i].shm.size)、标签(shm_zone[i].tag)、分配内存的起始地址(shm_zone[i].shm.addr)以及初始回调函数(shm_zone[i].init)等。

代码片段3.5-1,文件名: ngx_cycle.h typedef struct ngx_shm_zone_s ngx_shm_zone_t; ... struct ngx_shm_zone_s { void *data; ngx_shm_t shm; ngx_shm_zone_init_pt init; void *tag; };

16.共享内存的真正创建是在配置文件全部解析完后,所有代表共享内存的结构体ngx_shm_zone_t变量以链表的形式挂载在全局变量cf->cycle->shared_memory下,Nginx此时遍历该链表并逐个进行实际创建,即分配内存、管理机制(比如锁、slab)初始化等。

17.Nginx互斥锁接口函数

函数含义ngx_shmtx_create()创建ngx_shmtx_destory()销毁ngx_shmtx_trylock()尝试加锁(加锁失败则直接返回,不等待)ngx_shmtx_lock()加锁(持续等待,知道加锁成功)ngx_shmtx_unlock()解锁ngx_shmtx_force_unlock()强制解锁(可对其它进程进行解锁)ngx_shmtx_wakeup()唤醒等待加锁进程(系统支持信号量的情况下才可使用)

18.Nginx的slab机制与Linux的slab机制在基本原理上并没有什么特别大的不同(当然,相比而言,Linux的slab机制要复杂得多),简单来说也就是基于两点:缓存与对齐。缓存意味着预分配,即提前申请好内存并对内存做好划分形成内存池,当我们需要使用一块内存空间时,Nginx就直接从已经申请并划分好的内存池里取出一块合适大小的内存即可,而内存的释放也是把内存返还给Nginx的内存池,而不是操作系统;对齐则意味着内存的申请与分配总是按2的幂次方进行,即内存大小总是为8、16、32、64等,比如,虽然只申请33个字节的内存,但也将获得实际64字节可用大小的内存,这的确存在一些内存浪费,但对于内存性能的提升是显著的,更重要的是把内部碎片也掌握在可控的范围内。

    Nginx的slab机制主要是和共享内存一起使用,前面提到对于共享内存,Nginx在解析完配置文件,把即将使用的共享内存全部以list链表的形式组织在全局变量cf->cycle->shared_memory下之后,就会统一进行实际的内存分配,而Nginx的slab机制要做的就是对这些共享内存进行进一步的内部划分与管理。

19.函数ngx_init_zone_pool()是在共享内存分配号后进行的初始化调用,而该函数内又调用了本节结束骚的重点对象slab的初始化对象ngx_slab_init();此时的情况如图:

   

20.常变量的值与描述

变量名值描述ngx_pagesize4096

系统内存页大小,Linux下一般情况就是4KB

ngx_pagesize_shift12对应ngx_pagesize(4096),即是4096=1<<12;ngx_slab_max_size2048slots分配和pages分配的分割点,大于等于该值则需从pages里分配ngx_slab_exact_size128

正好能用一个uintptr_t类型的位图变量表示的页划分;比如在4KB内存页、32位系统环境下,一个uintptr_t类型的位图变量最多可以对应表示32个划分块的装填,所以要恰好完整地表示一个4KB内存页的每一个划分块状态,必须把这个4KB内存页划分为32块,即每一块大小为:

ngx_slab_exact_size = 4096/32=128

ngx_slab_exact_shift7对应ngx_slab_exact_size(128),即是128=1<<7;pool->min_shift3固定值为3pool->min_size8固定值为8,最小划分块大小,即是1<<pool->min_shift;

  再来看slab机制对page页的管理,初始结构示意图如下所示:

  

  21.Nginx对所有发往其自身的信号进行了统一管理,其封装了一个对应的ngx_signal_t结构体来描述一个信号。

代码片段3.7.1-1,文件名:ngx_process.c typedef struct { int signo; char *signame; char *name; void (*handler)(int signo); } ngx_signal_t;

    其中字段signo也就是对应的信号值,比如SIGHUP、SIGINT等。

    字段signame为信号名,信号值所对应宏的字符串,比如“SIGHUP”。字段name和信号名不一样,名称表明该信号的自定义作用,即Nginx根据自身对该信号的使用功能而设定的一个字符串,比如SIGHUP用于实现"在不终止Nginx服务的情况下更新配置"的功能,所以对应的该字段为"reload"。字段handler,处理信号的回调函数指针,未直接忽略的信号,其处理函数全部为函数ngx_signal_handler()。

22.字符串宏操作

宏定义说明举例#define Conn(x,y) x##y子串x和y连接起来形成新的串

int n = Conn(123,456);

结果为: n=123456;

char *str = Conn("abc", "def");

结果为:str = "abcdef";

#define ToChar(x) #@x给x加上单引号,因此返回是一个const字符,另外,x长度不可超过4

char a = ToChar(a);

结果为:a = 'a';

char a = ToChar(abcd);

结果为:a = 'd';

char a = ToChar(abcde);

结果为:error C2015: too many characters in constant

#define ToString(x) #x给x加上双引号,因此返回是一个字符串

char *str = ToString(abcde);

结果为:str="abcde";

23.对信号进行设置并生效是在fork()函数调用之前进行的,所以工作进程等都能受此作用。当然,一般情况下,我们不会向工作进程等子进程发送控制信息,而主要是向监控进程父进程发送,父进程收到信号做相应处理后,再根据情况看是否要把信号再通知到其他所有子进程。

24.ngx_pool_t结构图

   

25.

void *ngx_palloc(ngx_pool_t *pool, size_t size) void *ngx_pnalloc(ngx_pool_t *pool, size_t size) void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment) void *ngx_pcalloc(ngx_pool_t *pool, size_t size) static void *ngx_palloc_block(ngx_pool_t *pool, size_t size) static void *ngx_palloc_large(ngx_pool_t *pool, size_t size)

26.申请大块内存

27.释放大块内存

   

28.资源释放

  

  来看内存池的释放问题,从代码中不难看出Nginx仅提供对大块内存的释放(通过接口ngx_pfree()),而没有提供对小块内存的释放,这意味着从内存池里分配出去的内存不会再回收到内存池里来,而只有在销毁整个内存池时,所有这些内存才会回收到系统内存里,这里Nginx内存池一个很重要的特点,前面介绍的很多内存池设置于处理也都是基于这个特点。

   Nginx内存池这样设计的原因在于Web Server应用的特殊性,即阶段与时效,对于其处理的业务逻辑分忧明确的阶段,而对每一个阶段又有明确的时效,因此Nginx可针对阶段来分配内存池,针对时效来销毁内存池。比如,当一个阶段(比如request处理)开始(或其过程中)就创建对应所需的内存池,而当这个阶段结束时就销毁其对应的内存池,由于这个阶段有严格的时效性,即在一段时间后,其必定会因正常处理、异常错误或超时等而结束,所以不会出现Nginx长时间占据大量无用内存池的情况。

29.Nginx Hash数据结构的创建过程有点复杂,这从其初始函数ngx_hash_init()就占去200多行可知一二,但这种复杂是源于Nginx对高效率的极致追求。

    

   

  30.基树(Radix tree),是一种基于二进制表示键值的二叉查找树,正是由于其键值的这个特点,所以只有在特定的情况下才会使用,典型的应用场景有文件系统、路由表等。

   

31.配置文件格式结构图

   

32.Nginx利用ngx_command_s数据类型对所有的Nginx配置项进行了统一的描述。

代码片段5.2-2,文件名:ngx_conf_file.h struct ngx_command_s { ngx_str_t name; ngx_uint_t type; char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_uint_t conf; ngx_uint_t offset; void *post; };

    其中字段name指定与其对应的配置项目的名称,字段set指向配置指令处理回调函数,而字段offset指定转换后控制值的存放位置。

33.ngx_conf_parse()函数体代码量不算太多,但是它照样也将配置内容的解析过程分得很清楚,总体来看分成以下三个步骤:

    a.判断当前解析状态。

    b.读取配置标记token。

    c.当读取了合适数量的标记token后对其进行实际的处理,也就是将配置值转换为Nginx内对应控制变量的值。

34.在判断好当前解析状态之后就开始读取配置文件内容,前面已经提到配置文件都是由一个个token组成的,因此接下来就是循环从配置文件里读取token,而ngx_conf_read_token()函数就是用来做这个事情的。

rc = ngx_conf_read_token(cf);

  函数ngx_conf_read_token()对配置文件进行逐个字符扫描并解析出单个的token。当然,该函数并不会频繁的去读取配置文件,它每次将从文件内读取足够多的内容以填满一个大小为NGX_CONF_BUFFER(4096)的缓存区(除了最后一次,即配置文件剩余内容本来就不够了),这个缓存区在函数ngx_conf_parse()内申请并保存引用到变量cf->conf_file->buffer内,函数ngx_conf_read_token()反复使用该缓存区,该缓存区可能有如下一些状态。

   初始状态,即函数ngx_conf_parse()内申请缓存区后的初始状态,如下图所示:

    

   处理过程中的中间状态,有一部分配置内容已经被解析为一个个token并保存起来,而有一部分内容正要被组合成token,还有一部分内容等待处理,如下图所示:

    

    已解析字符和已扫描字符都属于已处理字符,但它们又是不同的:已解析字符表示这些字符已经被作为token额外保存起来了,所以这些字符已经完全没用了;而已扫描字符表示这些字符还未组成一个完整的token,所以它们还不能被丢弃。

    当缓存区里的字符都处理完时,需要继续从打开的配置文件中读取新的内容到缓冲区,此时的临界状态为,如下图所示:

    

      前面图示说过,已解析字符已经没用了,因此我们可以将已扫描但还未组成token的字符移动到缓存区的前面,然后从配置文件内读取内容填满缓存区剩余的空间,情况如下图所示:

     

    如果最后一次读取配置文件内容不够,那么情况如下图所示:

    

  35.下表列出了ngx_conf_parse()函数在解析nginx.conf配置文件时每次调用ngx_conf_read_token()函数后的cf->args里存储的内容是什么(这通过gdb调试Nginx时在ngx_conf_file.c:185处加断点就很容易看到这些信息),这会大大帮助对后续内容的理解。

    cf->args里存储内容实例

次数返回值rccf->args存储内容第1次NGX_OK

(gdb)p(*cf->args)->nelts

$43 = 2

(gdb)p*((ngx_str_t*)((*cf->args)->elts))

$44 = {len = 16, data = 0x80ec0c8 "worker_processes"}

(gdb)p*(ngx_str_t*)((*cf->args)->elts+sizeof(ngx_str_t))

$45 = {len = 1, data = 0x80ec0da "2"}

第2次NGX_OK

(gdb)p(*cf->args)->nelts

$46 = 3

(gdb)p*((ngx_str_t*)((*cf->args)->elts))

$47 = {len = 9, data = 0x80ec0dd "error_log"}

(gdb)p * (ngx_str_t*)((*cf->args)->elts+sizeof(ngx_str_t))

$48 = {len = 14, data = 0x80ec0e8 "logs/error.log"}

(gdb)p*(ngx_str_t*)((*cf->args)->elts + 2 *sizeof(ngx_str_t))

$49 = {len = 5, data = 0x80ec0f8 "debug"}

第3次NGX_CONF_BLOCK_START

(gdb)p(*cf->args)->nelts

$52 = 1

(gdb)p*((ngx_str_t *)((*cf->args)->elts))

$53 = {len = 6, data = 0x80ec11f"events"}

第...次......第6次NGX_CONF_BLOCK_DONE

(gdb)p(*cf->args)->nelts

$58 = 0

第...次......第n次NGX_CONF_BLOCK_START

(gdb)p(*cf->args)->nelts

$74 = 2

(gdb)p*((ngx_str_t*)((*cf->args)->elts))

$75 = {len = 8, data = 0x80f7392 "location"}

(gdb)p*(ngx_str_t*)((*cf->args)->elts+sizeof(ngx_str_t))

$76 = {len = 1, data = 0x80f739c "/"}

第...次......第末次NGX_CONF_FILE_DONE

(gdb)p(*cf->args)->nelts

$65 = 0

36.Nginx的每一个配置指令都对应一个ngx_command_s数据类型变量,记录着该配置指令的解析回调函数、装换值存储位置等,而每一个模块又都把自身所相关的所有指令以数组的形式组织起来,所以函数ngx_conf_handler()首先做的就是查找当前指令所对应的ngx_command_s变量,这通过循环遍历各个模块的指令数组即可。由于Nginx所有模块也是以数组的形式组织起来的,所以在ngx_conf_handler()函数体内我们可以看到有两个for循环的遍历查找。

代码片段5.3-4,文件名:ngx_conf_file.c static ngx_int_t ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last) { ... for (i=0; ngx_modules[i]; i++) { ... cmd = ngx_modules[i]->commands; ... for (/*void */; cmd->name.len; cmd++) { }

37.看一个Nginx配置文件解析的流程图:

   

38.以http配置项的处理为例,我们知道ngx_http_module虽然是核心模块, 但是其配置存储空间还没有实际申请,所以看第384行给conf进行赋值的语句右值是数组元素的地址,由于ngx_http_module模块对应7号数组元素,所以conf指针的当前指向如下图所示:

   

 

83: 代码片段5.4-8,文件名: ngx_http.c 84: { ngx_string("http"), 85: NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, 86: ngx_http_block, 87: 0, 88: 0, 89: NULL }, 90: ... 118: static char * 119: ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 120: { 121: ... 125: ngx_http_conf_ctx_t *ctx; 126: ... 132: ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t)); 133: ... 137: *(ngx_http_conf_ctx_t **) conf = ctx; 128:

   代码第132行申请了内存空间,而第137行通过conf参数间接地把这块内存空间“挂载”在7号数组元素下。经过ngx_http_block()函数的处理,我们能看到的配置信息最基本的组织结构如下图所示:

   

  39.配置继承示例图

    

  40.Nginx不会像Apache或Lighttpd那样在编译时生成so动态库,然后在程序执行时再进行动态加载,Nginx模块源文件会在生成Nginx时就直接被编译到其二进制执行文件中,所以,如果要选用不同的功能模块,必须对Nginx做重新配置和编译。对于功能模块的选择,如果要修改默认值,需要在进行configure时主动指定,比如新增http_flv功能模块(默认是没有这个功能的,各个选项的默认值可以在文件auto/options内看到).

[root@localhost nginx-1.2.0]# ./configure --with-http_flv_module

  执行后,生成的objs/ngx_modules.c源文件内就会包含对ngx_http_flv_module模块的引用,要再去掉http_flv功能模块,则需要重新configure,即不带--with-http_flv_module配置后再编译生成新的Nginx二进制程序。

41.根据模块的主要功能性质,大体可以将它们分为四个类别:

    a.handlers:协同完成客户端请求的处理、产生响应数据,比如ngx_http_rewrite_module模块,用于处理客户端请求的地址重写,ngx_http_static_module模块,负责处理客户端的静态页面请求,ngx_http_log_module模块,负责记录请求访问日志。

    b.filters: 对handlers产生的响应数据做各种过滤处理(即增/删/改),比如模块ngx_http_not_modified_filter_module,对待响应数据进行过滤检测,如果通过时间戳判断出前后两次请求的响应数据没有发生任何实质改变,那么可以直接响应"304 Not Modified"状态标识,让客户端使用本地缓存即可,而原本待发送的响应数据将被清除掉。

    c.upstream:如果存在后端真实服务器,Nginx可利用upstream模块充当反向代理(Reverse Proxy)的角色,对客户端发起的请求只负责进行转发(当然也包括对后端真实服务器响应数据的回转),比如ngx_http_proxy_module就为标准的upstraem模块。

    d.load-balance: 在Nginx充当中间代理角色时,由于后端真实服务器往往多于一个,对于某一次客户单的请求,如何选择对应的后端真实服务器来进行处理,有类似于ngx_http_upstream_ip_hash_module这样的load balance模块来实现不同的负责均衡算法

42.封装Nginx模块的结构体为ngx_module_s,定义如下:

代码片段6-1, 文件名: ngx_conf_file.h struct ngx_module_s { ngx_uint_t ctx_index; //当前模块在同类模块中的序号 ngx_uint_t index; //当前模块在所有模块中的序号 ... ngx_uint_t version; //当前模块版本号 void *ctx; //指向当前模块特有的数据 ngx_command_t *commands; //指向当前模块配置项解析数组 ngx_uint_t type; //模块类型 //以下为模块回调函数,回调时机可根据函数名看出 ngx_int_t (*init_master)(ngx_log_t *log); ... }; 代码片段6-2,文件名:ngx_core.h typedef struct ngx_module_s ngx_module_t;

   结构体ngx_module_s值得关注的几个字段分别为ctx、commands和type,其中commands字段标识当前模块可以解析的配置项目,表示模块类型的type只有5种可能的值,而同一类型模块的ctx指向的数据类型也相同,参见下表:

    type值的不同类型

序号type值ctx指向数据类型1NGX_CORE_MODULEngx_core_module_t2NGX_EVENT_MODULEngx_event_module_t3NGX_CONF_MODULENULL4NGX_HTTP_MODULEngx_http_module_t5NGX_MAIL_MODULEngx_mail_module_t

43.Handler模块

    http的请求的整个处理过程一共被分为11个阶段,每一个阶段对应的处理功能都比较单一,这样能尽量让Nginx模块代码更为内聚。这11个阶段是Nginx处理客户端请求的核心所在。

    请求处理状态机的11个阶段

序号阶段宏名阶段简单描述0NGX_HTTP_POST_READ_PHASE请求头读取完成之后的阶段1NGX_HTTP_SERVER_REWRITE_PHASEServer内请求地址重写阶段2NGX_HTTP_FIND_CONFIG_PHASE配置查找阶段3NGX_HTTP_REWRITE_PHASELocation内请求地址重写阶段4NGX_HTTP_POST_REWRITE_PHASE请求地址重写完成之后的阶段5NGX_HTTP_PREACCESS_PHASE访问权限检查准备阶段6NGX_HTTP_ACCESS_PHASE访问权限检查阶段7NGX_HTTP_POST_ACCESS_PHASE访问权限检查完成之后的阶段8NGX_HTTP_TRY_FILES_PHASE配置项try_files处理阶段9NGX_HTTP_CONTENT_PHASE内容产生阶段10NGX_HTTP_LOG_PHASE日志模块处理阶段

 a.NGX_HTTP_POST_READ_PHASE阶段。

当Nginx成功接收到一个客户端请求后(即函数accept()正确返回对应的套接口描述符,连接建立), 针对该请求所做的第一个实际工作就是读取客户端发过来的请求头内容,如果在这个阶段挂上对应的回调函数, 那么在Nginx读取并解析完客户端请求头内容后(阶段名称里的POST有在...之后的含义),就会执行这些回调函数。

 b.NGX_HTTP_SERVER_REWRITE_PHASE阶段,和第3阶段NGX_HTTP_REWRITE_PHASE都属于地址重写,也都是针对rewrite模块而设定的阶段,前者用于server上下文里的地址重写,而后者用于location上下文里的地址重写。

    NGX_HTTP_SERVER_REWRITE_PHASE阶段在NGX_HTTP_POST_READ_PHASE阶段之后,所以具体的先后顺序如下图所示:

    

  c.NGX_HTTP_FIND_CONFIG_PHASE阶段。

此阶段上不能挂载任何回调函数,因为它们永远也不会被执行,该阶段完成的是Nginx的特定任务, 即进行Location定位。只有把当前请求的对应location找到了,才能从该location上下文中取出 更多精确地用户配置值,做后续的进一步请求处理。

   d.经过上一阶段后,Nginx已经正确定位到当前请求的对应location,于是进入到NGX_HTTP_REWRITE_PHASE阶段进行地址重写,这和第1阶段的地址重写没什么特别。唯一的差别在于,定义在location里的地址重写规则只对被定位到当前location的请求才生效,用编程语言的说法就是,它们各自的作用域不一样。

   e.NGX_HTTP_POST_REWRITE_PHASE阶段。

该阶段是指在进行地址重写之后,当然,根据前面的列表来看,具体是在location请求地址重写阶段之后。 这个阶段不会执行任何回调函数,它本身也是为了完成Nginx的特定任务,即检查当前请求是否做了过多的 内部跳转(比如地址重写、redirect等),我们不能让对一个请求的处理在Nginx内部跳转很多次甚至是死循环 (包括在server上下文或是在location上下文所进行的跳转),毕竟跳转一次,基本所有流程就得重新走一遍,这是非常消耗性能的。

    f.NGX_HTTP_PREACCESS_PHASE、NGX_HTTP_ACCESS_PHASE、NGX_HTTP_POST_ACCESS_PHASE阶段

做访问权限检查的前期、中期、后期工作,其中后期工作是固定的,判断前面访问权限检查的结果 (状态码存放在字段r->access_code内), 如果当前请求没有访问权限,那么直接返回状态403错误,所以这个阶段也无法去挂载额外的回调函数。

    g.NGX_HTTP_TRY_FILES_PHASE阶段

    针对配置项try_files的特定处理阶段

    h.NGX_HTTP_LOG_PHASE阶段

       专门针对日志模块所设定的处理阶段。

   在一般条件下,我们的自定义模块回调函数都挂载在NGX_HTTP_CONTENT_PHASE阶段,毕竟大部分的业务需求都是修改http响应数据,Nginx自身的产生响应内容的模块,像ngx_http_statis_module、ngx_http_random_index_module、ngx_http_index_module、ngx_http_gzip_static_module、ngx_http_dav_module等也都挂载在这个阶段。

44.各个功能模块将其自身的功能函数挂载在cmcf->phases之后,内部的情况如下图所示:

   

45.在函数ngx_http_init_phase_handlers()里对所有这些回调函数进行一次重组,结果如下图所示:

   

46.对http请求进行分阶段处理核心函数ngx_http_core_run_phases

代码片段6.1-2,文件名:ngx_http_core_module.c void ngx_http_core_run_phases(ngx_http_request_t *r) { ngx_int_t rc; ngx_http_phase_handler_t *ph; ngx_http_core_main_conf_t *cmcf; cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); ph = cmcf->phase_engine.handlers; while(ph[r->phase_handler].checker) { rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]); if (rc == NGX_OK) { return; } } }

 47.handler函数各种返回值的含义

序号返回值含义1NGX_OK当前阶段已经被成功处理,必须进入到下一个阶段2NGX_DECLINED当前回调不处理当前情况,进入到下一个回调处理3NGX_AGAIN当前处理所需资源不足,需要等待所依赖事件发生4NGX_DONE当前处理结束,仍需等待进一步事件发生后做处理5NGX_ERROR,NGX_HTTP_...当前回调处理发生错误,需要进入到异常处理流程

48.所有的header过滤功能函数和body过滤功能函数会分别组成各自的两条过滤链,如下图所示:

 

49.Upstream模块与具体的协议无关,其除了支持HTTP以外,还支持包括FASTCGI、SCGI、UWSGI、MEMCACHED等在内的多种协议。Upstream模块的典型应用是反向代理。

50.对于任何一个Upstream模块而言,最核心的实现主要是7个回调函数,Upstream代理模块自然也不例外:

  Upstream实现并注册了7个回调函数如下表所示:

回调指针函数功能Upstream代理模块create_request根据Nginx与后端服务器通信协议(比如HTTP、Memcache),将客户端的HTTP请求信息转换为对应的发送到后端服务器的真实请求。

ngx_http_proxy_create_request()

由于Nginx与后端服务器通信协议也为HTTP,所以直接拷贝客户端的请求头、请求体(如果有)到变量r->upstream->request_bufs内

process_header根据Nginx和后端服务器通信协议,将后端服务器返回的头部信息装换为对客户端响应的HTTP响应头

ngx_http_proxy_process_status_line()

此时后端服务器返回的头部信息已经保存在变量r->upstream->buffer内,将这串字符串解析为HTTP响应头存储到变量r->upstream->headers_in内

input_filter_init根据前面获得的后端服务器返回的头部信息,为进一步处理后端服务器将返回的响应体做初始准备工作

ngx_http_proxy_input_filter_init()

根据已解析的后端服务器返回的头部信息,设置需进一步处理的后端服务器将返回的响应体的长度,该值保存在变量r->upstream->length内

input_filter正式处理后端服务器返回的响应体

ngx_http_proxy_buffered_copy_filter()

本次收到的响应体数据长度为bytes,数据长度存储在r->upstream->buffer内,把它加入到r->upstream->out_bufs响应数据连等待发送给客户端

finalize_request正常结束与后端服务器的交互,比如剩余待取数据长度为0或读到EOF等,之后就会调用该函数。由于Nginx会自动完成与后端服务器交互的清理工作,所以该函数一般仅做下日志,标识响应正常结束

ngx_http_proxy_finalize_request()

记录一条日志,标识正常结束语后端服务器的交互,然后函数返回

reinit_request对交互重新初始化,比如当Nginx发现一台后端服务器出错无法正常完成处理,需要尝试请求另一台后端服务器时就会调用该函数

ngx_http_proxy_reinit_request()

设置初始值,设置回调指针,处理比较简单

abort_request异常结束与后端服务器的交互后就会调用该函数。大部分情况下,该函数仅做下日志,标识响应异常结束

ngx_http_proxy_abort_request()

记录一条日志,标识异常结束与后端服务器的交互,然后函数返回

这5个函数执行的先后次序如下图所示:

  

 要写一个Upstream模块,我们只需要实现上面提到的这7个函数即可。当然,可以看到最主要的也就是create_request、process_header和input_filter这三个回调,它们实现从HTTP协议到Nginx与后端服务器之间交互协议的来回转换。

51.Load-balance模块

  Load-balance模块可以称为辅助模块,与前面介绍的以处理请求/响应数据为目标的三种模块完全不同,它主要为Upstream模块服务,目标明确且单一,即如何从多台后端服务器中选择出一台合适的服务器来处理当前请求。

   要实现一个具体的Load-balance模块,需要实现如下4个回调函数即可,见下表:

  Load-balance模块的4个回调接口

回调指针函数功能round_robin模块IP_hash模块uscf->peer.init_upstream解析配置文件过程中被调用,根据upstream里各个server配置项做初始准备工作,另外的核心工作是设置回调指针us->peer.init。配置文件解析完后就不再被调用

ngx_http_upstream_init_

round_robin()

设置:us->peer.init = ngx_http

_upstream_init_

round_robin_peer;

ngx_http_upstream_init_

ip_hash()

设置:us->peer.init = ngx_http_upstream_init_

ip_hash_peer;

us->peer.init在每一次Nginx准备转发客户端请求到后盾服务器前都会调用该函数,该函数为本次转发选择合适的后端服务器做初始准备工作,另外的核心工作是设置回调指针r->upstream->peer.get和r->upstream->peer.free等

ngx_http_upstream_init_

round_robin_peer()

设置:r->upstream->peer.get = ngx_http_upstream_get_

round_robin_peer;

r->upstream->peer.free = ngx_http_upstream_free_

round_robin_peer;

ngx_http_upstream_init_

ip_hash_peer()

设置:r->upstream->peer.get = ngx_http_upstream_get_

ip_hash_peer;r->upstream->peer.free为空

r->upstream->peer.get在每一次Nginx准备转发客户端请求到后端服务器前都会调用该函数,该函数实现具体的为本次转发悬则合适后端服务器的算法逻辑,即完成选择获取合适后端服务器的功能

ngx_http_upstream_get_

round_robin_peer()

加权选择当前全职最高

(即从各方面综合比较更

有能力处理当前请求)的后端服务器

ngx_http_upstream_get_

IP_hash_peer()

根据IP哈希值选择后端服务器

r->upstream->peer.free在每一次Nginx完成与后端服务器之间的交互后都会调用该函数。如果选择算法有前后依赖性,比如加权选择,那么需要做一些数值更新操作;如果选择算法没有前后依赖性,比如IP哈希,那么该函数可为空。

ngx_http_upstream_free_

round_robin_peer()

更新相关数值,比如rrp->current等

 

 52.Nginx是以事件驱动的,也就是说Nginx内部流程的向前推进基本都是靠各种事件的触发来驱动,否则Nginx将一直阻塞在函数epoll_wait()或sigsuspend()这样的系统调用上。

53.各种I/O事件处理机制

名称特点select标准的I/O复用模型,几乎所有的类UNIX系统上都有提供,但性能相对较差。如果在当前系统平台上找不到更优的I/O事件处理机制,那么Nginx默认编译并使用select复用模型,我们也可以通过使用--with-select_module或--without-select_module配置选项来启用或禁用select复用模型模块的编译poll标准的I/O复用模型,理论上比select复用模型要优。同select复用模型类似,可以通过使用--with-poll_module或--without-poll_module配置选项来启用或禁用poll复用模型模块的编译epoll系统Linux 2.6_上正式提供的性能更优秀的I/O复用模型kqueue在系统FreeBSD 4.1_, OpenBSD2.9_,NetBSD 2.0和MacOS X上特有的性能更优秀的I/O复用模型eventport在系统Solaris10上可用的高性能I/O复用模型/dev/poll在系统Solaris 7 11/99+,HP/UX 11.22+(eventport),IRIX 6.5.15+和Tru64 UNIX 5.1A+上可用的高性能I/O复用模型rtsig实时信号(real time signals)模型,在Linux 2.2.19+系统上可用。可以通过使用--with-rtsig_module配置选项来启用rtsig模块的编译aio异步I/O(Asynchronous Input and Output)模型,通过异步I/O函数,如aio_read、aio_write、aio_cancel、aio_error、aio_fsync、aio_return等实现

54.在Nginx源码里,I/O多路复用模型被封装在一个名为ngx_event_actions_t的结构体里,该结构体包含的字段主要就是回调函数,将各个I/O多路复用模型的功能接口进行统一,参见下表:

   I/O多路复用模型统一接口

ngx_event_actions_t接口说明init

初始化

add将某描述符的某个事件(可读/可写)添加到多路复用监控里del将某描述符的某个事件(可读/可写)从多路复用监控里删除enable启用对某个指定事件的监控disable禁用对某个指定事件的监控add_conn将指定连接关联的描述符加入到多路复用监控里del_conn将指定连接关联的描述符从多路复用监控里删除process_changes监控的事件发生变化,只有kqueue会用到这个接口process_events阻塞等待事件发生,对发生的事件进行逐个处理done回收资源

55.Nginx内对I/O多路复用模型的整体封装

   

56.epoll接口作为poll接口的变体在Linux 内核2.5中被引入。相比于select实现的多路复用I/O模型,epoll模型最大的好处在于它不会随着被监控描述符数目的增长而导致效率急速下降。

    epoll提供了三个系统调用接口,分别如下所示:

#include <sys/epoll.h> int epoll_create(int size);//创建一个epoll的句柄(epoll模型专用的文件描述符), size用来告诉内核监听的描述符数目的最大值,请求内核为存储事件分配空间, 并返回一个描述符(在epoll使用完后,必须调用close()关闭这个描述符,否则可能导致系统描述符被耗尽) int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//用来向内核注册、删除或修改事件 int epoll_wait(int epfd, struct *epoll_event *events, int maxevents, int timeout);//用来等待事件发生。 int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);//epoll_pwait()和函数epoll_wait()的差别在于其 可以通过最后一个参数设置阻塞过程中的信号屏蔽字

57.Nginx关注事件以及对应的回调处理函数变化过程

序号关注事件类型对应的回调函数1读ngx_http_init_request()2写ngx_http_empty_handler()3读ngx_http_process_request_line()4读ngx_http_process_request_headers()5读ngx_http_request_handler()6写ngx_http_request_handler()7写ngx_http_empty_handler()8读ngx_http_keepalive_handler()

58.函数ngx_trylock_accept_mutex()的内部流程

   

59.Nginx在多核平台上针对负载均衡和优化所做的工作,就是提供有worker_cpu_affinity配置指令,利用该指令可以将各个工作进程固定在指定的CPU核上执行。CPU亲和性,简单点说就是让某一段代码/数据尽量在指定的某一个或几个CPU核心上长时间运行/计算的机制。

60.事件超时意味着等待的事件没有在指定的时间内到达,Nginx有必要对这些可能发生超时的事件进行统一管理,并在发生事件超时时做出相应的处理,比如回收资源,返回错误等。

  Nginx把事件封装在一个名为ngx_event_s的结构体内,而该结构体有几个字段与Nginx的超时管理联系紧密。

代码片段7.5-1,文件名: ngx_event.h struct ngx_event_s { ... unsigned timedout:1; //用于标识当前事件是否已经超过,0为没有超时; unsigned timer_set:1;//用于标识当前事件是否已经加入到红黑树管理,需要对其是否超时做监控 ... ngx_rbtree_node_t timer;//属于红黑树节点类型变量,红黑树就是通过该字段来组织所有的超时事件对象。

61.红黑树的初始化函数ngx_event_timer_init()是在ngx_event_process_init()函数内被调用,所以每一个工作进程都会在自身的初始化时建立这颗红黑树,如下图所示:

  

62.通过红黑树,Nginx对那些需要关注其是否超时的事件对象就有了统一的管理,Nginx可以选择在合适的时机对事件计时红黑树管理的事件进行一次超时检测,对于超时了的事件对象进行相应的处理。

   

63.父子请求之间的可变变量值

   

64.主进程通过fork()函数创建子进程,也就是工作进程,它们将全部继承这些已初始化好的监听套接字。在每个工作进程的事件初始化函数ngx_event_process_init()内,对每一个监听套接字创建对应的connection连接对象(为什么不直接用一个event事件对象呢?主要是考虑到可以传递更多信息到函数ngx_event_accept()内,并且这个连接对象虽然没有对应的客户端,但可以与accept()创建的连接套接口统一起来,因为连接套接口对应的是connection连接对象,所以可以简化相关逻辑的代码实现而无需做复杂的判断与区分),并利用该connection的read事件对象(因为在监听套接口上触发的肯定是读事件)。

    可以看到Nginx主进程在创建完工作进程之后并没有关闭这些监听套接口,但主进程却又没有进行accept()客户端连接请求,那么是否会导致一些客户端请求失败呢?答案当然是否定的,虽然主进程也拥有那些监听套接口,并且它也的确能收到客户端的请求,但是主进程并没有监控这些监听套接口上的事件,没有去读取客户端的请求数据。既然主进程没有去读监听套接口上的数据,那么数据就阻塞在那里,等待任意一个工作进程捕获到对应的可读事件后,进而去处理并响应客户端请求。至于主进程为什么保留(不关闭)那些监听套接口,是因为在后续再创建新工作进程(比如某工作进程异常退出,主进程收到SIGCHLD信号)时,还要把这些监听套接口传承过去。

65.创建连接套接口

    当有客户端发起连接请求,监控监听套接口的事件管理机制就会捕获到可读事件,工作进程便执行对应的回调函数ngx_event_accept(),从而开始连接套接口的创建工作。

    函数ngx_event_accept()的整体逻辑都比较简单,但是有两个需要注意的地方。首先是每次调用accept()函数接受客户端请求的次数,默认情况下调用accept()函数一次,即工作进程每次捕获到监听套接口上的可读事件后,只接受一个客户端请求,如果同时收到多个客户端请求,那么除第一个以外的请求需等到再一次触发事件才能被accept()接受。但是如果用户配置有multi_accept on;,那么工作进程每次捕获到监听套接口上的可读事件后,将反复调用accept()函数,即一次接受当前所有到达的客户端连接请求。

代码片段9.2-1,文件名:ngx_event_accept.c void ngx_event_accept(ngx_event_t *ev) { ... ev->available = ecf->multi_accept; ... do { ... s = accept(lc->fd, (struct sockaddr *)sa, &socklen); ... if (s == -1) { ... return; ... } while(ev->available); }

66.如下所示,在读到NGX_AGAIN时,也就是需要的请求数据没有全部到达,将事件对象rev加入到超时管理机制和事件监控机制,以等待后续数据可读事件或超时事件。

代码片段9.2-4,文件名:ngx_http_request.c static ssize_t ngx_http_read_request_header(ngx_http_request_t *r) { ... if (n == NGX_AGAIN) { ... ngx_add_timer(rev, cscf->client_header_timeout); ... if (ngx_handle_read_event(rev, 0) != NGX_OK) {

67.函数ngx_http_init_request(),正式开始对一个客户端服务请求进行处理与响应工作。该函数的主要功能仍然只是做处理准备:建立http连接对象ngx_http_connection_t、http请求对象ngx_http_request_t、找到对应的server配置default_server、大量的初始化赋值操作,最后执行回调函数ngx_http_process_line(),进入到http请求头的处理中。

代码片段9.3-1,文件名:ngx_http_request.c static void ngx_http_init_request(ngx_event_t *rev) { ... hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t)); ... r = ngx_pcalloc(c->pool, sizeof(ngx_http_request_t)); ... addr = port->addrs; addr_conf = &addr[0].conf; ... /* the default server configuration for the address:port */ cscf = addr_conf->default_server; ... rev->handler = ngx_http_process_request_line; ... rev->handler(rev); }

68.函数ngx_http_process_request_line()处理的数据就是客户端发送过来得http请求头中的Request-Line。这个过程可分为三步:读取Request-Line数据、解析Request-Line、存储解析结果并设置相关值。

     a.第一步,读取Request-Line数据。通过函数ngx_http_read_request_header()将数据读到缓存区r->header_in内。由于客户端请求头部数据可能分多次到达,所以缓存区r->header_in内可能还有一些上一次没解析完的头部数据,所以会存在数据的移动等操作。

     b.第二步,解析Request-Line。对读取到的Request-Line数据进行解析的工作实现在函数ngx_http_parse_request_line()内。

     c.第三步,存储解析结果并设置相关值。在Request-Line的解析过程中会有一些赋值操作,但更多的是在成功解析后,ngx_http_request_t对象r内的相关字段值都将被设置,比如uri(/)、method_name(GET)、http_protocol(HTTP/1.0)等。

      Request-Line解析成功,即函数ngx_http_parse_request_line()返回NGX_OK,意味着这初步算是一个合法的http客户端请求。

代码片段9.3-2,文件名: ngx_http_request.c static void ngx_http_process_request_line(ngx_event_t *rev) { ... for (;;) { ... n = ngx_http_read_request_header(t); ... rc = ngx_http_parse_request_line(r, r->header_in); if (rc == NGX_OK) { ... if (ngx_list_init(&r->headers_in.headers, r->pool, 20, sizeof(ngx_table_elt_t)) ... rev->handler = ngx_http_process_request_headers; ngx_http_rpocess_request_headers(rev);

  函数ngx_http_process_request_headers()内的具体实现如下:

代码片段9.3-4,文件名:ngx_http_request.c static void ngx_http_process_request_headers(ngx_event_t *rev) { ... rc = ngx_http_parse_header_line(r, r->header_in, cscf->underscores_in_headers); if (rc == NGX_OK) { ... h = ngx_list_push(&r->headers_in.headers); ... hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash, h->lowcase_key, h->key.len); ... if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {

69.函数ngx_http_header_filter()完成响应头字符串数据的组织工作。该函数申请一个buf缓存块,然后根据最初设置以及经过过滤链的修改后的相关响应头字段值,组织响应头数据以字符串的形式存储在该缓存块内。    

代码片段9.4-1,文件名:ngx_http_header_filter_module.c static ngx_int_t ngx_http_header_filter(ngx_http_request_t *r) { ... ngx_chain_t out; ... out.buf = b; out.next = NULL; return ngx_http_write_filter(r, &out); }

    该缓存块被接入到发送链变量out(注意这是一个局部变量)内,之后进入到函数ngx_http_write_filter()进行"写入"操作,打上引号是因为此处只有在满足某些条件的情况下才会执行实际的数据写出。

代码片段9.4-2,文件名:ngx_http_write_filter_module.c ngx_int_t ngx_http_write_fitler(ngx_http_request_t *r, ngx_chain_t *in) { ... /*avoid the output if there are no last buf, no flush point, *there are the incoming bufs and the size of all bufs * is smaller than "postpone_output" directive */ if (!last && !flush && in && size < (off_t) clcf->postponse_output) { return NGX_OK; }

70.只有一块待发送缓存块的r->out链结构

    

  函数依次返回后到函数ngx_http_static_handler()内继续执行,看一下相关的完整代码:

代码片段9.4-3,文件名:ngx_http_static_module.c static ngx_int_t ngx_http_static_handler(ngx_http_request_t *r) { ... b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); ... b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t)); ... rc = ngx_http_send_header(r); ... b->last_buf = (r == r->main) ? 1: 0; ... b->file->fd = of.fd; b->file->name = path; ... return ngx_http_output_filter(r, &out); }

    有两块待发送缓存块的r->out链结构

    

71.在进行实际的数据写出操作时,关注我们的重点函数:

代码片段9.4-4,文件名:ngx_http_write_filter_module.c chain = c->send_chain(c, r->out, limit);

  回调函数send_chain根据系统环境的不同而指向不同的函数,相关代码如下:

代码片段9.4-5, 文件名:ngx_linux_sendfile_chain.c ngx_chain_t * ngx_linux_sendfile_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit) { ... for (;;) { ... for (cl = in; cl && send < limit; cl = cl->next) { if (file) { ... rc = sendfile(c->fd, file->file->fd, &offset, file_size); ... } else { rc = writev(c->fd, header.elts, header.nelts);

    客户端需要的数据都发送出去了,那么剩下的工作也就是进行连接关闭和一些连接相关资源的清理。

72.在进一步描述http连接关闭流程之前,有必要先介绍一下Nginx的子请求(sub request)概念,因为它的出现导致了http连接关闭流程的复杂化。所谓子请求,并不是由客户端直接发起的,它是由于Nginx在处理客户端的请求时,根据自身逻辑而内建的心情求,如下图所示:

   

  子请求几乎具有主请求的所有特征(比如有对应完整的ngx_http_request_t结构体对象),并且子请求本身也可以发起新的子请求,即这是一个可以嵌套的概念。在默认情况下,在读一个客户端请求(即主请求)的处理过程中,可以发起的总子请求数目(即包括子请求、孙子请求等)大约为200个(由宏NGX_HTTP_MAX_SUBREQUESTS限定),这在一般情况下,已经足够了。

 73.根据发起子请求的特征,即子请求可以递归发起子请求(树结构)以及同一个子请求可以发起多个子请求(链表结构),按照树加链表的形式对它们进行组织是自然而然的事情。而在结构体ngx_http_request_t内提供了两个与此相对应的字段。

代码片段9.5-2,文件名:ngx_http_request.h typedef struct ngx_http_postponed_request_s ngx_http_postponed_request_t; struct ngx_http_postponed_request_s { ngx_http_request_t *request; ngx_chain_t *out; ngx_http_postponed_request_t *next; }; ... struct ngx_http_request_s { ... ngx_http_request_t *parent; ngx_http_postponed_request_t *postponed;

74.在开始新的子请求、内部跳转、命名location跳转、开始upstream请求等多种情况下,都可能导致主请求request对象的引用计数count自增1,这意味着在对应的操作完成(比如内部跳转处理结束)之前不能释放主请求对象和连接对象。之所以需要做这样的设计,原因仍然在于Nginx是通过事件触发来向前推进的,资源相互关联的各个请求对象在执行过程中谁先谁后不可预知,虽然在其他大部分地方不需要同步而各自自由前进,但在结束点上做资源释放却需要同步,否则导致的结果就可想而知了。

    在HTTP 1.0协议里,客户端通过发送Connection:Keep-Alive的请求头来实现与服务器之间的keepalive;而在HTTP 1.1协议里,由于标准要求连接默认被保持,所以此时请求头Connection:Keep-Alive也不再有意义,但通过请求头Connection:Close可明确要求不进行keepalive连接保持,在Nginx内的具体判断,首先是获取Connection请求头并设置connection_type变量。

代码片段9.6.1-1,文件名:ngx_http_request.c if (ngx_strcasestrn(h->value.data, "close", 5 - 1)) { r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE; } else if (ngx_strcasestrn(h->value.data, "keep-alive", 10 - 1)) { r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE; }

75.

代码片段9.6.1-7,文件名:ngx_http_request.c static void ngx_http_keepalive_handler(ngx_event_t *rev) { ... if (rev->timeout || c->close) { ngx_http_close_connection(c); return; } ... n = c->recv(c, b->last, size); c->log_error = NGX_ERROR_INFO; if (n == NGX_AGAIN) { if (ngx_handle_read_event(rev, 0) != NGX_OK) { ngx_http_close_connection(c); } return; } ... ngx_http_init_request(rev); }

 下面SO_LINGER选项的全部相关代码:

代码片段9.6.2-2,文件名:ngx_http_request.c static void ngx_http_free_request(ngx_http_request_t *r, ngx_int_t rc) { ... if (r->connection->timedout) { clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); if (clcf->reset_timedout_connection) { linger.l_onoff = 1; linger.l_linger = 0; if (setsockopt(r->connection->fd, SOL_SOCKET, SO_LINGER, (const void *)&linger, sizeof(struct linger)) == -1)

  76.函数ngx_http_core_find_location()的处理流程图

    

77.Nginx+Fastcgi+PHP测试环境

   

  客户端通过HTTP/HTTPS协议对Web服务器端PHP资源的请求被Nginx的Fastcgi模块接手处理(上图中圆圈1),该模块将客户端请求数据的格式转换为FASTCGI协议格式后(图11-1中圆圈2),通过Upstream模块与PHP引擎之间建立的连接,把它们转发送到Php引擎(图11-1中圆圈3).PHP引擎根据转发请求进行处理后,通过同一条连接把数据再传回给Nginx(图11-1中圆圈4),Nginx通过Fastcgi模块将收到的响应数据转换回HTTP/HTTPS协议格式后(图11-1中圆圈5),发回最终的客户端(图11-1中圆圈6)。

78.对于客户端发送的PHP资源请求,Nginx在前期的处理和对HTML资源请求的处理没什么两样,仍然还是创建request请求对象、解析请求头、定位location并开始转动请求处理状态机等,真正出现分叉的地方在状态机进入NGX_HTTP_CONTENT_PHASE阶段后。

    Nginx针对HTML页面请求与PHP页面请求所做的不同处理

    

79.fastcgi模块提出了5个核心功能函数,分别如下:

代码片段11.2-2,文件名:ngx_http_fastcgi_module.c static ngx_int_t ngx_http_fastcgi_handler(ngx_http_request_t *r) { ... if (ngx_http_upstream_create(r) != NGX_OK) { ... u->create_request = ngx_http_fastcgi_create_request; u->reinit_request = ngx_http_fastcgi_reinit_request; u->process_header = ngx_http_fastcgi_process_header; u->abort_request = ngx_http_fastcgi_abort_request; u->finalize_request = ngx_http_fastcgi_finalize_request; ... rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);

    重要的是回调函数create_request()和process_header(),它们分别表示向后端服务器发起请求和从后端服务器接收数据,这一去一来无非是Nginx与后端服务器交互的核心。

80.Nginx服务客户端的请求体的函数调用流程

    

   从客户端读取的请求体存放在r->request_body内,受一些用户设置的影响,请求体数据可能放置在一块或多块内存缓存区,或者是某个临时文件内,但只要请求体大于一定的值(可通过指令client_body_buffer_size设置,默认情况下是2页内存,一般也就是8KB),则都会主动写到临时文件,这实现在函数ngx_http_write_request_body()内,注意它一次可以写入多个buf块。

81.Nginx传递发送给PHP引擎的请求数据

名称长度值长度名称值120QUERY_STRING空143REQUEST_METHODGET120CONTENT_TYPE空140CONTENT_LENGTH空116SCRIPT_NAME/t.php

Nginx要把请求数据发送到PHP引擎,首先得建立起Nginx到PHP引擎之间的通信连接,如果用户在配置文件里设置的PHP引擎监听地址是很明确的,即没有带上配置变量,(暂称之为静态变量),那么此时可直接调用函数ngx_http_upstream_connect()发起连接建立请求。

代码片段11.3.1-1,文件名:ngx_htp_upstream.c if (u->resolved == NULL) { uscf = u->conf->upstream; ... if (uscf->peer.init(r, uscf) != NGX_OK) { ... } ngx_http_upstream_connect(r, u); }

82.时刻记住Nginx的主要特性:非阻塞、事件驱动、异步。

83.连接建立准备工作

   

84.连接建立

    

85.接收并处理Fastcgi响应头

    函数ngx_http_upstream_process_header()用于读取后端服务器的响应数据,而在我们这里,Nginx读到的响应数据是FASTCGI协议格式的。

代码片段11.4.1-1,文件名:ngx_http_upstream.c static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u) { ... if (c->read->timeout) { ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT); return; } if (!u->request_sent && ngx_http_upstream_test_connect(c) != NGX_OK) { ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR); return; }

    这处理的是超时情况以及连接已断开的情况,对于这两种情况都将调用函数ngx_http_upstream_next()重新选择后端服务器。

    先来看函数ngx_http_fastcgi_process_header()里的处理逻辑,它循环处理每一条FASTCGI记录。

代码片段11.4.1-3,文件名:ngx_http_fastcgi_module.c static ngx_int_t ngx_http_fastcgi_process_header(ngx_http_request_t *r) { ... for (;;) { if (f->state < ngx_http_fastcgi_st_data) { rc = ngx_http_fastcgi_process_record(r, f); ... if (rc == NGX_AGAIN) { return NGX_AGAIN; } ... if (f->state == ngx_http_fastcgi_st_padding) { return NGX_AGAIN; } ... if (f->type == NGX_HTTP_FASTCGI_STDERR) { ... continue; } ... f->fastcgi_stdout = 1; start = u->buffer.pos; ... for (;;) { ... rc = ngx_http_parse_header_line(r, &u->buffer, 1); ... if (rc == NGX_AGAIN) { break; } if (rc == NGX_OK) { ... break; } if (rc == NGX_HTTP_PARSE_HEADER_DONE) { ... break; } ... return NGX_HTTP_UPSTREAM_INVALID_HEADER; } ... if (rc == NGX_HTTP_PARSE_HEADER_DONE) { return NGX_OK; } if (rc == NGX_OK) { continue; } /* rc == NGX_AGAIN */ ... return NGX_AGAIN; } }

 86.每一条Fastcgi记录在NGINX内对应的结构体为ngx_http_fastcgi_header_t,具体来看其数据分别表示:如下表所示:

响应数据中Fastcgi记录格式

字段名字段值说明

第一条记录:总长度为57+7+8=72字节

Version1取固定值1Type6NGX_HTTP_FASTCGI_STDOUTRequest Id1占两个字节,对应请求序号1Content Length57占两个字节,内容长度Padding Length 7即在该记录末尾处填充了7个0Reserved0取值0

第二条记录:从地址0x930b320开始的8字节,这一条标记结束的记录。

Version1取固定值·1Type3NGX_HTTP_FASTCGI_END_REQUESTRequest Id 1占两个字节,对应请求序号1Content Length8占两个字节,内容长度Padding Length0无填充Reserved 0取值0

  Fastcgi响应头的最后一点处理代码在函数ngx_http_upstream_process_header()内。

代码片段11.4.1-4,文件名:ngx_http_upstream.c static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u) { .. /* rc == NGX_OK */ if (u->headers_in.status_n > NGX_HTTP_SPECIAL_RESPONSE) { ... } if (ngx_http_upstream_process_headers(r, u) != NGX_OK) { return; } if (!r->subrequest_in_memory) { ngx_http_upstream_send_response(r, u); return; }

87.在函数ngx_http_upstream_send_response()内做了很多准备工作,其中最重要的就是给变量u->pipe做初始化处理。先看该变量所对应结构体ngx_event_pipe_t的具体定义。

代码片段11.4.2-2,文件名:ngx_event_pipe.h struct ngx_event_pipe_s { ngx_connection_t *upstream; //表示Nginx与后端服务器之间的连接对象 ngx_connection_t *downstream; //表示Nginx与客户端之间的连接对象 ngx_chain_t *free_raw_bufs; ngx_chain_t *in; ngx_chain_t **last_in; ngx_chain_t *out; ngx_chain_t *free; ngx_chain_t *busy; ... ngx_event_pipe_input_filter_pt input_filter;//Nginx接收到后端服务器的响应数据后所需执行的过滤回调,在这里也就是函数ngx_http_fastcgi_input_filter(),用于对Fastcgi记录做解析 void *input_ctx; ngx_event_pipe_output_filter_pt output_filter; //Nginx发送数据到客户端的过滤函数,这里直接指向ngx_http_output_filter(),即走普通的HTTP响应体过滤链。 void *output_ctx;

89.ngx_event_pipe_write_to_downstream()函数:

代码片段11.4.2-13,文件名:ngx_event_pipe.c static ngx_int_t ngx_event_pipe_write_to_downstream(ngx_event_pipe_t *p) { ... for (;;) { ... if (p->upstream_eof || p->upstream_error || p->upstream_done) { .. if (p->out) { ... rc = p->output_filter(p->output_ctx, p->out); ... p->out = NULL; } if (p->in) { ... rc = p->output_fitler(p->output_Ctx, p->in); ... p->in = NULL; } ... p->downstream_done = 1; break; } ... } return NGX_OK; }

90.Nginx的负载均衡

    

91.加权轮询的流程图

    

92.IP哈希的流程图

    

93.两种策略对比

    显而易见,加权轮询策略的实用性更强,它不依赖与客户端的任何信息,而完全依靠后端服务器的情况来进行选择,优势就是能把客户端请求更合理更均匀地分配到各个后端服务器处理,但其劣势也很明显,同一个客户端的多次请求可能会被分配到不同的后端服务器进行处理,所有无法满足做会话保持的应用的需求。

    与此同时,IP哈希策略能较好地把同一个客户端的多次请求分配到同一台后端服务器处理,所以避免了加权轮询策略无法适用会话保持的需求,但是,因为IP哈希策略是根据客户端的IP地址来对后端服务器做选择,所以如果某个时刻,来自某个IP地址的请求特别多(比如大量用户通过同一个NAT代理发起请求),那么将导致某台后端服务器的压力可能非常大,而其他后端服务器却还很空闲的不均衡情况。

 

转载请注明原文地址: https://www.6miu.com/read-4823638.html

最新回复(0)