某个项目上反馈投标文件导入异常慢,经过排查,发现慢在解压缩上。进一步排查发现,使用unzip命令解压缩并不慢,60MB的文件5秒就能解压缩完成,而通过Java代码解压缩需要5分钟。

通过以上信息,可以给出一个初步的结论:解压缩慢的问题和服务器本身关系不大(包括CPU、IO),大概率还是Java代码层面的问题。

既然定位是代码问题,那就将解压缩代码剥离出来,做成一个最小可复现测试用例,精简的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public static String unZip(String zipPath, String unZipPath) {
	try (ZipArchiveInputStream zis = new ZipArchiveInputStream(new FileInputStream(zipPath), "GBK")) {
		ZipArchiveEntry entry;
		while ((entry = zis.getNextZipEntry()) != null) {
			long start = System.currentTimeMillis();
			if (entry.isDirectory()) {
				String dirName = entry.getName();
				dirName = dirName.substring(dirName.lastIndexOf(File.separator) + 1);
				new File(unZipPath + File.separator + dirName).mkdirs();
			}
			else {
				String entryName = entry.getName();
				entryName = entryName.replace("\\", File.separator);
				File f = new File(unZipPath + File.separator + entryName);
				if (f.exists()) {
					new File(unZipPath + File.separator + entryName).delete();
				}
				if (!new File(f.getParent()).exists()) {
					new File(f.getParent()).mkdirs();
				}

				new File(unZipPath + File.separator).mkdirs();
				FileOutputStream os = new FileOutputStream(unZipPath + File.separator + entryName);
				int n = 0;
				byte[] content = new byte[1024 * 1024];
				long cost = 0;
				while (-1 != (n = zis.read(content))) {
					System.out.println(n + "==4==" + entry.getName());
					long start2 = System.currentTimeMillis();
					os.write(content, 0, n);
					cost += (System.currentTimeMillis() - start2);
				}
				System.out.println(cost + "==3==" + entry.getName());
				os.close();
			}
			System.out.println((System.currentTimeMillis() - start) + "==1==" + entry.getName());
		}
		return unZipPath;
	}
	catch (Exception e) {
		e.printStackTrace();
	}
	return "";
}

通过日志定位,耗时长的是在写文件内容上。一般优化写入速度的手段无非是扩大写缓冲区,减少写操作的系统调用次数。因此,首先尝试的是增加缓冲区的大小(L25,增加content字节数组长度)。但是验证下来没有效果。

此时,排查陷入了停滞,没有了明确的排查方向。在此期间,我又做了两件事:

  1. 测试磁盘性能

问题出现在文件写入上,还是得再确认一下文件的写入性能,使用dd命令测试。

1
2
time dd if=/dev/zero of=./test.dba bs=4k count=10000
time dd if=/dev/zero of=./test.dba bs=4M count=10

其中第一条命令执行需要4分钟,第二条命令执行只需要3秒。从测试结果看,增加写缓冲区大小确实可以提升写入速度。但是Java代码为什么没效果呢?

  1. 继续添加日志分析

尝试在27行while循环内增加日志,打印每次写入的耗时,分析日志后发现了问题。按照缓冲区10MB计算,压缩包中最大的一个文件为35MB,那照理这份文件只会打印4次日志,但实际打印日志次数远远多余4次。进一步,将while循环内的日志修改为上面代码中的第28行,打印每次读取的字节数,发现虽然缓冲区设置很大,但实际每次读取的字节数只有500多。

至此,问题原因终于查明:由于解压缩的ZipArchiveInputStream(ZipInputStream同样如此)每次读取的字节数有限(500左右),相当于以小缓冲区频繁地写文件,导致写性能下降。

查明了问题原因,解决方案就很简单,将原来的FileOutputStream(L23)用BufferedOutputStream包装一下:

1
BufferedOutputStream os = new BufferedOutputStream(os1, 4*1024*1024);

修改后,60MB文件解压缩解压缩从原来的5分钟,缩短到了5秒。

回顾这次问题排查过程,其中最大的盲点或误区是:认为给定长度的缓冲区,InputStream就会尽量把缓冲区填满后再返回。