当前位置: 首页 > 科技观察

运维排查注意事项:百万长连接压力测试Nginx内存溢出问题

时间:2023-03-20 15:51:27 科技观察

最近一次百万长连接压力测试中,4台32C128G的Nginx频繁出现OOM,出现问题时的内存监控如下展示。现将调查过程记录如下。现象描述这是一个压力测试环境,有数百万的websocket连接发送和接收消息。客户端jmeter用了上百台机器,4台Nginx服务器用来服务后端。简化的部署结构如下图所示。当保持百万连接不发送数据时,一切正常,Nginx内存稳定。当大量数据收发时,Nginx内存开始以每秒数百M的速度增长,直到占用内存接近128G,woker进程开始频繁OOM被系统kill掉。32个worker进程每一个占用内存接近4G。dmesg-T的输出如下所示。[FriMar1318:46:442020]Outofmemory:Killprocess28258(nginx)score30orsacrificechild[FriMar1318:46:442020]Killedprocess28258(nginx)total-vm:1092198764kB,anon-rss:3943668kB,file-rss:7之后,大量长-term连接断开,压力测试无法继续增加数据量。排查过程分析得到这个问题,首先检查Nginx和客户端两端的网络连接状态。使用ss-nt命令可以看到Nginx上有大量ESTABLISH状态的Send-Q连接,客户端的Recv-Q堆积非常多。大的。Nginx端ss部分的输出如下所示。StateRecv-QSend-QLocalAddress:PortPeerAddress:PortESTAB07920241.1.1.1:802.2.2.2:50664...在jmeter客户端抓包时偶尔会看到更多的零窗口,如下图。在这一点上,我们有一些基本的方向。第一个怀疑是jmeter客户端处理能力有限,很多消息都堆积在中转的Nginx中。为了验证思路,想办法dumpnginx的内存。因为在后期内存占用高的时候,dump内存很容易失败。在这里,转储在内存开始上升后不久就开始了。首先使用pmap查看任意一个worker进程的内存分布情况,这里是4199,使用pmap命令的输出结果如下。pmap-x4199|sort-k3-n-r00007f2340539000475240461696461696rw---[anon]...然后使用cat/proc/4199/smaps|grep7f2340539000查找某块内存的起始地址和结束地址,如下图。cat/proc/3492/smaps|grep7f23405390007f2340539000-7f235d553000rw-p0000000000:000然后用gdb连接这个进程,dump这段内存。gdb-pid4199dumpmemorymemory.dump0x7f23405390000x7f235d553000然后使用strings命令查看dump文件的可读字符串内容,可以看到有大量的请求和响应。这是坚定的,因为缓存大量消息导致内存增加。然后查看了Nginx的参数配置,location/{proxy_passhttp://xxx;proxy_set_headerX-Forwarded-Url"$scheme://$host$request_uri";proxy_redirectoff;proxy_http_version1.1;proxy_set_headerUpgrade$http_upgrade;proxy_set_headerConnection"upgrade";proxy_set_headerCookie$http_cookie;proxy_set_headerHost$host;proxy_set_headerX-Forwarded-Proto$scheme;proxy_set_headerX-Real-IP$remote_addr;proxy_set_headerX-Forwarded-For$proxy_add_x_forwarded_for;client_max_body_size512M;client_body_buffer_size64M;proxy_connect_timeout900;proxy_send_timeout900;proxy_read_timeout900;proxy_buffer_size64M;proxy_buffers6416M;proxy_busy_buffers_size256M;proxy_temp_file_write_size512M;可以看到proxy_buffers的值设置的很大。下面我们来模拟一下上下行收发速度不一致对Nginx内存占用的影响。模拟Nginx内存的增加。这里我模拟一个客户端收包比较慢,另一端是资源丰富的后端服务器,然后观察Nginx的内存是否会有变化。慢包接收客户端用golang编写,使用TCP模拟发送HTTP请求。代码如下。packagemainimport("bufio""fmt""net""time")funcmain(){conn,_:=net.Dial("tcp","10.211.55.10:80")text:="GET/demo.mp4HTTP/1.1rnHost:ya.test.mernrn"fmt.Fprintf(conn,text)for;;{_,_=bufio.NewReader(conn).ReadByte()time.Sleep(time.Second*3)println("readonebyte")}}在测试Nginx上开启pidstat监控内存变化pidstat-ppid-r11000运行上面的golang代码,Nginxworker进程的内存变化如下。04:12:13是golang程序的启动时间。可以看出,在很短的时间内,Nginx的内存使用量已经增加到464136kB(接近450M),而且还会持续很长时间。同时值得注意的是proxy_buffers的设置大小是针对单个连接的。如果发送多个连接,内存占用会不断增加。下面是同时运行两个golang进程对Nginx内存影响的结果。可以看到当两个慢客户端连接时,内存增加到900多M。最快的更改方法是将proxy_buffering设置为关闭,如下所示。proxy_bufferingoff;经过实测,在压测环境修改这个值,减小proxy_buffer_size的值后,内存稳定在20G左右,没有再次飙升。内存使用截图如下所示。在测试环境中重复前面的测试,结果如下。可以看到这次内存值增加了大约64M。为什么增加了64M?查看proxy_buffering的Nginx文档(nginx.org/en/docs/htt...当启用缓冲时,nginx会尽快收到来自代理服务器的响应,将其保存到由proxy_buffer_size和proxy_buffers指令。如果整个响应不适合内存,它的一部分可以保存到磁盘上的临时文件。写入临时文件由proxy_max_temp_file_size和proxy_temp_file_write_size指令控制。当禁用缓冲时,响应被传递到cch,在收到时立即发送。nginx不会尝试从代理服务器读取整个响应。nginx一次可以从服务器接收的数据的最大大小由proxy_buffer_size指令设置。你可以看到那就是当proxy_buffering处于on状态时,Nginx会尽可能多的接收后端服务器返回的内容,并存储在自己的缓冲区中,这个缓冲区的最大大小就是proxy_b的内存缓冲区大小*代理缓冲区。如果后端返回的消息很大,这些内存都放不下,会放到磁盘文件中。临时文件由proxy_max_temp_file_size和proxy_temp_file_write_size这两个指令决定,这里不展开。当proxy_bufferingoff时,Nginx不会从代理服务器读取尽可能多的数据,最多读取proxy_buffer_size的数据发送给客户端。Nginx的缓冲机制设计的初衷是为了解决发送端和接收端速度不一致的问题。无需缓冲,数据将直接从后端服务转发到客户端。如果客户端的接收速度足够快,可以完全关闭缓冲。.但是在海量连接的情况下,需要同时考虑资源消耗。如果有人故意伪造一个慢客户端,可以以很小的代价消耗服务器上的大量资源。其实这是非阻塞编程中的一个典型问题。接收数据不会阻塞发送数据,发送数据不会阻塞接收数据。如果Nginx两端发送和接收数据的速度不相等,缓冲区设置过大,就会出现问题。Nginx源码分析在src/event/ngx_event_pipe.c中的ngx_event_pipe_read_upstream方法中读取响应写入本地缓冲区的源码。该方法最终会调用ngx_create_temp_buf来创建内存缓冲区。创建的次数和每个缓冲区的大小由p->bufs.num(缓冲区个数)和p->bufs.size(每个缓冲区的大小)决定,这两个值就是我们在配置文件指定proxy_buffers的参数值。这部分源代码如下所示。staticngx_int_tngx_event_pipe_read_upstream(ngx_event_pipe_t*p){for(;;){if(p->free_raw_bufs){//...}elseif(p->allocatedbufs.num){//p->allocated当前Thenumberofallocatedbuffers,p->bufs.numThemaximumsizeofthenumberofbuffers/*allocateanewbufifit'sstillallowed*/b=ngx_create_temp_buf(p->pool,p->bufs.size);//创建大小为p->bufs.sizebufferif(b==NULL){returnNGX_ABORT;}p->allocated++;}}}Nginx源码调试界面如下。后记进程中还有一些辅助判断方法,比如通过strace和systemtap工具跟踪内存分配和释放过程,这里不展开。这些工具是分析黑盒程序的神器。另外,在本次压测中,还发现worker_connections参数设置不合理,导致Nginx启动后占用14G内存。这些问题在没有海量连接的情况下是很难发现的。最后,底层原理是必备技能,参数调优是一门艺术。上面提到的内容可能有误,看排错思路即可。