了解 Nginx 基本概念

liner999 7年前
   <h2><strong>前言</strong></h2>    <p>本篇是我学习Nginx的一些笔记,主要内容讲述了一些了解Nginx需要的基本概念。</p>    <p>然后探讨一下Nginx的模块化的组织架构,以及各个模块的分类、工作方式、职责和提供的相关指令。</p>    <p>主要达到以下目的:</p>    <ol>     <li> <p>了解Nginx的大概运行原理</p> </li>     <li> <p>了解Nginx的基本概念</p> </li>     <li> <p>知道怎么看官方文档。</p> </li>    </ol>    <h2><strong>关于Nginx</strong></h2>    <p>Nginx是一款面向性能设计的HTTP服务器,能反向代理HTTP,HTTPS和邮件相关(SMTP,POP3,IMAP)的协议链接。并且提供了负载均衡以及HTTP缓存。</p>    <p>它的设计充分使用异步事件模型,削减上下文调度的开销,提高服务器并发能力。</p>    <p>采用了模块化设计,提供了丰富模块的第三方模块。</p>    <p>所以关于Nginx,有这些标签:「异步」「事件」「模块化」「高性能」「高并发」「反向代理」「负载均衡」</p>    <h2><strong>基本概念</strong></h2>    <h2><strong>进程模型</strong></h2>    <p>Nginx的进程是使用经典的「Master-Worker」模型。</p>    <p>Nginx在启动后,会有一个master进程和多个worker进程。</p>    <p>master进程主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。</p>    <p>worker进程主要处理基本的网络事件,多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。需要注意的是,每个Worker只有主线程,即所谓的「单线程」。</p>    <p>一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。</p>    <p>worker进程的个数是可以设置的,一般会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的。nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效。更多的worker数,只会导致进程来竞争cpu资源。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/c7676ea475d736fdf21116878ab54b0c.png"></p>    <h2><strong>事件模型</strong></h2>    <p>Nginx对于事件,以「异步非阻塞」方式来实现。</p>    <p>异步和非异步,阻塞和非阻塞是两组不同的概念,前者更多对于应用程序而言,而后者更多对于CPU来说:</p>    <ol>     <li> <p>异步:执行一个动作之后,可以去操作别的操作,然后等待通知再回来执行刚才没执行完的操作。</p> </li>     <li> <p>非异步(同步):执行一个操作之后,等待结果,然后才继续执行下面的操作。</p> </li>     <li> <p>阻塞:给CPU传达任务之后,一直等待CPU处理完毕(即使会产生I/O),然后才执行下面操作。</p> </li>     <li> <p>非阻塞:给CPU传达任务之后,继续处理后面的操作,隔段时间再来询问之前的操作是否完成。这样的及过程也叫「轮询」</p> </li>    </ol>    <p>Nginx的「异步非阻塞」方式,具体到系统调用的话,就是像select/poll/epoll/kqueue这样的系统调用。它们提供了一种机制,让你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。</p>    <p>epoll是在Linux上关于事件的实现,而kqueue是OpenBSD或FreeBSD操作系统上采用类似epoll的事件模型。</p>    <p>所以重点讲解一下epoll的模型:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/209dc82d35e188b053493d6f533b738f.png"></p>    <p>该方案给是Linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件,将会进入休眠,直到事件将它唤醒。它是真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。</p>    <h2><strong>反向代理</strong></h2>    <p>要了解「反向代理」,首先需要知道什么是「代理服务器」和「正向代理」</p>    <h3><strong>代理服务器</strong></h3>    <p>在网络中,客户端发起一个请求,获取服务器端的资源。它们之间并不是建立一条直接的通道,而是被代理服务器所转发。</p>    <p>代理服务器作为网络中的媒介将互联网上获取的资源返回给相关的客户端。</p>    <p>我们通常所说的代理,一般都指的是「正向代理」,是相对于客户端来说的。</p>    <p>比方说我链接了一个V*N,我访问Google的时候,客户端发起的请求到了V*N,V*N帮忙转发请求Google的服务器,然后把Google响应返回给客户端。这个过程,V*N就充当了「正向代理服务器」的角色。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/da025027ae448275e5a42387c6d146ef.png"></p>    <h3><strong>反向代理</strong></h3>    <p>和「正向代理」不同,「反向代理」的说法面向于服务器端。一个客户端请求来到代理服务器,代理服务器根据客户端的请求的不同而把请求转发到不同的服务器,这个过程在「负载均衡」中,也会发生两个一样的请求,会转发到完全不一样的服务器中的情况。</p>    <p>「正向代理」是「负载均衡」实现的前提,正因为代理服务器有了解析请求,分发请求的能力,才能实现负载均衡,降低每一台服务器的负荷。</p>    <p>利用「反向代理」,除了实现负载均衡,还可以实现诸如:SSL加密,静态内容缓存,gzip压缩,减速上传,安全等功能</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e0a514fd981461815f7608ed14190a00.png"></p>    <h2><strong>负载均衡</strong></h2>    <p>负载均衡(Load balancing)是一种计算机网络技术,用来在多个服务器中分配负载,以达到最佳化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。</p>    <p>使用带有负载均衡的多个服务器组件,取代单一的组件,可以通过冗余提高可靠性。负载平衡服务的实现可以通过软件和硬件来实现。</p>    <p>负载均衡的分发,一般都会有多套算法来处理分发问题。</p>    <h2><strong>连接 Connection</strong></h2>    <p>在nginx中connection就是对tcp连接的封装,其中包括连接的socket,读事件,写事件。利用nginx封装的connection,我们可以很方便的使用nginx来处理与连接相关的事情,比如,建立连接,发送与接受数据等。</p>    <p>而nginx中的http请求的处理就是建立在connection之上的,所以nginx不仅可以作为一个web服务器,也可以作为邮件服务器。</p>    <p>当然,利用nginx提供的connection,我们可以与任何后端服务打交道。</p>    <h3><strong>最大连接数</strong></h3>    <p>在nginx中,每个进程会有一个连接数的最大上限,这个上限与系统对fd的限制不一样。</p>    <p>在操作系统中,通过ulimit -n,我们可以得到一个进程所能够打开的fd的最大数,即nofile,因为每个socket连接会占用掉一个fd,所以这也会限制我们进程的最大连接数,当然也会直接影响到我们程序所能支持的最大并发数,当fd用完后,再创建socket时,就会失败。</p>    <p>nginx通过设置 worker_connectons 来设置每个进程支持的最大连接数。如果该值大于nofile,那么实际的最大连接数是nofile,nginx会有警告。</p>    <p>nginx在实现时,是通过一个连接池来管理的,每个worker进程都有一个独立的连接池,连接池的大小是 worker_connections 。这里的连接池里面保存的其实不是真实的连接,它只是一个 worker_connections 大小的一个 ngx_connection_t 结构的数组。并且,nginx会通过一个链表 free_connections 来保存所有的空闲 ngx_connection_t ,每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面。</p>    <p>所以,一个nginx能建立的最大连接数: worker_connections * worker_processes ,</p>    <p>如果当nginx作为反向代理的话,因为一个请求nginx要建立客户端和服务器的请求,所以最大连接数是: worker_connections * worker_processes / 2</p>    <h2><strong>请求 Request</strong></h2>    <p>在nginx中我们指http请求,具体到nginx中的数据结构是 ngx_http_request_t 。</p>    <p>它是对一个http请求的封装,nginx通过 ngx_http_request_t 来保存解析请求与输出响应相关的数据。</p>    <p>一个http请求,包含请求行、请求头、请求体、响应行、响应头、响应体。</p>    <p>一般性的网络请求处理过程是:</p>    <ol>     <li> <p>客户端会发送请求过来。</p> </li>     <li> <p>然后我们读取一行数据,分析出请求行中包含的method、uri、http_version信息。</p> </li>     <li> <p>然后再一行一行处理请求头,并根据请求method与请求头的信息来决定是否有请求体以及请求体的长度,然后再去读取请求体。</p> </li>     <li> <p>得到请求后,我们处理请求产生需要输出的数据,然后再生成响应行,响应头以及响应体。</p> </li>     <li> <p>在将响应发送给客户端之后,一个完整的请求就处理完了。</p> </li>    </ol>    <p>而nginx处理请求的时候会有一些小小的区别,比如,当请求头读取完成后,就开始进行请求的处理了。</p>    <h3><strong>Nginx处理请求过程</strong></h3>    <p>nginx处理一个请求的抽象概念过程:</p>    <ol>     <li> <p>request 请求进来</p> </li>     <li> <p>初始化HTTP Request, 生成 HTTP Request对象</p> </li>     <li> <p>处理请求头</p> </li>     <li> <p>处理请求体</p> </li>     <li> <p>调用与此请求关联的handler(根据你URL或者Location配置)</p> </li>     <li> <p>依次调用各phase handler进行处理</p>      <ol>       <li> <p>获取location配置</p> </li>       <li> <p>产生适当的响应</p> </li>       <li> <p>发送response header</p> </li>       <li> <p>发送response body</p> </li>      </ol> </li>    </ol>    <h2><strong>基本数据结构</strong></h2>    <p>nginx的作者为追求极致的高效,自己实现了很多颇具特色的nginx风格的数据结构以及公共函数。比如,nginx提供了带长度的字符串,根据编译器选项优化过的字符串拷贝函数ngx_copy等。</p>    <p><em>ps: 下横线分割是C语言的变量名风格</em></p>    <table>     <thead>      <tr>       <th>Data Structure</th>       <th>Description</th>      </tr>     </thead>     <tbody>      <tr>       <td>ngx_str_t</td>       <td>字符串封装</td>      </tr>      <tr>       <td>ngx_pool_t</td>       <td>提供一种机制,帮助管理一系列的资源(内存,文件)</td>      </tr>      <tr>       <td>ngx_array_t</td>       <td>数组结构</td>      </tr>      <tr>       <td>ngx_chain_t</td>       <td>主要用于模块之间数据传递的链表实现</td>      </tr>      <tr>       <td>ngx_buf_t</td>       <td>就是ngx_chain_t链表的每个节点的实际实现,代表某种具体的数据。</td>      </tr>      <tr>       <td>ngx_list_t</td>       <td>list数据结构的实现,以及增强</td>      </tr>      <tr>       <td>ngx_queue_t</td>       <td>实现的双向链表</td>      </tr>      <tr>       <td>ngx_hash_t</td>       <td>hash表的实现</td>      </tr>      <tr>       <td>ngx_hash_wildcard_t</td>       <td>为处理带有通配符域名的匹配问题实现的hash表结构</td>      </tr>      <tr>       <td>ngx_combinded_t</td>       <td>在于提供一个方便的容器包含三个类型的hash表</td>      </tr>      <tr>       <td>ngx_hash_keys_arrays_t</td>       <td>用于构建其他类型的hash的辅助类</td>      </tr>     </tbody>    </table>    <h2><strong>配置</strong></h2>    <p>nginx的配置系统由一个主配置文件和其他一些辅助的配置文件构成。这些配置文件均是纯文本文件,全部位于nginx安装目录下的conf目录下。</p>    <p>指令由nginx的各个模块提供,不同的模块会提供不同的指令来实现配置。</p>    <p>指令除了Key-Value的形式,还有作用域指令。</p>    <p>nginx.conf中的配置信息,根据其逻辑上的意义,对它们进行了分类,也就是分成了多个作用域,或者称之为配置指令上下文。不同的作用域含有一个或者多个配置项。</p>    <p>下面的这些上下文指令是用的比较多:</p>    <table>     <thead>      <tr>       <th>Directive</th>       <th>Description</th>       <th>Contains Directive</th>      </tr>     </thead>     <tbody>      <tr>       <td>main</td>       <td>nginx在运行时与具体业务功能(比如http服务或者email服务代理)无关的一些参数,比如工作进程数,运行的身份等。</td>       <td>user, worker_processes, error_log, events, http, mail</td>      </tr>      <tr>       <td>http</td>       <td>与提供http服务相关的一些配置参数。例如:是否使用keepalive啊,是否使用gzip进行压缩等。</td>       <td>server</td>      </tr>      <tr>       <td>server</td>       <td>http服务上支持若干虚拟主机。每个虚拟主机一个对应的server配置项,配置项里面包含该虚拟主机相关的配置。在提供mail服务的代理时,也可以建立若干server.每个server通过监听的地址来区分。</td>       <td>listen, server_name, access_log, location, protocol, proxy, smtp_auth, xclient</td>      </tr>      <tr>       <td>location</td>       <td>http服务中,某些特定的URL对应的一系列配置项。</td>       <td>index, root</td>      </tr>      <tr>       <td>mail</td>       <td>实现email相关的SMTP/IMAP/POP3代理时,共享的一些配置项(因为可能实现多个代理,工作在多个监听地址上)。</td>       <td>server, http, imap_capabilities</td>      </tr>     </tbody>    </table>    <h2><strong>模块</strong></h2>    <p>nginx将各功能模块组织成一条链,当有请求到达的时候,请求依次经过这条链上的部分或者全部模块,进行处理。每个模块实现特定的功能。例如,实现对请求解压缩的模块,实现SSI的模块,实现与上游服务器进行通讯的模块,实现与FastCGI服务进行通讯的模块。</p>    <p>模块分三类:</p>    <ol>     <li> <p>核心模块</p> </li>     <li> <p>辅助模块</p> </li>     <li> <p>第三方模块</p> </li>    </ol>    <p>根据官方文档排版,辅助模块还分了以下几类:</p>    <ol>     <li> <p>http</p> </li>     <li> <p>mail</p> </li>     <li> <p>stream</p> </li>    </ol>    <p>而根据其功能可以分成这几大类:</p>    <ol>     <li> <p>handler模块</p> <p>此类型的模块也被直接称为handler模块。主要负责处理客户端请求并产生待响应内容,比如ngx_http_static_module模块,负责客户端的静态页面请求处理并将对应的磁盘文件准备为响应内容输出。</p> </li>     <li> <p>filter模块</p> <p>过滤响应头和内容的模块,可以对回复的头和内容进行处理。它的处理时间在获取回复内容之后,向用户发送响应之前。</p> </li>     <li> <p>upstream模块</p> <p>upstream模块实现反向代理的功能,将真正的请求转发到后端服务器上,并从后端服务器上读取响应,发回客户端。upstream模块是一种特殊的handler,只不过响应内容不是真正由自己产生的,而是从后端服务器上读取的。</p> </li>     <li> <p>load balance模块</p> <p>负载均衡模块,实现特定的算法,在众多的后端服务器中,选择一个服务器出来作为某个请求的转发服务器</p> </li>    </ol>    <h2>结尾</h2>    <p>本文讲述了Nginx的一些基本概念。</p>    <p>Nginx是线程模型是Master-Worker模式的,每个worker是单线程的,也就是处理请求是单线程处理的。而单线程并发的事件模型是「异步非阻塞I/O」模型。</p>    <p>并且讲述了「反向代理」「负载均衡」的概念,这是nginx能高性能处理高并发的原因之一。</p>    <p>Nginx对于网络请求是有Connection和Request的概念和封装的。</p>    <p>Nginx的源码组织架构是模块化的,不同的模块实现不一样的职责,然后它们被连接起来一起干一件大事,知道模块有哪些分类,可以让我们知道怎么查找官方文档。</p>    <p>在没有看过有哪些指令,哪些指令有什么功能之前,是不能完全知道nginx提供什么样的功能的,那就抱着,那就抱着「能想到的别人都想到并实现了」的想法来使用nginx吧。Nginx作为一个代理服务,在中间想做什么都可以啦。</p>    <h2><strong>参考</strong></h2>    <p><a href="/misc/goto?guid=4959627747668525921" rel="nofollow,noindex">Nginx开发从入门到精通</a></p>    <p><a href="/misc/goto?guid=4958187734501359851" rel="nofollow,noindex">Nginx官方网站</a></p>    <p>《计算机操作系统》</p>    <p>《深入浅出Node.js》</p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007172005</p>    <p> </p>