复现条件:
10个用户同时下载800MB的文件,下载到一半,发现串口有重启日志
测试结果:
下载的10个文件,本地有10个文件,但是文件内容不完整,大概200MB左右
排查
排查1:在Java的启动命令上加上-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
重启后未发现dump文件。
排查2:可能是由于系统资源耗尽导致。
1、CPU资源:使用top命令,观察CPU占用,并没有拉满
2、内存耗尽:可能是物理内存和交换内存被完全占用,但是暂未找到工具监控该指标(使用top命令时,返回结果并没有看见swap信息)
使用命令观察:watch -n 1 free -m
,每秒刷新一次
返回信息:
Every 1.0s: free -m 2024-07-29 15:28:46
total used free shared buff/cache available
Mem: 15947 2838 8273 3096 4835 8673
Swap: 0 0 0
在下载过程中,free与available都在逐渐减小
Every 1.0s: free -m 2024-07-29 15:28:02
total used free shared buff/cache available
Mem: 15947 6135 148 3096 9662 11
Swap: 0 0 0
nginx报错:2024/07/29 15:28:03 [alert] 6525#6525: worker process 4061 exited on signal 9
此时free内存又上升了
Every 1.0s: free -m 2024-07-29 15:28:03
total used free shared buff/cache available
Mem: 15947 3865 6287 3096 5794 6169
Swap: 0 0 0
发现下载客户端也报错了,设备也没有进行重启, 即未复现了。
增加并发量:20个用户同时下载800MB文件,又复现了该问题
最后显示的内存信息:
Every 1.0s: free -m 2024-07-29 15:37:20
total used free shared buff/cache available
Mem: 15947 4790 134 3096 11021 0
Swap: 0 0 0
此时available 值为0
设备自动重启后查看nginx的error日志,也发现了
2024/07/29 15:37:22 [alert] 6525#6525: worker process 25749 exited on signal 9
使用命令查看:dmesg | grep -i memory
root@localhost:~# dmesg | grep -i memory
[ 727.746017] Out of memory: Kill process 6533 (nginx) score 102 or sacrifice child
[ 2409.509693] ? out_of_memory+0x1a7/0x490
[ 2409.509879] Out of memory: Kill process 4062 (nginx) score 148 or sacrifice child
[ 2657.723430] ? out_of_memory+0x1a7/0x490
[ 2657.723619] Out of memory: Kill process 4061 (nginx) score 143 or sacrifice child
[ 3214.595561] ? out_of_memory+0x1a7/0x490
[ 3214.595749] Out of memory: Kill process 32282 (ncore) score 114 or sacrifice child
[ 3214.846882] ? out_of_memory+0x1a7/0x490
[ 3214.847071] Out of memory: Kill process 32282 (ncore) score 114 or sacrifice child
[ 3215.073249] ? out_of_memory+0x1a7/0x490
[ 3215.073428] Out of memory: Kill process 32282 (ncore) score 114 or sacrifice child
[ 3216.056706] ? out_of_memory+0x1a7/0x490
[ 3216.056886] Out of memory: Kill process 25749 (nginx) score 70 or sacrifice child
3、IO资源耗尽:
原因分析:
java操作导致内存占用持续增加
1、堆内存:由于设置了堆内存溢出生成dump文件,但是复现时并未生成该文件
2、栈内存:用同样的并发量测试小文件下载,并没有出现异常,栈内存应该没有问题。
3、元空间:启动命令设置:-XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=128M
4、直接内存
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8257 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=x.x.x.x
开启jmx,用于远程监控,监控发现元空间正常。
那么有可能是直接内存的问题,查看下载的代码:
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
IOUtils.copyLarge(inputStream, outputStream);
getOutputStream() 方法返回的输出流对象 (ServletOutputStream) 通常是一个普通的 Java 对象,存储在堆内存中。
如果你在使用的输出流实现了直接内存(通常通过 ByteBuffer.allocateDirect() 来实现),那么数据可能会存放在 Java 的直接内存中。输出流如 ServletOutputStream 可能会使用底层的 I/O 库,这些库可能会使用直接内存来提高性能。
但是IOUtils.copyLarge并没有使用直接内存,而是使用的大小为8192的数组作为缓冲区。
排查3:
将项目复制到自己虚拟机上,使用相同并发量测试,并使用free监控内存变化,发现 free内存与available内存一直保持在稳定状态,最终文件下载成功。所以内存不是java项目占用的。
排查4:
执行top命令后,按s,监控RSS的变化,发现nginx的一个进程飙升。
Mem total:16329908 anon:3377392 map:3205580 free:162544
slab:134520 buf:196564 cache:10758988 dirty:96 write:0
Swap total:0 free:0
PID VSZ^VSZRW^ RSS (SHR) DIRTY (SHR) STACK COMMAND
6537 1832m 1815m 1480m 7556 1480m 7552 132 nginx: worker process
确定是nginx吃内存
修改nginx配置:
location /downloads/ {
proxy_buffering off; # 关闭缓存
}
关闭缓存后,10的并发量下载确实正常了,但是20的并发量测试的时候,项目中出现有关数据库的连接超时的异常,经过定位发现是由于下载时一直开着事务,而数据库连接池的默认大小是10,导致后续的请求无法获取到数据库连接(刚开始一直以为是由于内存的问题,导致数据库操作无法正常进行,后续发现内存还未升高时,数据库操作也失败,才开始定位数据库连接异常的问题)。
当关闭缓存时,Nginx不再将文件内容存储在内存中,而是直接从磁盘或上游服务器传输到客户端。这意味着,如果客户端的下载速度较慢,Nginx可能需要维持打开的连接状态更长时间,从而占用更多的内存资源(可能会影响大文件下载)。
proxy_max_temp_file_size指令用于设置单个临时文件的最大允许大小。如果代理服务器的响应内容超过了这个值,Nginx将停止将更多内容写入临时文件,而是选择与上游服务器同步传递内容,而不是继续缓冲到硬盘.
proxy_temp_file_write_size指令定义了一次访问能写入临时文件的大小。这个值通常是proxy_buffer_size和proxy_buffers中设置的缓冲区大小的两倍。当临时文件的内容达到这个限制时,Nginx会等待现有数据被传输出去后再继续写入新的数据
排查4:
在排查3中,部署到虚拟机上测试时,并没有配置nginx,后来配置nginx的反向代理,下载时used内存一直处于小幅度波动,属于稳定状态。
甚至将排查3中认为是有关buffer的配置也复制到虚拟机的nginx配置中,下载内存还是稳定状态。
最后,领导说将设备上看不懂的nginx配置都注释掉,再次进行测试,发现此时下载内存处于稳定状态了。
通过控制变量,最后确定是由于modsecurity on;这个配置导致内存一直不释放,used内存持续增长。
反思与总结:
1、对linux的命令不熟悉,当从逻辑上确定了项目不会持续吃内存后,没有想到去查看到底是什么服务吃内存。
当找到是nginx吃内存后,也没有想到去使用工具定位内存的具体情况(后来有了解到hcache工具,但是并没有进行安装)
2、对nginx的配置不熟悉。由于内存持续上升,一直以为是nginx有关缓存区配置的问题,通过网络搜索后,修改了相关配置:
proxy_max_temp_file_size 0m;
proxy_buffers 4 128k;
proxy_busy_buffers_size 256k;
甚至是关闭了缓存:proxy_buffering off;
,发现现实结果与理论中的结果不一致,内存还是持续上升。由于之前没有测试过这几个参数的使用,并不确定是配置配错了还是没生效。
3、对报错没有认真分析,java有关数据库的报错,没有及时分析报错内容,有一定程度上误导了解决问题的方向。但是收获就是对proxy_buffers相关配置更加了解了。
4、考虑不周全:当使用虚拟机进行测试时,只考虑到了buffer相关配置,没有考虑到将其他配置进行对比测试。有一定程度上是由于总结3中的分析结果导致的方向错误。
如果这里能相信自己的配置没有问题的话,就应该排除是buffer的影响了,要去排查其他配置了。还是领导排查能力强。
5、modsecurity on:
ModSecurity是一个开源的Web应用防火墙(WAF),它通过配置和规则来提供安全保护。在ModSecurity中,规则是核心组成部分,它们决定了如何处理HTTP流量,以检测和阻止恶意活动。
常见的规则配置场景
- 防御SQL注入攻击:通过检查请求参数中的特定模式来阻止SQL注入尝试。
- 防止跨站脚本攻击(XSS):检测请求中的脚本代码,并阻止这些代码被执行。
- 文件上传防护:限制允许上传的文件类型,防止上传潜在危险的文件。
- IP地址黑名单/白名单:允许或阻止来自特定IP地址的流量。
- HTTP协议违规检测:确保HTTP请求遵守既定的协议规范。
由于此处是下载大文件的问题,主要看防火墙规则中有关响应体的配置
SecResponseBodyAccess On # 启用了ModSecurity对HTTP响应体的访问
SecResponseBodyMimeType text/plain text/html text/xml # 指定了ModSecurity将检查响应体的MIME类型。
SecResponseBodyLimit 524288 # 设置了ModSecurity可以缓存的最大响应体大小,单位为字节。这里的数值表示524,288字节,即512KB。
SecResponseBodyLimitAction ProcessPartial # 当响应体超过预设限制时的处理行为。ProcessPartial选项指示ModSecurity处理它已经收到的响应体部分,并允许剩余的部分继续传递。
其他的规则也看不懂,也没发现对响应体做了什么规则检测,不知道为什么一直占用内存。啊啊啊啊啊
询问ai后,给的结果:
当SecResponseBodyAccess On指令被激活后,ModSecurity Web应用防火墙会在响应体发送到客户端之前对其进行缓冲和分析。这个过程涉及到以下几个方面的检测:
-
响应内容检测:ModSecurity会检查响应体中是否含有潜在的安全威胁,如SQL注入、跨站脚本(XSS)、敏感信息泄露等。这是通过内置的规则集或自定义规则来实现的。
-
响应状态码和头部分析:ModSecurity可能会检查响应的HTTP状态码和头部信息,以确保它们符合预期,并且没有被篡改。
-
响应内容大小限制:通过SecResponseBodyLimit指令设置的响应体大小限制,ModSecurity可以防止过大的响应体被发送,这可能是由于恶意活动或系统配置错误造成的。
-
性能影响:启用SecResponseBodyAccess会增加内存消耗和响应延迟,因为ModSecurity需要在内存中缓冲整个响应体以进行分析.
-
日志记录:ModSecurity会记录有关响应体的安全事件,这些信息可以用于事后分析和安全审计。
-
响应内容修改:在某些情况下,ModSecurity可能会修改响应内容,以移除或替换掉检测到的恶意内容,这通常是通过配置规则来实现的.
综上所述,SecResponseBodyAccess On的激活允许ModSecurity对HTTP响应进行深度检查,以增强Web应用的安全性。