美文网首页
记Netty的一次的磁盘空间不足与内存溢出问题

记Netty的一次的磁盘空间不足与内存溢出问题

作者: 不加糖的开水 | 来源:发表于2018-08-19 03:39 被阅读0次
  • 背景
    • 最近在使用netty作为一个文件上传与查询的服务器,用于文件上传分析,期间踩了不少的坑,为此记录两次踩坑的经历与大家分享,由于公司的源码无法公布,这里就通过netty源码进行分析给出解决问题的方向与思路

一.关于netty的请求参数分析

  • 在某天清晨兴高采烈的去上班,在上班的路途上同事,打电话告诉我请求端口无法正常使用,返回的异常信息为java.io.IOException: No space left on device,当时还在车上的我,一看这个异常,哈哈,小儿科,肯定是服务器内存满了,待会也就小清理一下日志啊,磁盘堆积已久的文件啊之类的,so easy,结果到了公司,打开电脑连上服务器,输入以下命令:
  df -h
1534013192532.png

当时就看傻了,不对啊,项目的磁盘空是足够的,没有问题,当时就陷入了一个迷茫的排查当中,服务器明明报的是java.io.IOException: No space left on device,很明显告诉我磁盘的空间不足,但是明显磁盘是足够的,此时应有搜索引擎,立马搜索了一下度娘跟谷歌爸爸,最终查到一个原因,目录的inode满了(具体是什么自己百度,这里不是重点),这是什么鬼,奇了怪了,顺腾摸瓜的查询了所在分区的inode,结果发现没毛病,此时应有源码观察分析:
1. 由于在整个项目之中,netty充当的是https文件上传服务器,使用的是HttpPostRequestDecoder获取请求的相关参数信息,并且抛出的异常也在HttpPostRequestDecoder解析请求参数前后,此时就跟进代码:

        public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
            if (factory == null) {
                throw new NullPointerException("factory");
            }
            if (request == null) {
                throw new NullPointerException("request");
            }
            if (charset == null) {
                throw new NullPointerException("charset");
            }
            // Fill default values
            if (isMultipart(request)) {
                decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
            } else {
                decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
            }
        }

在此跟进入到decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);当中:

        public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
            if (factory == null) {
                throw new NullPointerException("factory");
            }
            if (request == null) {
                throw new NullPointerException("request");
            }
            if (charset == null) {
                throw new NullPointerException("charset");
            }
            this.request = request;
            this.charset = charset;
            this.factory = factory;
            // Fill default values
        
            setMultipart(this.request.headers().getAndConvert(HttpHeaderNames.CONTENT_TYPE));
            if (request instanceof HttpContent) {
                // Offer automatically if the given request is als type of HttpContent
                // See #1089
                offer((HttpContent) request);
            } else {
                undecodedChunk = buffer();
                parseBody();
            }
        }

对于以上代码,我注意到parseBody();,继续跟进:

        private void parseBody() {
            if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
                if (isLastChunk) {
                    currentStatus = MultiPartStatus.EPILOGUE;
                }
                return;
            }
            parseBodyMultipart();
        }

在此处会发现一段代码,parseBodyMultipart(),这里就是解析请求参数的核心方法:

        private void parseBodyMultipart() {
            if (undecodedChunk == null || undecodedChunk.readableBytes() == 0) {
                // nothing to decode
                return;
            }
            InterfaceHttpData data = decodeMultipart(currentStatus);
            while (data != null) {
                addHttpData(data);
                if (currentStatus == MultiPartStatus.PREEPILOGUE || currentStatus == MultiPartStatus.EPILOGUE) {
                    break;
                }
                data = decodeMultipart(currentStatus);
            }
        }

这么一段代码,跟进decodeMultipart()中:

        private InterfaceHttpData decodeMultipart(MultiPartStatus state) {
            switch (state) {
                case NOTSTARTED:
                    throw new ErrorDataDecoderException("Should not be called with the current getStatus");
                case PREAMBLE:
                    // Content-type: multipart/form-data, boundary=AaB03x
                    throw new ErrorDataDecoderException("Should not be called with the current getStatus");
                case HEADERDELIMITER: {
                    // --AaB03x or --AaB03x--
                    return findMultipartDelimiter(multipartDataBoundary, MultiPartStatus.DISPOSITION,
                                                  MultiPartStatus.PREEPILOGUE);
                }
                case DISPOSITION: {
                    // content-disposition: form-data; name="field1"
                    // content-disposition: form-data; name="pics"; filename="file1.txt"
                    // and other immediate values like
                    // Content-type: image/gif
                    // Content-Type: text/plain
                    // Content-Type: text/plain; charset=ISO-8859-1
                    // Content-Transfer-Encoding: binary
                    // The following line implies a change of mode (mixed mode)
                    // Content-type: multipart/mixed, boundary=BbC04y
                    return findMultipartDisposition();
                }
                case FIELD: {
                    // Now get value according to Content-Type and Charset
                    Charset localCharset = null;
                    Attribute charsetAttribute = currentFieldAttributes.get(HttpHeaderValues.CHARSET);
                    if (charsetAttribute != null) {
                        try {
                            localCharset = Charset.forName(charsetAttribute.getValue());
                        } catch (IOException e) {
                            throw new ErrorDataDecoderException(e);
                        }
                    }
                    Attribute nameAttribute = currentFieldAttributes.get(HttpHeaderValues.NAME);
                    if (currentAttribute == null) {
                        try {
                            currentAttribute = factory.createAttribute(request,
                                                                       cleanString(nameAttribute.getValue()));
                        } catch (NullPointerException e) {
                            throw new ErrorDataDecoderException(e);
                        } catch (IllegalArgumentException e) {
                            throw new ErrorDataDecoderException(e);
                        } catch (IOException e) {
                            throw new ErrorDataDecoderException(e);
                        }
                        if (localCharset != null) {
                            currentAttribute.setCharset(localCharset);
                        }
                    }
                    // load data
                    try {
                        loadFieldMultipart(multipartDataBoundary);
                    } catch (NotEnoughDataDecoderException ignored) {
                        return null;
                    }
                    Attribute finalAttribute = currentAttribute;
                    currentAttribute = null;
                    currentFieldAttributes = null;
                    // ready to load the next one
                    currentStatus = MultiPartStatus.HEADERDELIMITER;
                    return finalAttribute;
                }
                case FILEUPLOAD: {
                    // eventually restart from existing FileUpload
                    return getFileUpload(multipartDataBoundary);
                }
                case MIXEDDELIMITER: {
                    // --AaB03x or --AaB03x--
                    // Note that currentFieldAttributes exists
                    return findMultipartDelimiter(multipartMixedBoundary, MultiPartStatus.MIXEDDISPOSITION,
                                                  MultiPartStatus.HEADERDELIMITER);
                }
                case MIXEDDISPOSITION: {
                    return findMultipartDisposition();
                }
                case MIXEDFILEUPLOAD: {
                    // eventually restart from existing FileUpload
                    return getFileUpload(multipartMixedBoundary);
                }
                case PREEPILOGUE:
                    return null;
                case EPILOGUE:
                    return null;
                default:
                    throw new ErrorDataDecoderException("Shouldn't reach here.");
            }
        }

在这当中,看到那么一段代码

          // Is it a FileUpload
        Attribute filenameAttribute = currentFieldAttributes.get(HttpHeaderValues.FILENAME);
        if (currentStatus == MultiPartStatus.DISPOSITION) {
            if (filenameAttribute != null) {
                // FileUpload
                currentStatus = MultiPartStatus.FILEUPLOAD;
                // do not change the buffer position
                return decodeMultipart(MultiPartStatus.FILEUPLOAD);
            } else {
                // Field
                currentStatus = MultiPartStatus.FIELD;
                // do not change the buffer position
                return decodeMultipart(MultiPartStatus.FIELD);
            }
        } else {
            if (filenameAttribute != null) {
                // FileUpload
                currentStatus = MultiPartStatus.MIXEDFILEUPLOAD;
                // do not change the buffer position
                return decodeMultipart(MultiPartStatus.MIXEDFILEUPLOAD);
            } else {
                // Field is not supported in MIXED mode
                throw new ErrorDataDecoderException("Filename not found");
            }
        }

在这段代码中有那么一个工厂类,会生成一个attrbute对象,factory.createAttribute(request,cleanString(nameAttribute.getValue()));,跟进到方法当中
在实现的方法中,我在初始化decode的时候用了一下那么一段代码(此处有坑,埋得相当的深):

        HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MAXSIZE);
        HttpPostRequestDecoder decoder=new HttpPostRequestDecoder(factory, httpRequest, Charset.forName("UTF-8"));

HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MAXSIZE);在此处代码中,由于对netty不熟悉,在此处买下了一个深坑,这个后续道来,由前面的代码可以看到

        public class DefaultHttpDataFactory implements HttpDataFactory {
            ...
            /**
             * Proposed default MINSIZE as 16 KB.
             */
            public static final long MINSIZE = 0x4000;
            /**
             * Proposed default MAXSIZE = -1 as UNLIMITED
             */
            public static final long MAXSIZE = -1;
        
            private final boolean useDisk;
        
            private final boolean checkSize;
        
            private long minSize;
        
            private long maxSize = MAXSIZE;
        
            private Charset charset = HttpConstants.DEFAULT_CHARSET;
        
            /**
             * Keep all HttpDatas until cleanAllHttpData() is called.
             */
            private final Map<HttpRequest, List<HttpData>> requestFileDeleteMap = PlatformDependent.newConcurrentHashMap();
        
            /**
             * HttpData will be in memory if less than default size (16KB).
             * The type will be Mixed.
             */
            public DefaultHttpDataFactory() {
                useDisk = false;
                checkSize = true;
                minSize = MINSIZE;
            }
        
            public DefaultHttpDataFactory(Charset charset) {
                this();
                this.charset = charset;
            }
        
            /**
             * HttpData will be always on Disk if useDisk is True, else always in Memory if False
             */
            public DefaultHttpDataFactory(boolean useDisk) {
                this.useDisk = useDisk;
                checkSize = false;
            }
        
            public DefaultHttpDataFactory(boolean useDisk, Charset charset) {
                this(useDisk);
                this.charset = charset;
            }
            /**
             * HttpData will be on Disk if the size of the file is greater than minSize, else it
             * will be in memory. The type will be Mixed.
             */
            public DefaultHttpDataFactory(long minSize) {
                useDisk = false;
                checkSize = true;
                this.minSize = minSize;
            }
        
            public DefaultHttpDataFactory(long minSize, Charset charset) {
                this(minSize);
                this.charset = charset;
            }
            
            @Override
            public Attribute createAttribute(HttpRequest request, String name, long definedSize) {
                if (useDisk) {
                    Attribute attribute = new DiskAttribute(name, definedSize, charset);
                    attribute.setMaxSize(maxSize);
                    List<HttpData> list = getList(request);
                    list.add(attribute);
                    return attribute;
                }
                if (checkSize) {
                    Attribute attribute = new MixedAttribute(name, definedSize, minSize, charset);
                    attribute.setMaxSize(maxSize);
                    List<HttpData> list = getList(request);
                    list.add(attribute);
                    return attribute;
                }
                MemoryAttribute attribute = new MemoryAttribute(name, definedSize);
                attribute.setMaxSize(maxSize);
                return attribute;
            }
            ...
        }

DefaultHttpDataFactory是在解析请求的时候生成一个Attribute对象的,在此处要关注一个变量useDisk,从DefaultHttpDataFactory的构造方法中可以找到,这个值是在初始化时候有创建方进行赋值,如果为true则使用DiskAttribute这个对象进行变量的存储解析,DiskAttribute对象主要是用于在临时文件夹生成一个临时文件来存储属性,如果为false则进入下一步,判断是否要进行长度校验。为此,useDisk在对象初始化的时候我并没有对其赋值,则为false,程序继续往下执行,在前面的方法中,我使用的是public DefaultHttpDataFactory(long minSize)构造函数,则checkSize = true;,由此可以得出结论,产生MixedAttribute对象,该对象的注释Mixed implementation using both in Memory and in File with a limit of size,大致可以知道,如果长度超过限制,则会生成一个File,也就是DiskAttribute对象来存请求的参数,如果请求参数长度小于长度的限制,则使用MemoryAttribute,见文知意,这个对象是将请求参数存放在内存当中。按照正常逻辑思维来看,一个请求参数不会太大,通过以下代码可以看到

        @Override
        public void setContent(ByteBuf buffer) throws IOException {
            checkSize(buffer.readableBytes());
            if (buffer.readableBytes() > limitSize) {
                if (attribute instanceof MemoryAttribute) {
                    // change to Disk
                    attribute = new DiskAttribute(attribute.getName());
                    attribute.setMaxSize(maxSize);
                }
            }
            attribute.setContent(buffer);
        }

netty是用过limitSize这个长度来限制的,而默认的长度由之前的代码可以看到minSize = MINSIZE;限制在16K,应该是通过MemoryAttribute来存储请求参数,那么除了文件上传,其余的属性就不会产生任何的问题。为此,我们回顾之前的初始化代码new DefaultHttpDataFactory(DefaultHttpDataFactory.MAXSIZE);,通过这里看到,这个长度,在初始化的时候,传入了DefaultHttpDataFactory.MAXSIZE这么一个值,但是这个值由之前的源代码可以看到public static final long MAXSIZE = -1;,为此,这个坑就很明显了,当buffer.readableBytes() > limitSize时,由于limitSize的值为-1,导致了change to Disk的发生,产生了DiskAttribute对象

二.对于文件磁盘空间不足问题解决

public class DiskAttribute extends AbstractDiskHttpData implements Attribute {
    public static String baseDirectory;

    public static boolean deleteOnExitTemporaryFile = true;

    public static final String prefix = "Attr_";

    public static final String postfix = ".att";

    /**
     * Constructor used for huge Attribute
     */
    public DiskAttribute(String name) {
        this(name, HttpConstants.DEFAULT_CHARSET);
    }

    public DiskAttribute(String name, Charset charset) {
        super(name, charset, 0);
    }

    public DiskAttribute(String name, String value) throws IOException {
        this(name, value, HttpConstants.DEFAULT_CHARSET);
    }

    public DiskAttribute(String name, String value, Charset charset) throws IOException {
        super(name, charset, 0); // Attribute have no default size
        setValue(value);
    }
}
//其父类的两个方法
public void setContent(ByteBuf buffer) throws IOException {
    if (buffer == null) {
        throw new NullPointerException("buffer");
    }
    try {
        size = buffer.readableBytes();
        checkSize(size);
        if (definedSize > 0 && definedSize < size) {
            throw new IOException("Out of size: " + size + " > " + definedSize);
        }
        if (file == null) {
            file = tempFile();
        }
        if (buffer.readableBytes() == 0) {
            // empty file
            if (!file.createNewFile()) {
                throw new IOException("file exists already: " + file);
            }
            return;
        }
        FileOutputStream outputStream = new FileOutputStream(file);
        try {
            FileChannel localfileChannel = outputStream.getChannel();
            ByteBuffer byteBuffer = buffer.nioBuffer();
            int written = 0;
            while (written < size) {
                written += localfileChannel.write(byteBuffer);
            }
            buffer.readerIndex(buffer.readerIndex() + written);
            localfileChannel.force(false);
        } finally {
            outputStream.close();
        }
        setCompleted();
    } finally {
        // Release the buffer as it was retained before and we not need a reference to it at all
        // See https://github.com/netty/netty/issues/1516
        buffer.release();
    }
}

private File tempFile() throws IOException {
    String newpostfix;
    String diskFilename = getDiskFilename();
    if (diskFilename != null) {
        newpostfix = '_' + diskFilename;
    } else {
        newpostfix = getPostfix();
    }
    File tmpFile;
    if (getBaseDirectory() == null) {
        // create a temporary file
        tmpFile = File.createTempFile(getPrefix(), newpostfix);
    } else {
        tmpFile = File.createTempFile(getPrefix(), newpostfix, new File(
            getBaseDirectory()));
    }
    if (deleteOnExit()) {
        tmpFile.deleteOnExit();
    }
    return tmpFile;
}
  • 在这一部分可以看到,DiskAttribute通过tempFile创建文件保存参数,由于项目是跑在tomcat中的,先不说代码的编写问题,由于上文的那个坑,导致了当有post请求的时候,就会在tomcat的temp的目录中产生
1534617419885.png

类似的临时文件,项目在服务器跑了两个月后,由于在netty请求过后没有主动的去清理这一类文件,导致了临时文件一致在无限的堆积。最终导致了temp目录中产生了上千万的文件,撑爆了temp目录的indode(具体自己百度),导致了文件无法往内写入,为此导致了java.io.IOException: No space left on device的异常的抛出。随后,在每当请求结束的时候,通过调用decodercleanfiler直接取清理临时文件。

三.对于内存溢出问题的分析

  • 在上一个问题件解决后,服务器频繁的出现了内存溢出的问题,即使jvm最大内存配置到了8G,也无法避免溢出的问题,为此,在tomcat的参数中加上了这么一个配置

    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/apache-tomcat-7.0.88/bin/

    配置之后,根据内存溢出后自动会在bin目录下产生一个jvm溢出时候的dump,通过mat进行内存溢出的分析,得到的异常报告如下:


    1534619622657.png

    为此,从内存堆积着一堆无法GC的LinkedHashMap,而这个Map来自于java.io.DeleteOnExitHook这个对象。为此,从源码中可以看到:

    private File tempFile() throws IOException {
        String newpostfix;
        String diskFilename = getDiskFilename();
        if (diskFilename != null) {
            newpostfix = '_' + diskFilename;
        } else {
            newpostfix = getPostfix();
        }
        File tmpFile;
        if (getBaseDirectory() == null) {
            // create a temporary file
            tmpFile = File.createTempFile(getPrefix(), newpostfix);
        } else {
            tmpFile = File.createTempFile(getPrefix(), newpostfix, new File(
                getBaseDirectory()));
        }
        if (deleteOnExit()) {
            tmpFile.deleteOnExit();
        }
        return tmpFile;
    }

在新建temp文件的时候会调用tmpFile.deleteOnExit();这么一句话,跟进源码:

    public void deleteOnExit() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkDelete(path);
        }
        if (isInvalid()) {
            return;
        }
        DeleteOnExitHook.add(path);
    }
    package java.io;
    
    import java.util.*;
    import java.io.File;
    
    /**
     * This class holds a set of filenames to be deleted on VM exit through a shutdown hook.
     * A set is used both to prevent double-insertion of the same file as well as offer
     * quick removal.
     */
    
    class DeleteOnExitHook {
        private static LinkedHashSet<String> files = new LinkedHashSet<>();
        static {
            // DeleteOnExitHook must be the last shutdown hook to be invoked.
            // Application shutdown hooks may add the first file to the
            // delete on exit list and cause the DeleteOnExitHook to be
            // registered during shutdown in progress. So set the
            // registerShutdownInProgress parameter to true.
            sun.misc.SharedSecrets.getJavaLangAccess()
                .registerShutdownHook(2 /* Shutdown hook invocation order */,
                    true /* register even if shutdown in progress */,
                    new Runnable() {
                        public void run() {
                           runHooks();
                        }
                    }
            );
        }
    
        private DeleteOnExitHook() {}
    
        static synchronized void add(String file) {
            if(files == null) {
                // DeleteOnExitHook is running. Too late to add a file
                throw new IllegalStateException("Shutdown in progress");
            }
    
            files.add(file);
        }
    
        static void runHooks() {
            LinkedHashSet<String> theFiles;
    
            synchronized (DeleteOnExitHook.class) {
                theFiles = files;
                files = null;
            }
    
            ArrayList<String> toBeDeleted = new ArrayList<>(theFiles);
    
            // reverse the list to maintain previous jdk deletion order.
            // Last in first deleted.
            Collections.reverse(toBeDeleted);
            for (String filename : toBeDeleted) {
                (new File(filename)).delete();
            }
        }
    }
  • DeleteOnExitHook主要用于当File对象调用deleteOnExit方法时,会在DeleteOnExitHookLinkedHashMap存放相关的文件信息,用于在JVM正常退出的时候进行文件的清理
  • 为此,由于DeleteOnExitHook中的LinkedHashMap中存在着tomcat的temp目录下的大量引用信息,在decoder.cleanFiles();中,虽然清除了临时文件,但是,文件已经正常了清理,检查代码发现没有什么异常问题,最终调用的是file.delete(),但是file.delete()并没有对LinkedHashMap中的索引进行删除,导致了文件的堆积。
    @Override
    public void cleanRequestHttpData(HttpRequest request) {
        List<HttpData> fileToDelete = requestFileDeleteMap.remove(request);
        if (fileToDelete != null) {
            for (HttpData data: fileToDelete) {
                data.delete();
            }
            fileToDelete.clear();
        }
    }
    
    @Override
    public void delete() {
        if (fileChannel != null) {
            try {
                fileChannel.force(false);
                fileChannel.close();
            } catch (IOException e) {
                logger.warn("Failed to close a file.", e);
            }
            fileChannel = null;
        }
        if (! isRenamed) {
            if (file != null && file.exists()) {
                if (!file.delete()) {
                    logger.warn("Failed to delete: {}", file);
                }
            }
            file = null;
        }
    }

四.总结

  • 对于以上问题,最终根源在于DefaultHttpDataFactory(DefaultHttpDataFactory.MAXSIZE),为此绕了一个很大的弯子,只需要将其改为DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE)即可,为此记录主要是对于netty对请求参数的解析以及相关的坑的留意。
  • 在此次的问题中在于自己对netty的使用上还不熟悉,但是在这个解决问题过程中,自己学会通过jmap进行jvm分析,通过jstack进行线程堆栈的分析。

相关文章

  • 记Netty的一次的磁盘空间不足与内存溢出问题

    背景最近在使用netty作为一个文件上传与查询的服务器,用于文件上传分析,期间踩了不少的坑,为此记录两次踩坑的经历...

  • OOM

    概念 内存溢出 - VM在分配内存时内存不足导致内存溢出 内存泄漏 - 有对象在释放时没有完全...

  • java数据库之sql优化

    一、导致SQL慢的原因 (1)硬件问题:网络慢,IO慢,内存不足,吞吐量小,磁盘空间满 (2)Sql写法问题 (3...

  • 栈内存溢出的问题

    制造一个栈内存溢出的问题。 错误原因:因为test()方法不停的进栈,栈的内存不足,就是抛出StackOverfl...

  • Netty之ByteBuf深入分析

    netty中的PlatformDependent Netty之ByteBuf深入分析[TOC]分析思路内存与内存管...

  • JVM 常见内容汇总

    面试题 对象 对象的创建 分配内存 对象头 内存溢出 内存溢出与内存泄漏 内存溢出:系统无法再分配内存空间。 内存...

  • 内存泄漏

    1、内存溢出和内存泄漏的区别,常见内存泄漏 内存溢出:一个程序被分配的内存空间是一定的,当申请的内存容量不足时就会...

  • Android面试 内存泄漏连环炮

    面试问题 什么是内存泄露,什么是内存溢出 什么情况下会造成堆溢出、栈溢出 常见造成内存泄露的情况 常见造成内存溢出...

  • OC内存管理

    内存管理的方式 为什么要管理内存 内存问题体现在两个-----内存溢出、野指针异常。 内部溢出 野指针异常 内存管...

  • JVM学习系列学习七

    5:实战:内存溢出的定位与分析 内存溢出在日常工作中,这个错误很容易遇到。遇到内存溢出,首先我们需要快速定位内存溢...

网友评论

      本文标题:记Netty的一次的磁盘空间不足与内存溢出问题

      本文链接:https://www.haomeiwen.com/subject/fjwqiftx.html