百万并发下 Nginx 的优化之道

5,853 阅读15分钟
原文链接: mp.weixin.qq.com

分享:陶辉编辑:白凡

讲师介绍:陶辉,曾在华为、腾讯公司做底层数据相关的工作,写过一本书叫《深入理解Nginx:模块开发与架构解析》,目前在杭州智链达作为联合创始人担任技术总监一职,目前专注于使用互联网技术助力建筑行业实现转型升级。

今天的分享主要在Nginx的性能方面,希望能给大家带来一些系统化思考,帮助大家更有效地去做Nginx。

1. 优化方法论

今天我的分享重点会看两个问题:

  • 第一,保持并发连接数,怎么样做到内存有效使用

  • 第二,在高并发的同时保持高吞吐量的重要要点

实现层面主要是三方面优化,主要聚焦在应用、框架、内核。

硬件限制刚刚也都讲过,可能有的同学也都听过,把网卡调到万兆、10G 或者40G 是最好的,磁盘会根据成本的预算和应用场景来选择固态硬盘或者机械式硬盘,关注IOPS 或者 BPS。CPU 是我们重点看的一个指标。

这一页重点来说下,实际上它是把操作系统的切换代价换到了进程内部,所以它从一个连接器到另外一个连接器的切换成本非常低,它性能很好,协程 Openresty 其实是一样的。

资源的高效使用,降低内存是对我们增大并发性是有帮助的,减少RTT、提升容量。

reuseport都是围绕着提升CPU的机核性。还有fastsocket,因为我之前在阿里云的时候还做过阿里云的网络,所以它其实能够带来很大的性能提升,但是它问题也很明显,把内核本身的那套东西绕过去了。

2. 请求的“一生”

下面我首先会去聊一下怎么看“请求”,了解完这个以后再去看怎么优化就会很清楚了。

说这个之前必须再说一下Nginx的模块结构,像Nginx以外,任何一个外部框架都有个特点,如果想形成整个生态必须允许第三方的代码接进来,构成一个序列,让一个请求挨个被模块共同处理。

那Nginx也一样,这些模块会串成一个序列,一个请求会被挨个的处理。在核心模块里有两个,一个是steam 和 NGX。

2.1 请求到来

一个连接开始刚刚建立请求到来的时候会发生怎么样的事情,先是操作系统内核中有一个队列,等着我们的进程去系统调用,这时候因为有很多工作进程,谁会去调用呢,这有个负载均衡策略,下面有个PPT会专门说这个事情。

现在有一个事件模块,调用了epoll wait 这样的接口,accept 建立好一个新连接,这时会分配到连接内存池,这个内存池不同于所有的内存池,它在连接刚创建的时候会分配,什么时候会释放呢?

只有这个连接关闭的时候才会去释放。接下来就到了ngx模块,这时候会加一个定时器,60秒,就是在建立好连接以后60秒之内没有接到客户端发来的就自动关闭,如果60秒过来之后会去分配内存,读缓冲区。什么意思呢?

现在操作系统内核已经收到这个请求了,但是我的应用程序处理不了,因为没有给它读到用户态的内存里去,所以这时候要分配内存,从连接内存池这里分配,那要分配多大呢?会扩到1K。

2.2 收到请求

当收到请求以后,接收uri 和 header,分配请求内存池,这时候 request pool size是4K,大家发现是不是和刚才的有一个8倍的差距,这是因为利用态的内存是非常消耗资源。

再看为什么会消耗资源,首先会用状态机解去形容,所谓状态机解就是把它当做一个序列,一个支节一个支节往下解,如果发现换行了那就是请求行解完了;

但如果这个请求特别长的时候,就会去再分配更大的,刚刚1K不够用了,为什么是4乘8K呢?就是因为当1K不够了不会一次性分配 32K,而是一次性分配8K。如果 8K以后还没有解析到刚才的标识符,就会分配第二个8K。

我之前收到的所有东西都不会释放,只是放一个指针,指到 url 或者指到那个协议,标识它有多长就可以了。

接下来解决header,这个流程一模一样的没有什么区别,这时候还会有一个不够用的情况,当我接收完所有的header以后,会把刚刚的定时器给移除,移除后接下来做11个阶段的处理,也就是说刚刚所有的外部服务器都是通过很多的模块串成在一起处理一个请求的。

像刚刚两页PPT都在说蓝色的区域,那么请求接下来11个阶段是什么意思呢?这个黄色的、绿色的,还有右边这个都是在11阶段之中。

这11个阶段大家也不用记,非常简单,只要掌握三个关键词就可以。

刚刚读完 header 要做处理,所以这时候第一阶段是 post-read。接下来会有rewrite,还有access和preaccess。

先看左手边,当我们下载完 Nginx 源码编以后会有一个referer,所有的第三方数据都会在这里呈现有序排列,这些序列中并不是简单的一个请求给它再给它,先是分为11个阶段,每个阶段之内大家是有序一个个往后来的,但在11个阶段中是按阶段来的。

我把它分解一下,第一个referer这阶段有很多模块,后面这是有序的。

这个图比刚刚的图多了两个关键点,第一到了某一个模块可以决定继续向这序列后的模块执行,也可以说直接跳到下个阶段,但不能说跳多个阶段。

第二是生成了向客户端反映的响应,这时候要对响应做些处理,这里是有序的,先做缩略图再做压缩,所以它是有严格顺序的。

2.3 请求的反向代理

请求的反向代理,反向代理这块是我们Nginx的重点应用场景,因为Nginx会考虑一种场景,我不知道大家有没有用过,客户端走的是公网,所以网络环境非常差,网速非常慢,如果简单用一个缓冲区从客户端收一点发给上游服务器,那上游服务器的压力会很大,因为上游服务器往往它的效率高,所以都是一个请求被处理完之前不会再处理下一个请求。

Nginx考虑到这个场景,它会先把整个请求全部收完以后,再向上游服务器建立连接,所以是默认第一个配置,就是proxy request buffering on,存放包体至文件,默认size是8K。那建立上游连接的时候会放timeout,60秒,添加超时定时器,也是60秒的。

发出请求(读取文体包件),如果向上游传一个很大的包体的话,那sizk就是8K。默认proxy limit rate是打开的,我们会先把这个请求全部缓存到端来,所以这时候有个8×8K,如果关掉的话,也就是从上游发一点就往下游发一点。知道这个流程以后,再说这里的话大家可以感觉到这里的内存消耗还是蛮大的。

2.4 返回响应

返回响应,这里面其实内容蛮多的,我给大家简化一下,还是刚刚官方的那个包,这也是有顺的从下往上看,如果有大量第三方模块进来的话,数量会非常高。

第一个关键点是上面的header filter,上面是write filter,下面是postpone filter,这里还有一个copy filter,它又分为两类,一类是需要处理,一类是不需要处理的。openresty的指令,第一代码是在哪里执行的,第二个是SDK。

3. 应用层优化

3.1 协议

做应用层的优化我们会先看协议层有没有什么优化,比如说编码方式、header每次都去传用Nginx的架构,以至于浪费了很多的流量。我们可以改善http2,有很多这样的协议会有大幅度提升它的性能。

当然如果你改善http2了,会带来其他的问题,比如说http2必须走这条路线。这条路线又是一个很大的话题,它涉及到安全性和性能,是互相冲突的东西。

3.2 压缩

我们希望“商”越大越好,压缩这里会有一个重点提出来的动态和静态,比如说我们用了拷贝,比如说可以从磁盘中直接由内核来发网卡,但一旦做压缩的话就不得不先把这个文件读到Nginx,交给后面的极内核去做一下处理。

keepalive长连接也是一样的,它也涉及到很多东西,简单来看这也就是附用连接。因为连接有一个慢启动的过程,一开始它的窗口是比较小,一次可能只传送很小的1K的,但后面可能会传送几十K,所以你每次新建连接它都会重新开始,这是很慢的。

当然这里还涉及到一个问题,因为Nginx内核它默认打开了一个连接空闲的时候,长连接产生的作用也会下降。提高内存使用率

刚刚在说具体的请求处理过程中已经比较详细的把这问题说清楚了,这里再总结一下,在我看来有一个角度,Nginx对下游只是必须要有的这些模块,client header、buffer size:1K,上游网络http包头和包体。

CPU通过缓存去取储存上东西的时候,它是一批一批取的,每一批目前是64字节,所以默认的是8K,如果你配了32它会给你上升到64,如果你配了65会升到128,因为它是一个一个序列化重组的,所以了解这个东西以后自己再配的时候就不会再犯问题。红黑树这里用的非常多,因为是和具体的模块相关。

3.4 限速

大部分我们在做分公司流控的时候,主要在限什么呢?主要限Nginx向客户端发送响应的速度。这东西它非常好用,因为可以和Nginx定量连接在一起。这不是限上游发请求的速度,而是在限从上游接响应的速度。

3.5 Worker间负载均衡

当时我在用0.6版本的时候那时候都在默认用这个,这个“锁”它是在用进程间同步方式去实现负载均衡,这个负载均衡怎么实现呢?就是保证所有的Worker进程,同一时刻只有一个Worker进程在处理距离,这里就会有好几个问题,绿色的框代表它的吞吐量,吞吐量不高,所以会导致第二个问题requests,也是比较长的,这个方差就非常的大。

如果把这个“锁”关掉以后,可以看到吞吐量是上升的,方差也在下降,但是它的时间在上升,为什么会出现这样的情况?因为会导致一个Worker可能会非常忙,它的连接数已经非常高了,但是还有其他的worker进程是很闲的。

如果用了requests,它会在内核层面上做负载均衡。这是一个专用场景,如果在复杂应用场景下开requests和不开是能看到明显变化的。

3.6 超时

这里其实我刚刚都说了好多,它是一个红黑树在实现的。唯一要说的也就是这里,Nginx现在做四层的反向代理也很成熟了。

像utp协议是可以做反向代理的,但要把有问题的连接迅速踢掉的话,要遵循这个原则,一个请求对一个响应。

3.7 缓存

只要想提升性能必须要在缓存上下工夫。比如说我以前在阿里云做云盘,云盘缓存的时候就会有个概念叫空间维度缓存,在读一块内容的时候可能会把这内容周边的其他内容也读到缓存中,大家如果熟悉优化的话也会知道有分支预测先把代码读到那空间中,这个用的比较少,基于时间维度用的比较多了。

3.8 减少磁盘IO

其实要做的事也非常多,优化读取,Sendfile零拷贝、内存盘、SSD盘。减少写入,AIO,磁盘是远大于内存的,当它把你内存消化完的时候还会退化成一个调用。像thread pool只用读文件,当退化成这种模式变多线程可以防止它的主进程被阻塞住,这时候官方的博客上说是有9倍的性能提升。

4. 系统优化

第一就是提升容量配置,我们建连接的时候也有,还有些向客户端发起连接的时候会有一个端口范围,还有一些像对于网卡设备的。

第二CPU缓存的亲和性,这看情况了,现在用L3缓存差不多也20兆的规模,CPU缓存的亲和性是一个非常大的话题,这里就不再展开了。

第三,NUMA架构的CPU亲和性,把内存分成两部分,一部分是靠近这个核,一部分靠近那个核,如果访问本核的话就会很快,靠近另一边大概会耗费三倍的损耗。对于多核CPU的使用对性能提升很大的话就不要在意这个事情。

第四,网络的快速容错,因为TCP的连接最麻烦的是在建立连接和关闭连接,这里有很多参数都是在调,每个地方重发,多长时间重发,重发多少次。

这里给大家展示的是快启动,有好几个概念在里面,第一个概念在快速启动的时候是以两倍的速度。因为网的带宽是有限的,当你超出网络带宽的时候其中的网络设备是会被丢包的,也就是控制量在往下降,那再恢复就会比较慢。

TCP协议优化,本来可能差不多要四个来回才能达到每次的传输在网络中有几十K,那么提前配好的话用增大初始窗口让它一开始就达到最大流量。

提高资源效率,这一页东西就挺多了,比如说先从CPU看,TCP defer accept,如果有这个的话,实际上会牺牲一些即时性,但带来的好处是第一次建立好连接没有内容过来的时候是不会激活Nginx做切换的。

内存在说的时候是系统态的内存,在内存大和小的时候操作系统做了一次优化,在压力模式和非压力模式下为每一个连接分配的系统内存可以动态调整。

网络设备它们的核心只解决一个问题,变单个处理为批量处理,批量处理以后吞吐量一定是会上升的,因为消耗的资源变少了,切换次数变少了,它们的逻辑是一样的,就这些逻辑和我一直在说的逻辑都是同一个逻辑,只是应用在不同层面会产生不同的效果。

端口复用,像reals是很好用的,因为它可以把端口用在上游服务连接的层面上,没有带来隐患。

提升多CPU使用效率,上面很多东西都说到了,重点就两个,一是CPU绑定,绑定以后缓存更有效。多队列网卡,从硬件层面上已经能够做到了。

BDP,带宽肯定是知道的,带宽和时延就决定了带宽时延积,那吞吐量等于窗口或者时延。

内存分配速度也是我们关注的重点,当并发量很大的时候内存的分配是比较糟糕的,大家可以看到有很多它的竞品。

PCRE的优化,这用最新的版本就好。

本文根据陶辉老师在 GOPS 2018 · 上海站分享整理而成。