Nginx笔记之架构介绍

date: 2017.02.16; modification:2017.05.17

目录:

1 概述

1.1 Nginx架构综述

传统基于进程或线程的模型使用单独的进程或线程处理并发连接, 并且会阻塞于网络或I/O 操作. 根据不同的应用, 就内存和CPU而言, 这是非常低效的. 派生进程或线程需要准备新 的运行环境, 包括在内存上分配堆和栈, 生成一个新的运行上下文. 创建这些东西还需要 额外的CPU时间, 而且过度的上下文切换引起的线程抖动最终会导致性能低下.

所有这些复杂性在如Apache web服务器的老架构上一览无遗. 在提供丰富的通用应用功能 和优化服务器资源使用之间需要做一个权衡.

最早的时候, Nginx希望为动态增长的网站获得更好的性能, 并且密集高效的使用服务器资 源, 所以其使用了另外一个模型. 它实际上受到了不断发展的在不同操作系统上开发高级 事件驱动的启发, 最终一个模块化, 事件驱动, 异步, 单线程, 非阻塞的架构成为Nginx代 码的基础.

Nginx大量使用多路复用和事件通知, 并且给不同的进程分配不同的任务. 数量有限的单线 程进程(Worker)高效的循环处理连接. 每个worker进程每秒可以处理数千个并发连接和请 求.

1.2 代码结构

Nginx worker的代码包含核心和功能模块.

核心: 负责维护一个紧凑的事件处理循环, 并且在请求处理的每个阶段执行对应的模 块代码段.

模块: 完成了大部分展现和应用层功能. 包括从网络和存储设备读取, 写入, 转换内 容, 进行输出过滤, SSI(server-side include)处理, 或者在启用代理时转发请求给上层 服务器.

Nginx模块化的架构允许开发者扩展web服务器的功能, 而不需要修改Nginx核心. Nginx模 块可分为:

Nginx在Linux, Solaris和BSD系统上使用kqueue, epoll和event ports等技术, 通过事件 通知机制来处理网络连接和内容获取, 包括接受, 处理和管理连接, 并且大大增强了磁盘 IO性能. 目的在于给操作系统及时异步地获取网络进出流量, 磁盘操作, 套接字读取和写 入, 超时等事件的反馈. Nginx为每个基于Unix的操作系统大量优化了这些多路复用和高 级I/O操作的方法.

Nginx架构图

Nginx架构图

Nginx不为每个连接派生新的进程或线程, 而是由worker进程通过监听共享套接字接受新请求, 并且使用高效的循环来处理数千个连接. Nginx不使用仲裁器或分发器来分发连接, 这个工作由操作系统内核机制完成. 监听套接字在启动时就完成初始化, worker进程在处理HTTP请求和响应的时候持续接受, 读取和输出到套接字.

事件处理循环是Nginx worker代码中最复杂的部分, 它包含复杂的内部调用, 并且严重依赖异步任务处理的思想. 异步操作通过模块化, 事件通知, 大量回调函数以及微调定时器等实现. 总的来说, 基本原则就是尽可能做到非阻塞. Nginx唯一会被阻塞的情形就是没有足够的磁盘存储给worker进程.

由于Nginx的并不会为每个连接产生的进程或线程, 所以内存使用在大多数情况下是很节约并且高效的. 同时由于不用频繁的生成和销毁进程或线程, 所以Nginx也很节省CPU时间. Nginx所做的就是检查网络和存储的状态, 初始化新连接并添加到主循环, 异步处理直到请求结束才从主循环中释放并删除. 兼具精心设计的系统调用和诸如内存池等支持接口的精确实现, Nginx在极端负载的情况下通常能做到中低CPU使用率.

Nginx派生多个worker进程处理连接, 所以能够很好的利用多核CPU. 通常一个单独的worker进程使用一个处理器核, 这样能完全利用多核体系结构, 并且避免线程抖动和死锁. 在一个单线程的worker进程内部不存在资源匮乏, 并且资源控制机制是隔离的. 这个模型也允许在物理存储设备之间进行扩展, 提高磁盘利用率并避免磁盘I/O导致的阻塞. 将工作负载分布到多个worker进程上最终能使服务器资源被更高效的利用.

针对某些磁盘使用和CPU负载的模式, Nginx worker进程数应该进行调整. 这里的规则比较基本, 系统管理员应根据负载多尝试几种配置. 通常推荐: 如果负载模式是CPU密集型, 例如处理大量TCP/IP协议, 使用SSL, 或者压缩数据等, Nginx worker进程应该和CPU核心数相匹配; 如果是磁盘密集型, 例如从存储中提供多种内容服务, 或者是大量的代理服务, worker的进程数应该是1.5到2倍的CPU核心数. 虽然有效性取决于磁盘存储配置的类型, 一些工程师也采用基于独立存储单元的数目来决定worker进程数的方法.

Nginx开发者在下个版本中要解决的一个主要问题是怎么避免磁盘I/O引起的阻塞. 目前,如果没有足够的存储性能为一个worker进程的磁盘操作提供服务, 这个进程就会阻塞在磁盘读写操作上. 一些机制和配置指令用于缓解这个磁盘I/O阻塞的场景, 最显著的是sendfile和AIO指令, 这通常可以降低许多磁盘利用率. 应该根据数据集(data set), 可用内存数, 以及底层存储架构等来规划安装Nginx.

2 架构分析

2.1 Nginx进程角色

Nginx在内存中运行多个进程:

在Nginx 1.x版本, 所有进程都是单线程的, 使用共享内存作为进程间通信机制. Master进程使用root用户权限运行, 其他进程使用非特权用户权限运行.

master进程负责下列工作:

Worker进程:

由于worker进程是web服务器每日操作的实际执行者, 所以对于监控Nginx实例行为, 系统管理员应该保持关注worker进程.

缓存加载进程负责检查磁盘上的缓存数据并且在内存中维护缓存元数据的数据库. 基本上, 缓存加载进程使用特定分配好的目录结构来管理已经存储在磁盘上的文件, 为Nginx提供准备, 它会遍历目录, 检查缓存内容元数据, 当所有数据可用时就更新相关的共享内存项.

缓存管理进程主要负责缓存过期和失效. 它在Nginx正常工作时常驻内存中, 当有异常则由master进程重启.

2.2 一个典型的HTTP请求处理流程

2.2.1 模块化简介

由于Nginx的core模块几乎不做什么实质性的工作, 而是交给模块去做, 所以就有必要在介绍流程之前先简单说说模块类型.

Nginx模块具有的三个角色:

2.3 典型HTTP流程

一个典型的HTTP请求处理流程如下:

  1. 客户端发送HTTP请求.
  2. Nginx core 依据给定的 location(在配置文件中配置)选择合适的handler模块.
  3. 如果配置为反向代理, load-balancer(负载均衡器)选择一个后端服务器.
  4. Handler 处理自己的事情, 并将每个输出缓冲传递给第一个 filter.
  5. 第一个filter传递输出给第二个filter.
  6. 第二个filter传递输出给第三个filter. 以此类推.
  7. 最终响应发送给客户端.

提炼一下就是:

 user请求 -> Nginx core -> hanlder -> filter_1...n -> user
                              ^
                              |
                              v
                         load-banlancer

具体点说就是:

在Worker内部, 生成响应的过程如下:

  1. 开始ngx_worker_process_cycle()
  2. 通过操作系统的机制处理事件(如 epoll 或 kqueue).
  3. 接受事件并调用对应的动作.
  4. 处理或转发请求头和请求体.
  5. 生成响应内容, 并流式发送给客户端.
  6. 完成请求处理.
  7. 重新初始化定时器和事件.

事件循环自身(步骤5和6)确保增量产生响应并且流式发送给客户端.

更详细的处理HTTP请求过程如下:

3 反向代理

上游(upstream)和负载均衡器同样也值得简单介绍一下. 上游用于实现反向代理处理器(proxy_pass处理器). 上游模块组装好请求发送给上游服务器(或称为"后端"), 然后接收上游服务器返回的响应. 这个过程不调用输出过滤器. 上游模块仅仅设置回调函数, 用于当上游服务器可读或可写时调用. 回调函数实现下列功能:

如果上游服务器大于一个, 负载均衡器模块可附加在proxy_pass处理器上, 用于提供选择上游服务器的能力. 负载均衡器注册了一个配置文件指令, 提供附加的上游服务器初始化功能(通过DNS解析上游服务器名字等), 初始化连接结构体, 决定如何路由请求, 并且更新状态信息. 目前, Nginx支持两种标准的上游服务器负载均衡规则: 轮询和ip哈希.

4 模块开发

Nginx模块是高度可定制化的. 它通过一系列指向可执行函数的回调指针来工作. 因而, 带来的副作用就是为第三方开发者加重了负担, 因为他们必须精确的定义模块应怎么运行和何时运行. Nginx的API和开发者文档都经过优化使之更具有可用性来减轻开发难度.

一些在Nginx中插入模块时机的例子:

5 Nginx事件模型

总体结构:

ngx_module_t ngx_events_module // 通过ngx_events_commands -> ngx_events_block()来解析配置
|-- ngx_module_t ngx_event_core_module 
|-- ngx_module_t ngx_epoll_module

ngx_events_block流程:

ngx_events_block
|-- m = cf->cycle->modules[i]->ctx;
|-- m->create_conf(cf->cycle);          // 创建配置结构
|-- ngx_conf_parse();                   // 解析配置
|-- m->init_conf();

6 Nginx socket

ngx_http_core_module
|-- ngx_http_core_commands()
    |-- ngx_http_core_server_name() // 解析server name
    |-- ngx_http_core_listen()      // 解析listen的url, ip地址和端口号.
    |   |-- ngx_http_add_listen()
    |   |   |-- ngx_http_add_address()
    |   |   |   |-- ngx_http_add_server()

在ngx_event_process_init函数(ngx_event_core_moduel的process_init回调函数, 在创建完worker进程后调用) 中将这些监听socket添加到事件循环中.

7 参考资料

http://www.mutouxiaogui.cn/blog/?p=17