深入理解Nginx读书笔记

nginx能帮我们做什么

  1. 一般情况下,一万个非活跃的HTTP Keep-Alive连接在Nginx中仅消耗2.5MB内存,这是Nginx支持高并发连接的基础。

  2. 单机支持10w以上的并发连接。理论上,Nginx支持的并发连接上限取决于内存。

  3. master和work进程分离设计,使得Nginx能够提供热部署功能。

  4. Nginx先天的事件驱动型设计、全异步的网络IO处理机制、极少的进程间切换以及许多优化设计,都使得Nginx天生善于处理高并发压力下的互联网请求。

  5. 必备软件: GCC编译器,可用来编译C语言程序。 PCRE库,用来支持正则表达式。 zlib库,用于对HTTP包的内容做gzip格式的压缩。 OpenSSL开发库,用于支持SSL协议上传输HTTP,想使用MD5,SHA1等也需要使用它。

  6. 内核参数的优化。打开/etc/sysctl.conf。修改后需要sysctl -p来生效。 file-max,表示进程(如一个work进程)可以同时打开的最大句柄数。这个参数直接限制最大并发连接数。tcp_tw.reuse,这个参数设置为1,表示允许将time-wait状态的socket重新用于新的TCP连接。 tcp_keepalive_time,这个参数表示当keepalive启用时,TCP发送keepalive消息的频度。默认为两小时,改小点能更快清理无效连接。 其他的若干参数也应该看看。

  7. 编译安装Nginx。 configure检测操作系统内核及安装软件,参数解析,中间目录生成以及根据参数生成c源码、Makefile文件等。 make命令根据上面的Makefile编译Nginx工程并生成目标文件、最终二进制文件。 make install命令根据configure执行时的参数将Nginx部署到指定目录。

  8. configure命令的参数。 –prefix,Nginx安装部署后的根目录,默认/usr/local/nginx。 —sbin-path,—conf-path,—error-log-path,—pid-path,—lock-path,可执行文件、配置文件、错误日志、pid文件(存放master进程id)存放目录和lock文件的放置目录。默认放在下的位置。 —with-cc,C编译器的路径。 –with-cpp,C预编译器的路径。 –with-ld-opt,用于加入链接时的参数。 –with-cpu-opt,指定CPU处理器架构。 另外还包括事件模块、默认编译、默认不编译、邮件代理相关、其他模块。 —user,指定worker进程运行时所属用户,不要用root,其出问题时master要有权限停止worker进程。

  9. nginx -V 可以查看编译阶段的详细信息。

  10. -s stop 可以强制停止Nginx服务。-s参数其实是告诉Nginx程序向正在运行的Nginx服务发送信号量。

  11. -s quit 可以优雅停止服务。先处理完当前请求再停止。

  12. -s reload 重新加载nginx.conf文件。其会先检测文件格式,并以优雅方式关闭并重启服务。

  13. -s reopen,重新打开日志文件,避免日志文件过大。

  14. 平滑升级Nginx。 需要替换二进制文件的Nginx。它支持不重启来完成新版本平滑升级。 首先通知正在运行的Nginx准备升级,发送USR2信号。这时运行中的Nginx会将pid文件重命名为pid.oldbin。这样新的Nginx才可能启动成功。 启动新版本Nginx。 给旧的master进程发送SIGQUIT信号以优雅关闭。 随后只有新版本Nginx服务运行。

Nginx的配置

  1. 一般使用一个master进程管理多个worker进程。一般worker进程数量和CPU核心数相等。
  2. worker进程很忙,而master进程很闲,他们只负责管理worker进程。当一个worker进程coredump时,master进程会立刻启动新的worker进程继续服务。
  3. 为何worker进程要和CPU数量一致?因为Apache上一个进程同一时刻只处理一个请求,所以就需要开多个进程或线程,大量进程间切换将带来系统资源消耗。 Nginx一个worker进程可以同时处理只受限于内存大小的请求数。而且不同worker进程之间处理并发请求几乎没有同步锁,worker进程通常不会进入睡眠状态。所以当其进程数和CPU核心数相等时(最好每个进程和特定CPU绑定),进程间切换代价最小。 每个worker进程都是单线程的进程。他们会调用各个模块来实现功能。如果这些模块确认不会出现阻塞式调用,进程数和CPU数量应该一致。反之则需要配多一些的worker进程。
  4. 在gdb调试时关闭以守护进程方式运行Nginx。daemon off。这样可以看到终端上的执行信息。
  5. 同上,当关闭以master/worker方式工作,就不会fork出worker进程,而是用master进程来处理请求。master_process off。
  6. error_log logs/error.log error 这儿的error也可以是其他级别,包括debug,info,notice,warn,error,crit,alert,emerg,从左到右依次增大。 如果设定为debug,则编译时需要加入—with-debug参数。
  7. debug_points 是否处理几个特殊的调试点。
  8. debug_connection IP/CIDR,可以对某些特定客户端输出debug级别日志。
  9. Linux系统中,当进程发生错误或收到信号终止时,系统会将进程执行的内存内容(核心映像)写入一个文件(core文件),以作为调试之用,这就是所谓的核心转储(coredumps)。
  10. 通过work_rlimit_core size来限制core文件大小。 working_directory path,coredump文件放置目录。
  11. worker_rlimit_nofile limit,设置一个worker进程可以打开的最大文件句柄数。
  12. 可以绑定worker进程到指定CPU内核,worker_cpu_affinity cpumask。
  13. SSL硬件加速,使用openssl engine -t来查看是否有SSL硬件加速设备。ssl_engin device。
  14. 系统调用gettimeofday的频率,timer_resolution t。 每次内核事件调用如epoll、select、poll、kqueue等返回,都会执行一次gettimeofday,实现用内核的时钟来更新Nginx中的缓存时钟。早期Linux内核中这个代价不小,因为有一次内核态到用户态的内存复制。但是目前一般不必配置。
  15. 是否打开accept锁,accept_mutex on。它是Nginx的负载均衡锁,它可以让多个worker进程轮流的、序列化地与新客户端建立tcp连接。当某个worker进程建立的连接数量达到worker_connections配置的7/8时,会大大减小该worker进程试图建立新tcp的机会。
  16. sendfile,当启用时减少了内核态与用户态之间的两次内存复制,这样就会从磁盘中读取文件后直接在内核态发送到网卡设备,提高了发送文件的效率。
  17. ngx_http_core_module模块提供的变量。$uri,$host等等。
  18. Nginx做反向代理服务器,会先把用户发来的请求完整得缓存到Nginx代理服务器,然后才向后端转发,与squid不同。这样能降低后端服务器的负载。 比如用户上传一个1G的文件,squid马上向上游服务器转发,在接受整个文件过程中,后端始终要维持这个链接,对上游服务器的并发能力提出了挑战。 而Nginx在完整接收文件后才与上游服务器建立连接转发请求,由于是内网速度很快。

如何编写HTTP模块

  1. worker进程会在一个for循环语句中反复调用事件模块检测网络事件。当事件模块检测到某个客户端发起tcp请求时(收到SYN包),将会为他建立tcp连接。成功建立链接后根据nginx.conf文件中的配置交由HTTP框架处理。

配置、error日志和请求上下文

访问第三方服务

  1. 当需要访问第三方服务时,Nginx提供了两种全异步的方式来与第三方服务器通信:Upstream和subrequest。 Upstream可以保证与第三方服务器交互时不会阻塞Nginx进程处理其他请求。因此如果需要访问第三方服务是不能自己简单地用套接字编程实现的,这样会破坏Nginx的全异步架构。
  2. subrequest访问第三方服务最终也是基于Upstream实现的。
  3. subrequest将会为客户请求创建子请求,将一个复杂请求分解为多个子请求,每个子请求负责一种功能。
  4. 当我们希望把第三方服务原封不动返回给用户时,一般使用Upstream。当我们访问第三方服务只是为了获取某些信息,再重新处理后返回给用户时,使用subrequest。
  5. 启动Upstream的流程图。 调用ngx_http_upstream_create方法为请求创建upstream——设置第三方服务器地址——设置upstream的回调方法——调用ngx_http_upstream_init方法启动upstream

开发一个简单的HTTP过滤模块

Nginx提供的高级数据结构

Nginx基础架构

  1. Nginx采用完全的事件驱动架构来处理业务。
  2. 为了避免出现内存碎片、减少向操作系统申请内存的次数,降低各个模块的开发复杂度,Nginx设计了简单的内存池。这个内存池不负责回收内存池中已经分配的内存,有点在于,把多次向系统申请的内存的操作整合成一次,大大减少了CPU的消耗,同时减少了内存碎片。 因此通常每个请求都有个这样建议的独立内存池(每个tcp连接都分配了一个内存池,每个HTTP请求又分配了一个内存池),而在请求结束时则会销毁整个内存池。
  3. worker进程接受的信号 QUIT 优雅关闭进程 TERM/INT 强制关闭进程 USR1 重新打开所有文件 WINCH 目前无实际意义
  4. master进程接受的信号 QUIT 优雅关闭整个服务 TERM/INT 强制关闭整个服务 USR1 重新打开服务中的所有文件 WINCH 所有子进程不在接受处理新的连接,实际相当于向所有子进程发送QUIT信号量 USR2 平滑升级到新版本的Nginx程序 HUP 重新读取配置文件并对新配置项生效 CHLD 有子进程意外 结束,这时需要监控所有的子进程

事件模块

  1. epoll的原理和用法。 假设有100w个用户同时与一个进程保持tcp连接,而同一时刻只有几十个活跃的(接收到tcp包)。 也就是说同一时刻进程只需要处理这些连接。 以前的select和poll事件驱动每次都把这100w个连接告诉操作系统,让操作系统去找出有事件发生的连接。 epoll在Linux内核中申请了一个简易的文件系统,把之前的select或poll调用分成3部分,建立epoll对象,调用epoll_ctl向epoll对象中添加这100w个连接的套接字,调用epoll_wait收集发生事件的连接。
  2. Nginx实现了自己的定时器触发机制。它与内核无关。Nginx使用的时间是缓存在其内存中的,通过nginx.conf中的timer_resolution配置项可以设置更新的最小频率,来保证缓存时间的精度。定时器通过红黑树来实现。
  3. 当多个worker子进程同事监听同一个web端口,当所有子进程都休眠时,一个用户向服务器发起连接,内核在收到SYN包时会激活所有休眠的worker子进程。而只有最开始执行的子进程可以成功建立链接,其余的被唤醒是不必要的。为了解决这个问题,它规定了同一时刻只能有唯一的worker子进程监听web端口。这是通过accept_mutex锁来实现的。
  4. Linux内核提供文件异步IO。
  5. 内核在我们调用listen方法时,为这个监听端口建立了SYN队列和ACCEPT队列。 当客户端使用connect向服务器发起TCP连接,当SYN包达到服务器后,内核会把这信息放到SYN队列(未完成握手队列),同事回一个SYN+ACK包给客户端。 客户端再次发来ACK时,内核会把连接从SYN队列中取出放到ACCEPT队列(已握手队列)。 Nginx建立链接时其实就是直接从ACCEPT队列中取出已经建立好的连接。

HTTP框架的初始化

  1. HTTP请求有11个处理阶段。

HTTP框架的执行流程

  1. Nginx事件框架主要是针对传输层tcp的,作为web服务器HTTP模块需要处理的则是HTTP,HTTP框架必须要针对基于tcp的事件框架解决好HTTP的网络传输、解析、组装等问题。
  2. 事件驱动的开发效率不高,所以HTTP框架就需要为HTTP模块屏蔽事件驱动架构,使得HTTP模块不需要关心网络事件的处理,同时又能灵活介入11个执行阶段。
  3. HTTP框架在动态执行中的大概流程:先与客户端建立tcp连接,接受HTTP请求行、头部并解析出他们的意义,再根据nginx.conf配置文件找到一些HTTP模块,使其依次处理这个请求。同时HTTP框架还提供了接受HTTP包体、发送HTTP响应、派生子请求等工具和方法。
  4. 对于tcp网络事件,可粗略分为可读事件和可写事件,然而可读事件中又可分为收到SYN包带来的新连接事件、收到FIN包带来的连接关闭事件,以及套接字缓冲区上真正收到tcp流。 可写事件虽然相对简单,但Nginx提供限制速度功能,有时可写事件触发时未必可以去发送响应,同时为了精确控制超时,还需要把读写事件放置到定时器中。 这些事件的管理都需要依靠HTTP框架,HTTP模块完全是由HTTP框架设计、定义的。
  5. 每个事件都是由ngx_event_t结构体表示,而tcp连接则由ngx_connection_t结构体表示。HTTP请求是基于tcp连接实现的,每个tcp连接包括一个读事件和一个写事件,他们放在ngx_connection_t中的read成员和write成员,可以把相应的事件添加到epoll中。当满足事件触发条件时,Nginx会调用ngx_event_t事件的handle回调方法执行业务。而通过事件模块提供的ngx_add_timer方法可以将上面的读事件或写事件添加到定时器中,在满足超时条件后,Nginx进程同样会调用nginx_event_t事件的handle回调方法执行业务。
  6. HTTP框架需要完成的第一项工作是集成事件驱动机制,管理用户发起的tcp连接,处理网络读写事件,并在定时器中处理请求超时的事件。 第二项工作是与各个HTTP模块共同处理请求。HTTP框架定义了11个阶段,其中4个基本的阶段只能由HTTP框架处理,其余的7个框架可以让各HTTP模块介入来共同处理请求。 第三项工作为了实现复杂业务,允许将请求分解成多个子请求。 第四项工作是提供基本的工具接口,供各HTTP模块使用,比如接受HTTP包体,发送HTTP响应头部,响应包体等。
  7. 新连接建立时的行为。当Nginx接收到用户发起tcp连接的请求时,事件框架会负责把tcp连接建立起来,如果tcp连接建立起来,HTTP框架就会介入请求的处理了。
  8. 第一次可读事件的处理。当tcp连接上第一次出现可读事件时,会调用ngx_http_init_request方法初始化这个HTTP请求。HTTP框架并不会在连接建立成功后就开始初始化请求,而是在这个连接对应的套接字缓冲区上确实接收到了用户发来的请求时才进行,这样来减少无谓的内存消耗。 读事件被触发,意味着对应套接字缓冲区已经接收到用户请求了,这时需要在用户态的进程空间分配内存,用来把内核缓冲区上的tcp流复制到用户态的内存中,并使用状态机来解析它是否是合法完整的HTTP请求。
  9. 接收HTTP请求行。使用ngx_http_process_request_line方法来接受HTTP请求行。这样的请求行长度不定,意味着在读事件被触发时,内核套接字缓冲区的大小未必足够接受全部HTTP请求行,所以它可能被epoll事件驱动机制多次调度,反复接受tcp流并使用状态机解析它们,直到确认接收到了完整的HTTP请求行才会进入下个阶段接受HTTP头部。
  10. 接收HTTP头部。这阶段是通过ngx_http_process_request_headers方法实现,作为回调方法,也可能被反复多次调用。HTTP头部也属于可变长度的字符串,与HTTP请求行和包体间都是通过换行符来区分的。也需要通过状态机来解析数据。
  11. 处理HTTP请求。使用ngx_http_process_request方法处理请求。 在处理过程中ngx_http_request_t结构体的internal标志位如果为0,则继续执行,为1表示请求当前需要做内部跳转,这时把phase_handler序号设为server_rewrite_index,意味着无论之前执行到哪都会马上重新从NGX_HTTP_SERVER_REWRITE_PHASE阶段开始再次执行。这是Nginx的请求可以反复rewrite重定向的基础。 ngx_http_request_t结构体中的phase_handler成员将决定执行到哪一阶段,以及下一个阶段应当执行哪个HTTP模块实现的内容。
  12. ngx_http_core_generic_phase可以帮助我们较为简单地实现强大的异步无阻塞处理能力。
  13. ngx_http_core_rewrite_phase方法充当了用于重写URL的两个rewrite阶段的checker方法。
  14. ngx_http_core_access_phase方法仅用于ACCESS阶段的处理方法,用于控制用户发起的请求是否合法。
  15. ngx_http_core_content_phase是NGX_HTTP_CONTENT_PHASE阶段的checker方法。它是真正处理请求的内容。
  16. subrequest与post请求。每当派生出子请求时,原始请求的count引用计数加1,真正销毁请求前,通过检查count成员是否为零来确认是否销毁原始请求。
  17. 处理HTTP包体。HTTP框架提供了两种方式处理HTTP包体,都保持了完全无阻塞的事件驱动机制。第一种是把请求的包体接受放到内存或文件中,但是由于内存有限,所以一般都是讲包体放到文件中。第二种是丢弃包体,这个丢弃只是针对HTTP模块而言的,HTTP框架需要接受包体,在接收后丢弃。 HTTP模块在处理请求时,接受包体的同时可能还要处理其他业务,涉及多个动作多次请求,为了避免销毁请求错误导致其他动作出现问题,需要用到引用计数。 HTTP模块中每进行一类新的操作,包括为一个请求添加新的事件,或者把一些已经由定时器、epoll中移除的事件重新加入其中,都需要给这个请求的引用计数加1.这是因为HTTP模块对该请求有独立的异步处理机制,将由该HTTP模块决定这个操作什么时候结束,防止在这个操作还未结束时HTTP框架却把这个请求销毁了。所以就要求每个操作在确认自己动作结束时,都调用到ngx_http_close_request方法,该方法会自动检查引用计数,当引用计数为0时才真正销毁请求。 包体的变长可能导致HTTP框架将tcp连接上的读事件再次添加到epoll和定时器中,表示事件驱动器希望发现tcp连接上接收到全部或部分HTTP包体时,回调响应的方法读取套接字缓冲区的tcp流,这时必须把引用计数加1.同理subrequest和upstream在新连接时也会把引用计数加1,当这类操作结束时,如HTTP包体接受全部的完毕时,务必调用ngx_http_close_request方法把引用计数减1.
  18. 发送HTTP响应。Nginx是一个全异步的事件框架,可能多次调用ngx_http_send_header方法和ngx_http_output_filter方法将应答行、头部、包体发送给客户端。因为响应过大时(tcp的滑动窗口是有限的,一次非阻塞的发送多半是无法发送完整的HTTP响应的),就需要向epoll以及定时器中添加写事件,当连接再次可用时,就调用ngx_http_writer继续发送响应,直到全部发送完毕为止。
  19. 结束HTTP请求。销毁请求很复杂,不小心可能造成错误,因为一个请求销毁时可能有些事件在定时器或者epoll中,当事件回调时,请求不在了造成严重的内存越界错误。 在HTTP框架中,一个请求分为了很多动作,如果这个请求再次派生出新的请求如upstream或subrequest,会把他们当做新的独立的动作。所以每个动作对于整个请求来说都是独立的。HTTP框架希望每个动作结束时仅维护自己的业务,不去关心这个请求是否还做了其他动作。用来降低复杂度。这一套是通过引用计数来实现的。

upstream机制的设计与实现

  1. Nginx访问上游服务器的流程大致可以分为六个阶段:启动upstream机制、连接上游服务器、向上游服务器发送请求、接受上游服务器响应包头、处理接收到的响应包体、结束请求。
  2. 上游和下游,离消费者近的环节属于下游,离消费者远的环节属于上游。upstream属于上游。downsteam表示下游。
  3. 上游服务器提供的协议。他是HTTP模块,所以使用upstream机制的客户端请求必须基于HTTP。 基于事件驱动架构的upstream机制所要访问的就是所有支持tcp的上游服务器。
  4. 每个客户端请求实际上可以向多个上游服务器发起请求。 每个ngx_http_request_t请求只能访问一个上游服务器,但是每个客户端请求可以派生出许多子请求,任何一个子请求都可以访问一个上游服务器。 对于巨大的响应,其还具备了将上游服务器响应即时转发给下游客户端的功能,所以每个ngx_http_request_t只能用来访问一个上游服务器,大大简化了设计。
  5. 反向代理和转发上游服务器的响应。由于下游协议是HTTP,上游协议可以是基于tcp的任何协议,这就需要一个适配的过程。所以upstream机制会将上游响应划分为包头包体两部分,包头必须由HTTP模块实现的process_header方法解析、处理,包体则有upstream不做修改地进行转发。 对于上下游网速差别大的情况。当上下游网速差别不大时,或者下游更快时,会开辟出一块固定大小内存,即接受上游的响应,也用来把保存的响应转发给下游。缺点是当下游速度过慢时,内存写满后无法再 接受上游的响应。必须等缓冲区全部发送完毕后才能继续接受。 当上游网速远快于下游网速时,就必须要开辟足够的内存缓冲区来缓存上游响应。当缓冲区用完时还会把上游响应缓存到磁盘文件中。
  6. 启动upstream。
  7. 与上游服务器建立连接。由于tcp连接需要三次握手,其时间不可控,为了保证建立tcp连接操作不会阻塞进程,使用无阻塞的套接字来连接上游服务器。当方法返回时与上游之间的tcp连接未必建立成功,可能还需要等待上游服务器返回tcp的SYN/ACK包。
  8. 发送请求到上游服务器。多次反复发送。
  9. 接受上游服务器的响应头部。 只要上游服务器提供的应用层协议是基于tcp实现的,那么upstream机制都是适用的。 应用层协议通常都会将请求和响应分为两部分:包头和包体。包头在前包头在后。包头相当于把不同的协议包之间的共同部分抽象出来,不同数据包之间包头都具备相同的格式,服务器必须解析包头。而包体则完全不做格式上的要求。 包头长度要么是固定大小,要么是限制在一个数值以内。而包体长度则非常灵活。 包头和包体存储什么信息完全取决于应用层协议,包头中的信息通常必须包含包体的长度。
  10. 处理包体的三种方式。使用ngx_http_request_t结构体中的subrequest_in_memory标志位做选择,1时不转发响应。0时转发响应。而ngx_http_upstream_conf_t配置结构体中的buffering标志位为0时,则以下游网速优先,使用固定大小的内存作为缓存。如果buffering为1,则上游网速优先,使用更多内存、硬盘文件作为缓存。 不转发请求,是upstream机制最基本的功能,特别是客户端请求派生出的子请求多半不需要转发包体,upstream机制最低目标就是允许HTTP模块以tcp访问上游服务器,这时HTTP模块仅希望解析包头、包体,没有转发上游响应的需求。 转发响应时下游网速优先。当下游快于上游,或差不多时,不需要开辟大块内存来缓存上游响应。 转发响应时上游网速优先。需要开辟内存或磁盘文件缓存上游服务器的响应。
  11. 接受响应头的流程。upstream机制是不涉及应用层协议的,谁用了upstream谁就要负责解析应用层协议,所以必须由HTTP模块实现的process_header方法解析响应包头。当包头接受解析完毕后,ngx_http_upstream_process_header方法还会决定以哪种方式处理包体(如上)。 在接受响应
  12. 不转发响应时的处理流程。当请求属于subrequest子请求,需要在内存中处理上游的数据重新构造后再发往客户端,而不把响应直接转发给客户端。
  13. 当下游网速优先时。其实只需要开辟一块固定长度的内存作为缓冲区。
  14. 以上游网速优先来转发响应。使用更大的缓冲区来缓存那些来不及转发的响应。使用bufs成员指定内存缓冲区大小,最多拥有bufs.num个,每个缓冲区大小固定为bufs.size。
  15. 结束upstream请求。 在启动upstream机制时,ngx_http_upstream_cleanup方法会挂载到请求的cleanup链表中,HTTP框架在请求结束时就会调用ngx_http_upstream_cleanup方法,这样保证了ngx_http_upstream_cleanup方法一定会被调用。而ngx_http_upstream_cleanup方法实际上还是通过调用ngx_http_upstream_finalize_request来结束请求的。 当请求处理流程中出错,往往会调用ngx_http_upstream_next方法,可以重新向这台或另一台上游服务器发起连接、发送请求、接受响应。这功能能帮HTTP模块实现简单的负载均衡机制。结束前会检查ngx_peer_connection_t结构体的tries成员。它会重置为最大重试次数,每当连接出错时都会-1,出错时ngx_http_upstream_next会检查tries,减到0才会真正调用ngx_http_upstream_finalize_request方法来结束请求。

进程间的通信机制

  1. Linux提供了多种进程间传递消息的方式,如共享内存、套接字、管道、消息队列、信号等。 Nginx使用了三种:共享内存、套接字、信号。
  2. 由于每个worker进程都会同时处理千万个请求,所以处理任何一个请求时都不应该阻塞当前进程处理后续的其他请求。所以不能随意使用信号量互斥锁,会导致worker进程进入睡眠状态。 Nginx使用原子操作、信号量和文件锁实现了一套ngx_shmtx_t互斥锁。当系统支持原子操作的ngx_shmtx_t就由原子变量来实现,否则由文件锁来使用。他是可以在共享内存上使用的锁。
  3. 共享内存是Linux下提供的最基本的进程间通信方法,通过mmap或shmget系统调用在内存中创建了一块连续的线性地址空间,而通过munmap或shmdt系统调用能释放这块内存。 使用共享内存时,任何一个进程修改共享内存中的内容后,其他进程访问这段内存时都能得到修改后的内容。
  4. 原子操作。
  5. 自旋锁。基于原子操作,Nginx实现了一个自旋锁。自旋锁是一种非睡眠锁(可对比信号量),也就是说,某进程如果试图得到自旋锁,当发现锁已经被其他进程获得时,那么不会使得当前进程进入睡眠状态,而是始终保持在可执行状态。每当内核调度到这个进程执行时就持续检查是否可以获得锁。在拿不到锁时,这个进程的代码将会一直在自旋锁代码处执行,直到其他进程释放了锁且该进程得到了锁,代码才会继续执行。 自旋锁主要是为多处理器操作系统设置的,它要解决的共享资源保护场景就是进程使用锁的时间非常短(如果锁的使用时间长,会占用大量CPU资源)。 自旋锁对单处理器一样有效,不进入睡眠状态不代表其他执行状态的进程得不到执行。Linux内核中对每个处理器都有一个运行队列,自旋锁可以仅仅调整当前进程在运行队列中的顺序或调整进程的时间陪,都会为当前处理器上其他进程提供被调度的机会,以使得锁被其他进程释放。 用户可以根据锁的使用时间长短角度来选择,当锁的使用时间很短时,使用自旋锁非常合适。 如果使用时间很长,一旦进程拿不到锁就不应该执行任何操作了,则应该使用睡眠锁将资源释放出来给其他进程使用。另外如果想让进程的某一类请求不能继续执行,而epoll上其他请求还是可以执行的,则应该使用非阻塞的互斥锁,而不是自旋锁。
  6. Nginx频道。ngx_channel_t频道是master进程和worker进程之间通信的常用工具,他是使用本机套接字实现的。基于本机tcp套接字可以进行复杂的双工通信。套接字都被设为无阻塞模式,防止执行时阻塞了进程导致其他请求得不到处理。
  7. 信号。Linux提供了以信号传递进程间消息的机制。信号是一种非常短的消息,短到只有一个数字。 信号量与信号完全不同,信号量用于同步代码段,而信号用于传递消息。 一个进程可以向另一个进程或者另一组进程发送信号消息,通知目标仅此执行特定的代码。 Linux定义的前31个信号是最常用的,Nginx则通过重定义一些信号的处理方法来使用信号,如SIGUSR1意味重新打开文件。
  8. 信号量。信号量是用来保证两个或多个代码段不会被并发访问。使用信号量作为互斥锁可能导致进程睡眠。
  9. 文件锁。Linux内核提供了基于文件的互斥锁。
  10. 互斥锁。基于原子操作、信号量和文件锁,Nginx封装了一个互斥锁。

变量

  1. 官方的ngx_http_rewrite_module模块提供了配置文件脚本式语法的执行,允许在配置文件里直接定义全新的变量,由于它不在代码中而是在nginx.conf中定义,所以称其为外部变量。 set $parameter1 “abcd”; set $memchached_key “$uri?$args”;
  2. 使用Nginx预定义的内部变量只需要将使用的变量名作为参数传入,调用ngx_http_get_variable_index方法,获取到这个变量名对应的索引值。其变量名必须是某个Nginx模块定义过的。 使用索引值而不是变量字符串来获取变量值是个好主意,可以加快Nginx的执行速度。 事实上Nginx提供了两种方式找出内部变量,第一种是索引过的变量,可直接由数组下标找到元素。 另一种是添加到散列表的变量,需要将字符串变量名通过散列方法算出散列值,再从散列表中找出元素,遇到元素冲突时需要遍历开散列表的槽位链表。 索引变量更快,但是会占用更多一点的内存。
  3. 在配置文件中使用变量可以提高模块功能的灵活性。建议在使用变量时在变量名前加一个$符号。
  4. 内部变量是指Nginx的代码内部定义的,也就是说是由Nginx模块在c代码中定义的。 当请求到来的时候,Nginx对变量的赋值通常是采用用时赋值的策略,也就是说只有当某个模块尝试取变量的值时才会对变量进行赋值,而不是接受了完整的HTTP头部后就开始解析变量。 对于一个请求而言,首次使用一个变量时才去解析,赋值,缓存变量值,之后就直接取缓存值。这种方式性能高得多。
  5. 5类特殊HTTP变量。 arg_ 前缀,请求的url参数。 http_ 前缀,请求中的HTTP头部。 senthttp 前缀,发送响应中的HTTP头部。 cookie_ cookie头部中的某项。 upstreamhttp 后端服务器HTTP响应头部。
  6. 变量值如果可以被缓存,那么它一定只能缓存在每一个HTTP请求内,不能为不同的HTTP请求缓存同一个值。因此缓存的变量值就在表述一个HTTP请求的ngx_http_request_t结构体中。
  7. 外部变量指在nginx.conf的配置文件里声明的,set $variable value;这种定义的方式很像脚本语言,但它又不是执行到某一行才解释它,而是启动时就将其解释为c程序,等待HTTP请求到来时执行。 外部变量虽然在启动时就被编译成c代码,但是在请求处理过程中才被执行和生效。

slab共享内存

  1. 很多场景下不同的Nginx请求间必须交互才能执行下去,例如限制客户端的并发访问数。 Nginx在共享内存的基础上实现了一套高效的slab内存管理机制。
  2. 操作slab方法里包括加锁和不加锁的分配和释放。一般使用加锁的方法。在调用add方法时,里面有个void *tag参数,用于防止两个不相关的Nginx模块所定义的内存池恰好具有相同名字,造成数据错乱。
  3. 当我们执行-s reload时,Nginx会重新加载配置文件,此时会触发再次初始化slab共享内存池。该过程中tag地址同样将用于区分先后两次的初始化是否对应同一块共享内存。所以tag中应传入全局变量地址,以使得两次设置tag时传入的是相同地址。 如果两次tag不同,即使size相同,也会导致旧的共享内存被释放掉,然后重新分配一块。
  4. Nginx的reload重载配置文件流程,重新解析配置文件意味着所有的模块(包括HTTP模块)都会重新初始化,然后之前正处于使用的共享内存可能是有数据的、可以复用的,如果丢弃了会造成严重错误。所以重读配置文件的过程中,会尽可能使用旧共享内存数据。表现在ngx_shm_zone_init_pt的第二个参数void *data上。首次启动指向null,reload时指向的是上次的ngx_shm_zone_t中的data成员。
  5. 假设我们要对同IP访问URL每十秒最多一次。 首先Nginx有多个work进程,所以需要共享内存来存放用户访问记录。为了高效,可以使用Nginx的红黑树来存放,关键字就是IP+URL字符串,值存放上次访问时间。 为了回收不活跃的记录,将所有节点通过一个链表连接起来,插入顺序按最后一次访问时间组织,这样可以从链表首部插入新访问的记录,从链表尾部取出最后一行记录,从而检查是否需要淘汰出共享内存。
  6. 怎样动态管理内存呢,主要面临两个问题: 时间上,使用者会随机申请分配、释放内存。 空间上,每次申请分配的内存大小也是随机的。 为了尽量避免内存碎片造成的浪费和变慢,常见算法有两个方向:first-fit和best-fit。 若已使用的内存之间有许多不等长的空闲内存,那么分配内存时,first-fit将从头遍历空闲内存块构成的链表,当找到第一块空间大于请求size的内存块时,就把它返回给申请者。 best-fit也会遍历空闲链表,但如果一块空闲内存远大于请求size,为了避免浪费它会继续向后遍历,会寻找最合适的内存块。
  7. Nginx的slab内存分配方式是基于best-fit的。 Nginx有个假设:所有需要使用slab内存的模块请求分配的内存都是比较小的(绝大部分小于4kb)。有了这个假设,就有快速找到合适内存块的方法,主要有五个。 1,把整块内存按4k分成许多页,如果每一页只存放一种固定大小的内存块。由于一页上能分配的内存块数量有限,可以在页首用bitmap的方式,按二进制位表示是否在用,遍历bitmap去寻找空闲内存块,使得消耗时间有限。 2,基于空间换时间,slab内存分配器会把请求分配的内存大小简化为极有限的几种,按2的倍数,将内存块分为8、16、32、64字节等,当申请字节数大于8小于16时,使用16字节的内存块,以此类推,这样会最多造成一倍内存浪费,但使得页种类大大减少了,降低了碎片的产生。 3,让有限的几种页面构成链表,且个链表按序保存在数组中,这样直接寻址就可以很快找到。在slab中,用slots数组来存放链表首页,如申请的内存大小为30字节,根据最小内存块为8字节,可以算出从小到大第3种内存块存放的内存大小为32字节,符合需求,从slots数组中取第三个元素就可以寻找到32字节的页面。 4,这些页面中分为空闲也、半满页、全满页。因为同页面链表不宜包含太多元素,否则遍历链表同样费时。所以全满页应当脱离链表,分配内存时不要访问到它。 5,虽然大部分情况下申请内存块是小于4k的,但极个别大于4k的,可以遍历空闲也链表寻找地址连续的空闲页来分配,如分配11k内存,遍历到3个连续空闲页就可。

← Go代码片段