前几天在图书馆看书,恰好看到这本《深入剖析nginx》,花了快一周的时间看完了这本书,写点笔记心得便于以后复习。
以前对nginx的认识就只是停留在一个反向代理服务器上。百度了一下nginx也很火,仅次于apache和微软的iis。nginx的主要特点就是占用系统资源少,并发能力强,稳定性好。
第1,2章主要讲了下基本的代码分析的准备工作,介绍了一些便于调试代码的工具,以及在linux环境下运用gdb对其代码进行调试,这里不多描述。
第3章主要介绍了Nginx的进程模型。一般情况下,在启动Nginx后,将会出现多个Nginx进程,各个进程各司其职共同完成对客户端处理响应的任务。从整体架构上来看,有监控进程(也叫做主进程),也有工作进程以及缓存进程。监控进程大部分时间都处于挂起等待状态,直到监控进程收到信号为止,通俗点来说也就是有客户端发送请求。对于nginx的工作进程,此时它充当了客户端与后端服务器之间的代理服务器,它的重心主要就是客户端与后端服务器之间的I数据读写的I/O交互事件,而不是进程信号。关于缓存进程,它不处理客户端的请求,就是它不管数据的读写操作,它只管超时操作。如果开启缓存功能,则会同时开启两个缓存进程,分别是缓存管理进程和缓存加载进程。缓存管理进程的主要任务就是清理超时缓存文件,限制缓存文件的总大小,此后这个过程来回往复,一直到整个进程退出为止。缓存加载进程一般是在nginx正常启动后(一般为60秒)将磁盘中上次缓存的对象加载到内存中,这个过程一般是一次性的,当缓存加载进程完成它的任务以后它就自动退出了。
进程间通信,这里介绍了第一种channel方式的通信,其实channel就是一个元素个数为2的一个数组,里面存放着一对socket描述符,对于继承了父进程的子进程,他们都拥有了这一对socket描述符,而Nginx将channel[0]给父进程使用,channel[1]给子进程使用,利用数组下标的不同来错开的使用不同的描述符,从而来实现父子进程之间的通信。但是子进程并没有向父进程发送任何消息,子进程之间也没有相互进行通信的逻辑,即channel通信目前只作为父进程来给子进程发送消息使用。
进程间通信的第二种方式,也是在linux下最有效的进程通信的方式之一,共享内存。在nginx中,共享内存使用一个结构体来表示,其中封装的有共享内存的名字(主要用作共享内存的唯一标识,便于让nginx知道我们想使用的哪一块共享内存),大小,标签(主要用于解决不同模块使用相同名字的共享内存而引发的冲突,使用标签后,比如模块A和模块B以相同的名称sa去获得模块A所创建的共享内存sa,模块A将获得它之间创建的共享内存sa的引用,而模块B则将获得一个共享内存sa已有他用的错误提示)以及分配内存的起始地址。在Nginx配置解析完成后,所有共享内存结构体将连在一起构成一个全局链表。Nginx此时通过遍历链表来实现实际的分配内存,管理机制初始化(例如锁,slab)等。一个完整的共享内存的初始化主要基于slab高效的访问机制,而关于共享内存的使用,由于是多进程共同使用共享内存,则要考虑到进程间的互斥问题,记得以前在上java课的时候老师讲多线程的时候也讲过锁的机制,运用在这里大概就是强制同一时刻只能有一个进程去访问共享内存。 关于Nginx的slab机制主要是两点:缓存和对齐,缓存意味着提前申请好内存并对内存进行划分形成内存池,当我们需要一块内存空间时,Nginx直接从内存池中取出一块大小合适的内存空间即可,而内存释放也是把内存又重新返还给内存池,而不是操作系统。对齐意味着内存的申请总是遵循2的幂次方,8,16,32,64,如果只申请33个字节的内存,也将获得64字节的内存,虽然存在内存的浪费,但有利于提升性能。Nginx的共享内存与slab机制共同使用,对于共享内存(一个以结构体为节点的全局链表),在初始化完成之后,就由slab机制来对它进行内部的划分以及管理。信号处理部分,通过对signal信号的处理,使得Nginx支持与用户进行信息交互(比如在不终止Nginx服务的情况下更新配置)。
第四章主要讲的是Nginx里用到的稍微复杂一点的数据结构(内存池,哈希)。内存池的实现用到了指针以及链表,链表节点也就是相当于是内存池节点。这里要区别一下大小内存的分配问题,对于小内存的分配,新链表节点的加入都是在链表尾部进行的,通过遍历链表的方式来搜索可以进行分配的内存,对与不可分配的内存池节点,下次再次进行内存分配时可以直接跳过。对于分配大块内存,Nginx里可以调用系统API接口向系统申请内存,一般对于连续的大块内存的分配,会挂载在链表节点的头部,这一点区别于小块内存的分配。Nginx仅提供对大块内存的释放,而没有对小块内存的释放。即从内存池中分配出去的内存不会再回收到内存池中来,而只有在销毁整个内存池时,所有这些内存才会回收到系统中来。Nginx的这种特性取决于它作为webserver的特殊性,即阶段和时效,对于其处理的业务逻辑有明确的阶段,而对于每一个阶段又有明确的时效性,因此Nginx可以针对阶段来分配内存,针对时效来销毁内存池。比如当一个阶段(一个request请求处理)开始(或其过程中)就创建内存池,在一段时间后,必定会因为正常处理,异常错误或超时等而结束,即不会出现Nginx长时间占据大量无用内存池的情况,所以在其阶段过程中回收不用的小块内存自然是不必要的,等到时间一起回收就好了。但是内存池的使用也会带来诸如使valgrind无法正确捕获内存中异常的问题(Valgrind是一款用于内存调试、内存泄漏检测以及性能分析的软件开发工具),当遇到一些奇怪的内存问题无法处理时,可以禁用Nginx的内存池,直接采用mallc/free接口来直接使用系统内存,避免内存池的干扰。Hash,以前数据结构课上了解到主要用于元素的查找,而影响查找效率的因素就是冲突。Nginx解决冲突主要有两种,第一种是采用好的映射函数,这里采用的是两重计算,先计算hash键值key(这是一个字符串)的哈希码hashcode,然后对hashcode做一个对一个按实际hash内存空间大小取模运算就得到其在这个内存空间的具体位置。第二种是选一种合适的空间大小。hash结构创建之后就不可再修改,只供高效查找。
第五章主要讲的是Nginx的配置解析,使用近似于key-value对的形式,这种形式只针对对配置文件静态格式上的描述。Nginx的配置文件可以认为是一种上下文相关,高度可扩展的,有作用域以及可自定义变量等诸多高级语言特性的脚本语言。Nginx的配置文件是由多个配置项组成的,每一个配置项都有一个项目名和对应的项目值,项目名又称为指令,而项目值可能是简单的字符串(以分号结尾),也可能是由简单字符串和多个配置项组合而成配置块的复合结构(以大括号}结尾),我们可以将配置项归纳为两种,简单配置项以及复杂配置项。
1 error log /var/log/nginx.error_log info;
这一条指令不带大括号,所以是一个简单配置项。
1 location ~ \.php${ fastcgi_pass 127.0.0.1:1025; }
这一条指令带大括号,所以是一个复杂配置项。
Nginx配置文件里面的注释信息用#作为开头标记。
第六章模块综述主要介绍了四大模块,Nginx的模块不像apache或者lighttpd在编译时生成so动态库,然后在程序执行时动态加载,而是在生成Nginx时就直接被编译到二进制可执行文件中,即如果要选用不同的功能模块,则需要重新对nginx进行配置和编译。①handlers:协同完成客户端请求的处理、产生响应数据,比如ngx_http_rewrite_moudle模块,用于处理客户端请求的地址重写,ngx_http_static_moudle模块,负责处理客户端的静态页面请求,ngx_http_log_moudle模块,负责记录请求访问日志。在客户端的请求被Nginx接收后,首先做server的查找与定位。 如果直接访问的是一个目录,Nginx先是查看当前目录是否存在index..html/index.htm/index.php等这样的默认页面
第10章是请求定位,对于任何一个客户端的请求,在Nginx内都必须有与之对应的server以及location来匹配,以提供处理该请求的上下文环境,否则Nginx将无法进行正常处理而返回错误。在一般的应用中,Nginx内的server和location会有多个,这一章主要解决如何将客户端的请求正确定位到对应的server和location。即便是同一个server里,Nginx的location一般也会有多个,以便于灵活地处理客户端的各种请求。location可以简单理解成就是我们在客户端向服务端所请求的一个地址,而服务端用location来进行相应的匹配,Nginx一般采用最佳匹配。对于在server内部的不同location,如果没有上下层的嵌套,则在配置文件解析后,以队列形式存在。如果有多个location的层次嵌套,则形成一棵树,树的节点为队列。形成这样的树的好处在于在整个http配置解析完以后,所有的location都成功收集并已根据各自所属server形成多棵不同的树(不同的server的location不会相互干扰,因为对于请求的处理,先定位到具体server,再在这个server之内去查找对应的location)。为一台服务器配置不同的网站也就是开了不同的虚拟主机,Nginx可以通过不同的域名来决定把用户引到哪一台虚拟主机来进行操作。