Nginx做下载代理IO极高问题

由于某些原因我们的项目使用了NGINX代理下载请求,当NGINX的STATUS中的Writing达到500左右时,服务器的CPU就达到100%了。

项目背景

项目主要提供用户的下载,包大则有300M,小则10来M。项目需要记下载日志,会把下载时间,用户请求的大小,下载了多少,下载时的并发数,是否进行了断点续传等信息记录下来,日志记录使用的是Servlet来实现,故而引入了WEB容器Tomcat,前端加入了Nginx代理,来支持多个Tomcat,以解决单个Tomcat性能有限的问题。

 问题分析

首先是确认CPU是谁引起的较高,通过使用iostat命令查看大概是这样的效果。

[java]
[root@nth-server ~]# iostat -x 1
Linux 3.12.9-x86_64-linode37 (nth-server) 05/07/2014 _x86_64_ (8 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
0.06 0.00 0.01 0.00 0.11 99.81

Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util
xvda 0.00 0.34 0.02 0.14 0.52 3.84 27.82 0.00 18.64 2.45 0.04
xvdb 0.00 0.00 0.00 0.00 0.00 0.00 24.99 0.00 12.02 7.68 0.00
参数简介:
rrqm/s : 每秒进行 merge 的读操作数目。即 delta(rmerge)/s
wrqm/s : 每秒进行 merge 的写操作数目。即 delta(wmerge)/s
r/s : 每秒完成的读 I/O 设备次数。即 delta(rio)/s
w/s : 每秒完成的写 I/O 设备次数。即 delta(wio)/s
rsec/s : 每秒读扇区数。即 delta(rsect)/s
wsec/s : 每秒写扇区数。即 delta(wsect)/s
rkB/s : 每秒读K字节数。是 rsect/s 的一半,因为每扇区大小为512字节。(需要计算)
wkB/s : 每秒写K字节数。是 wsect/s 的一半。(需要计算)
avgrq-sz: 平均每次设备I/O操作的数据大小 (扇区)。delta(rsect+wsect)/delta(rio+wio)
avgqu-sz: 平均I/O队列长度。即 delta(aveq)/s/1000 (因为aveq的单位为毫秒)。
await : 平均每次设备I/O操作的等待时间 (毫秒)。即 delta(ruse+wuse)/delta(rio+wio)
svctm : 平均每次设备I/O操作的服务时间 (毫秒)。即 delta(use)/delta(rio+wio)
%util : 一秒中有百分之多少的时间用于 I/O 操作,或者说一秒中有多少时间 I/O 队列是非空的。即 delta(use)/s/1000 (因为use的单位为毫秒)
[/java]

上述只是展示了一个示例,由于上班无法截图。大概是iowait达到90%,同时avgqu-sz达到300左右,写扇区数达到14000,但是奇怪的是svctm的时间在3ms,而且比较稳定,await的时间在500以上。由于svctm比较小,说明不是磁盘性能的问题,据说在20ms以上才不正常。总体而言应该是写请求太多了,这个结论当时并没有立刻得到。而是通过找了一下Nginx的proxy_buffer的相关资料,关掉proxy_buffer后CPU降下来得到的结论。

Nginx Proxy buffer 工作原理

Proxy buffer是Nginx在使用proxy功能是缓存后台返回数据的一种机制。通过proxy_buffering off/on;来关闭和打开缓存,默认他是打开的。proxy_buffer的更多介绍。这里来说一下关键的要点。

  1. 若后端返回的文件较大,会将这些来不及发到客户端的请求写到临时文件中。
  2. 这样的缓存是一个请求一个缓存。

这样在我们这样一个下载服务器的环境中就将意味着每一次请求可能都意味着写一个临时的下载文件,若有500左右的同时下载则将意味着500个同时的请求写临时文件,这就将引起大量的IO请求,这就是造成CPU飙高的主因了。但是大量的直接下载一般不会出现CPU很高的情况呢,这是因为操作系统缓存了常用的文件,可通过下面的命令看到。

[java]
[root@nth-server ~]# vmstat 1
procs ———–memory———- —swap– —–io—- –system– —–cpu—–
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 5928 78464 94284 346500 0 0 0 0 1 0 0 0 100 0 0
0 0 5928 78464 94284 346500 0 0 0 0 35 28 0 0 100 0 0
0 0 5928 78464 94284 346500 0 0 0 0 13 10 0 0 100 0 0
0 0 5928 78464 94284 346500 0 0 0 0 15 14 0 0 100 0 0
参数简介:
r 表示运行队列(就是说多少个进程真的分配到CPU),我测试的服务器目前CPU比较空闲,没什么程序在跑,当这个值超过了CPU数目,就会出现CPU瓶颈了。这个也和top的负载有关系,一般负载超过了3就比较高,超过了5就高,超过了10就不正常了,服务器的状态很危险。top的负载类似每秒的运行队列。如果运行队列过大,表示你的CPU很繁忙,一般会造成CPU使用率很高。

b 表示阻塞的进程

swpd 虚拟内存已使用的大小,如果大于0,表示你的机器物理内存不足了,但若si、so项长期为0也说明是正常。的

free 空闲的物理内存的大小,我的机器内存总共8G,剩余3415M。

buff Linux/Unix系统是用来存储,目录里面有什么内容,权限等的缓存,我本机大概占用300多M

cache cache直接用来记忆我们打开的文件,给文件做缓冲,我本机大概占用300多M(这里是Linux/Unix的聪明之处,把空闲的物理内存的一部分拿来做文件和目录的缓存,是为了提高 程序执行的性能,当程序使用内存时,buffer/cached会很快地被使用。)

si 每秒从磁盘读入虚拟内存的大小,如果这个值大于0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉。我的机器内存充裕,一切正常。

so 每秒虚拟内存写入磁盘的大小,如果这个值大于0,同上。

bi 块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是1024byte,我本机上没什么IO操作,所以一直是0,但是我曾在处理拷贝大量数据(2-3T)的机器上看过可以达到140000/s,磁盘写入速度差不多140M每秒

bo 块设备每秒发送的块数量,例如我们读取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO过于频繁,需要调整。

in 每秒CPU的中断次数,包括时间中断

cs 每秒上下文切换次数,例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择web服务器的进程可以由进程或者线程的峰值一直下调,压测,直到cs到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换,导致CPU干正经事的时间少了,CPU没有充分利用,是不可取的。

us 用户CPU时间,我曾经在一个做加密解密很频繁的服务器上,可以看到us接近100,r运行队列达到80(机器在做压力测试,性能表现不佳)。

sy 系统CPU时间,如果太高,表示系统调用时间长,例如是IO操作频繁。

id 空闲CPU时间,一般来说,id + us + sy = 100,一般我认为id是空闲CPU使用率,us是用户CPU使用率,sy是系统CPU使用率。

wt 等待IO CPU时间。
[/java]

上面的值项中的cache就是表示的操作系统缓存的文件。由于此,所以大量下载同时读的时候性能还是不错的。

解决办法

通过以上的分析我们已经知道了造成问题的罪魁祸首,所以我们的解决办法就是使用proxy_buffering off配置关掉NGINX默认的缓存。关掉这个缓存后,NGINX仍会保留一个基本的MAIN BUFFER,每次从后端读取BUFFER返回内容,然后直同步返回给用户。

影响及其它解决办法

关掉BUFFER将意味着后端的WEB服务器将实时返回结果,此时NGINX的好处仅仅是可以方便扩展,性能上肯定不及直接让后端的服务器直接面对用户,因为NGINX在其中多建立了一次请求,同时后端WEB服务器的压力也得不到释放(将一直保持连接,直到请求完成)。由于一般后端单请求返回的内容小于1M,我们通过设置一个较大的buffer完全可以将返回的内容放进内存(此时后端的WEB容器就可以释放连接,由NGINX来慢慢向慢速的用户返回结果),毕竟使用代理下载是一个太特殊的例子。有两个建议

  1. 下载请求中是否能找出一部分没必要使用到动态脚本,取而代之以NGINX的静态下载来支持,NGINX是支持断点续传的,如带有参数,可使用NGINX的rewrite来改写URL到静态地址。
  2. 若必须使用。则后端的Tomcat的一定要使用NIO或者Apr来解决自身的性能问题,增大可支持的连接数。

如何进行nginx或tomcat的性能调优

最近花了一点时间进行了NGINX加TOMCAT7集群压力测试,下面通过对一些常见问题的回答来说明如何调优服务器的性能,是自己的一些经验,且无实际数据,如有纰漏请见谅。

背景: TOMCAT7已加APR或者NIO。已装简单监控JCONSOLE,监控服务器内存,线程等基本情况。

问题1  一个Tomcat他的maxThreads到底配置多少合适?

一个好的maxThreads的配置就是达到资源的合理化应用。

资源池

在讲其它东西之前,我们先引入一个概念,就是资源池。tomcat7中,他对http请求的处理,也有一个池的概念,配置可以参考这里。每一个请求进来后都是使用线程池中的一个来处理,线程池的大小是由maxThreads来限定的。

异步IO:

当前Tomcat通过使用JAVA NIO或者Apache Portable Runtime这样的异步IO来支持性能的优化。异步IO就是当应用需要进行耗时的IO操作时,向内核发出请求,不用真正等IO操作完成,就去处理其它的请求了,当IO真正完成时会有回调或通知机制通知并完成余下工作。而一般的同步IO是当应用需要IO操作时,向操作系统发出IO Read/Write请求。同时阻塞当前应用,并等待IO返回,返回后才进行后续的操作。从这里可以看出异步IO实际是将请求的处理和IO处理并行了,这样自然能较大的提高系统的吞吐量。

maxThreads的大小:

第一点:从上面的异步IO的机制来看,实际上我们可能可以用一个很小的线程池处理较大的连接数。如当前有100个请求要被处理,处理过程中50个进程都处于IO等待的状态,所以我们实际可能只需要50就能够处理那些不处于IO等待状态的请求就能满足需要了。注意在Tomcat中是使用maxConnection这个配置参数来配置Tomcat的同时处理连接数的。

第二点:盲目的加大线程数会带来一些下面的影响。由于Tomcat处理的线程均会在操作系统中产生对应的实际线程,这就意味着对应的资源消耗(内存,SOCKET等)。另一个影响就是同时处理的请求加大可能导致JAVA内存回收的问题,不同的并发对内存的占用是不同,而实际上90%的内存都是临时变量,可以很快回收。较大的并发同时占用较多的临时变量就会导致容易撑满年青代,从而导致部分内存进入老年代,从而引起更多的Stop The World,甚至OOM,影响JVM性能。其它的影响还包括更高的CPU占用和更多的硬盘读写。这些实际都跟硬件有关。

第三点: 我们可以通过配置一个较合理的资源池,由于资源充裕,单个请求处理迅速,这样能达到最优的系统效率。但是有的时候我们并不总是追求这样的一种情况。比如下载时,单个请求的响应时间将受限于网络,下100M的包可能需要20分钟,我们就不应该通过一个较小的资源池来提升整体的效率,而应该配置一个较大的资源池,让较多用户连接上并进行下载,否则多数的用户都将会因超时被拒绝,从而造成连接上的超快,连不上的就直接被拒绝。

第四点:单个JVM的内存分配较大将导致Full Gc(Stop The World)的中断时间变得更长,影响实时性。高的可达10秒以上的停顿,这段时间所有的东西将被挂起。

配置大小优化思路:

配置时应该根据你应用的实际情况,是最占CPU,内存还是IO,最后达到一个平衡就好,下面来说明思路。

1. 自行保证服务器的资源较够用,如IO、CPU、内存。

2. 在硬件较充裕的情况下尝试以maxThreads配置300、600、1200、1800,分析Tomcat的连接时间,请求耗时,吞吐量等参数。在测试的时候需要密切注意硬盘、带宽、CPU、内存是否处于一个瓶颈情况下。

3. 其实所有的东西最后都有一个极限就是硬件。应用分CPU,IO,内存密集型,这些都会成为你最终的限制性因素。一般应用根据自己的特性划分到不同的机群中,如CPU密集型的会分到一群有更好CPU的集群中。这样可以能充分利用资源。我们以常见的内存为最终限制性因素,并假设CPU足够好,且IO很少来说明思路。通过一些压测工具,我们能容易的找到一个在300~8000的并发数的情况下一个性能的拐点,通过对比不同线程数下请求连接时间、单请求的平均响应时间,总体的吞吐量。这个拐点往往意味着此时的内存回收出现异常,JVM花了更多的时间在回收内存,我们一般可以通过打出gc日志,并使用jmeter等工具来分析得知。此时你可以尝试优化内存结构或加大内存 来解决,若不能解决,可能就意味你前一次的配置就是一个好的选择。当然这些限制因素是可能互相转换的,可能你增加了内存之后内存没有问题了,但是却导致CPU达到100%,从而导致性能下降。此时则要以CPU为最终限制性因素了。

优化测试中陷阱:

以一个下载服务器来例子说明。我们以下载10m的包来做测试,其实你会发现整个服务器的吞吐量很差,响应时间慢。但细心的人会发现此时连接服务器的时间却是很快的,也就是说服务器很快accpet了你的请求,虽然你的吞吐量不大,处理耗时也大。原因是什么呢,其实是你的带宽已经被占满了,你会发现并发下载10个文件就能占满你的所有带宽。所以此时呢你的测试时的对比对象变成了对比连接时间会更加合理。

当然你也可以通过减少包的大小,比如降到 1k,以使带宽不成为瓶颈.这样可能测试出来你的服务器并发极限量,但该并发量可能并不能反应出实际下载的情况,实际的情况就是带宽容易被占满,下载服务器会有一个很大量的连接存在的情况。

问题2. NGINX到底能带来怎么样的性能提升,或者说有什么好处?

1. 测试后发现,NGINX并不能加快响应的速度,为什么呢,因为这是由于NGINX会代理你同后端的请求。也就意味着你原来只需要建立同服务器的一次连接即可完成请求,现在变成了先同NGINX建立连接,NGINX再同后端建立连接。所以引入NGINX后带来了更多的时间消耗,两倍的SOCKET连接消耗。

2. 引入后的好处体现如下。

1) 整体的性能会有提升,通过实测后发现能很大程度上降低最大返回耗时的情况。请求返回更稳定。

2) 降低后端的资源消耗。原来由于客户端网络较慢等因素会让后端在返回数据时处于繁忙的情况,占用资源。通过NGINX向后端代理,同时由于NGINX的缓存机制,后端可以快速返回,并将资源更集中用到处理请求上,这样可以发挥后端的能力。NGINX在保持大量连接这块就得很优秀,内存,CPU都占用很少。

3) 支持非常方便的扩展,高可用性等。