记录一次下载邮箱附件到本地过慢的问题排查

505 阅读2分钟

简介

最近做了一个功能,是读取邮箱的附件文件并保存到本地的功能,使用的Java mail下工具包,使用imap协议进行读取。 在上到生产环境时,偶然会出现保存文件到本地会超时。经过排查发现这些文件都比较大,所以初步就认为是文件过大导致应用性能变差,所以保存超时。 然后就在测试过程中发现:10mb的文件10分钟都无法保存下来,1mb的文件也需要2分钟。而10kb左右的文件基本1秒钟就可保存完成。

排查过程

第一步:排查jvm性能情况

使用ps -ef | grep java查看应用pid(使用jps也可以)

使用jinfo pid 查看jvm的启动参数

使用jstat -gcutil pid 5000(每5s执行一次)

观察发现在保存到本地时,并没有发生YGC, FGC次数都没有增加

总结:说明内存,gc等没有限制文件保存

第二步:查看磁盘io情况

因为项目是docker环境,所以只验证了一下磁盘写速度,指令如下:

dd if=/dev/zero of=test bs=16k count=10000 oflag=direct

写速度基本可以保证在百兆每秒,所以也不是磁盘io的问题

第三步 日志

外部原因都已经排查完了,只好通过日志来查看到底是哪里慢了,先上代码

    public static void saveFile(BodyPart fileBodyPart, Path path, Integer timeout) throws IOException {
        // 写入文件
        long timeoutMillis = timeout * 1000L;
        File file = path.toFile();
        long startTime = System.currentTimeMillis();
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
             BufferedInputStream bis = new BufferedInputStream(fileBodyPart.getInputStream())) {
            int len;
            long maxTimeMillis = System.currentTimeMillis() + timeoutMillis;
            byte[] bytes = new byte[1024 * 8];
            while ((len = bis.read(bytes)) != -1 && System.currentTimeMillis() < maxTimeMillis) {
                bos.write(bytes, 0, len);
            }
            log.info("保存文件{}到本地,耗时{}ms",file.getName(), System.currentTimeMillis() - startTime);
            if (System.currentTimeMillis() >= maxTimeMillis && file.exists()) {
                file.delete();
                throw new IOException("文件保存超时");
            }
        } catch (Exception e) {
            log.error("文件[{}]保存失败:[{}]", path.getFileName(), e);
            throw new IOException("文件保存失败:", e);
        } finally {
            try {
                if (fileBodyPart.getInputStream() != null) {
                    fileBodyPart.getInputStream().close();
                }
            } catch (MessagingException e) {
                log.error("");
            }
        }
    }

我在bos.write(bytes, 0, len);语句前后都增加了日志用以记录读取邮件inputStream,写入本地文件outputStream来查看到底是哪里慢。 排查后发现,bisRead会调用次数巨多,并且时间很慢!就此定位到问题:读取邮件的inputStream次数多且慢 这时候就去登上stackOverFlow,果然就查到了问题:stackoverflow.com/questions/4… 定位到mail.imap.partialfetch,mail.imap.fetchsize这两个参数。看下api介绍

mail.imap.fetchsize: int Partial fetch size in bytes. Defaults to 16K.

mail.imap.partialfetch: boolean Controls whether the IMAP partial-fetch capability should be used. Defaults to true.

所以imap协议默认每次都是部分读取,且读取大小为16k,这就能解释为何10k左右的文件读取速度ms级,而mb级别文件保存极慢的原因了。 知道问题了,那解决办法就清晰了,直接上代码了,把fetchSize调整到1m,数据读取立刻就快了

    public Store createStore() throws MessagingException {
        // 连接邮件服务器
        String protocol = "imap";
        Properties props = new Properties();
        props.setProperty("mail.store.protocol", protocol);
        props.setProperty("mail.imap.port", "993");
        props.setProperty("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.setProperty("mail.imap.socketFactory.fallback", "false");
        props.setProperty("mail.imap.partialfetch","false");
        props.setProperty("mail.imap.fetchsize", "1048576");
        Session session = Session.getInstance(props);
        Store store = session.getStore(protocol);
        store.connect("xxhost", 993, "xxuserName", "xxpassword");
        return store;
        }