开发者社区> 问答> 正文

Nginx性能为什么如此吊


Nginx性能为什么如此吊,Nginx性能为什么如此吊,Nginx性能为什么如此吊 (重要的事情说三遍)的性能为什么如此吊!!!



        最近几年,web架构拥抱解耦的理念并且将应用层设施从web服务器中分离。虽然现在仅仅是将原先基于LAMP(Linux, Apache, MySQL, PHP, Python or Perl)所构建的网站,变为基于LEMP(E表示Engine x)的。但是,越来越多的实践是将web服务器推入基础设施的边缘,并且用不同的方法整合这些相同或更新的应用和数据库工具集。
        Nginx很适合做这些工作。他提供了必要的关键功能用于方便将下列功能从应用层剥离到更高效的边缘web服务器层:并发、长连接处理、SSL,静态内容、压缩和缓存、连接和请求限速,以及HTTP媒体流等。Nginx同时也允许直接整合memcached、Redis或者其他的NoSQL解决方案,增强为处理大规模并发用户的性能。
        随着现代编程语言和开发包广泛使用,越来越多的公司改变了应用开发和部署的方式。Nginx已经成为这些改变范例之中的最重要的部件之一,并且已经帮助许多公司在预算内快速启动和开发他们的web服务。
Nginx开发始于2002年,2004年基于2-clause BSD授权正式对外发布。自发布起,Nginx用户就在不断增长,并且贡献提议,提交bug报告、建议和评测报告,这极大的帮助和促进了整个社区的发展。
        Nginx代码完全用C语言从头写成,已经移植到许多体系结构和操作系统,包括:Linux、FreeBSD、Solaris、Mac OS X、AIX以及Microsoft Windows。Nginx有自己的函数库,并且除了zlib、PCRE和OpenSSL之外,标准模块只使用系统C库函数。而且,如果不需要或者考虑到潜在的授权冲突,可以不使用这些第三方库。
       谈谈关于Windows版本nginx。当nignx在Windows环境下工作时,Windows版本的nginx更像是概念验证版本,而不是全功能移植。这是由于目前nginx和Windows内核架构之间交互的某些限制导致。Windows版本ngnix已知的问题包括:低并发连接数、性能降低、不支持缓存和带宽策略。未来Windows版本的nginx的功能会更接近主流版本。


Nginx架构综览
       传统基于进程或线程的模型使用单独的进程或线程处理并发连接,因而会阻塞于网络或I/O操作。根据不同的应用,就内存和CPU而言,这是非常低效的。派生进程或线程需要准备新的运行环境,包括在内存上分配堆和栈、生成一个新的运行上下文。创建这些东西还需要额外的CPU时间,而且过度的上下文切换引起的线程抖动最终会导致性能低下。所有这些复杂性在如Apache web服务器的老架构上一览无遗。在提供丰富的通用应用功能和优化服务器资源使用之间需要做一个权衡。
       最早的时候,nginx希望为动态增长的网站获得更好的性能,并且密集高效的使用服务器资源,所以其使用了另外一个模型。受不断发展的在不同操作系统上开发基于事件模型的技术驱动,最终一个模块化,事件驱动,异步,单线程,非阻塞架构成为nginx代码的基础。
Nginx大量使用多路复用和事件通知,并且给不同的进程分配不同的任务。数量有限的工作进程(Worker)使用高效的单线程循环处理连接。每个worker进程每秒可以处理数千个并发连接、请求。
代码结构
       Nginx worker的代码包含核心和功能模块。核心负责维护一个紧凑的事件处理循环,并且在请求处理的每个阶段执行对应的模块代码段。模块完成了大部分展现和应用层功能。包括从网络和存储设备读取、写入,转换内容,进行输出过滤,SSI(server-side include)处理,或者如果启用代理则转发请求给后端服务器。
nginx模块化的架构允许开发者扩展web服务器的功能,而不需要修改nginx核心。Nginx模块可分为:核心、事件模块,阶段处理器,协议、变量处理器,过滤器,上游和负载均衡器等。目前,nginx不支持动态加载模块,即模块代码是和nginx核心代码一起编译的。模块动态加载和ABI已经计划在将来的某个版本开发。更多关于不同模块角色的详细信息可在14.4章找到。
Nginx在Linux、Solaris和BSD系统上使用kqueue、epoll和event ports等技术,通过事件通知机制来处理网络连接和内容获取,包括接受、处理和管理连接,并且大大增强了磁盘IO性能。目的在于尽可能的提供操作系统建议的手段,用于从网络进出流量,磁盘操作,套接字读取和写入,超时等事件中及时异步地获取反馈。Nginx为每个基于Unix的操作系统大量优化了这些多路复用和高级I/O操作的方法。


工作进程模型
       前面提到过,nginx不为每个连接派生进程或线程,而是由worker进程通过监听共享套接字接受新请求,并且使用高效的循环来处理数千个连接。Nginx不使用仲裁器或分发器来分发连接,这个工作由操作系统内核机制完成。监听套接字在启动时就完成初始化,worker进程通过这些套接字接受、读取请求和输出响应。
事件处理循环是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。
当前的worker模型的另一个问题是对嵌入脚本的支持有限。举例来说,标准的nginx发布版只支持Perl作为嵌入脚本语言。这个原因很简单:嵌入脚本很可能会在任何操作上阻塞或者异常退出,这两个行为都会导致worker进程挂住而同时影响数千个连接。将脚本更简单,更可靠地嵌入nginx,并且更适合广泛应用的工作已经列入计划。
nginx 进程角色
       Nginx在内存中运行多个进程,一个master进程和多个worker进程。同时还有一些特殊用途的进程,例如缓存加载和缓存管理进程。在nginx 1.x版本,所有进程都是单线程的,使用共享内存作为进程间通信机制。Master进程使用root用户权限运行,其他进程使用非特权用户权限运行。
master进程负责下列工作:

  • 读取和校验配置文件
  • 创建、绑定、关闭套接字
  • 启动、终止、维护所配置数目的worker进程
  • 不中断服务刷新配置文件
  • 不中断服务升级程序(启动新程序或在需要时回滚)
  • 重新打开日志文件
  • 编译嵌入Perl脚本

Worker进程接受、处理来自客户端的连接,提供反向代理和过滤功能以及其他nginx所具有的所有功能。由于worker进程是web服务器每日操作的实际执行者,所以对于监控nginx实例行为,系统管理员应该保持关注worker进程。
缓存加载进程负责检查磁盘上的缓存数据并且在内存中维护缓存元数据的数据库。基本上,缓存加载进程使用特定分配好的目录结构来管理已经存储在磁盘上的文件,为nginx提供准备,它会遍历目录,检查缓存内容元数据,当所有数据可用时就更新相关的共享内存项。
缓存管理进程主要负责缓存过期和失效。它在nginx正常工作时常驻内存中,当有异常则由master进程重启。
Nginx缓存简介
Nginx在文件系统上使用分层数据存储实现缓存。缓存主键可配置,并且可使用不同特定请求参数来控制缓存内容。缓存主键和元数据存储在共享内存段中,缓存加载进程、缓存管理进程和worker进程都能访问。目前不支持在内存中缓存文件,但可以用操作系统的虚拟文件系统机制进行优化。每个缓存的响应存储到文件系统上的不同文件,Nginx配置指令控制存储的层级(分几级和命名方式)。如果响应需要缓存到缓存目录,就从URL的MD5哈希值中获取缓存的路径和文件名。
将响应内容缓存到磁盘的过程如下:当nginx从后端服务器读取响应时,响应内容先写到缓存目录之外的一个临时文件。nginx完成请求处理后,就将这个临时文件重命名并移到缓存目录。如果用于代理功能的临时目录位于另外一个文件系统,则临时文件会被拷贝一次,所以建议将临时目录和缓存目录放到同一个文件系统上。如果需要清除缓存目录,也可以很安全的删除文件。一些第三方扩展可以远程控制缓存内容,而且整合这些功能到主发布版的工作已经列入计划。


Nginx配置文件
Nginx配置系统来自于Igor Sysoev使用Apache的经验。他认为可扩展的配置系统是web服务器的基础。当维护庞大复杂的包括大量的虚拟服务器、目录、位置和数据集等配置时,会遇到可伸缩性问题。对于一个相对大点的网站,系统管理员如果没有在应用层进行恰当的配置,那么这将会是一个噩梦。
所以,nginx配置为简化日常维护而设计,并且提供了简单的手段用于web服务器将来的扩展。
配置文件是一些文本文件,通常位于/usr/local/etc/nginx或/etc/nginx。主配置文件通常命名为nginx.conf。为了保持整洁,部分配置可以放到单独的文件中,再自动地被包含到主配置文件。但应该注意的是,nginx目前不支持Apache风格的分布式配置文件(如.htaccess文件),所有和nginx行为相关的配置都应该位于一个集中的配置文件目录中。
Master进程启动时读取和校验这些配置文件。由于worker进程是从master进程派生的,所以可以使用一份编译好、只读的配置信息。配置信息结构通过常见的虚拟内存管理机制自动共享。
Nginx配置具有多个不同的上下文,如:main, http, server, upstream, location (以及用于邮件代理的 mail ) 等指令块。这些上下文不重叠,例如,一个location 指令块是不能放入main指令块中。并且,为了避免不必要的歧义,不存在一个类似于“全局web服务器”的配置。Nginx配置特意做的整洁和富有逻辑性,允许用户可以建立包含上千个指令的复杂的配置文件。在一次私人谈话中,Sysoev说:“全局服务器配置中的位置、目录和其他一些指令是Apache中我所不喜欢的特性,所以这就是不在nginx实现这些的原因。”
配置语法、格式和定义遵循一个所谓的C风格协定。这种构建配置文件的方法以及在开源软件和商业软件中广泛的应用。通过设计,C风格配置很适合嵌套描述,富有逻辑性,易于创建、读取和维护,深受广大工程师喜欢。同时nginx的C风格配置也易于自动化。
虽然一些nginx配置指令看起来像Apahce配置的一部分,但是设置一个nginx实例是完全不同的体验。例如,虽然nginx支持重写规则,但是系统管理员要手工的转换Apache重写配置使之适合nginx风格。同样,重写引擎的实现也是不一样的。
通常来说,nginx设置也提供了几种原始机制的支持,对于高效的web服务器配置很有帮助。有必要简单了解下变量和try_files指令,这些差不多是nginx所独有的。Nginx开发了变量用于提供附加的更强大的机制来控制运行时的web服务器配置。变量为快速赋值做了优化,并且在内部预编译为索引。赋值是按需计算的,例如,变量的值通常只在这个请求的生命周期中计算一次,而后缓存起来。变量可在不同的配置指令中使用,为描述条件请求处理行为提供了更多弹性。
try_files指令对于用更适当的方式逐渐替换if 条件配置语句是很重要的,并且它设计来快速高效的尝试不同的URI与内容之间的映射。总的来说,try_files指令很好用,并且及其高效和有用。推荐读者完整的看看这个指令,并在任何能用的地方用上它。


深入nginx
前面提到过,nginx代码包含核心和其他模块。核心负责提供web服务器的基础,web和邮件反向代理功能;实现底层网络协议,构建必要的运行环境,并且保证不同模块之间的无缝交互。但是,大部分协议相关以及应用相关的特性是由其他模块完成,而不是核心模块。
在内部,nginx通过模块流水线或模块链处理连接。换言之,每个操作都有一个模块做对应的工作。例如:压缩,修改内容,执行SSI,通过FastCGI或uwsgi协议同后端应用服务器通信,以及同memcached通信等。
在核心和实际功能模块之间,有两个模块http 和 mail。这两个模块在核心和底层组件之间提供了附加抽象层。这些模块处理同各自应用层协议相关的事件序列,如实现HTTP、SMTP或IMAP。与核心一起,这些上层模块负责以正确的次序调用各自的功能模块。虽然目前HTTP协议是作为http模块的一部分实现的,但将来计划将其独立为一个功能模块,以支持其他协议,如SPDY(参考“SPDY: An experimental protocol for a faster web”)。
功能模块可以分为事件模块,阶段处理器,输出过滤器,变量处理器,协议模块,上游和负载均衡器等类型。虽然事件模块和协议也用于mail模块,但是这些模块大部分用于补充nginx的HTTP功能。事件模块提供了基于操作系统的事件通知机制,如kqueue 或 epoll,这些取决于操作系统的能力和构建配置。协议模块允许nginx通过HTTPS, TLS/SSL, SMTP, POP3 和 IMAP等协议通信。
一个典型的HTTP请求处理周期如下:1. 客户端发送HTTP请求。2. nginx核心从配置文件查找匹配该请求的位置,根据这个位置信息选择适当的阶段处理器。3. 如果配置为反向代理,负载均衡器挑选一个上游服务器用于转发请求。4. 阶段处理器完成工作,并且传递每个输出缓冲区给第一个过滤器。5. 第一个过滤器传递输出给第二个过滤器。6. 第二个过滤器传递输出给第三个等等。7. 最终响应发送给客户端。
Nginx模块是高度可定制化的。它通过一系列指向可执行函数的回调指针来工作。因而,带来的副作用就是为第三方开发者加重了负担,因为他们必须精确的定义模块应怎么运行和何时运行。Nginx的API和开发者文档都经过优化使之更具有可用性来减轻开发难度。
一些在nginx中插入模块的例子:
  • 配置文件读取和处理之前
  • Location和server的每个配置指令生效时
  • Main配置初始化时
  • Server配置初始化时
  • Server配置合并到main配置时
  • Location配置初始化或者合并到上级server配置时
  • Master进程启动或退出时
  • 新的worker进程启动或退出时
  • 处理请求时
  • 过滤响应头和响应体时
  • 挑选,初始化和重新初始化上游服务器时
  • 处理上游服务器响应时
  • 完成与上游服务器的交互时

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

事件循环自身(步骤5和6)确保增量产生响应并且流式发送给客户端。
更详细的处理HTTP请求过程如下:
  1. 初始化请求处理
  2. 处理请求头
  3. 处理请求体
  4. 调用对应的处理器
  5. 执行所有的处理阶段

当nginx处理一个HTTP请求时,会经过多个处理阶段。每个阶段都调用对应的处理器。通常,阶段处理器处理一个请求后产生对应的输出,阶段处理器在配置文件的location中定义。
阶段处理器一般做四件事情:获取location配置,产生适当的响应,发送响应头,发送响应体。处理器函数有一个参数:描述请求的结构体。请求结构体有许多关于客户端请求的有用信息,例如:请求方法类型,URI和请求头等。
当读取完HTTP请求头之后,nginx查找相关的虚拟服务器配置,如果找到虚拟服务器,请求会经过下面六个阶段:
  1. server rewrite phase
  2. location phase
  3. location rewrite phase (which can bring the request back to the previous phase可以将请求带回到前面的阶段)
  4. access control phase
  5. try_files phase
  6. log phase

为了给请求生成必要的响应内容,nginx传递请求给匹配的内容处理器。根据location配置,nginx会先尝试无条件处理器,如perl, proxy_pass, flv,mp4等。如果这个请求不匹配这几个内容处理器,将会按下面顺序挑选一个处理器:random index, index, autoindex, gzip_static, static。
Nginx文档中有Index模块的详细内容,这个模块只处理结尾为斜杠的请求。如果不匹配mp4或autoindex模块,则认为响应内容是磁盘上的一个文件或目录(即静态的),这由static内容处理器完成服务。如果是目录,将自动重写URI保证结尾是一个斜杠(从而发起一个HTTP重定向)。
内容处理器产生的内容则被传递到过滤器。过滤器也同location相关,一个location可配置多个过滤器。过滤器加工处理器产生的输出。处理器的执行顺序在编译时决定,对于原生过滤器,顺序是已经定义好的,对于第三方过滤器,可以在编译阶段设置先后顺序。当前的nginx实现中,过滤器只能修改输出的数据,还不能编写修改输入的数据的过滤器。输入过滤器将在将来的版本提供。
过滤器遵循一个特定的设计模式。过滤器被调用后开始工作,调用下一个过滤器直到过滤器链中的最后一个。完成之后,nginx结束响应。过滤器不用等待前面的过滤器结束。一旦前一个过滤器提供的输入已经可用,下一个过滤器便可以启动自己的工作(很像Unix中的管道)。因而,在从上游服务器接收到所有的响应之前,所生成的输出响应已经被发送给客户端。
过滤器有header filter和body filter,nginx将响应的header和body分别发送给相关的过滤器。
Header filter包含3个基本步骤:
  1. 决定是否处理这个响应
  2. 处理响应
  3. 调用下一个过滤器

body filter转换所生成的内容。body filter的一些例子:
  • SSI
  • XSLT过滤
  • 图片过滤(例如调整图片大小)
  • 字符集转换
  • Gzip压缩
  • Chunked编码

经过过滤器链之后,响应被发送到writer。有两个额外的具有特定功能的过滤器与writer相关,copy filter和postpone filter。Copy filter负责将相关的响应内容填充到内存缓冲区,这些响应内容有可能存储在反向代理的临时目录。Postpone filter用于子请求处理。
子请求是一个处理请求、响应很重要的机制,同时也是nginx最强大的功能之一。通过子请求,Nginx可以返回另一个URL的响应,这个URL与客户端最初请求的URL不同。一些web框架称之为内部跳转,但nginx功能更强,不仅能运行多个子请求并将这些子请求的响应合并成一个,而且还能嵌套和分级。子请求可以产生子-子请求,子-子请求能产生子-子-子请求。子请求可以映射到磁盘文件,其他处理,或者上游服务器。子请求在根据原始响应数据插入附加内容时很有用。例如,SSI模块使用一个过滤器解析返回文档的内容,然后用指定URL的内容来替换include指令。或者做一个过滤器,能够在一个URL产生的响应内容之后附加一些新的文档内容。
上游(upstream)和负载均衡器同样也值得简单介绍一下。上游用于实现反向代理处理器(proxy_pass处理器)。上游模块组装好请求发送给上游服务器(或称为“后端”),然后接收上游服务器返回的响应。这个过程不调用输出过滤器。上游模块仅仅设置回调函数,用于当上游服务器可读或可写时调用。回调函数实现下列功能:
  • 准备请求缓冲区(或缓冲区链),用于发送给上游服务器
  • 重新初始化、重置与上游服务器之间的连接(应在重新发起请求之前)
  • 处理上游服务器响应的首字节,并且保存响应内容的指针
  • 放弃请求(当客户端过早关闭连接时)
  • 结束请求(当nginx完成读取上游服务器响应时)
  • 修整响应体内容(例如除去空白)

如果上游服务器大于一个,负载均衡器模块可附加在proxy_pass处理器上,用于提供选择上游服务器的能力。负载均衡器注册了一个配置文件指令,提供附加的上游服务器初始化功能(通过DNS解析上游服务器名字等),初始化连接结构体,决定如何路由请求,并且更新状态信息。目前,nginx支持两种标准的上游服务器负载均衡规则:轮询和ip哈希。
上游模块和负载均衡处理机制的算法能检测上游服务器异常,并将新请求重新路由到可用的上游服务器,还有更多的工作计划加强这个功能。总之,负载均衡器的改进计划更多些,下个版本的nginx将大幅度提升在不同上游服务器之间分发负载和健康检测的机制。
还有一些有意思的模块在配置文件中提供了额外的变量供使用。这些变量通过不同的模块生成和更新,有两个模块完全用于变量:geo和map。geo模块用于更方便的基于IP地址追踪客户端地址,这个模块可以根据客户端IP地址生成任意变量。另一个map模块允许从一个变量生成另一个变量,提供将主机名和其他变量方便的进行映射的基本能力。这类模块称为变量处理器。
nginx worker进程实现的内存分配机制从某方面来说来自于Apache。Nginx内存管理的高层描述:对于每个连接,必要的内存缓冲区是动态分配的,用于存储或操纵请求、响应的头和体,当连接关闭时释放。很重要的一点是nginx尽可能的去避免在内存中拷贝数据,大部分的数据通过指针进行传递,而不是调用memcpy。
更深入一点,当一个模块产生响应时,这些响应内容放入内存缓冲区,并被添加到一个缓冲区链。这个缓冲区链同样适用于子请求处理。由于根据模块类型不同存在多个处理场景,所以nginx中的缓冲区链相当复杂。例如,在实现body filter模块时,精确的管理缓冲区是很棘手的。这个模块同一时间只能处理缓冲区链中的一个缓冲区,它必须决定是否覆盖输入缓冲区,是否用新分配的缓冲区替换这个缓冲区,或者在这个缓冲区之前或之后插入一个新缓冲区。更复杂的情况,有时一个模块收到的数据需要多个缓冲区存储,因此它必须处理一个不完整的缓冲区链。但是由于目前nginx仅提供了底层API用于操纵缓冲区链,所以开发者应该真正掌握nginx这一晦涩难懂的部分之后,再去开发第三方模块。
上面提到的内容中需要注意的一点,内存缓冲区是为连接的整个生命周期分配的,所以对于长连接需要消耗额外的内存。同时,对于空闲的keepalive连接,nginx仅消耗550字节内存。将来的nginx版本可能进行优化以使长连接重用和共用内存缓冲区。
内存分配管理的任务由nginx内存池分配器完成。共享内存区用于存放接受互斥锁(accept mutex),缓存元数据,SSL会话缓存,以及和带宽策略管理(限速)相关的信息。Nginx实现了slab分配器用于管理共享内存,提供了一系列锁机制(互斥锁和信号量),以允许安全的并发使用共享内存。为了组织复杂的数据结构,nginx也提供了红黑树的实现。红黑树用于在内存中保存缓存元数据,查找非正则location定义,以及其他一些任务。
不幸的是,上述内容从未以一致并且简单的方式介绍过,以致开发第三方模块的工作相当复杂。虽然有一些nginx内部实现的好文档,例如,Evan Miller写的,但是这些文档需要做很多还原工作,nginx模块的开发还是像变魔术一样。
虽然开发第三方模块是如此困难,nginx社区最近还是涌现大量有用的第三方模块。例如,将Lua解释器嵌入nginx,负载均衡附加模块,完整的WebDAV支持,高级缓存控制,以及其他本章作者所鼓励和将来支持的有趣的第三方工作。


优秀实践
Igor Sysoev开始编写nginx时,大部分构建互联网的软件都已经存在,这些软件的架构一般遵循传统服务器和网络硬件、操作系统、以及过去互联网架构的定义。但是这并未阻止Igor考虑在web服务器领域做进一步的工作。所以,显然第一个优秀实践是:总有提升空间。
带着开发更好web软件的想法,Igor花了很多时间开发原始代码结构,并研究在多个操作系统下优化代码的不同手段。十年后,考虑到1.0版本已经经过十年活跃开发,Igor开发了2.0版本原型。很明显,这个新架构的初始原型和代码结构,对于软件的后续开发及其重要。
另外值得提到的一点是聚焦开发。Nginx 的windows版本是个好例子,说明无论在开发者的核心技能或应用目标上避免稀释开发工作是值得的。同样努力加强nginx重写引擎对现存遗留配置的后向兼容能力,也是值得的。
最后特别值得提到的是,尽管nginx开发者社区并不大,nginx的第三方模块和扩展还是成为nginx受欢迎的一个很重要的因素。Nginx用户社区和作者们很感谢Evan Miller, Piotr Sikora, Valery Kholodkov, Zhang Yichun (agentzh)以及其他优秀软件工程师所做的工作。
        一直听说Nginx在处理静态数据方面的能力十分优秀,但是没有深入进行刨根问底,近期学习Nginx模块开发,加入Nginx开发邮件列表,通过对大牛们的问题的琢磨以及学习,对这个问题有了稍微深入一点的认识。


        首先,得益于Linux内核提供的文件异步I/O和ngx_epoll_module模块中实现的针对文件的异步I/O。这里提到的文件异步I/O并不是glibc库提供的文件异步I/O,glibc库提供的异步I/O是基于多线程实现的,它并不是真正意义上的异步I/O。这里说的异步I/O时由Linux内核实现,只有在内核中成功地完成了磁盘操作,内核才会通知进程,进而使得磁盘文件的处理与网络事件的处理同样高效。Linux内核版本2.6.2x以后支持文件异步I/O,Nginx把读取文件的操作异步地提交给内核后,内核会通知I/O设备独立地执行操作,这样,Nginx进程可以继续充分地占用CPU,而且,当大量读事件堆积到I/O设备的队列中时,将会发挥出内核中“电梯算法”的优势,从而降低随机读取磁盘扇区的成本。Nginx仅支持在读取文件时使用异步I/O,因为正常写入文件时往往时写入内存中就立刻返回,效率很高,而使用异步I/O写入时速度会明显下降。


        其次,Linux SendFile系统调用带来的优势。Nginx启用了Linux上的SendFile系统调用来发送文件,它减少了内核态与用户态之间的两次内存复制,这样就会从磁盘中读取文件后直接在内核态发送到网卡设备,提高了发送文件的效率。


        要搞明白sendfile的原理,必须弄明白这样一个问题,数据是如何发送的?也就是数据从主机进入线路之前所发生的一切。


        1、应用程序首先得将要发送的数据写入该进程的内存地址空间中。
        2、应用程序通过系统函数库接口(如send函数)向内核发出系统调用,由系统内核来进行随后的操作,它将这些数据从用户态内存区复制到由内核维护的一段称为内核缓冲区的内存地址空间,也就是内核态。这块地址空间的大小通常是有限的,所有要发送的数据都将以队列的形式进入到这里,这些数据可能来自于多个进程,每块数据都有一定的记号来标记它们的去向。如果要发送的数据比较多,那么系统调用需要进行多次,每次复制一定的数据大小,这个大小取决于网络数据包的大小以及内核缓冲区的承载能力。重复的系统调用体现在应用编程层面重复调用send函数。


        3、当数据写入内核缓冲区以后,内核将通知网卡控制器来取数据,同时CPU转而处理其他进程。网卡控制器接到通知后,便根据网卡驱动器信息得知对应内核缓冲区的地址,将要发送的数据复制到网卡的缓冲区中。这系列的数据复制中,数据始终按照连接两端设备的内部总线宽度来复制,采用PCI-X总线接口的网卡一般使用32位总线宽度,那么从内核缓冲区到网卡缓冲区的数据复制过程中,任何时刻只能复制32位的比特信息。


        4、网卡缓冲区中的数据需要发送到线路中,同时释放缓冲区来索取更多要发送的数据,只有二进制的数字信号才能在线路中传输,所以这时候需要对数据进行字节到位的转换。


        5、发送时,网卡会使用内部特定的物理装置来生成可以传播的各种信号(光信号、电信号)。



展开
收起
小柒2012 2015-10-16 20:24:49 20480 0
3 条回答
写回答
取消 提交回答
  • 解决方案工程师,负责为企业规划上云迁移方案和云上架构设计,在网站建设开发和云计算领域有多年经验,专注于Linux平台的系统维护以及应用部署。致力于以场景化的方式让云计算,用更加通俗易懂的方式让更多人体验云计算,让云端的计算更质朴的落地。
    二楼的老张亮了。。

    为什么不是老王捏
    2015-10-17 14:41:56
    赞同 展开评论 打赏
  • 我竟然没看完。辛苦了。
    2015-10-16 23:50:42
    赞同 展开评论 打赏
  • 一个程序员,欢迎骚扰!!!
    这里做一个简单的说明

    同步I/O操作:导致请求进程阻塞,直到I/O操作完成;
    异步I/O操作:不导致请求进程阻塞。
    好,下面我用我的语言来总结一下阻塞,非阻塞,同步,异步
    阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待;
    同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写。
    ====================================================================

    举一个简单的例子===

    老张爱喝茶,废话不说,煮开水。
    出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
    1 老张把水壶放到火上,立等水开。(同步阻塞)
    老张觉得自己有点傻
    2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
    老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
    3 老张把响水壶放到火上,立等水开。(异步阻塞)
    老张觉得这样傻等意义不大
    4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
    老张觉得自己聪明了。


    所谓同步异步,只是对于水壶而言。
    普通水壶,同步;响水壶,异步。
    虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
    同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

    所谓阻塞非阻塞,仅仅对于老张而言。
    立等的老张,阻塞;看电视的老张,非阻塞。
    情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
    ======================================================================
    同步和异步仅仅是关于所关注的消息如何通知的机制。同步的情况下,是由处理消息者自己去等待消息是否被触发,而异步的情况下是由触发机制来通知处理消息者
    阻塞和非阻塞应该是发生在消息的处理的时刻。阻塞其实就是等待,发出通知,等待结果完成。非阻塞属于发出通知,立即返回结果,没有等待过程。
    2015-10-16 20:25:47
    赞同 展开评论 打赏
问答排行榜
最热
最新

相关电子书

更多
《Nginx 代理系统常用手册》 立即下载
CentOS Nginx PHP JAVA 多语言镜像使用手 立即下载
CentOS Nginx PHP JAVA多语言镜像使用手册 立即下载