背景
公司的存储项目主要代码都是java实现, 使用引用计数判断一个数据块是否要被回收.
当数据块的引用计数变为0时, 会触发回收这个数据块, 把它移动到trash 文件夹.
回收数据块前需要删除重删库中数据块的指纹, 这个过程中, 有可能有新的数据引用到这些数据块.
为了解决这个问题,又减少对性能影响. 规定对如果数据块对应的数据指纹最近被查询, 那么这个数据块不能被回收.
这导致了存储长期运行, 会有一些数据块已经没用但是没有被回收. 为此需要定期扫描全部的数据块, 尝试对所有数据块进行一次回收,从而回收那些无用的数据块.
因此存储开发了一个遍历文件夹过程找出数据块的功能.使用java 的Files.walk 接口实现的.
在客户环境实际使用这个功能时, 发现远比直接使用linux find 命令慢. 使用存储的遍历功能执行了5个小时没有结束, 而使用find命令一个小时可以执行完.
测试环境
调研了一下, 类似linux find的工具, 有一个bfs工具
在测试环境对同一个文件夹进行遍历, 过滤出数据块.
存储功能花费时间: 1161秒
find: 114秒
bfs: 63秒
发现, java的Files.walk 遍历找数据块的性能只有linux find 的十分之一. 然后bfs比find的性能快了接近一倍.
bfs 作者写了篇文章讲述bfs的实现原理. 主要是使用了linux readdir 接口.
实现不同遍历文件夹的接口, 测试性能
为此我写了一个程序, 使用java 实现调用不同接口遍历文件夹, 测试它们的性能差异. github地址: https://github.com/IUHHUI/benmark-traversal-dir
0 JAVA_LIST_FILES 使用java File listFiles接口 实现
1 JAVA_FILES_WALk 使用java Files.walk接口 实现
2 JAVA_FILES_PARALLEL 使用java Files.walk接口+stream并行 实现
3 AWS_CRT 使用aws crt 库的DirectoryTraversal.traverse 实现
4 JNR_READ_DIR 使用java jnr调用linux readdir接口 实现
5 JNR_READ_DIR_FORK_JOIN 使用java jnr调用linux readdir接口+java ForkJoin框架 实现
6 JNR_TRAVERSAL_SO_SINGLE 使用java jnr调用c语言写的遍历文件夹so 实现
7 JNR_TRAVERSAL_SO_PARALLEL 使用java jnr调用c语言写的多线程遍历文件夹so 实现
测试脚本:
for i in $(seq 0 7) ; do
echo 3 > /proc/sys/vm/drop_caches
sync
sleep 2
rm -f "/meta/data${i}";
java -jar benmark-traversal-dir-1.0-SNAPSHOT-jar-with-dependencies.jar "${i}" /data/ "/meta/data${i}";
done
测试结果如下:
JAVA_LIST_FILES Time elapsed: 1217766ms. result : SUCCESS
JAVA_FILES_WALk Time elapsed: 1233453ms. result : SUCCESS
JAVA_FILES_PARALLEL Time elapsed: 1165995ms. result : SUCCESS
AWS_CRT Time elapsed: 1403918ms. result : SUCCESS
JNR_READ_DIR Time elapsed: 100373ms. result : SUCCESS
JNR_READ_DIR_FORK_JOIN Time elapsed: 36833ms. result : SUCCESS
JNR_TRAVERSAL_SO_SINGLE Time elapsed: 113996ms. result : SUCCESS
JNR_TRAVERSAL_SO_PARALLEL Time elapsed: 38974ms. result : SUCCESS
测试发现 JNR_READ_DIR_FORK_JOIN
的性能是 使用java 原生接口 的40倍.
火焰图分析
使用 阿里的arthas 生成火焰图, 分析 java 原生接口到底慢在哪里.
使用java原生接口java/io/File.ListFiles时, 需要使用java/io/File.isFile判断是否文件, 从火焰图看到基本都在 java/io/File.isFile 上,而这个函数底层基本都是在调用vfs_statfs.
使用java原生接口Files.walk时, 从火焰图看到基本都在 java/nio/file/FileTreeWalker.visit 上, 而这个函数底层基本都是在调用vfs_statfs.
而使用linux readdir, 可以避免查询每一个文件的属性.