最近测试提了个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 ErrorOOMKiller,莫名奇妙,文件上传怎么会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,使用堆外内存只需要在堆上分配一个指针指向堆外内存即可

FileChannel#in
buffer
FileChannel#out
buffer
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();
        }
    }
Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐