不同实现递归遍历文件夹的性能差异

背景

公司的存储项目主要代码都是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, 可以避免查询每一个文件的属性.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.