Blog

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

    背景

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

  • The Spring Expression Language (SpEL)

    The Spring Expression Language (SpEL) is a powerful expression language that supports querying and manipulating an object graph at runtime.

    https://www.baeldung.com/spring-expression-language

    https://jishu.dev/2021/05/23/spring-expression-language/

  • 通过arthas 打印spring项目的配置

    spring程序运行起来后, 有时候想看看配置是否正确或者spring项目的一些默认配置项是什么.可以通过阿里云的arthas 将内存中的spring 配置类打印出来.

    查找实例的类加载器

    [arthas@439]$ sc -d org.springframework.boot.autoconfigure.web.ServerProperties
     class-info        org.springframework.boot.autoconfigure.web.ServerProperties
     code-source       /usr/share/octopus/lib/spring-boot-autoconfigure-3.2.1.jar
     name              org.springframework.boot.autoconfigure.web.ServerProperties
     isInterface       false
     isAnnotation      false
     isEnum            false
     isAnonymousClass  false
     isArray           false
     isLocalClass      false
     isMemberClass     false
     isPrimitive       false
     isSynthetic       false
     simple-name       ServerProperties
     modifier          public
     annotation        org.springframework.boot.context.properties.ConfigurationProperties
     interfaces
     super-class       +-java.lang.Object
     class-loader      +-jdk.internal.loader.ClassLoaders$AppClassLoader@5ffd2b27
                         +-jdk.internal.loader.ClassLoaders$PlatformClassLoader@34cd072c
     classLoaderHash   5ffd2b27
    

    打印类情况

    [arthas@439]$ vmtool -c  5ffd2b27 --action getInstances  --className org.springframework.boot.autoconfigure.web.ServerProperties  -x 2
    @ServerProperties[][
        @ServerProperties[
            port=@Integer[19091],
            address=null,
            error=@ErrorProperties[org.springframework.boot.autoconfigure.web.ErrorProperties@2b101c68],
            forwardHeadersStrategy=null,
            serverHeader=null,
            maxHttpRequestHeaderSize=@DataSize[8192B],
            shutdown=@Shutdown[IMMEDIATE],
            ssl=@Ssl[org.springframework.boot.web.server.Ssl@4d349bbc],
            compression=@Compression[org.springframework.boot.web.server.Compression@195400d1],
            http2=@Http2[org.springframework.boot.web.server.Http2@1654070d],
            servlet=@Servlet[org.springframework.boot.autoconfigure.web.ServerProperties$Servlet@3d8cbaf3],
            reactive=@Reactive[org.springframework.boot.autoconfigure.web.ServerProperties$Reactive@548c3f85],
            tomcat=@Tomcat[org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat@6d67ef4d],
            jetty=@Jetty[org.springframework.boot.autoconfigure.web.ServerProperties$Jetty@5b3df183],
            netty=@Netty[org.springframework.boot.autoconfigure.web.ServerProperties$Netty@3be9335],
            undertow=@Undertow[org.springframework.boot.autoconfigure.web.ServerProperties$Undertow@36fbf4d0],
        ],
    ]
    
  • 如何用iftop查看某一个进程的tcp流量

    背景

    写的存储系统平常是3个节点部署。 每个节点都会部署volume-node, meta-node, storage-node三个服务。

    volume-node 和meta-node 服务都会和storage-node 建立多个tcp连接, 并且往2222端口写数据。

    现在希望观察storage-node 上2222端口来自于meta node 服务的流量情况。

    实现

    ps -ef |grep meta
    
    ### 获取过滤的端口。
    netstat -anp \
    |grep (meta_node_pid) |grep :2222 \
    |awk '{print $4}'|awk -F ':' '{print $2}' \
    |xargs -i echo "src port {} or" \
    |tr '\n' ' '
    

    示例iftop 过滤流量

    [root@node3 ~]# netstat -anp |grep 2222|grep $(cat /var/run/octopus/meta-node.pid ) |awk '{print $4}'|awk -F ':' '{print $2}' |xargs -i echo "src port {} or" |tr '\n' ' '
    src port 40696 or src port 36020 or src port 36026 or src port 36030 or src port 36022 or src port 41066 or src port 41064 or src port 36034 or src port 36028 or src port 41062 or src port 41070 or src port 41058 or src port 40692 or src port 36032 or src port 41068 or src port 40694 or src port 40698 or src port 36024 or src port 40706 or src port 41056 or src port 40690 or src port 41060 or src port 40688 or src port 40700 or [root@node3 ~]#
    
    #得到过滤的src 端口。去掉最后一个'or', 然后使用iftop -f 过滤流量。
    iftop -i enp101s0f0 -P -B -N \
     -f "src port 40696 or src port 36020 or src port 36026 or src port 36030 or src port 36022 or src port 41066 or src port 41064 or src port 36034 or src port 36028 or src port 41062 or src port 41070 or src port 41058 or src port 40692 or src port 36032 or src port 41068 or src port 40694 or src port 40698 or src port 36024 or src port 40706 or src port 41056 or src port 40690 or src port 41060 or src port 40688 or src port 40700"
    
    
  • linux安装多个版本的golang

    直接下载压缩包

    下载压缩包,然后改变软连

    [wjh@node1 ~]$ go version
    go version go1.22.5 linux/amd64
    [wjh@node1 ~]$ which go
    /usr/bin/go
    [wjh@node1 ~]$ ls -al /usr/bin/|grep go$
    lrwxrwxrwx   1 root root          24 Jan 12  2022 go -> /usr/local/golang/bin/go
    [wjh@node1 ~]$
    [wjh@node1 ~]$ ls -al /usr/local/golang
    total 12
    drwxr-xr-x   5 root root   65 Aug  5 17:09 .
    drwxr-xr-x. 27 root root 4096 Jul 14 03:07 ..
    lrwxrwxrwx   1 root root   30 Aug  5 17:09 bin -> /usr/local/golang/go1.22.5/bin
    drwxr-xr-x  10 wjh  wjh   257 Jan  7  2022 go1.17.6
    drwxr-xr-x  10 root root 4096 Aug 31  2023 go1.21.0
    drwxr-xr-x  10 root root 4096 Jun 28 04:11 go1.22.5
    [wjh@node1 ~]$
    

    这样要切换golang 版本我要自己更新软连.

    使用 go install 安装

    [wjh@node1 ~]$ go install golang.org/dl/go1.23.0@latest
    
    [wjh@node1 ~]$ go1.23.0 version
    go1.23.0: not downloaded. Run 'go1.23.0 download' to install to /home/wjh/sdk/go1.23.0
    
    [wjh@node1 ~]$ go1.23.0 download
    Downloaded   0.0% (   16384 / 73590011 bytes) ...
    Downloaded  13.7% (10059776 / 73590011 bytes) ...
    Downloaded  36.2% (26607616 / 73590011 bytes) ...
    Downloaded  57.1% (42024928 / 73590011 bytes) ...
    Downloaded  75.0% (55197536 / 73590011 bytes) ...
    Downloaded  90.2% (66387760 / 73590011 bytes) ...
    Downloaded 100.0% (73590011 / 73590011 bytes)
    Unpacking /home/wjh/sdk/go1.23.0/go1.23.0.linux-amd64.tar.gz ...
    Success. You may now run 'go1.23.0'
    
    [wjh@node1 ~]$ go1.23.0 version
    go version go2.23.0 linux/amd64