在k8s下坑人的OOM问题
最近测试提了个bug,说批量图片上传会返回500错误。然后我弄了两个zip压缩包一个3MB,一个120MB,发现3MB的压缩包每次上传都没问题,而120MB的每次上传都会报500,于是上spring cloud gateway网关查看了下日志,发现网关报了一个莫名奇妙的错误Connection prematurely closed BEFORE response意思为连接在响应前过早关闭了。显得莫
最近测试提了个bug
,说批量图片上传会返回500
错误。然后我弄了两个zip
压缩包一个3MB
,一个120MB
,发现3MB
的压缩包每次上传都没问题,而120MB
的每次上传都会报500
,于是上spring cloud gateway
网关查看了下日志,发现网关报了一个莫名奇妙的错误
Connection prematurely closed BEFORE response
意思为连接在响应前过早关闭了。显得莫名奇妙,然后拿着这个报错百度google
了一番,没找到答案,于是换个方向。小文件没有问题,而大文件出问题,那么会不会是spring cloud gateway
不支持大文件上传?然后到官方找对应issue
,果然找到了一个类似的issue
是17年的。dose gateway support large file transfer
,然而并未找到答案。又去问了下测试,以前可不可以,测试说,以前是正常的。就是这段时间不行的。原因是这个接口改动了。然后本地起项目调试了下,发现一切正常。然后又测试了下测试环境的,发现每次返回这个错误的时候,相关服务就宕机重启了。好吧,去看下pod的状态
kubectl -n test describe po/xxxx
查看Last Error
为OOMKiller
,莫名奇妙,文件上传怎么会oom呢?然后去看了下相关配置
k8s 的配置
resources:
limits:
cpu: 2
memory: "4Gi"
requests:
cpu: 2
memory: "4Gi"
jvm配置
"-Xms4096m -Xmx4096m -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m
我的天,开发在部署项目的时候啥都不看的,这样配置肯定导致native memory不足啊。接着又去看了下代码。
所以里面调整下参数,预留500MB给native重启下,在测试下,果断就好了。
所以部署项目的时候还是要稍微注意下此类问题。不然排查问题又要老半天。
那么这里引出一个问题。不管有没有以上问题,这个堆外内存预留本身就是必要的。
为什么要预留的原因
- 操作系统本身需要一定的内存空间运行
- java的应用除了metaspace本身占用了堆外内存外,还有其它的方式可能占用堆外内存,比如nio,jni等。
关于堆外内存
- 堆外内存的最大大小可以通过 -XX:MaxDirectMemorySize 设置
- Java VisualVM中可以安装Buffer Pools插件监控堆外内存
所以规划好内存是一件极其重要的事情。比如给docker分配了多少内存,给堆多少,给元空间多少,应用有没有使用直接内存的情况,该预留多少。
而此次oom的原因就是没有堆内存做规划,图片批量上传,由于文件比较大,使用到了nio的堆外内存.
关于NIO
在NIO中使用FileChannel作为读写文件的通道,并且读写使用了缓冲区。而这个缓冲区有两种实现,一种是使用堆内存HeapByteBuffer,第二种是使用堆外内存DirectByteBuffer,使用堆外内存只需要在堆上分配一个指针指向堆外内存即可
使用堆外内存单从性能上来说的优势是少了一层操作系统到jvm虚拟机间的内存拷贝,使得性能得到了一定层度上的提升。从垃圾回收上来说,减少了GC的负担。
而本次的批量图片上传接口就使用到了nio的堆外内存,加上配置问题,导致了OOM,代码如下
public static void copy(FileInputStream fis, FileOutputStream fos) {
FileChannel in = null;
FileChannel out = null;
try {
in = fis.getChannel();
out = fos.getChannel();
int maxCount = (64 * 1024 * 1024) - (32 * 1024);
long size = in.size();
long position = 0;
while (position < size) {
position += in.transferTo(position, maxCount, out);
}
} catch (Exception e) {
throw ExceptionUtils.wrap2Runtime(e);
} finally {
if(in != null) {try{in.close();}catch(Exception e){}}
if(out != null) {try{out.close();}catch(Exception e){}}
if(fis != null) {try{fis.close();}catch(Exception e){}}
if(fos != null) {try{fos.close();}catch(Exception e){}}
}
}
//该方法需要三个参数一个是当前位置,一个是传输的最大大小和需要输出到哪里去
public long transferTo(long position, long maxCount, WritableByteChannel out) throws IOException {
this.ensureOpen();
if (!var5.isOpen()) {
throw new ClosedChannelException();
} else if (!this.readable) {
throw new NonReadableChannelException();
} else if (var5 instanceof FileChannelImpl && !((FileChannelImpl)var5).writable) {
throw new NonWritableChannelException();
//判断当前位置是否大于0,传输的最大大小是否大于0
} else if (position>= 0L && maxCount>= 0L) {
long var6 = this.size();//获取输入流大小
if (position> var6) {
return 0L;
} else {
//将缓冲区大小和Ineger比较取小,也就是一次传输的大小最大不能超过2GB
int var8 = (int)Math.min(maxCount, 2147483647L);
//判断文件大小-已传输的大小是否小于需传输的大小,如果小于则让需要传入的大小变更为当前大小。
if (var6 - position< (long)var8) {
var8 = (int)(var6 - position);
}
long var9;
if ((var9 = this.transferToDirectly(position, var8, var5)) >= 0L) {
return var9;
} else {
return (var9 = this.transferToTrustedChannel(position, (long)var8, var5)) >= 0L ? var9 : this.transferToArbitraryChannel(position, var8, var5);
}
}
} else {
throw new IllegalArgumentException();
}
}
更多推荐
所有评论(0)