Repository: juicedata/juicefs
Pull request #2718 (2022-09-10 05:32:21)

docs: doctor command reference
add docs for doctor command

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=juicedata_juicefs&pullRequest=2718)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=juicedata_juicefs&pullRequest=2718&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=juicedata_juicefs&pullRequest=2718) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=juicedata_juicefs&pullRequest=2718&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/.github/actions/clickhouse/action.yml b/.github/actions/clickhouse/action.yml
index dc374dc6..d62c53fa 100644
--- a/.github/actions/clickhouse/action.yml
+++ b/.github/actions/clickhouse/action.yml
@@ -22,9 +22,9 @@ runs:
         path: |
           ~/.cache/go-build
           ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        key: ${{ runner.os }}-go
         restore-keys: |
-          ${{ runner.os }}-go-
+          ${{ runner.os }}-go
 
     - name: Build linux target
       shell: bash
@@ -96,11 +96,11 @@ runs:
       shell: bash
       run: |
         cd /data/jfs/
-        git clone https://github.com/sanwan/chdata.git
+        git clone https://github.com/sanwan/chdata.git || (rm -rf chdata;git clone https://github.com/sanwan/chdata.git)
         cd chdata
         cat lineorder.tbl.gz* >lineorder.tbl.gz
         gzip -dk lineorder.tbl.gz
-        git clone https://github.com/sanwan/actionfile.git
+        git clone https://github.com/sanwan/actionfile.git || (rm -rf actionfile;git clone https://github.com/sanwan/actionfile.git)
         mv actionfile/*.tbl .
         rm lineorder.tbl.gz*
         ls -lrth
diff --git a/.github/actions/filebench/action.yml b/.github/actions/filebench/action.yml
new file mode 100644
index 00000000..1533a59f
--- /dev/null
+++ b/.github/actions/filebench/action.yml
@@ -0,0 +1,121 @@
+name: 'JuiceFS Filebench Action'
+description: 'JuiceFS Filebench Action'
+inputs:
+  workload:
+    description: 'type of the workload'
+    required: true
+    default: 'none'
+
+runs:
+  using: "composite"
+  steps:
+    - name: Set up Go
+      uses: actions/setup-go@v2
+      with:
+        go-version: '1.18.x'
+
+    - name: Set up Docker
+      uses: docker-practice/actions-setup-docker@master
+
+    - uses: actions/cache@v3
+      with:
+        path: |
+          ~/.cache/go-build
+          ~/go/pkg/mod
+        key: ${{ runner.os }}-go
+        restore-keys: |
+          ${{ runner.os }}-go
+
+    - name: Build linux target
+      shell: bash
+      run: make juicefs
+
+    - name: Run Redis
+      shell: bash
+      run: |
+        sudo docker run -d --name redis -v redis-data:/data  \
+        -p 6379:6379  redis redis-server --appendonly yes
+
+
+    - name: Setup minio
+      shell: bash
+      run: |
+        docker run -d -p 9000:9000 --name minio \
+                  -e "MINIO_ACCESS_KEY=minioadmin" \
+                  -e "MINIO_SECRET_KEY=minioadmin" \
+                  -v /tmp/data:/data \
+                  -v /tmp/config:/root/.minio \
+                  minio/minio server /data
+        sleep 5
+
+    - name: Juicefs Format
+      shell: bash
+      run: |
+        pwd
+        echo "hello"
+        ls
+        sudo ./juicefs format --trash-days 0  --storage minio --bucket http://127.0.0.1:9000/mypics \
+           --access-key minioadmin \
+           --secret-key minioadmin \
+           redis://127.0.0.1:6379/1 pics
+
+    - name: Juicefs Mount
+      shell: bash
+      run: |
+        sudo ./juicefs mount -d redis://127.0.0.1:6379/1 /data/jfs --no-usage-report &
+
+
+    - name: Install Dependency
+      shell: bash
+      run: |
+        wget -O filebench.tar.gz https://github.com/filebench/filebench/releases/download/1.5-alpha3/filebench-1.5-alpha3.tar.gz
+        tar -zxvf filebench.tar.gz
+        cd filebench-1.5-alpha3
+        #sudo apt install -y bison
+        #sudo apt install -y byacc
+        #sudo apt install -y flex
+        ./configure
+        make
+        sudo make install
+        filebench -h
+        echo 0 >randomize_va_space
+        sudo cp randomize_va_space /proc/sys/kernel/
+        echo "randomize_va_space"
+        cat /proc/sys/kernel/randomize_va_space
+
+
+    - name: Run Workload
+      shell: bash
+      run: |
+        cd /usr/local/share/filebench/workloads/
+        sudo chmod 777 *
+        if [ ${{inputs.workload}} == "varmail" ];then
+          echo "run varmail workload"
+          sudo sed -i "s?/tmp?/data/jfs?" varmail.f
+          sudo sed -i "s?run 60?run 1800?" varmail.f
+          cat varmail.f
+          sudo filebench -f varmail.f
+        elif [ ${{inputs.workload}} == "webserver" ];then
+          echo "run webserver workload"
+          sudo sed -i "s?/tmp?/data/jfs?" webserver.f
+          sudo sed -i "s?run 60?run 1800?" webserver.f
+          cat webserver.f
+          sudo filebench -f webserver.f
+        elif [ ${{inputs.workload}} == "videoserver" ];then
+          echo "run videoserver worload"
+          sudo sed -i "s?/tmp?/data/jfs?" videoserver.f
+          sudo sed -i "s?$filesize=10g?$filesize=100m?" videoserver.f
+          sudo sed -i "s?$numpassivevids=194?$numpassivevids=100?" videoserver.f
+          sudo echo "run 1800" >>videoserver.f
+          cat videoserver.f
+          sudo filebench -f videoserver.f
+        fi
+
+    - name: Log
+      shell: bash
+      if: ${{ always() }}
+      run: |
+        df -lh
+        echo "juicefs log"
+        sudo tail -n 1000 /var/log/juicefs.log
+        grep "<FATAL>:" /var/log/juicefs.log && exit 1 || true
\ No newline at end of file
diff --git a/.github/actions/fio/action.yml b/.github/actions/fio/action.yml
index fead90b3..458b2de5 100644
--- a/.github/actions/fio/action.yml
+++ b/.github/actions/fio/action.yml
@@ -74,9 +74,9 @@ runs:
         path: |
           ~/.cache/go-build
           ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        key: ${{ runner.os }}-go
         restore-keys: |
-          ${{ runner.os }}-go-
+          ${{ runner.os }}-go
           
     - name: Build linux target
       run: |
diff --git a/.github/actions/gateway/action.yml b/.github/actions/gateway/action.yml
deleted file mode 100644
index e7d2d2b8..00000000
--- a/.github/actions/gateway/action.yml
+++ /dev/null
@@ -1,197 +0,0 @@
-name: 'JuiceFS Gateway Action'
-description: 'JuiceFS gateway action'
-inputs:
-  meta_url:  
-    description: 'meta url'
-    required: true
-    default: ''
-  file_count:  
-    description: 'count of files to upload to gateway'
-    required: true
-    default: '100'
-  file_size:
-    description: 'size of each file in human read format, eg 1K, 1M, 1G.'
-    required: true
-    default: '4K'
-  threads:
-    description: 'thread number for juicefs sync command'
-    required: true
-    default: 10
-  isolation_level:
-    description: 'isolation level for mysql, include read uncommitted, read committed, repeatable read, serializable'
-    required: true
-    default: "repeatable read"
-  mount_point:
-    description: 'mount point'
-    required: true
-    default: /tmp/myjfs
-  volume_name:
-    description: 'volume name'
-    required: true
-    default: myjfs
-  
-runs:
-  using: "composite"
-  steps:
-    - name: Checkout
-      uses: actions/checkout@v2
-      with:
-        fetch-depth: 1
-
-    - name: Set up Go
-      uses: actions/setup-go@v2
-      with:
-        go-version: '1.18.x'
-
-    - uses: actions/cache@v3
-      with:
-        path: |
-          ~/.cache/go-build
-          ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-        restore-keys: |
-          ${{ runner.os }}-go-
-
-    - name: Build linux target
-      run: |
-        #if it is self ruuner
-        hostname | grep bench && export GOPATH=/usr/local/go && export HOME=/root
-        make juicefs 
-        #wget -q https://github.com/juicedata/juicefs/releases/download/v1.0.0-beta3/juicefs-1.0.0-beta3-linux-amd64.tar.gz
-        #tar -xzf juicefs-1.0.0-beta3-linux-amd64.tar.gz
-      shell: bash
-
-    - name: Install tools
-      run: | 
-        wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
-        chmod +x mc 
-      shell: bash
-
-    - name: Init
-      shell: bash
-      run: | 
-        set -x
-        mp=${{inputs.mount_point}}
-        meta_url=${{inputs.meta_url}}
-        volume=${{inputs.volume_name}}
-        test -d $mp && ./juicefs umount -f $mp
-        ./juicefs status $meta_url && UUID=$(./juicefs status $meta_url | grep UUID | cut -d '"' -f 4) || echo "meta not exist"
-        if [ -n "$UUID" ];then
-          ./juicefs destroy --force $meta_url $UUID
-        fi
-        test -d /var/jfs/$volume && rm -rf /var/jfs/$volume || true
-        echo finished
-      
-    - name: create database
-      shell: bash
-      run: | 
-        meta_url=${{inputs.meta_url}}
-        isolation_level="${{inputs.isolation_level}}"
-        db_name=$(basename $meta_url | awk -F? '{print $1}')
-        if [[ "$meta_url" == mysql* ]]; then
-          user=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $1}')
-          password=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $2}')
-          test -n "$password" && password="-p$password" || password=""
-          host=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $1}')
-          port=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $2}')
-          test -z "$port" && port="3306"
-          mysql -u$user $password -h $host -P $port -e "set global transaction isolation level $isolation_level;" 
-          mysql -u$user $password -h $host -P $port -e "show variables like '%isolation%;'" 
-          mysql -u$user $password -h $host -P $port -e "drop database if exists $db_name; create database $db_name;" 
-        elif [[ "$meta_url" == postgres* ]]; then
-          export PGPASSWORD="postgres"
-          printf "\set AUTOCOMMIT on\ndrop database if exists $db_name; create database $db_name; " |  psql -U postgres -h localhost
-          printf "\set AUTOCOMMIT on\nALTER DATABASE $db_name SET DEFAULT_TRANSACTION_ISOLATION TO '$isolation_level';" |  psql -U postgres -h localhost
-        fi      
-      
-    - name: start gateway
-      shell: bash
-      run: |
-        meta_url=${{inputs.meta_url}}
-        mp=${{inputs.mount_point}}
-        volume=${{inputs.volume_name}}
-        export MINIO_ROOT_USER=minioadmin
-        export MINIO_ROOT_PASSWORD=minioadmin
-        ./juicefs format $meta_url $volume --trash-days 0
-        if [[ "$meta_url" == badger* ]]; then
-          ./juicefs gateway $meta_url localhost:8080 --no-usage-report /tmp/access1.log &
-        else
-          ./juicefs gateway $meta_url localhost:8080 --access-log /tmp/access1.log &
-          #./juicefs gateway $meta_url localhost:9000 --access-log /tmp/access1.log &
-          #./juicefs gateway $meta_url localhost:9001 --access-log /tmp/access2.log &
-          #sudo cp .github/workflows/resources/load-balancer.conf /etc/nginx/conf.d/load-balancer.conf
-          #sudo rm /etc/nginx/sites-enabled/default
-          #sudo systemctl restart nginx
-        fi
-      
-    - name: Sync with multiple process
-      shell: bash
-      run: |
-        meta_url=${{inputs.meta_url}}
-        file_size=${{inputs.file_size}}
-        file_count=${{inputs.file_count}}
-        threads=${{inputs.threads}}
-        mp=${{inputs.mount_point}}
-        volume=${{inputs.volume_name}}
-        
-        dd if=/dev/urandom of=file iflag=fullblock,count_bytes bs=4k count="$file_size" > /dev/null
-        mkdir data
-        for i in $(seq 1 $file_count); do
-          cp file data/file$i
-        done
-        start=`date +%s`
-        declare -a pids   
-        ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:8080/$volume/data/ --no-https -p $threads &
-        pids+=($!)
-        ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:8080/$volume/data/ --no-https -p $threads &
-        pids+=($!)
-        ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:8080/$volume/data/ --no-https -p $threads &
-        pids+=($!)
-        wait "${pids[@]}"
-        rm -rf $HOME/.juicefs/cache/ || true
-        # ./mc alias set minio http://localhost:9000 minioadmin minioadmin --api S3v4
-        # ./mc mb minio/$volume
-        # ./mc cp --recursive data/  minio/$volume/data
-        end=`date +%s`
-        time=$((end-start))
-        echo time cost is: $time second
-        killall juicefs 
-        sleep 3
-        ./juicefs mount -d $meta_url $mp
-        diff -ur data/ $mp/data/ 
-        echo "diff succeed"
-        ./juicefs umount  $mp --force
-      
-    - name: Sync with empty dir
-      run: |
-        meta_url=${{inputs.meta_url}}
-        threads=${{inputs.threads}}
-        mp=${{inputs.mount_point}}
-        volume=${{inputs.volume_name}}
-        export MINIO_ROOT_USER=minioadmin
-        export MINIO_ROOT_PASSWORD=minioadmin
-        ./juicefs mdtest $meta_url test --dirs 10 --depth 2 --files 10 --threads 10 --no-usage-report
-        ./juicefs gateway $meta_url localhost:8080 --access-log /tmp/access1.log &
-        sleep 10
-        mkdir emptydir
-        declare -a pids  
-        ./juicefs sync emptydir/ s3://minioadmin:minioadmin@localhost:8080/$volume/test/ --delete-dst --no-https -p $threads &
-        pids+=($!)
-        ./juicefs sync emptydir/ s3://minioadmin:minioadmin@localhost:8080/$volume/test/ --delete-dst --no-https -p $threads &
-        pids+=($!)
-        ./juicefs sync emptydir/ s3://minioadmin:minioadmin@localhost:8080/$volume/test/ --delete-dst --no-https -p $threads &
-        pids+=($!)
-        wait "${pids[@]}"
-        killall juicefs
-        sleep 3
-        ./juicefs mount -d $meta_url $mp
-        [ -d "$mp/test/" ] && exit 1 
-        ./juicefs umount  $mp --force
-      shell: bash
-
-    - name: log
-      if: ${{ always() }}
-      shell: bash
-      run: | 
-        tail -300 ~/.juicefs/juicefs.log
-        grep "<FATAL>:" ~/.juicefs/juicefs.log && exit 1 || true
\ No newline at end of file
diff --git a/.github/actions/load/action.yml b/.github/actions/load/action.yml
deleted file mode 100644
index 5bd93178..00000000
--- a/.github/actions/load/action.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-name: 'Load Action'
-description: 'Juicefs load action'
-inputs:
-  meta_url:  
-    description: 'meta url'
-    required: true
-    default: ''
-  load_file:  
-    description: 'path of the file to load'
-    required: true
-    default: ''
-  
-runs:
-  using: "composite"
-  steps:
-    - name: Set up Go
-      uses: actions/setup-go@v2
-      with:
-        go-version: '1.18.x'
-
-    - uses: actions/cache@v3
-      with:
-        path: |
-          ~/.cache/go-build
-          ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-        restore-keys: |
-          ${{ runner.os }}-go-
-
-    - name: Build linux target
-      run: |
-        make juicefs 
-      shell: bash
-
-    - name: Test
-      run: |
-        mount_point=/tmp/juicefs-load-test
-        meta_url=${{inputs.meta_url}}
-        load_file=${{inputs.load_file}}
-        echo meta_url is: $meta_url
-        db_name=$(basename $meta_url | awk -F? '{print $1}')
-        if [[ "$meta_url" == mysql* ]]; then
-          user=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $1}')
-          password=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $2}')
-          test -n "$password" && password="-p$password" || password=""
-          host=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $1}')
-          port=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $2}')
-          test -z "$port" && port="3306" 
-          mysql -u$user $password -h $host -P $port -e "drop database if exists $db_name; create database $db_name;" 
-        elif [[ "$meta_url" == postgres* ]]; then
-          export PGPASSWORD="postgres"
-          printf "\set AUTOCOMMIT on\ndrop database if exists $db_name; create database $db_name; " |  psql -U postgres -h localhost
-          printf "\set AUTOCOMMIT on\nALTER DATABASE $db_name SET DEFAULT_TRANSACTION_ISOLATION TO '$isolation_level';" |  psql -U postgres -h localhost
-        fi
-        echo `date`, start load
-        sudo ./juicefs load $meta_url $load_file
-        echo `date`, finish load
-        echo `date`, start dump
-        sudo ./juicefs dump $meta_url dump.json
-        echo `date`, finish dump
-        sudo ./juicefs mount $meta_url $mount_point -d
-
-      shell: bash
-
-    - name: log
-      if: ${{ always() }}
-      shell: bash
-      run: | 
-        tail -300 /var/log/juicefs.log
-        grep "<FATAL>:" /var/log/juicefs.log && exit 1 || true
\ No newline at end of file
diff --git a/.github/actions/rmr/action.yml b/.github/actions/rmr/action.yml
index 233554cf..0539e6b6 100644
--- a/.github/actions/rmr/action.yml
+++ b/.github/actions/rmr/action.yml
@@ -27,9 +27,9 @@ runs:
         path: |
           ~/.cache/go-build
           ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        key: ${{ runner.os }}-go
         restore-keys: |
-          ${{ runner.os }}-go-
+          ${{ runner.os }}-go
 
     - name: Build linux target
       run: |
diff --git a/.github/actions/sysbenchrndrw/action.yml b/.github/actions/sysbenchrndrw/action.yml
index c811cae5..170ca6ff 100644
--- a/.github/actions/sysbenchrndrw/action.yml
+++ b/.github/actions/sysbenchrndrw/action.yml
@@ -22,9 +22,9 @@ runs:
         path: |
           ~/.cache/go-build
           ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        key: ${{ runner.os }}-go
         restore-keys: |
-          ${{ runner.os }}-go-
+          ${{ runner.os }}-go
 
     - name: Build linux target
       shell: bash
diff --git a/.github/actions/sysbenchseqrw/action.yml b/.github/actions/sysbenchseqrw/action.yml
index 8e149fba..15f3cd7c 100644
--- a/.github/actions/sysbenchseqrw/action.yml
+++ b/.github/actions/sysbenchseqrw/action.yml
@@ -22,9 +22,9 @@ runs:
         path: |
           ~/.cache/go-build
           ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        key: ${{ runner.os }}-go
         restore-keys: |
-          ${{ runner.os }}-go-
+          ${{ runner.os }}-go
 
     - name: Build linux target
       shell: bash
diff --git a/.github/actions/vdbench/action.yml b/.github/actions/vdbench/action.yml
index 1a5eef34..0a189aaa 100644
--- a/.github/actions/vdbench/action.yml
+++ b/.github/actions/vdbench/action.yml
@@ -70,9 +70,9 @@ runs:
         path: |
           ~/.cache/go-build
           ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        key: ${{ runner.os }}-go
         restore-keys: |
-          ${{ runner.os }}-go-
+          ${{ runner.os }}-go
 
     - name: Build linux target
       shell: bash
diff --git a/.github/actions/version-compatible-hypo/action.yml b/.github/actions/version-compatible-hypo/action.yml
deleted file mode 100644
index 8418906a..00000000
--- a/.github/actions/version-compatible-hypo/action.yml
+++ /dev/null
@@ -1,108 +0,0 @@
-name: 'Version Compatible Hypo'
-description: 'Version Compatible Hypo action'
-inputs:
-  old_juicefs_version:
-    description: 'old juicefs'
-    required: true
-    default: ''
-  meta_url:  
-    description: 'meta url'
-    required: true
-    default: 'redis://localhost/1'
-  storage:
-    description: 'storage'
-    required: true
-    default: "minio"
-
-runs:
-  using: "composite"
-  steps:  
-
-    - name: create database
-      shell: bash
-      run: | 
-        meta_url=${{inputs.meta_url}}
-        db_name=$(basename $meta_url | awk -F? '{print $1}')
-        if [[ "$meta_url" == mysql* ]]; then
-          user=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $1}')
-          password=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $2}')
-          test -n "$password" && password="-p$password" || password=""
-          host=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $1}')
-          port=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $2}')
-          test -z "$port" && port="3306"
-          mysql -u$user $password -h $host -P $port -e "drop database if exists $db_name; create database $db_name;" 
-        elif [[ "$meta_url" == postgres* ]]; then
-          export PGPASSWORD="postgres"
-          printf "\set AUTOCOMMIT on\ndrop database if exists $db_name; create database $db_name; " |  psql -U postgres -h localhost
-        fi
-
-    - name: Download old version
-      shell: bash
-      run: | 
-        filename=juicefs-${{inputs.old_juicefs_version}}-linux-amd64.tar.gz
-        echo filename is: $filename
-        url=$(curl -s https://api.github.com/repos/juicedata/juicefs/releases | grep browser_download_url | grep $filename | awk -F\" '{print $4}' | head -1)
-        echo download url is: $url
-        wget -q $url
-        tar -zxf $filename
-        mv juicefs juicefs-${{inputs.old_juicefs_version}}
-        
-    - name: Set up Go
-      uses: actions/setup-go@v2
-      with:
-        go-version: '1.18.x'
-
-    - uses: actions/cache@v3
-      with:
-        path: |
-          ~/.cache/go-build
-          ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-        restore-keys: |
-          ${{ runner.os }}-go-
-
-    - name: Make Juicefs
-      shell: bash
-      run: | 
-        make juicefs
-        
-
-    - name: Setup minio
-      shell: bash
-      run: |
-        docker run -d -p 9000:9000 --name minio \
-                    -e "MINIO_ACCESS_KEY=minioadmin" \
-                    -e "MINIO_SECRET_KEY=minioadmin" \
-                    -v /tmp/data:/data \
-                    -v /tmp/config:/root/.minio \
-                    minio/minio server /data
-        
-    - name: Install tools
-      shell: bash
-      run: | 
-        wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
-        chmod +x mc
-        sudo mv mc /usr/local/bin
-        sudo apt install redis-tools
-        sudo pip install hypothesis
-        sudo pip install minio
-        
-    - name: Test
-      shell: bash
-      run: |
-        export META_URL=${{inputs.meta_url}}
-        export STORAGE=${{inputs.storage}}
-        version=`./juicefs version | awk -F" " '{print $3}' | awk -F+ '{print $1}'`
-        mv juicefs juicefs-$version
-        export NEW_JFS_BIN="juicefs-$version"
-        export OLD_JFS_BIN="juicefs-${{inputs.old_juicefs_version}}"
-
-        # python3 .github/scripts/test_action_log.py
-        python3 .github/scripts/testVersionCompatible.py
-
-    - name: Upload command log
-      if: ${{ always() }}
-      uses: actions/upload-artifact@v3
-      with:
-        name: command.log
-        path: command.log
diff --git a/.github/actions/version-compatible/action.yml b/.github/actions/version-compatible/action.yml
deleted file mode 100644
index 233ca33b..00000000
--- a/.github/actions/version-compatible/action.yml
+++ /dev/null
@@ -1,229 +0,0 @@
-name: 'Version Compatible'
-description: 'Version Compatible action'
-inputs:
-  meta_url:  
-    description: 'meta url'
-    required: true
-    default: ''
-  mount_point:
-    description: 'mount point'
-    required: true
-    default: /tmp/myjfs
-  volume_name:
-    description: 'volume name'
-    required: true
-    default: myjfs
-  storage:
-    description: 'storage'
-    required: true
-    default: "minio"
-  bucket:
-    description: 'bucket'
-    required: true
-    default: "http://127.0.0.1:9000/juicefs-version-compatible"
-  access_key:
-    description: 'access key'
-    required: true
-    default: "minioadmin"
-  sercret_key:
-    description: 'secret key'
-    required: true
-    default: "minioadmin"
-  backward_versions:
-    description: 'backward versions'
-    required: true
-    default: 5
-runs:
-  using: "composite"
-  steps:  
-    - name: Set up Go
-      uses: actions/setup-go@v2
-      with:
-        go-version: '1.18.x'
-
-    - uses: actions/cache@v3
-      with:
-        path: |
-          ~/.cache/go-build
-          ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-        restore-keys: |
-          ${{ runner.os }}-go-
-
-    - name: Build linux target
-      run: |
-        make juicefs
-        mv juicefs jfs_new
-        echo juicefs new version: $(./jfs_new -V)
-      shell: bash
-
-    - name: create database
-      shell: bash
-      run: | 
-        meta_url=${{inputs.meta_url}}
-        db_name=$(basename $meta_url | awk -F? '{print $1}')
-        if [[ "$meta_url" == mysql* ]]; then
-          user=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $1}')
-          password=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $2}')
-          test -n "$password" && password="-p$password" || password=""
-          host=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $1}')
-          port=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $2}')
-          test -z "$port" && port="3306"
-          mysql -u$user $password -h $host -P $port -e "drop database if exists $db_name; create database $db_name;" 
-        elif [[ "$meta_url" == postgres* ]]; then
-          export PGPASSWORD="postgres"
-          printf "\set AUTOCOMMIT on\ndrop database if exists $db_name; create database $db_name; " |  psql -U postgres -h localhost
-        fi
-    - name: Test
-      shell: bash
-      run: | 
-        # # Consider command as failed when any component of the pipe fails:
-        # https://stackoverflow.com/questions/1221833/pipe-output-and-capture-exit-status-in-bash
-        set -o pipefail
-        ./mc config host add minio http://127.0.0.1:9000 minioadmin minioadmin
-        tests=(
-            "format:jfs_new, format:jfs_old, destroy:jfs_new"
-            "format:jfs_new, format:jfs_old, destroy:jfs_old"
-            "format:jfs_old, format:jfs_new, destroy:jfs_old"
-            "format:jfs_old, format:jfs_new, destroy:jfs_new"
-            "format:jfs_old, mount:jfs_old, write_file1_100M:jfs_old, umount:jfs_old, dump_backup1:jfs_old, flushmeta:-, load_backup1:jfs_new,  config:jfs_new, fsck:jfs_new, mount:jfs_new, read_file1:jfs_new, umount:jfs_new, fsck:jfs_new, destroy:jfs_new "
-            "format:jfs_old, mount:jfs_old, write_file1_100M:jfs_old, umount:jfs_old, format:jfs_new, mount:jfs_new, read_file1:jfs_new, umount:jfs_new, fsck:jfs_new, destroy:jfs_new"
-            "format:jfs_old, mount:jfs_old, write_file1_10K:jfs_old, umount:jfs_old, format:jfs_new, mount:jfs_new, read_file1:jfs_new, umount:jfs_new, fsck:jfs_new,  destroy:jfs_new"
-            "format:jfs_old, mount:jfs_old, write_file1_10K:jfs_old, write_file2_10K:jfs_old, umount:jfs_old, dump_backup1:jfs_old, flushmeta:-, load_backup1:jfs_new, config_encrypt:jfs_new, mount:jfs_new, read_file1:jfs_new, read_file2:jfs_new, umount:jfs_new, fsck:jfs_new, destroy:jfs_new "
-        )    
-        urls=($(curl -s https://api.github.com/repos/juicedata/juicefs/releases | grep browser_download_url | grep linux-amd64.tar.gz | awk -F\" '{print $4}' | head -${{ inputs.backward_versions }}))
-        for url in "${urls[@]}"; do 
-            echo download url is: $url
-            wget -q $url
-            tar -zxf $(basename $url)
-            rm $(basename $url)
-            mv juicefs jfs_old
-            echo juicefs new version: $(./jfs_new -V)
-            echo juicefs old version: $(./jfs_old -V)
-            old_rev=$(./jfs_old -V | awk -F- '{print $1}' | awk -F ' ' '{print $3}')
-            if [[ "$old_rev" < "1.0.0" ]]; then
-              echo "Warning: we don't test compatibily for version under 1.0.0"
-              break
-            fi
-            for test in "${tests[@]}"; do
-                echo 'test is ' $test
-                echo 'new version is:' $(./jfs_new -V) 
-                echo 'old version is:' $(./jfs_old -V) 
-                IFS=',' read -a command_arr <<< "$test"
-                for command in "${command_arr[@]}"; do
-                    jfs_bin=$(echo $command | awk -F: '{print $2}'| xargs)
-                    jfs_cmd=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $1}')
-                    echo 'jfs_bin is ' $jfs_bin, "cmd is "  $jfs_cmd
-                    case "$jfs_cmd" in
-                      "format") 
-                          ./mc admin user remove minio juicedata || true
-                          ./mc admin user add minio juicedata juicedata
-                          ./mc admin policy set minio consoleAdmin user=juicedata  
-                          sudo ./$jfs_bin format --trash-days 0 --storage minio --bucket  ${{ inputs.bucket }} --access-key juicedata --secret-key juicedata  ${{ inputs.meta_url }} juicefs-version-compatible
-                          echo "format finished" 
-                          ;;
-                      "mount") 
-                          sudo ./$jfs_bin mount --no-usage-report -d ${{ inputs.meta_url }}  ${{ inputs.mount_point }} 
-                          echo "mount finished" 
-                          ;;
-                      "umount") 
-                          sudo ./$jfs_bin umount ${{ inputs.mount_point }} 
-                          echo "umount finished" 
-                          ;;
-                      "destroy") 
-                          UUID=$(./$jfs_bin status ${{ inputs.meta_url }} | grep UUID | cut -d '"' -f 4)
-                          if [ -n "$UUID" ];then
-                            echo UUID is $UUID
-                            sudo ./$jfs_bin destroy --force ${{ inputs.meta_url }} $UUID
-                          fi
-                          echo "destroy finished" 
-                          ;;
-                      "write") 
-                          filename=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $2}')
-                          size=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $3}')
-                          dd if=/dev/urandom of=${{ inputs.mount_point }}/tempfile iflag=fullblock,count_bytes bs=4k count=$size
-                          md5_write="$filename:$(cat ${{ inputs.mount_point }}/tempfile | md5sum)"
-                          echo "write: filename is: " $filename "md5_write is: " $md5_write
-                          cp ${{inputs.mount_point}}/tempfile ${{ inputs.mount_point }}/$filename
-                          echo "write finished" 
-                          ;;
-                      "read") 
-                          filename=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $2}')
-                          read_md5=$(cat ${{ inputs.mount_point }}/$filename | md5sum)
-                          written_filename=$(echo $md5_write | awk -F: '{print $1}')
-                          written_md5=$(echo $md5_write | awk -F: '{print $2}')
-                          echo "read: filename is: " $filename "read_md5 is: " $read_md5, "written_filename is:", $written_filename, "written_md5 is:", $written_md5, 
-                          if [ "$filename" == "$wrtten_filename" ] && [ "$read_md5" != "$written_md5" ];then
-                            echo "FATAL: the md5 does not match for: ", $filename, "read_md5:", $read_md5, "written_md5:", $written_md5
-                            exit 1
-                          fi
-                          echo "read finished"
-                          ;;
-                      "dump")
-                          filename=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $2}')
-                          sudo ./$jfs_bin dump ${{ inputs.meta_url }} $filename
-                          echo "dump finished"
-                          ;;
-                      "load")
-                          filename=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $2}')
-                          echo filename is : $filename
-                          sudo ./$jfs_bin load ${{inputs.meta_url}} $filename
-                          echo "load finished"
-                          ;;
-                      "config")                         
-                          encrypt=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $2}')
-                          ./mc admin user remove minio juicedata || true
-                          ./mc admin user add minio juicedata juicedata1
-                          ./mc admin policy set minio consoleAdmin user=juicedata  
-                          if [ -n "$encrypt" ]; then
-                            sudo ./$jfs_bin config ${{inputs.meta_url}} --secret-key juicedata1 --encrypt-secret
-                          else
-                            sudo ./$jfs_bin config ${{inputs.meta_url}} --secret-key juicedata1
-                          fi
-                          sleep 60
-                          echo "config finished"
-                          ;;
-                      "load")
-                          filename=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $2}')
-                          sudo ./$jfs_bin load ${{ inputs.meta_url }} $filename
-                          echo "load finished"
-                          ;;
-                      "status")
-                          sudo ./$jfs_bin status ${{ inputs.meta_url }} 
-                          echo "status finished"
-                          ;;
-                      "fsck")
-                          sudo ./$jfs_bin fsck ${{ inputs.meta_url }} 
-                          echo "fsck finished"
-                          ;;
-                      "flushmeta")
-                          meta_url=${{inputs.meta_url}}
-                          if [[ "$meta_url" == redis* ]]; then
-                            redis-cli flushall
-                          elif [[ "$meta_url" == mysql* ]]; then
-                            db_name=$(basename $meta_url | awk -F? '{print $1}')
-                            user=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $1}')
-                            password=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $2}')
-                            test -n "$password" && password="-p$password" || password=""
-                            host=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $1}')
-                            port=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $2}')
-                            test -z "$port" && port="3306"
-                            mysql -u$user $password -h $host -P $port -e "drop database if exists $db_name; create database $db_name;" 
-                          elif [[ "$meta_url" == postgres* ]]; then
-                            db_name=$(basename $meta_url | awk -F? '{print $1}')
-                            export PGPASSWORD="postgres"
-                            printf "\set AUTOCOMMIT on\ndrop database if exists $db_name; create database $db_name; " |  psql -U postgres -h localhost
-                          fi
-                          echo "fsck finished"
-                          ;;
-                    esac
-                done
-            done
-        done
-
-    - name: log
-      if: ${{ always() }}
-      shell: bash
-      run: | 
-        tail -300 /var/log/juicefs.log
-        grep "<FATAL>:" /var/log/juicefs.log && exit 1 || true
\ No newline at end of file
diff --git a/.github/scripts/start_meta_engine.sh b/.github/scripts/start_meta_engine.sh
new file mode 100644
index 00000000..cd23d1df
--- /dev/null
+++ b/.github/scripts/start_meta_engine.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+
+start_meta_engine(){
+    meta=$1
+    if [ "$meta" == "mysql" ]; then
+        sudo /etc/init.d/mysql start
+    elif [ "$meta" == "redis" ]; then
+        sudo apt-get install -y redis-tools redis-server
+    elif [ "$meta" == "tikv" ]; then
+        curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
+        source /home/runner/.bash_profile
+        source /home/runner/.profile
+        tiup playground --mode tikv-slim &
+        sleep 5
+    elif [ "$meta" == "badgerdb" ]; then
+        sudo go get github.com/dgraph-io/badger/v3
+    elif [ "$meta" == "mariadb" ]; then
+        docker run -p 127.0.0.1:3306:3306  --name mdb -e MARIADB_ROOT_PASSWORD=root -d mariadb:latest
+        sleep 10
+    elif [ "$meta" == "tidb" ]; then
+        curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
+        source /home/runner/.profile
+        tiup playground 5.4.0 &
+        sleep 120
+        mysql -h127.0.0.1 -P4000 -uroot -e "set global tidb_enable_noop_functions=1;"
+    elif [ "$meta" == "etcd" ]; then
+        sudo apt install etcd
+    elif [ "$meta" == "ob" ]; then
+        docker rm obstandalone --force || echo "remove obstandalone failed"
+        docker run -p 2881:2881 --name obstandalone -e MINI_MODE=1 -d oceanbase/oceanbase-ce
+        sleep 60
+        mysql -h127.0.0.1 -P2881 -uroot -e "ALTER SYSTEM SET _ob_enable_prepared_statement=TRUE;" 
+    fi
+}
+
+get_meta_url(){
+    meta=$1
+    if [ "$meta" == "postgres" ]; then
+        meta_url="postgres://postgres:postgres@127.0.0.1:5432/sync_test?sslmode=disable" 
+    elif [ "$meta" == "mysql" ]; then
+        meta_url="mysql://root:root@(127.0.0.1)/sync_test"
+    elif [ "$meta" == "redis" ]; then
+        meta_url="redis://127.0.0.1:6379/1"
+    elif [ "$meta" == "sqlite3" ]; then
+        meta_url="sqlite3://sync-test.db"
+    elif [ "$meta" == "tikv" ]; then
+        meta_url="tikv://127.0.0.1:2379/load_test"
+    elif [ "$meta" == "badgerdb" ]; then
+        meta_url="badger://load_test"
+    elif [ "$meta" == "mariadb" ]; then
+        meta_url="mysql://root:root@(127.0.0.1)/load_test"
+    elif [ "$meta" == "tidb" ]; then
+        meta_url="mysql://root:@(127.0.0.1:4000)/load_test"
+    elif [ "$meta" == "etcd" ]; then
+        meta_url="etcd://localhost:2379/jfs"
+    elif [ "$meta" == "ob" ]; then
+        meta_url="mysql://root:@\\(127.0.0.1:2881\\)/test"
+    else
+        echo >&2 "<FATAL>: meta $meta is not supported"
+        meta_url=""
+        return 1
+    fi
+    echo $meta_url
+    return 0
+}
+
+create_database(){
+    meta_url=$1
+    db_name=$(basename $meta_url | awk -F? '{print $1}')
+    if [[ "$meta_url" == mysql* ]]; then
+        user=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $1}')
+        password=$(echo $meta_url |  awk -F/ '{print $3}' | awk -F@ '{print $1}' | awk -F: '{print $2}')
+        test -n "$password" && password="-p$password" || password=""
+        host=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $1}')
+        port=$(basename $(dirname $meta_url) | awk -F@ '{print $2}'| sed 's/(//g' | sed 's/)//g' | awk -F: '{print $2}')
+        test -z "$port" && port="3306"
+        echo user=$user, password=$password, host=$host, port=$port, db_name=$db_name
+        if [ "$#" -eq 2 ]; then
+            echo isolation_level=$2
+            mysql -u$user $password -h $host -P $port -e "set global transaction isolation level $2;" 
+            mysql -u$user $password -h $host -P $port -e "show variables like '%isolation%;'" 
+        fi
+        mysql -u$user $password -h $host -P $port -e "drop database if exists $db_name; create database $db_name;" 
+    elif [[ "$meta_url" == postgres* ]]; then
+        export PGPASSWORD="postgres"
+        printf "\set AUTOCOMMIT on\ndrop database if exists $db_name; create database $db_name; " |  psql -U postgres -h localhost
+        if [ "$#" -eq 2 ]; then
+            echo isolation_level=$2
+            printf "\set AUTOCOMMIT on\nALTER DATABASE $db_name SET DEFAULT_TRANSACTION_ISOLATION TO '$2';" |  psql -U postgres -h localhost
+        fi
+    fi      
+}
\ No newline at end of file
diff --git a/.github/scripts/utils.py b/.github/scripts/utils.py
index d03208b8..29710c9a 100644
--- a/.github/scripts/utils.py
+++ b/.github/scripts/utils.py
@@ -1,3 +1,4 @@
+from curses import meta
 import json
 import os
 from posixpath import expanduser
@@ -7,7 +8,6 @@ import sys
 import time
 from minio import Minio
 
-
 def flush_meta(meta_url):
     print('start flush meta')
     if meta_url.startswith('sqlite3://'):
@@ -15,11 +15,45 @@ def flush_meta(meta_url):
         if os.path.isfile(path):
             os.remove(path)
             print(f'remove meta file {path} succeed')
+    elif meta_url.startswith('badger://'):
+        path = meta_url[len('badger://'):]
+        if os.path.isdir(path):
+            shutil.rmtree(path)
+            print(f'remove badger dir {path} succeed')
     elif meta_url.startswith('redis://'):
         run_cmd('redis-cli flushall')
         print(f'flush redis succeed')
+    elif meta_url.startswith('mysql://'):
+        create_mysql_db(meta_url)
+    elif meta_url.startswith('postgres://'): 
+        create_postgres_db(meta_url)
+    elif meta_url.startswith('tikv://'):
+        run_cmd('echo "delall --yes" |tcli -pd localhost:2379')
+    else:
+        raise Exception(f'{meta_url} not supported')
     print('flush meta succeed')
 
+def create_mysql_db(meta_url):
+    db_name = meta_url[8:].split('@')[1].split('/')[1]
+    user = meta_url[8:].split('@')[0].split(':')[0]
+    password = meta_url[8:].split('@')[0].split(':')[1]
+    if password: 
+        password = f'-p{password}'
+    host_port= meta_url[8:].split('@')[1].split('/')[0].replace('(', '').replace(')', '')
+    if ':' in host_port:
+        host = host_port.split(':')[0]
+        port = host_port.split(':')[1]
+    else:
+        host = host_port
+        port = '3306'
+    run_cmd(f'mysql -u{user} {password} -h {host} -P {port} -e "drop database if exists {db_name}; create database {db_name};"')
+
+def create_postgres_db(meta_url):
+    db_name = meta_url[8:].split('@')[1].split('/')[1]
+    if '?' in db_name:
+        db_name = db_name.split('?')[0]
+    run_cmd(f'printf "\set AUTOCOMMIT on\ndrop database if exists {db_name}; create database {db_name}; " |  psql -U postgres -h localhost')
+
 def clear_storage(storage, bucket, volume):
     print('start clear storage')
     if storage == 'file':
@@ -45,6 +79,14 @@ def clear_storage(storage, bucket, volume):
         print(f'remove bucket {bucket_name} succeed')
         if c.bucket_exists(bucket_name):
             assert not list(c.list_objects(bucket_name))
+    elif storage == 'mysql':
+        db_name = bucket.split('/')[-1]
+        run_cmd(f'mysql -uroot -proot -h localhost -P 3306 -e "drop database if exists {db_name};create database {db_name};"')
+    elif storage == 'postgres':
+        db_name = bucket.split('/')[1]
+        if '?' in db_name:
+            db_name = db_name.split('?')[0]
+        run_cmd(f'printf "\set AUTOCOMMIT on\ndrop database if exists {db_name}; create database {db_name}; " |  psql -U postgres -h localhost')
     print('clear storage succeed')
 
 
@@ -63,7 +105,7 @@ def is_readonly(filesystem):
 
 def get_upload_delay_seconds(filesystem):
     if not os.path.exists(f'{filesystem}/.config'):
-        return False
+        return 0
     with open(f'{filesystem}/.config') as f:
         config = json.load(f)
         return config['Chunk']['UploadDelay']/1000000000
@@ -81,49 +123,80 @@ def get_stage_blocks(filesystem):
 def write_data(filesystem, path, data):
     with open(path, "wb") as f:
         f.write(data)
-    time.sleep(get_upload_delay_seconds(filesystem)+1)
-    retry = 10
+    retry = get_upload_delay_seconds(filesystem) + 10
     while get_stage_blocks(filesystem) != 0 and retry > 0:
         print('sleep for stage')
         retry = retry - 1
         time.sleep(1)
-    assert get_stage_blocks(filesystem) == 0
+    # assert get_stage_blocks(filesystem) == 0
 
 def write_block(filesystem, filepath, bs, count):
     run_cmd(f'dd if=/dev/urandom of={filepath} bs={bs} count={count}')
+    retry = get_upload_delay_seconds(filesystem) + 10
+    while get_stage_blocks(filesystem) != 0 and retry > 0:
+        print('sleep for stage')
+        retry = retry - 1
+        time.sleep(1)
+    # assert get_stage_blocks(filesystem) == 0
+
+def mdtest(filesystem, meta_url):
+    juicefs_new = './'+os.environ.get('NEW_JFS_BIN')
+    cwd = os.getcwd()
+    if not os.path.exists(f'{filesystem}/{juicefs_new}'):
+        run_cmd(f'ln -s {cwd}/{juicefs_new} {filesystem}/{juicefs_new}')
+    os.chdir(filesystem)
+    run_jfs_cmd(f'{juicefs_new} mdtest {meta_url} mdtest --dirs 5 --depth 2 --files 5 --threads 5 --write 8192'.split())
+    os.chdir(cwd)
     time.sleep(get_upload_delay_seconds(filesystem)+1)
-    retry = 10
+    retry = 5
     while get_stage_blocks(filesystem) != 0 and retry > 0:
         print('sleep for stage')
         retry = retry - 1
         time.sleep(1)
-    assert get_stage_blocks(filesystem) == 0
+    assert os.path.exists(filesystem+'mdtest')
 
 def run_jfs_cmd( options):
     options.append('--debug')
     print('run_jfs_cmd:'+' '.join(options))
     with open('command.log', 'a') as f:
-        f.write(' '.join(options))
+        f.write(' '.join(options).replace('/home/runner', '~'))
         f.write('\n')
     try:
         output = subprocess.run(options, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     except subprocess.CalledProcessError as e:
-        print(f'<FATAL>: subprocess run error: {e.output.decode()}')
+        print(f'<FATAL>: subprocess run error, return code: {e.returncode} , error message: {e.output.decode()}')
         raise Exception('subprocess run error')
-    print(output.stdout.decode())
+    print(f'run_jfs_cmd return code: {output.returncode}, output: {output.stdout.decode()}')
     print('run_jfs_cmd succeed')
     return output.stdout.decode()
 
 def run_cmd(command):
     print('run_cmd:'+command)
-    if '|' in command:
+    if '|' in command or '"' in command:
         return os.system(command)
     try:
         output = subprocess.run(command.split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     except subprocess.CalledProcessError as e:
-        print(f'FATAL: subprocess run error: {e.output.decode()}')
+        print(f'<FATAL>: subprocess run error, return code: {e.returncode} , error message: {e.output.decode()}')
         return e.returncode
     if output.stdout:
         print(output.stdout.decode())
     print('run_cmd succeed')
-    return output.returncode
\ No newline at end of file
+    return output.returncode
+
+def is_port_in_use(port: int) -> bool:
+    import socket
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+        return s.connect_ex(('localhost', port)) == 0
+
+def get_storage(juicefs, meta_url):
+    output = subprocess.run([juicefs, 'status', meta_url], check=True, stdout=subprocess.PIPE).stdout.decode()
+    if 'get timestamp too slow' in output: 
+        # remove the first line caust it is tikv log message
+        output = '\n'.join(output.split('\n')[1:])
+    print(f'status output: {output}')
+    storage = json.loads(output.replace("'", '"'))['Setting']['Storage']
+    return storage
+
+if __name__ == "__main__":
+    run_jfs_cmd(['./juicefs-1.1.0-dev', 'rmr', '/tmp/sync-test/file_to_rmr', '--debug'])
\ No newline at end of file
diff --git a/.github/workflows/bash/rm_syscalls b/.github/workflows/bash/rm_syscalls
index 2b649d91..1ad440c8 100644
--- a/.github/workflows/bash/rm_syscalls
+++ b/.github/workflows/bash/rm_syscalls
@@ -147,4 +147,7 @@ kill11 kill11
 mkdir02 mkdir02
 setxattr03 setxattr03
 quotactl08 quotactl08
-statx04 statx04
\ No newline at end of file
+statx04 statx04
+chdir01 chdir01
+sync01 sync01
+fanotify15 fanotify15
\ No newline at end of file
diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
index cb0df188..d6b89e36 100644
--- a/.github/workflows/config.yml
+++ b/.github/workflows/config.yml
@@ -58,9 +58,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
             
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/encoding_mysql.yml b/.github/workflows/encoding_mysql.yml
index f722237c..392637ce 100644
--- a/.github/workflows/encoding_mysql.yml
+++ b/.github/workflows/encoding_mysql.yml
@@ -45,9 +45,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/filebench.yml b/.github/workflows/filebench.yml
new file mode 100644
index 00000000..8b0b784f
--- /dev/null
+++ b/.github/workflows/filebench.yml
@@ -0,0 +1,73 @@
+name: "filebench"
+
+on:
+  push:
+    branches:
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+  pull_request:
+    #The branches below must be a subset of the branches above
+    branches:
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+  schedule:
+    - cron:  '30 20 * * *'
+  workflow_dispatch:
+
+
+jobs:
+  filebench_varmail:
+    timeout-minutes: 60
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+      - name: Varmail
+        uses: ./.github/actions/filebench/
+        with:
+          workload: varmail
+
+
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
+
+
+  filebench_webserver:
+    timeout-minutes: 60
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+      - name: Webserver
+        uses: ./.github/actions/filebench/
+        with:
+          workload: webserver
+
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
+
+
+  filebench_videoserver:
+    timeout-minutes: 60
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+      - name: Videoserver
+        uses: ./.github/actions/filebench/
+        with:
+          workload: videoserver
+
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index 057e158d..452aa5dc 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -48,9 +48,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml
index 5ad00b15..286fb34c 100644
--- a/.github/workflows/gateway.yml
+++ b/.github/workflows/gateway.yml
@@ -17,238 +17,47 @@ on:
   workflow_dispatch:
 
 jobs:
-  upload-big-file-with-redis:
-    runs-on: ubuntu-latest
-    services:
-      redis:
-        # Docker Hub image
-        image: redis
-        # Set health checks to wait until redis has started
-        options: >-
-          --health-cmd "redis-cli ping"
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps port 6379 on service container to the host
-          - 6379:6379
-    steps:
-      - uses: actions/checkout@v3
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "redis://localhost/1"
-          file_size: "100M"
-          file_count: 5
-
-  upload-small-files-with-redis:
-    runs-on: ubuntu-latest
-    services:
-      redis:
-        # Docker Hub image
-        image: redis
-        # Set health checks to wait until redis has started
-        options: >-
-          --health-cmd "redis-cli ping"
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps port 6379 on service container to the host
-          - 6379:6379
-    steps:
-      - uses: actions/checkout@v3
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "redis://localhost/1"
-          file_size: "8K"
-          file_count: 2500
-
-
-  upload-small-files-with-mysql-read-uncommitted:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "8K"
-          file_count: 2500
-          isolation_level: "read uncommitted"    
-
+  gateway:
+    strategy:
+      fail-fast: false
+      matrix:
+        meta: [  'sqlite3', 'redis','tikv', 'badgerdb',  'etcd']
+        file_size: ['100M']
+        isolation_level: ['']
+        include:
+          - meta: 'mariadb'
+            file_size: '100M'
+            isolation_level: "read committed"
+
+          - meta: 'mysql'
+            file_size: '100M'
+            isolation_level: "read committed"
+          - meta: 'mysql'
+            file_size: '100M'
+            isolation_level: "repeatable read"
+          - meta: 'mysql'
+            file_size: '100M'
+            isolation_level: "serializable"
+
+          - meta: 'postgres'
+            file_size: '100M'
+            isolation_level: "read committed"
+          - meta: 'postgres'
+            file_size: '100M'
+            isolation_level: "repeatable read"
+          - meta: 'postgres'
+            file_size: '100M'
+            isolation_level: "serializable"
+
+          - meta: 'tidb'
+            file_size: '100M'
+            isolation_level: "read committed"
+          - meta: 'tidb'
+            file_size: '100M'
+            isolation_level: "repeatable read"
 
-  upload-small-files-with-mysql-read-committed:
     runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "8K"
-          file_count: 2500
-          isolation_level: "read committed"    
-
-
-  upload-small-files-with-mysql-repeatable-read:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "8K"
-          file_count: 2500
-          isolation_level: "repeatable read"   
-
-
-  upload-small-files-with-mysql-serializable:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "8K"
-          file_count: 2500
-          isolation_level: "serializable"     
-
-
-  upload-big-files-with-mysql-read-uncommitted:
-    runs-on: ubuntu-latest          
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "100M"
-          file_count: 5
-          isolation_level: "read uncommitted"
-
-
-  upload-big-files-with-mysql-read-committed:
-    runs-on: ubuntu-latest          
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "100M"
-          file_count: 5
-          isolation_level: "read committed"
-
-
-  upload-big-files-with-mysql-repeatable-read:
-    runs-on: ubuntu-latest          
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "100M"
-          file_count: 5
-          isolation_level: "repeatable read"
-
-
-  upload-big-files-with-mysql-serializable:
-    runs-on: ubuntu-latest          
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "100M"
-          file_count: 5
-          isolation_level: "serializable"
-
 
-  upload-big-files-with-mariadb-read-committed:
-    runs-on: ubuntu-latest          
-    steps:
-      - uses: actions/checkout@v3
-      - name: install 
-        run: | 
-          docker run -p 127.0.0.1:3306:3306  --name mdb -e MARIADB_ROOT_PASSWORD=root -d mariadb:latest
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "100M"
-          file_count: 5
-          isolation_level: "read committed"
-
-
-  upload-big-files-with-badgerdb:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo go get github.com/dgraph-io/badger/v3
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "badger://badger-data"
-          file_size: "100M"
-          file_count: 1
-
-
-  upload-small-files-with-badgerdb:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo go get github.com/dgraph-io/badger/v3
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "badger://badger-data"
-          file_size: "8K"
-          file_count: 2500
-
-
-
-  upload-small-files-with-sqlite:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "sqlite3://test.db"
-          file_size: "8K"
-          file_count: 2500    
-
-
-  upload-big-files-with-sqlite:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo /etc/init.d/mysql start
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "sqlite3://test.db"
-          file_size: "100M"
-          file_count: 5    
-
-
-  upload-small-files-with-pg:
-    runs-on: ubuntu-latest
     services:
       postgres:
         image: postgres
@@ -260,270 +69,14 @@ jobs:
           --health-timeout 5s
           --health-retries 5
         ports:
-          # Maps tcp port 5432 on service container to the host
           - 5432:5432
-    steps:
-      - uses: actions/checkout@v3
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
-          file_size: "8K"
-          file_count: 2500
-          threads: 10    
-
 
-  upload-big-files-with-pg-read-committed:
-    runs-on: ubuntu-latest
-    services:
-      postgres:
-        image: postgres
-        env:
-          POSTGRES_PASSWORD: postgres
-        options: >-
-          --health-cmd pg_isready
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps tcp port 5432 on service container to the host
-          - 5432:5432
-    steps:
-      - uses: actions/checkout@v3
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
-          file_size: "100M"
-          file_count: 3
-          threads: 10
-          isolation_level: "read committed"
-
-
-
-  upload-big-files-with-pg-repeatable-read:
-    runs-on: ubuntu-latest
-    services:
-      postgres:
-        image: postgres
-        env:
-          POSTGRES_PASSWORD: postgres
-        options: >-
-          --health-cmd pg_isready
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps tcp port 5432 on service container to the host
-          - 5432:5432
-    steps:
-      - uses: actions/checkout@v3
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
-          file_size: "100M"
-          file_count: 5
-          threads: 10
-          isolation_level: "repeatable read"
-
-
-
-  upload-big-files-with-pg-serializable:
-    runs-on: ubuntu-latest
-    services:
-      postgres:
-        image: postgres
-        env:
-          POSTGRES_PASSWORD: postgres
-        options: >-
-          --health-cmd pg_isready
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps tcp port 5432 on service container to the host
-          - 5432:5432
-    steps:
-      - uses: actions/checkout@v3
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
-          file_size: "100M"
-          file_count: 5
-          threads: 10
-          isolation_level: "serializable"
-
-
-
-  upload-big-files-with-tidb-read-committed:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: Install TiDB
-        run: |
-          curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
-          source /home/runner/.profile
-          tiup playground 5.4.0 &
-          sleep 120
-          mysql -h127.0.0.1 -P4000 -uroot -e "set global tidb_enable_noop_functions=1;"
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:@\\(127.0.0.1:4000\\)/test"
-          file_size: "100M"
-          file_count: 5
-          isolation_level: "read committed"
-
-  upload-big-files-with-tidb-repeatable-read:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: Install TiDB
-        run: |
-          curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
-          source /home/runner/.profile
-          tiup playground 5.4.0 &
-          sleep 120
-          mysql -h127.0.0.1 -P4000 -uroot -e "set global tidb_enable_noop_functions=1;"
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "mysql://root:@\\(127.0.0.1:4000\\)/test"
-          file_size: "100M"
-          file_count: 5
-          isolation_level: "repeatable read"
-
-
-  upload-small-files-with-etcd:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo apt install etcd
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "etcd://localhost:2379/jfs"
-          file_size: "8K"
-          file_count: 2500    
-
-
-  upload-big-files-with-etcd:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - run: sudo apt install etcd
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "etcd://localhost:2379/jfs"
-          file_size: "200M"
-          file_count: 5    
-
-
-  # upload-big-file-with-oceanbase-read-committed:
-  #   runs-on: [self-hosted, daily-build, bench-02]
-  #   steps:
-  #     - uses: actions/checkout@v3
-  #     - name: install ob
-  #       run: |
-  #         docker rm obstandalone --force || echo "remove obstandalone failed"
-  #         docker run -p 2881:2881 --name obstandalone -e MINI_MODE=1 -d oceanbase/oceanbase-ce
-  #         sleep 60
-  #         mysql -h127.0.0.1 -P2881 -uroot -e "ALTER SYSTEM SET _ob_enable_prepared_statement=TRUE;" 
-  #     - name: Test
-  #       uses: ./.github/actions/gateway
-  #       with:
-  #         meta_url: "mysql://root:@\\(127.0.0.1:2881\\)/test"
-  #         file_size: "200M"
-  #         file_count: 5
-  #         isolation_level: "read committed"   
-
-  # upload-big-file-with-oceanbase-repeatable-read:
-  #   runs-on: [self-hosted, daily-build, bench-03]
-  #   steps:
-  #     - uses: actions/checkout@v3
-  #     - name: install ob
-  #       run: |
-  #         docker rm obstandalone --force || echo "remove obstandalone failed"
-  #         docker run -p 2881:2881 --name obstandalone -e MINI_MODE=1 -d oceanbase/oceanbase-ce
-  #         sleep 60
-  #         mysql -h127.0.0.1 -P2881 -uroot -e "ALTER SYSTEM SET _ob_enable_prepared_statement=TRUE;" 
-  #     - name: Test
-  #       uses: ./.github/actions/gateway
-  #       with:
-  #         meta_url: "mysql://root:@\\(localhost:2881\\)/test"
-  #         file_size: "200M"
-  #         file_count: 5
-  #         isolation_level: "repeatable read"   
-
-  # upload-big-file-with-oceanbase-serializable:
-  #   runs-on: [self-hosted, daily-build, bench-03]
-  #   steps:
-  #     - uses: actions/checkout@v3
-  #     - name: install ob
-  #       run: |
-  #         docker rm obstandalone --force || echo "remove obstandalone failed"
-  #         docker run -p 2881:2881 --name obstandalone -e MINI_MODE=1 -d oceanbase/oceanbase-ce
-  #         sleep 60
-  #         mysql -h127.0.0.1 -P2881 -uroot -e "ALTER SYSTEM SET _ob_enable_prepared_statement=TRUE;" 
-  #     - name: Test
-  #       uses: ./.github/actions/gateway
-  #       with:
-  #         meta_url: "mysql://root:@\\(localhost:2881\\)/test"
-  #         file_size: "200M"
-  #         file_count: 5
-  #         isolation_level: "serializable"    
-
-  upload-small-files-with-tikv:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: Install TiKV
-        run: |
-          curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
-          source /home/runner/.bash_profile
-          source /home/runner/.profile
-          tiup playground --mode tikv-slim &
-          sleep 5
-
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "tikv://127.0.0.1:2379"
-          file_size: "8K"
-          file_count: 2500
-
-
-  upload-big-files-with-tikv:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: Install TiKV
-        run: |
-          curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
-          source /home/runner/.bash_profile
-          source /home/runner/.profile
-          tiup playground --mode tikv-slim &
-          sleep 5
-
-      - name: Test
-        uses: ./.github/actions/gateway
-        with:
-          meta_url: "tikv://127.0.0.1:2379"
-          file_size: "100M"
-          file_count: 5
-
-
-  sync-to-gateway-with-multithread:
-    runs-on: ubuntu-latest
-    steps:
+    steps: 
       - name: Checkout
         uses: actions/checkout@v2
         with:
           fetch-depth: 1
-          
+
       - name: Set up Go
         uses: actions/setup-go@v2
         with:
@@ -534,36 +87,141 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
-            
+            ${{ runner.os }}-go
+
       - name: Build linux target
         run: |
-          make juicefs
+          #if it is self ruuner
+          hostname | grep bench && export GOPATH=/usr/local/go && export HOME=/root
+          make juicefs 
           #wget -q https://github.com/juicedata/juicefs/releases/download/v1.0.0-beta3/juicefs-1.0.0-beta3-linux-amd64.tar.gz
           #tar -xzf juicefs-1.0.0-beta3-linux-amd64.tar.gz
-      - name: Test sync to gateway with multithread
+        shell: bash
+
+      - name: Start meta
+        run: | 
+          sudo chmod +x .github/scripts/start_meta_engine.sh
+          source .github/scripts/start_meta_engine.sh
+          start_meta_engine ${{matrix.meta}}
+
+      - name: Install tools
         run: | 
-          #test for PR: https://github.com/juicedata/juicefs/pull/2024
           wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
           chmod +x mc 
-          sudo apt install md5deep
-          sudo /etc/init.d/mysql start
-          meta_url="mysql://root:root@(127.0.0.1)/test"
-          volume=myjfs
-          mp=/tmp/myjfs
-          db_name=$(basename $meta_url)
-          mysql -uroot -proot -e "set global transaction isolation level repeatable read;" 
-          mysql -uroot -proot -e "drop database if exists $db_name; create database $db_name;" 
+        shell: bash
+        
+      - name: start gateway
+        shell: bash
+        run: |
+          source .github/scripts/start_meta_engine.sh
+          meta_url=$(get_meta_url ${{matrix.meta}})
+          create_database $meta_url ${{matrix.isolation_level}}
+          mp="/tmp/myjfs"
+          volume="myjfs"
           export MINIO_ROOT_USER=minioadmin
           export MINIO_ROOT_PASSWORD=minioadmin
-          ./juicefs format $meta_url $volume
-          ./juicefs gateway $meta_url localhost:9000 &
+          ./juicefs format $meta_url $volume --trash-days 0
+          if [[ "$meta_url" == badger* ]]; then
+            ./juicefs gateway $meta_url localhost:8080 --no-usage-report /tmp/access1.log &
+          else
+            ./juicefs gateway $meta_url localhost:8080 --access-log /tmp/access1.log &
+            #./juicefs gateway $meta_url localhost:9000 --access-log /tmp/access1.log &
+            #./juicefs gateway $meta_url localhost:9001 --access-log /tmp/access2.log &
+            #sudo cp .github/workflows/resources/load-balancer.conf /etc/nginx/conf.d/load-balancer.conf
+            #sudo rm /etc/nginx/sites-enabled/default
+            #sudo systemctl restart nginx
+          fi
+        
+      - name: Sync with multiple process
+        shell: bash
+        run: |
+          source .github/scripts/start_meta_engine.sh
+          meta_url=$(get_meta_url ${{matrix.meta}})
+          file_size=${{matrix.file_size}}
+          if [ $file_size="100M" ]; then 
+            file_count=5
+          else
+            file_count=2000
+          fi
+          threads=20
+          mp=/tmp/myjfs
+          volume=myjfs
+          
+          dd if=/dev/urandom of=file iflag=fullblock,count_bytes bs=4k count="$file_size" > /dev/null
+          mkdir data
+          for i in $(seq 1 $file_count); do
+            cp file data/file$i
+          done
           start=`date +%s`
-          ./juicefs sync --dirs ./  s3://minioadmin:minioadmin@localhost:9000/$volume/source_dir/ --no-https -p 50
+          declare -a pids   
+          ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:8080/$volume/data/ --no-https -p $threads &
+          pids+=($!)
+          ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:8080/$volume/data/ --no-https -p $threads &
+          pids+=($!)
+          ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:8080/$volume/data/ --no-https -p $threads &
+          pids+=($!)
+          wait "${pids[@]}"
+          rm -rf $HOME/.juicefs/cache/ || true
+          # ./mc alias set minio http://localhost:9000 minioadmin minioadmin --api S3v4
+          # ./mc mb minio/$volume
+          # ./mc cp --recursive data/  minio/$volume/data
           end=`date +%s`
           time=$((end-start))
-          echo time is: $time
+          echo time cost is: $time second
+          killall juicefs 
+          sleep 3
+          ./juicefs mount -d $meta_url $mp
+          diff -ur data/ $mp/data/ 
+          echo "diff succeed"
+          ./juicefs umount  $mp --force
+        
+      - name: Sync with empty dir
+        run: |
+          source .github/scripts/start_meta_engine.sh
+          meta_url=$(get_meta_url ${{matrix.meta}})
+          threads=20
+          mp=/tmp/myjfs
+          volume=myjfs
+          export MINIO_ROOT_USER=minioadmin
+          export MINIO_ROOT_PASSWORD=minioadmin
+          ./juicefs mdtest $meta_url test --dirs 10 --depth 2 --files 10 --threads 10 --no-usage-report
+          ./juicefs gateway $meta_url localhost:8080 --access-log /tmp/access1.log &
+          sleep 10
+          mkdir emptydir
+          declare -a pids  
+          ./juicefs sync emptydir/ s3://minioadmin:minioadmin@localhost:8080/$volume/test/ --delete-dst --no-https -p $threads &
+          pids+=($!)
+          ./juicefs sync emptydir/ s3://minioadmin:minioadmin@localhost:8080/$volume/test/ --delete-dst --no-https -p $threads &
+          pids+=($!)
+          ./juicefs sync emptydir/ s3://minioadmin:minioadmin@localhost:8080/$volume/test/ --delete-dst --no-https -p $threads &
+          pids+=($!)
+          wait "${pids[@]}"
+          killall juicefs
+          sleep 3
           ./juicefs mount -d $meta_url $mp
-          rm -rf $mp/source_dir
+          [ -d "$mp/test/" ] && exit 1 
+          ./juicefs umount  $mp --force
+        shell: bash
+
+      - name: log
+        if: ${{ always() }}
+        shell: bash
+        run: | 
+          if [ -f ~/.juicefs/juicefs.log ]; then
+            tail -300 ~/.juicefs/juicefs.log
+            grep "<FATAL>:" ~/.juicefs/juicefs.log && exit 1 || true
+          fi
+
+      - name: Send Slack Notification
+        if: ${{ failure() }}
+        uses: juicedata/slack-notify-action@main
+        with:
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
+
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        timeout-minutes: 1
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/gc.yml b/.github/workflows/gc.yml
index d4f680e0..b543301e 100644
--- a/.github/workflows/gc.yml
+++ b/.github/workflows/gc.yml
@@ -46,9 +46,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/list.yml b/.github/workflows/list.yml
index b8a334a0..901be104 100644
--- a/.github/workflows/list.yml
+++ b/.github/workflows/list.yml
@@ -1,24 +1,18 @@
 name: "list-test"
 
 on:
-#  push:
-#    branches:
-#      - 'release-**'
-#    paths-ignore:
-#      - 'docs/**'
-#  pull_request:
-#    #The branches below must be a subset of the branches above
-#    branches:
-#      - 'release-**'
-#    paths-ignore:
-#      - 'docs/**'
   schedule:
     - cron:  '0 20 * * *'
   workflow_dispatch:
 
 jobs:
-  pg-list:
+  list:
+    strategy:
+      fail-fast: false
+      matrix:
+        meta: [ 'sqlite3', 'redis', 'mysql',  'postgres']
     runs-on: ubuntu-latest
+
     services:
       postgres:
         image: postgres
@@ -31,144 +25,39 @@ jobs:
           --health-retries 5
         ports:
           - 5432:5432
-    steps:
-      - uses: actions/checkout@v3
-      - name: Checkout
-        uses: actions/checkout@v2
-        with:
-          fetch-depth: 1
-      - name: Init
-        run: |
-          sudo chmod 777 /var
-          git clone https://github.com/sanwan/actionfile.git
-          cd actionfile
-          cat META_7M_EMPTY_FILE.json.gz* >META_7M_EMPTY_FILE.json.gz
-          gzip -dk  META_7M_EMPTY_FILE.json.gz
-          ls -lh
-          mv META_7M_EMPTY_FILE.json ../
-          cd ../
-          ls -lh
-      - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: "postgres://postgres:postgres@127.0.0.1:5432/sync_test?sslmode=disable"
-          load_file: "META_7M_EMPTY_FILE.json"
-      - name: List
-        run: |
-          cd /tmp/juicefs-load-test/createfile/
-          ls -lh |wc -l >/tmp/count.log
-          cat /tmp/count.log | grep 7000001
-          if [ $? != 0 ];then echo "file num is error";exit 1; fi
 
-      - name: Send Slack Notification
-        if: ${{ failure() }}
-        uses: juicedata/slack-notify-action@main
-        with:
-          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
-
-
-  mysql-list:
-    runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
       - name: Checkout
         uses: actions/checkout@v2
         with:
           fetch-depth: 1
-      - name: Init
-        run: |
-          sudo chmod 777 /var
-          sudo /etc/init.d/mysql start
-          git clone https://github.com/sanwan/actionfile.git
-          cd actionfile
-          cat META_7M_EMPTY_FILE.json.gz* >META_7M_EMPTY_FILE.json.gz
-          gzip -dk  META_7M_EMPTY_FILE.json.gz
-          ls -lh
-          mv META_7M_EMPTY_FILE.json ../
-          cd ../
-          ls -lh
-      - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/sync_test"
-          load_file: "META_7M_EMPTY_FILE.json"
-      - name: List
-        run: |
-          cd /tmp/juicefs-load-test/createfile/
-          ls -lh |wc -l >/tmp/count.log
-          cat /tmp/count.log | grep 7000001
-          if [ $? != 0 ];then echo "file num is error";exit 1; fi
 
-      - name: Send Slack Notification
-        if: ${{ failure() }}
-        uses: juicedata/slack-notify-action@main
+      - name: Set up Go
+        uses: actions/setup-go@v2
         with:
-          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
-
+          go-version: '1.18.x'
 
-  redis-list:
-    runs-on: ubuntu-latest
-    services:
-      redis:
-        # Docker Hub image
-        image: redis
-        # Set health checks to wait until redis has started
-        options: >-
-          --health-cmd "redis-cli ping"
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps port 6379 on service container to the host
-          - 6379:6379
-    steps:
-      - uses: actions/checkout@v3
-      - name: Checkout
-        uses: actions/checkout@v2
+      - uses: actions/cache@v3
         with:
-          fetch-depth: 1
-      - name: Init
-        run: |
-          sudo chmod 777 /var
-          git clone https://github.com/sanwan/actionfile.git
-          cd actionfile
-          cat META_7M_EMPTY_FILE.json.gz* >META_7M_EMPTY_FILE.json.gz
-          gzip -dk  META_7M_EMPTY_FILE.json.gz
-          ls -lh
-          mv META_7M_EMPTY_FILE.json ../
-          cd ../
-          ls -lh
-      - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: redis://127.0.0.1:6379/1
-          load_file: "META_7M_EMPTY_FILE.json"
-      - name: List
-        run: |
-          cd /tmp/juicefs-load-test/createfile/
-          ls -lh |wc -l >/tmp/count.log
-          cat /tmp/count.log | grep 7000001
-          if [ $? != 0 ];then echo "file num is error";exit 1; fi
+          path: |
+            ~/.cache/go-build
+            ~/go/pkg/mod
+          key: ${{ runner.os }}-go
+          restore-keys: |
+            ${{ runner.os }}-go
 
-      - name: Send Slack Notification
-        if: ${{ failure() }}
-        uses: juicedata/slack-notify-action@main
-        with:
-          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
+      - name: Build linux target
+        run: |
+          make juicefs 
+        shell: bash
 
+      - name: Prepare meta db
+        run: | 
+          sudo chmod +x .github/scripts/start_meta_engine.sh
+          source .github/scripts/start_meta_engine.sh
+          start_meta_engine ${{matrix.meta}}
 
-  sqlite3-list:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: Checkout
-        uses: actions/checkout@v2
-        with:
-          fetch-depth: 1
-      - name: Init
+      - name: Download dump file
         run: |
           sudo chmod 777 /var
           git clone https://github.com/sanwan/actionfile.git
@@ -179,11 +68,20 @@ jobs:
           mv META_7M_EMPTY_FILE.json ../
           cd ../
           ls -lh
+      
       - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: sqlite3://sync-test.db
-          load_file: "META_7M_EMPTY_FILE.json"
+        run: | 
+          sudo chmod +x .github/scripts/start_meta_engine.sh
+          source .github/scripts/start_meta_engine.sh
+          meta_url=$(get_meta_url ${{matrix.meta}})
+          create_database $meta_url
+          mount_point=/tmp/juicefs-load-test
+          load_file=META_7M_EMPTY_FILE.json
+          echo `date`, start load
+          sudo ./juicefs load $meta_url $load_file
+          echo `date`, finish load
+          sudo ./juicefs mount $meta_url $mount_point -d
+
       - name: List
         run: |
           cd /tmp/juicefs-load-test/createfile/
@@ -196,4 +94,4 @@ jobs:
         uses: juicedata/slack-notify-action@main
         with:
           channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
\ No newline at end of file
diff --git a/.github/workflows/load.yml b/.github/workflows/load.yml
index 3ef2aa5b..0047475a 100644
--- a/.github/workflows/load.yml
+++ b/.github/workflows/load.yml
@@ -1,12 +1,17 @@
 name: "load-test"
 on:
   schedule:
-    - cron:  '0 3 * * *'
+    - cron:  '0 16 * * *'
   workflow_dispatch:
 
 jobs:
-  load-to-pg:
+  load:
+    strategy:
+      fail-fast: false
+      matrix:
+        meta: [ 'tidb', 'sqlite3', 'redis', 'mysql', 'tikv', 'postgres', 'badgerdb', 'mariadb']
     runs-on: ubuntu-latest
+
     services:
       postgres:
         image: postgres
@@ -19,154 +24,72 @@ jobs:
           --health-retries 5
         ports:
           - 5432:5432
+
     steps:
-      - uses: actions/checkout@v3
       - name: Checkout
         uses: actions/checkout@v2
         with:
           fetch-depth: 1
-      - name: download data
-        run: |
-          wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
-          gzip -dk  2M_emtpy_files.dump.gz
-      - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: "postgres://postgres:postgres@127.0.0.1:5432/sync_test?sslmode=disable" 
-          load_file: "2M_emtpy_files.dump"            
-          
-  load-to-mysql:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: download data
-        run: |
-          wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
-          gzip -dk  2M_emtpy_files.dump.gz
-      - name: start mysql
-        run: |
-          # sudo chmod 777 /var
-          sudo /etc/init.d/mysql start
-      - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/sync_test" 
-          load_file: "2M_emtpy_files.dump"      
-            
-  load-to-redis:
-    runs-on: ubuntu-latest
-    services:
-      redis:
-        # Docker Hub image
-        image: redis
-        # Set health checks to wait until redis has started
-        options: >-
-          --health-cmd "redis-cli ping"
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps port 6379 on service container to the host
-          - 6379:6379
-    steps:
-      - uses: actions/checkout@v3
-      - name: download data
-        run: |
-          wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
-          gzip -dk  2M_emtpy_files.dump.gz
-      - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: redis://127.0.0.1:6379/1 
-          load_file: "2M_emtpy_files.dump"      
 
-  load-to-sqlite3:
-    runs-on: ubuntu-latest   
-    steps:
-      - uses: actions/checkout@v3
-      - name: download data
-        run: |
-          wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
-          gzip -dk  2M_emtpy_files.dump.gz
-      - name: Load
-        uses: ./.github/actions/load
+      - name: Set up Go
+        uses: actions/setup-go@v2
         with:
-          meta_url: sqlite3://sync-test.db
-          load_file: "2M_emtpy_files.dump"      
-    
-  load-to-tikv:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: download data
-        run: |
-          wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
-          gzip -dk  2M_emtpy_files.dump.gz
-      - name: Install TiKV
-        run: |
-          curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
-          source /home/runner/.bash_profile
-          source /home/runner/.profile
-          tiup playground --mode tikv-slim &
-          sleep 5
-      - name: Test
-        uses: ./.github/actions/load
-        with:
-          meta_url: "tikv://127.0.0.1:2379/load_test"
-          load_file: "2M_emtpy_files.dump"
+          go-version: '1.18.x'
 
-
-  load-to-badgerdb:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: download data
-        run: |
-          wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
-          gzip -dk  2M_emtpy_files.dump.gz
-      - run: sudo go get github.com/dgraph-io/badger/v3
-      - name: Test
-        uses: ./.github/actions/load
+      - uses: actions/cache@v3
         with:
-          meta_url: "badger://load_test"
-          load_file: "2M_emtpy_files.dump"    
+          path: |
+            ~/.cache/go-build
+            ~/go/pkg/mod
+          key: ${{ runner.os }}-go
+          restore-keys: |
+            ${{ runner.os }}-go
 
-
-  load-to-mariadb:
-    runs-on: ubuntu-latest          
-    steps:
-      - uses: actions/checkout@v3
-      - name: download data
+      - name: Build linux target
         run: |
-          wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
-          gzip -dk  2M_emtpy_files.dump.gz
-      - name: install 
-        run: | 
-          docker run -p 127.0.0.1:3306:3306  --name mdb -e MARIADB_ROOT_PASSWORD=root -d mariadb:latest
-      - name: Test
-        uses: ./.github/actions/load
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/load_test"
-          load_file: "2M_emtpy_files.dump"   
+          make juicefs 
+        shell: bash
 
+      - name: Prepare meta db
+        run: | 
+          sudo chmod +x .github/scripts/start_meta_engine.sh
+          source .github/scripts/start_meta_engine.sh
+          start_meta_engine ${{matrix.meta}}
 
-  load-to-tidb:
-    runs-on: ubuntu-latest          
-    steps:
-      - uses: actions/checkout@v3
-      - name: download data
+      - name: Test
         run: |
+          sudo chmod +x .github/scripts/start_meta_engine.sh
+          source .github/scripts/start_meta_engine.sh
+          meta_url=$(get_meta_url ${{matrix.meta}})
+          create_database $meta_url
+          echo meta_url is: $meta_url
+          mount_point=/tmp/juicefs-load-test
           wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
           gzip -dk  2M_emtpy_files.dump.gz
-      - name: install 
+          load_file=2M_emtpy_files.dump
+          echo `date`, start load
+          sudo ./juicefs load $meta_url $load_file
+          echo `date`, finish load
+          echo `date`, start dump
+          sudo ./juicefs dump $meta_url dump.json
+          echo `date`, finish dump
+          sudo ./juicefs mount $meta_url $mount_point -d
+
+      - name: log
+        if: ${{ always() }}
+        shell: bash
         run: | 
-          curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
-          source /home/runner/.profile
-          tiup playground 5.4.0 &
-          sleep 120
-          mysql -h127.0.0.1 -P4000 -uroot -e "set global tidb_enable_noop_functions=1;"
-      - name: Test
-        uses: ./.github/actions/load
+          tail -300 /var/log/juicefs.log
+          grep "<FATAL>:" /var/log/juicefs.log && exit 1 || true
+
+      - name: Send Slack Notification
+        if: ${{ failure() }}
+        uses: juicedata/slack-notify-action@main
         with:
-          meta_url: "mysql://root:@\\(127.0.0.1:4000\\)/load_test"
-          load_file: "2M_emtpy_files.dump"   
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
+          
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        timeout-minutes: 1
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/ltpfs.yml b/.github/workflows/ltpfs.yml
index c7dd5162..b6d278a7 100644
--- a/.github/workflows/ltpfs.yml
+++ b/.github/workflows/ltpfs.yml
@@ -38,9 +38,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/ltpsyscallshead.yml b/.github/workflows/ltpsyscallshead.yml
index 5a356578..b9ca98c5 100644
--- a/.github/workflows/ltpsyscallshead.yml
+++ b/.github/workflows/ltpsyscallshead.yml
@@ -38,9 +38,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/ltpsyscallsmiddle.yml b/.github/workflows/ltpsyscallsmiddle.yml
index 63a883d0..c7debe78 100644
--- a/.github/workflows/ltpsyscallsmiddle.yml
+++ b/.github/workflows/ltpsyscallsmiddle.yml
@@ -38,9 +38,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/ltpsyscallstail.yml b/.github/workflows/ltpsyscallstail.yml
index aa657a5a..fd907109 100644
--- a/.github/workflows/ltpsyscallstail.yml
+++ b/.github/workflows/ltpsyscallstail.yml
@@ -38,9 +38,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/mongodb_mmap.yml b/.github/workflows/mongodb_mmap.yml
index 9a18826f..be3af5d9 100644
--- a/.github/workflows/mongodb_mmap.yml
+++ b/.github/workflows/mongodb_mmap.yml
@@ -42,9 +42,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/redis_compile.yml b/.github/workflows/redis_compile.yml
index 270a07cf..08c56904 100644
--- a/.github/workflows/redis_compile.yml
+++ b/.github/workflows/redis_compile.yml
@@ -35,9 +35,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index 74d84567..36b18ca6 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -46,9 +46,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
@@ -126,9 +126,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
@@ -185,9 +185,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/tpcds.yml b/.github/workflows/tpcds.yml
index d0259d4e..75d99868 100644
--- a/.github/workflows/tpcds.yml
+++ b/.github/workflows/tpcds.yml
@@ -56,9 +56,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 0f2088ea..da508a40 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -53,9 +53,9 @@ jobs:
         path: |
           ~/.cache/go-build
           ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        key: ${{ runner.os }}-go
         restore-keys: |
-          ${{ runner.os }}-go-
+          ${{ runner.os }}-go
 
     - name: Build linux target
       run: GOPATH=$HOME/go make
@@ -73,4 +73,10 @@ jobs:
       run: |
         sudo apt-get update
         sudo apt-get install librados-dev
-        GOPATH=$HOME/go make juicefs.ceph
\ No newline at end of file
+        GOPATH=$HOME/go make juicefs.ceph
+
+    - name: build fdb
+      run: |
+        wget https://github.com/apple/foundationdb/releases/download/6.3.23/foundationdb-clients_6.3.23-1_amd64.deb
+        sudo dpkg -i foundationdb-clients_6.3.23-1_amd64.deb
+        GOPATH=$HOME/go make juicefs.fdb
\ No newline at end of file
diff --git a/.github/workflows/version_compatible.yml b/.github/workflows/version_compatible.yml
deleted file mode 100644
index 6c99d2b4..00000000
--- a/.github/workflows/version_compatible.yml
+++ /dev/null
@@ -1,147 +0,0 @@
-name: "version-compatible-test"
-
-on:
-  push:
-    branches: 
-      - main
-      - release-**
-    paths-ignore:
-      - 'docs/**'
-      - '**.md'
-  pull_request:
-    #The branches below must be a subset of the branches above
-    branches: 
-      - main
-      - release-**
-    paths-ignore:
-      - 'docs/**'
-      - '**.md'
-      - '.github/**'
-  workflow_dispatch:
-
-jobs:
-  version-compatible-test-redis:
-    runs-on: ubuntu-latest
-    services:
-      redis:
-        # Docker Hub image
-        image: redis
-        # Set health checks to wait until redis has started
-        options: >-
-          --health-cmd "redis-cli ping"
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps port 6379 on service container to the host
-          - 6379:6379
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 1
-
-      - name: Setup minio
-        run: |
-          docker run -d -p 9000:9000 --name minio \
-                     -e "MINIO_ACCESS_KEY=minioadmin" \
-                     -e "MINIO_SECRET_KEY=minioadmin" \
-                     -v /tmp/data:/data \
-                     -v /tmp/config:/root/.minio \
-                     minio/minio server /data
-          wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
-          chmod +x mc
-          sudo apt install redis-tools
-          
-      - name: Test
-        uses: ./.github/actions/version-compatible
-        with:
-          meta_url: "redis://127.0.0.1/1"
-
-      - name: Send Slack Notification
-        if: ${{ failure() }}
-        uses: juicedata/slack-notify-action@main
-        with:
-          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
-
-
-  version-compatible-test-mysql:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 1
-
-      - name: start mysql
-        run: |
-          sudo /etc/init.d/mysql start
-
-      - name: Setup minio
-        run: |
-          docker run -d -p 9000:9000 --name minio \
-                     -e "MINIO_ACCESS_KEY=minioadmin" \
-                     -e "MINIO_SECRET_KEY=minioadmin" \
-                     -v /tmp/data:/data \
-                     -v /tmp/config:/root/.minio \
-                     minio/minio server /data
-          wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
-          chmod +x mc
-
-      - name: Test
-        uses: ./.github/actions/version-compatible
-        with:
-          meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-
-      - name: Send Slack Notification
-        if: ${{ failure() }}
-        uses: juicedata/slack-notify-action@main
-        with:
-          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
-
-
-  version-compatible-test-pg:
-    runs-on: ubuntu-latest
-    services:
-      postgres:
-        image: postgres
-        env:
-          POSTGRES_PASSWORD: postgres
-        options: >-
-          --health-cmd pg_isready
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          # Maps tcp port 5432 on service container to the host
-          - 5432:5432
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 1
-
-      - name: Setup minio
-        run: |
-          docker run -d -p 9000:9000 --name minio \
-                     -e "MINIO_ACCESS_KEY=minioadmin" \
-                     -e "MINIO_SECRET_KEY=minioadmin" \
-                     -v /tmp/data:/data \
-                     -v /tmp/config:/root/.minio \
-                     minio/minio server /data
-          wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
-          chmod +x mc
-
-      - name: Test
-        uses: ./.github/actions/version-compatible
-        with:
-          meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
-
-      - name: Send Slack Notification
-        if: ${{ failure() }}
-        uses: juicedata/slack-notify-action@main
-        with:
-          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
\ No newline at end of file
diff --git a/.github/workflows/version_compatible_hypo.yml b/.github/workflows/version_compatible_hypo.yml
index 560b91c8..5fd8749b 100644
--- a/.github/workflows/version_compatible_hypo.yml
+++ b/.github/workflows/version_compatible_hypo.yml
@@ -1,51 +1,156 @@
 name: "version-compatible-test-hypo"
 
 on:
-  # push:
-  #   branches: 
-  #     - main
-  #     - release-**
-  #   paths-ignore:
-  #     - 'docs/**'
-  #     - '**.md'
-  # pull_request:
-  #   #The branches below must be a subset of the branches above
-  #   branches: 
-  #     - main
-  #     - release-**
-  #   paths-ignore:
-  #     - 'docs/**'
-  #     - '**.md'
   schedule:
-    - cron:  '0 0 * * *'
+    - cron:  '0 16 * * *'
   workflow_dispatch:
 
 jobs:
-  test-1-0-0-redis:
-    runs-on: ubuntu-latest
+  vc-hypo:
+    continue-on-error: false
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest]
+        # old_juicefs_version: ['release-1.0']
+        old_juicefs_version: ['main', 'release-1.0', 'release-1.0-rc1']
+        # meta: ['redis']
+        meta: ['redis', 'mysql', 'tikv', 'postgres']
+        storage: ['minio']
+        include:
+          - os: ubuntu-latest
+            old_juicefs_version: 'release-1.0'
+            meta: 'redis'
+            storage: 'postgres'
+          - os: ubuntu-latest
+            old_juicefs_version: 'release-1.0'
+            meta: 'redis'
+            storage: 'mysql'
+          - os: ubuntu-latest
+            old_juicefs_version: 'release-1.0-beta1'
+            meta: 'redis'
+            storage: 'minio'
+          - os: ubuntu-latest
+            old_juicefs_version: 'release-0.17'
+            meta: 'redis'
+            storage: 'minio'
+
+    runs-on: ${{matrix.os}}
+
     services:
-      redis:
-        # Docker Hub image
-        image: redis
-        # Set health checks to wait until redis has started
+      postgres:
+        image: postgres
+        env:
+          POSTGRES_PASSWORD: postgres
         options: >-
-          --health-cmd "redis-cli ping"
+          --health-cmd pg_isready
           --health-interval 10s
           --health-timeout 5s
           --health-retries 5
         ports:
-          # Maps port 6379 on service container to the host
-          - 6379:6379
+          - 5432:5432
+
     steps:
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: '1.18.x'
+
+      - uses: actions/cache@v3
+        with:
+          path: |
+            ~/.cache/go-build
+            ~/go/pkg/mod
+          key: ${{ runner.os }}-go
+          restore-keys: |
+            ${{ runner.os }}-go
+
       - name: Checkout
         uses: actions/checkout@v3
         with:
           fetch-depth: 1
-      
+
+      - name: Make Juicefs
+        shell: bash
+        run: | 
+          make juicefs
+
+      - name: Checkout old version
+        uses: actions/checkout@v3
+        with:
+          ref: ${{matrix.old_juicefs_version}}
+          path: ${{matrix.old_juicefs_version}}
+
+      - name: Make old build
+        run: | 
+          cd ${{matrix.old_juicefs_version}}
+          make juicefs
+          cd -
+
+      - name: Prepare meta database
+        run: | 
+          meta=${{matrix.meta}}
+          storage=${{matrix.storage}}
+          if [[ "$meta" == redis ]]; then
+            sudo apt-get install -y redis-tools redis-server
+          fi
+          if [[ "$meta" == mysql || "$storage" == mysql ]]; then
+            sudo /etc/init.d/mysql start
+          fi
+          if [[ "$meta" == tikv ]]; then
+            curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
+            source /home/runner/.bash_profile
+            source /home/runner/.profile
+            tiup playground --mode tikv-slim &
+            git clone https://github.com/c4pt0r/tcli
+            cd tcli && make
+            sudo cp bin/tcli /usr/local/bin
+            cd -
+          fi
+          if [[ "$meta" == badger ]]; then
+            sudo go get github.com/dgraph-io/badger/v3
+          fi
+          if [[ "$meta" == mariadb ]]; then
+            docker run -p 127.0.0.1:3306:3306  --name mdb -e MARIADB_ROOT_PASSWORD=root -d mariadb:latest
+          fi
+
+          if [[ "$storage" == minio ]]; then
+            docker run -d -p 9000:9000 --name minio \
+                        -e "MINIO_ACCESS_KEY=minioadmin" \
+                        -e "MINIO_SECRET_KEY=minioadmin" \
+                        -v /tmp/data:/data \
+                        -v /tmp/config:/root/.minio \
+                        minio/minio server /data
+          fi
+          
+      - name: Install tools
+        run: | 
+          wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
+          chmod +x mc
+          sudo mv mc /usr/local/bin
+          sudo apt install redis-tools
+          sudo pip install hypothesis
+          sudo pip install minio
+          
       - name: Test
-        uses: ./.github/actions/version-compatible-hypo
+        timeout-minutes: 60
+        run: |          
+          export META=${{matrix.meta}}
+          export STORAGE=${{matrix.storage}}
+          new_version=`./juicefs --version | awk -F" " '{print $3}' | awk -F+ '{print $1}'`
+          mv juicefs juicefs-$new_version
+          export NEW_JFS_BIN="juicefs-$new_version"
+          old_version=`${{matrix.old_juicefs_version}}/juicefs --version | awk -F" " '{print $3}' | awk -F+ '{print $1}'`
+          mv ${{matrix.old_juicefs_version}}/juicefs juicefs-$old_version
+          export OLD_JFS_BIN="juicefs-$old_version"
+          python3 .github/scripts/testVersionCompatible.py
+
+      - name: Upload command log
+        if: ${{ always() }}
+        uses: actions/upload-artifact@v3
         with:
-          old_juicefs_version: '1.0.0'
+          name: ${{matrix.os}}-${{matrix.meta}}-${{matrix.old_juicefs_version}}.command.log
+          path: command.log
 
       - name: Display log
         if: ${{ always() }}
@@ -71,148 +176,7 @@ jobs:
           channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
           slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
 
-      # - name: Setup upterm session
-      #   if: ${{ failure() }}
-      #   timeout-minutes: 30
-      #   uses: lhotari/action-upterm@v1
-
-  # test-1-0-0-rc2-redis:
-  #   runs-on: ubuntu-latest
-  #   services:
-  #     redis:
-  #       # Docker Hub image
-  #       image: redis
-  #       # Set health checks to wait until redis has started
-  #       options: >-
-  #         --health-cmd "redis-cli ping"
-  #         --health-interval 10s
-  #         --health-timeout 5s
-  #         --health-retries 5
-  #       ports:
-  #         # Maps port 6379 on service container to the host
-  #         - 6379:6379
-  #   steps:
-  #     - name: Checkout
-  #       uses: actions/checkout@v3
-  #       with:
-  #         fetch-depth: 1
-      
-  #     - name: Test
-  #       uses: ./.github/actions/version-compatible-hypo
-  #       with:
-  #         old_juicefs_version: '1.0.0-rc2'
-
-  #     - name: Display log
-  #       if: ${{ always() }}
-  #       shell: bash
-  #       run: | 
-  #         if [ -f "/home/runner/.juicefs/juicefs.log" ]; then
-  #           cat /home/runner/.juicefs/juicefs.log
-  #           grep "<FATAL>:" /home/runner/.juicefs/juicefs.log && exit 1 || true
-  #         fi
-
-  #     - name: Send Slack Notification
-  #       if: ${{ failure() }}
-  #       uses: juicedata/slack-notify-action@main
-  #       with:
-  #         channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-  #         slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
-
-      # - name: Setup upterm session
-      #   if: ${{ failure() }}
-      #   timeout-minutes: 30
-      #   uses: lhotari/action-upterm@v1
-
-  # test-1-0-0-beta1-redis:
-  #   runs-on: ubuntu-latest
-  #   services:
-  #     redis:
-  #       # Docker Hub image
-  #       image: redis
-  #       # Set health checks to wait until redis has started
-  #       options: >-
-  #         --health-cmd "redis-cli ping"
-  #         --health-interval 10s
-  #         --health-timeout 5s
-  #         --health-retries 5
-  #       ports:
-  #         # Maps port 6379 on service container to the host
-  #         - 6379:6379
-  #   steps:
-  #     - name: Checkout
-  #       uses: actions/checkout@v3
-  #       with:
-  #         fetch-depth: 1
-      
-  #     - name: Test
-  #       uses: ./.github/actions/version-compatible-hypo
-  #       with:
-  #         old_juicefs_version: '1.0.0-beta1'
-
-  #     - name: Display log
-  #       if: ${{ always() }}
-  #       shell: bash
-  #       run: | 
-  #         if [ -f "/home/runner/.juicefs/juicefs.log" ]; then
-  #           cat /home/runner/.juicefs/juicefs.log
-  #           grep "<FATAL>:" /home/runner/.juicefs/juicefs.log && exit 1 || true
-  #         fi
-
-  #     - name: Send Slack Notification
-  #       if: ${{ failure() }}
-  #       uses: juicedata/slack-notify-action@main
-  #       with:
-  #         channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-  #         slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
-
-      # - name: Setup upterm session
-      #   if: ${{ failure() }}
-      #   timeout-minutes: 30
-      #   uses: lhotari/action-upterm@v1
-
-  # test-0-17-1-redis:
-  #   runs-on: ubuntu-latest
-  #   services:
-  #     redis:
-  #       # Docker Hub image
-  #       image: redis
-  #       # Set health checks to wait until redis has started
-  #       options: >-
-  #         --health-cmd "redis-cli ping"
-  #         --health-interval 10s
-  #         --health-timeout 5s
-  #         --health-retries 5
-  #       ports:
-  #         # Maps port 6379 on service container to the host
-  #         - 6379:6379
-  #   steps:
-  #     - name: Checkout
-  #       uses: actions/checkout@v3
-  #       with:
-  #         fetch-depth: 1
-      
-  #     - name: Test
-  #       uses: ./.github/actions/version-compatible-hypo
-  #       with:
-  #         old_juicefs_version: '0.17.1'
-
-  #     - name: Display log
-  #       if: ${{ always() }}
-  #       shell: bash
-  #       run: | 
-  #         if [ -f "/home/runner/.juicefs/juicefs.log" ]; then
-  #           cat /home/runner/.juicefs/juicefs.log
-  #           grep "<FATAL>:" /home/runner/.juicefs/juicefs.log && exit 1 || true
-  #         fi
-
-  #     - name: Send Slack Notification
-  #       if: ${{ failure() }}
-  #       uses: juicedata/slack-notify-action@main
-  #       with:
-  #         channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
-  #         slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
-
-      # - name: Setup upterm session
-      #   if: ${{ failure() }}
-      #   timeout-minutes: 30
-      #   uses: lhotari/action-upterm@v1
\ No newline at end of file
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        timeout-minutes: 1
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 490f6adb..e8469095 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,5 @@ docs/node_modules
 cmd/cmd
 *.dump
 *.out
+.hypothesis
+__pycache__
diff --git a/Makefile b/Makefile
index 0b85ddbb..c259dc6e 100644
--- a/Makefile
+++ b/Makefile
@@ -30,6 +30,8 @@ juicefs.lite: Makefile cmd/*.go pkg/*/*.go
 juicefs.ceph: Makefile cmd/*.go pkg/*/*.go
 	go build -tags ceph -ldflags="$(LDFLAGS)"  -o juicefs.ceph .
 
+juicefs.fdb: Makefile cmd/*.go pkg/*/*.go
+	go build -tags fdb -ldflags="$(LDFLAGS)"  -o juicefs.fdb .
 
 # This is the script for compiling the Linux version on the MacOS platform.
 # Please execute the `brew install FiloSottile/musl-cross/musl-cross` command before using it.
@@ -71,3 +73,6 @@ release:
 test:
 	go test -v -cover -count=1 -timeout=8m ./pkg/... -coverprofile=cov1.out
 	sudo JFS_GC_SKIPPEDTIME=1 MINIO_ACCESS_KEY=testUser MINIO_SECRET_KEY=testUserPassword go test -v -count=1 -cover -timeout=8m ./cmd/... -coverprofile=cov2.out -coverpkg=./pkg/...,./cmd/...
+
+test.fdb:
+	go test -v -cover -count=1 -timeout=8m ./pkg/meta/ -tags fdb -run=TestFdb -coverprofile=cov3.out
\ No newline at end of file
diff --git a/cmd/doctor.go b/cmd/doctor.go
new file mode 100644
index 00000000..0e21de36
--- /dev/null
+++ b/cmd/doctor.go
@@ -0,0 +1,566 @@
+/*
+ * JuiceFS, Copyright 2022 Juicedata, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"archive/zip"
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/juicedata/juicefs/pkg/meta"
+	"github.com/juicedata/juicefs/pkg/utils"
+
+	"github.com/urfave/cli/v2"
+)
+
+var defaultOutDir = filepath.Join(".", "doctor")
+
+func cmdDoctor() *cli.Command {
+	return &cli.Command{
+		Name:      "doctor",
+		Action:    doctor,
+		Category:  "INSPECTOR",
+		ArgsUsage: "MOUNTPOINT",
+		Usage:     "Collect and display system static and runtime information",
+		Description: `
+It collects and displays information from multiple dimensions such as the running environment and system logs, etc.
+
+Examples:
+$ juicefs doctor /mnt/jfs
+
+# Result will be output to /var/log/
+$ juicefs doctor --out-dir=/var/log /mnt/jfs
+
+# Get the last up to 1000 log entries
+$ juicefs doctor --out-dir=/var/log --collect-log --limit=1000 /mnt/jfs
+
+# Get pprof information
+$ juicefs doctor --out-dir=/var/log --collect-log --limit=1000 --collect-pprof /mnt/jfs
+`,
+		Flags: []cli.Flag{
+			&cli.StringFlag{
+				Name:  "out-dir",
+				Value: defaultOutDir,
+				Usage: "the output directory of the result file",
+			},
+			&cli.Uint64Flag{
+				Name:  "stats-sec",
+				Value: 5,
+				Usage: "stats sampling duration",
+			},
+			&cli.BoolFlag{
+				Name:  "collect-log",
+				Usage: "enable log collection",
+			},
+			&cli.Uint64Flag{
+				Name:  "limit",
+				Usage: "the number of last entries to be collected",
+			},
+			&cli.BoolFlag{
+				Name:  "collect-pprof",
+				Usage: "enable pprof collection",
+			},
+			&cli.Uint64Flag{
+				Name:  "trace-sec",
+				Value: 5,
+				Usage: "trace sampling duration",
+			},
+			&cli.Uint64Flag{
+				Name:  "profile-sec",
+				Value: 30,
+				Usage: "profile sampling duration",
+			},
+		},
+	}
+}
+
+func copyVolumeConfWindows(srcPath, destPath string) error {
+	srcFile, err := os.Open(srcPath)
+	if err != nil {
+		return err
+	}
+	defer closeFile(srcFile)
+	destFile, err := os.Create(destPath)
+	if err != nil {
+		return err
+	}
+	defer closeFile(destFile)
+	if _, err := io.Copy(destFile, srcFile); err != nil {
+		return err
+	}
+	return nil
+}
+
+func copyConfigFile(srcPath, destPath string, rootPrivileges bool) error {
+	if runtime.GOOS == "windows" {
+		return copyVolumeConfWindows(srcPath, destPath)
+	}
+
+	var copyArgs []string
+	if rootPrivileges {
+		copyArgs = append(copyArgs, "sudo")
+	}
+	copyArgs = append(copyArgs, "/bin/sh", "-c", fmt.Sprintf("cat %s > %s", srcPath, destPath))
+	return exec.Command(copyArgs[0], copyArgs[1:]...).Run()
+}
+
+func getCmdMount(mp string) (uid, pid, cmd string, err error) {
+	psArgs := []string{"/bin/sh", "-c", "ps -ef | grep -v grep | grep 'juicefs mount' | grep " + mp}
+	ret, err := exec.Command(psArgs[0], psArgs[1:]...).CombinedOutput()
+	if err != nil {
+		return "", "", "", fmt.Errorf("failed to execute command `%s`: %v", strings.Join(psArgs, " "), err)
+	}
+
+	lines := strings.Split(string(ret), "\n")
+	for i := len(lines) - 1; i >= 0; i-- {
+		line := lines[i]
+		fields := strings.Fields(line)
+		if len(fields) <= 7 {
+			continue
+		}
+		cmdFields := fields[7:]
+		for _, arg := range cmdFields {
+			if mp == arg {
+				cmd = strings.Join(fields[7:], " ")
+				uid, pid = strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1])
+				break
+			}
+		}
+	}
+	if cmd == "" {
+		return "", "", "", fmt.Errorf("no mount command found for %s", mp)
+	}
+	return uid, pid, cmd, nil
+}
+
+func getDefaultLogDir(rootPrivileges bool) (string, error) {
+	var defaultLogDir = "/var/log"
+	switch runtime.GOOS {
+	case "linux":
+		if rootPrivileges {
+			break
+		}
+		fallthrough
+	case "darwin":
+		if rootPrivileges {
+			defaultLogDir = "/var/root/.juicefs"
+			break
+		}
+		homeDir, err := os.UserHomeDir()
+		if err != nil {
+			return "", fmt.Errorf("failed to get home directory")
+		}
+		defaultLogDir = filepath.Join(homeDir, ".juicefs")
+	}
+	return defaultLogDir, nil
+}
+
+var logArg = regexp.MustCompile(`--log(\s*=?\s*)(\S+)`)
+
+func getLogPath(cmd string, rootPrivileges bool) (string, error) {
+	var logPath string
+	tmp := logArg.FindStringSubmatch(cmd)
+	if len(tmp) == 3 {
+		logPath = tmp[2]
+	} else {
+		defaultLogDir, err := getDefaultLogDir(rootPrivileges)
+		if err != nil {
+			return "", err
+		}
+		logPath = filepath.Join(defaultLogDir, "juicefs.log")
+	}
+
+	return logPath, nil
+}
+
+func closeFile(file *os.File) {
+	if err := file.Close(); err != nil {
+		logger.Fatalf("failed to close file %s: %v", file.Name(), err)
+	}
+}
+
+func copyLogFile(logPath, retLogPath string, limit uint64, rootPrivileges bool) error {
+	var copyArgs []string
+	if rootPrivileges {
+		copyArgs = append(copyArgs, "sudo")
+	}
+	copyArgs = append(copyArgs, "/bin/sh", "-c")
+	if limit > 0 {
+		copyArgs = append(copyArgs, fmt.Sprintf("tail -n %d %s > %s", limit, logPath, retLogPath))
+	} else {
+		copyArgs = append(copyArgs, fmt.Sprintf("cat %s > %s", logPath, retLogPath))
+	}
+	return exec.Command(copyArgs[0], copyArgs[1:]...).Run()
+}
+
+func getPprofPort(pid, mp string, rootPrivileges bool) (int, error) {
+	var lsofArgs []string
+	if rootPrivileges {
+		lsofArgs = append(lsofArgs, "sudo")
+	}
+	lsofArgs = append(lsofArgs, "/bin/sh", "-c", "lsof -i -nP | grep -v grep | grep LISTEN | grep "+pid)
+	ret, err := exec.Command(lsofArgs[0], lsofArgs[1:]...).CombinedOutput()
+	if err != nil {
+		return 0, fmt.Errorf("failed to execute command `%s`: %v", strings.Join(lsofArgs, " "), err)
+	}
+	lines := strings.Split(string(ret), "\n")
+	if len(lines) == 0 {
+		return 0, fmt.Errorf("pprof will be collected, but no listen port")
+	}
+
+	var listenPort = -1
+	for _, line := range lines {
+		fields := strings.Fields(line)
+		if len(fields) != 0 {
+			port, err := strconv.Atoi(strings.Split(fields[len(fields)-2], ":")[1])
+			if err != nil {
+				logger.Errorf("failed to parse port %v: %v", port, err)
+			}
+			if port >= 6060 && port <= 6099 && port > listenPort {
+				if err := checkPort(port, mp); err == nil {
+					listenPort = port
+				}
+				continue
+			}
+		}
+	}
+
+	if listenPort == -1 {
+		return 0, fmt.Errorf("no valid pprof port found")
+	}
+	return listenPort, nil
+}
+
+func getRequest(url string) ([]byte, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, fmt.Errorf("error GET request: %v", err)
+	}
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf("error GET request, status code %d", resp.StatusCode)
+	}
+
+	defer func(body io.ReadCloser) {
+		if err := body.Close(); err != nil {
+			logger.Errorf("error closing body: %v", err)
+		}
+	}(resp.Body)
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("error reading response: %v", err)
+	}
+
+	return body, nil
+}
+
+// check pprof service status
+func checkPort(port int, mp string) error {
+	url := fmt.Sprintf("http://localhost:%d/debug/pprof/cmdline?debug=1", port)
+	resp, err := getRequest(url)
+	if err != nil {
+		return fmt.Errorf("error checking pprof alive: %v", err)
+	}
+	resp = bytes.ReplaceAll(resp, []byte{0}, []byte{' '})
+	fields := strings.Fields(string(resp))
+	flag := false
+	for _, field := range fields {
+		if mp == field {
+			flag = true
+		}
+	}
+	if !flag {
+		return fmt.Errorf("mount point mismatch: \n%s\n%s", resp, mp)
+	}
+
+	return nil
+}
+
+type metricItem struct {
+	name, url string
+}
+
+func reqAndSaveMetric(name string, metric metricItem, outDir string) error {
+	resp, err := getRequest(metric.url)
+	if err != nil {
+		return fmt.Errorf("error getting metric: %v", err)
+	}
+	retPath := filepath.Join(outDir, fmt.Sprintf("juicefs.%s", metric.name))
+	retFile, err := os.Create(retPath)
+	if err != nil {
+		logger.Fatalf("error creating metric file %s: %v", retPath, err)
+	}
+	defer closeFile(retFile)
+
+	if name == "cmdline" {
+		resp = bytes.ReplaceAll(resp, []byte{0}, []byte{' '})
+	}
+
+	writer := bufio.NewWriter(retFile)
+	if _, err := writer.Write(resp); err != nil {
+		return fmt.Errorf("failed to write metric %s: %v", name, err)
+	}
+	if err := writer.Flush(); err != nil {
+		return fmt.Errorf("failed to flush writer: %v", err)
+	}
+
+	return nil
+}
+
+func isUnix() bool {
+	return runtime.GOOS == "linux" || runtime.GOOS == "darwin"
+}
+
+func checkAgent(cmd string) bool {
+	fields := strings.Fields(cmd)
+	for _, field := range fields {
+		if field == "--no-agent" {
+			return false
+		}
+	}
+	return true
+}
+
+func geneZipFile(srcPath, destPath string) error {
+	zipFile, err := os.Create(destPath)
+	if err != nil {
+		return err
+	}
+	defer closeFile(zipFile)
+	archive := zip.NewWriter(zipFile)
+	defer func() {
+		if err := archive.Close(); err != nil {
+			logger.Fatalf("error closing zip archive: %v", err)
+		}
+	}()
+
+	if err = filepath.Walk(srcPath, func(path string, info os.FileInfo, _ error) error {
+		if path == srcPath {
+			return nil
+		}
+
+		header, err := zip.FileInfoHeader(info)
+		if err != nil {
+			return err
+		}
+		header.Name = strings.TrimPrefix(path, srcPath+`/`)
+		if info.IsDir() {
+			header.Name += `/`
+		} else {
+			header.Method = zip.Deflate
+		}
+
+		writer, err := archive.CreateHeader(header)
+		if err != nil {
+			return err
+		}
+		if !info.IsDir() {
+			file, err := os.Open(path)
+			if err != nil {
+				return err
+			}
+			defer closeFile(file)
+			if _, err := io.Copy(writer, file); err != nil {
+				return err
+			}
+		}
+		return nil
+	}); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func doctor(ctx *cli.Context) error {
+	setup(ctx, 1)
+	mp := ctx.Args().First()
+	inode, err := utils.GetFileInode(mp)
+	if err != nil {
+		return fmt.Errorf("failed to lookup inode for %s: %s", mp, err)
+	}
+	if inode != uint64(meta.RootInode) {
+		return fmt.Errorf("path %s is not a mount point", mp)
+	}
+
+	outDir := ctx.String("out-dir")
+	// special treatment for non-existing out dir
+	if outDirInfo, err := os.Stat(outDir); os.IsNotExist(err) {
+		if err := os.MkdirAll(outDir, os.ModePerm); err != nil {
+			return fmt.Errorf("failed to create out dir %s: %v", outDir, err)
+		}
+	} else if err == nil && !outDirInfo.IsDir() {
+		return fmt.Errorf("argument --out-dir is not directory: %s", outDir)
+	}
+
+	mp, _ = filepath.Abs(mp)
+	timestamp := time.Now().Format("20060102150405")
+	prefix := strings.Trim(strings.Join(strings.Split(mp, "/"), "-"), "-")
+	currDir := filepath.Join(outDir, fmt.Sprintf("%s-%s", prefix, timestamp))
+	if err := os.Mkdir(currDir, os.ModePerm); err != nil {
+		return fmt.Errorf("failed to create current out dir %s: %v", currDir, err)
+	}
+
+	sysInfo, err := utils.GetSysInfo()
+	if err != nil {
+		return fmt.Errorf("failed to get system info: %v", err)
+	}
+
+	result := fmt.Sprintf(`Platform: 
+%s %s
+%s
+JuiceFS Version:
+%s`, runtime.GOOS, runtime.GOARCH, sysInfo, ctx.App.Version)
+
+	sysPath := filepath.Join(currDir, "system-info.log")
+	sysFile, err := os.Create(sysPath)
+	if err != nil {
+		return fmt.Errorf("failed to create system info file %s: %v", sysPath, err)
+	}
+	defer closeFile(sysFile)
+	if _, err = sysFile.WriteString(result); err != nil {
+		return fmt.Errorf("failed to write system info file %s: %v", sysPath, err)
+	}
+
+	fmt.Printf("\n%s\n", result)
+
+	uid, pid, cmd, err := getCmdMount(mp)
+	if err != nil {
+		return fmt.Errorf("failed to get mount command: %v", err)
+	}
+	fmt.Printf("\nMount Command:\n%s\n\n", cmd)
+
+	rootPrivileges := false
+	if (uid == "0" || uid == "root") && os.Getuid() != 0 {
+		fmt.Println("Mount point is mounted by the root user, may ask for root privilege...")
+		rootPrivileges = true
+	}
+
+	configName := ".config"
+	if err := copyConfigFile(filepath.Join(mp, configName), filepath.Join(currDir, "config.txt"), rootPrivileges); err != nil {
+		return fmt.Errorf("failed to get volume config %s: %v", configName, err)
+	}
+
+	statsName := ".stats"
+	stats := ctx.Uint64("stats-sec")
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		srcPath := filepath.Join(mp, statsName)
+		destPath := filepath.Join(currDir, "stats.txt")
+		if err := copyConfigFile(srcPath, destPath, rootPrivileges); err != nil {
+			logger.Errorf("Failed to get volume config %s: %v", statsName, err)
+		}
+
+		logger.Infof("Stats metrics are being sampled, sampling duration: %ds", stats)
+		time.Sleep(time.Second * time.Duration(stats))
+
+		destPath = filepath.Join(currDir, fmt.Sprintf("stats.%ds.txt", stats))
+		if err := copyConfigFile(srcPath, destPath, rootPrivileges); err != nil {
+			logger.Errorf("Failed to get volume config %s: %v", statsName, err)
+		}
+	}()
+
+	if !isUnix() {
+		logger.Warnf("Collecting log currently only support Linux/macOS")
+	}
+
+	if isUnix() && ctx.Bool("collect-log") {
+		logPath, err := getLogPath(cmd, rootPrivileges)
+		if err != nil {
+			return fmt.Errorf("failed to get log path: %v", err)
+		}
+		limit := ctx.Uint64("limit")
+		retLogPath := filepath.Join(currDir, "juicefs.log")
+
+		logger.Infof("Log %s is being collected", logPath)
+		if err := copyLogFile(logPath, retLogPath, limit, rootPrivileges); err != nil {
+			return fmt.Errorf("failed to get log file: %v", err)
+		}
+	}
+
+	enableAgent := checkAgent(cmd)
+	if !enableAgent {
+		logger.Warnf("No agent found, the pprof metrics will not be collected")
+	}
+
+	if !isUnix() {
+		logger.Warnf("Collecting pprof currently only support Linux/macOS")
+	}
+
+	if isUnix() && enableAgent && ctx.Bool("collect-pprof") {
+		port, err := getPprofPort(pid, mp, rootPrivileges)
+		if err != nil {
+			return fmt.Errorf("failed to get pprof port: %v", err)
+		}
+		baseUrl := fmt.Sprintf("http://localhost:%d/debug/pprof/", port)
+		trace := ctx.Uint64("trace-sec")
+		profile := ctx.Uint64("profile-sec")
+		metrics := map[string]metricItem{
+			"allocs":       {name: "allocs.pb.gz", url: baseUrl + "allocs"},
+			"blocks":       {name: "block.pb.gz", url: baseUrl + "block"},
+			"cmdline":      {name: "cmdline.txt", url: baseUrl + "cmdline"},
+			"goroutine":    {name: "goroutine.pb.gz", url: baseUrl + "goroutine"},
+			"stack":        {name: "goroutine.stack.txt", url: baseUrl + "goroutine?debug=1"},
+			"heap":         {name: "heap.pb.gz", url: baseUrl + "heap"},
+			"mutex":        {name: "mutex.pb.gz", url: baseUrl + "mutex"},
+			"threadcreate": {name: "threadcreate.pb.gz", url: baseUrl + "threadcreate"},
+			"trace":        {name: fmt.Sprintf("trace.%ds.pb.gz", trace), url: fmt.Sprintf("%strace?seconds=%d", baseUrl, trace)},
+			"profile":      {name: fmt.Sprintf("profile.%ds.pb.gz", profile), url: fmt.Sprintf("%sprofile?seconds=%d", baseUrl, profile)},
+		}
+
+		pprofOutDir := filepath.Join(currDir, "pprof")
+		if err := os.Mkdir(pprofOutDir, os.ModePerm); err != nil {
+			return fmt.Errorf("failed to create out directory: %v", err)
+		}
+
+		for name, metric := range metrics {
+			wg.Add(1)
+			go func(name string, metric metricItem) {
+				defer wg.Done()
+
+				if name == "profile" {
+					logger.Infof("Profile metrics are being sampled, sampling duration: %ds", profile)
+				}
+				if name == "trace" {
+					logger.Infof("Trace metrics are being sampled, sampling duration: %ds", trace)
+				}
+				if err := reqAndSaveMetric(name, metric, pprofOutDir); err != nil {
+					logger.Errorf("Failed to get and save metric %s: %v", name, err)
+				}
+			}(name, metric)
+		}
+	}
+	wg.Wait()
+
+	if err := geneZipFile(currDir, filepath.Join(outDir, fmt.Sprintf("%s-%s.zip", prefix, timestamp))); err != nil {
+		return fmt.Errorf("failed to zip result %s: %v", currDir, err)
+	}
+	return nil
+}
diff --git a/cmd/fsck.go b/cmd/fsck.go
index 64e32289..4a474879 100644
--- a/cmd/fsck.go
+++ b/cmd/fsck.go
@@ -46,7 +46,10 @@ Examples:
 $ juicefs fsck redis://localhost
 
 # Repair broken directories
-$ juicefs fsck redis://localhost --path /d1/d2 --repair`,
+$ juicefs fsck redis://localhost --path /d1/d2 --repair
+
+# recursively check
+$ juicefs fsck redis://localhost --path /d1/d2 --recursive`,
 		Flags: []cli.Flag{
 			&cli.StringFlag{
 				Name:  "path",
@@ -56,6 +59,11 @@ $ juicefs fsck redis://localhost --path /d1/d2 --repair`,
 				Name:  "repair",
 				Usage: "repair specified path if it's broken",
 			},
+			&cli.BoolFlag{
+				Name:    "recursive",
+				Aliases: []string{"r"},
+				Usage:   "recursively check or repair",
+			},
 		},
 	}
 }
@@ -76,7 +84,7 @@ func fsck(ctx *cli.Context) error {
 		if !strings.HasPrefix(p, "/") {
 			logger.Fatalf("File path should be the absolute path within JuiceFS")
 		}
-		return m.Check(c, p, ctx.Bool("repair"))
+		return m.Check(c, p, ctx.Bool("repair"), ctx.Bool("recursive"))
 	}
 
 	chunkConf := chunk.Config{
diff --git a/cmd/main.go b/cmd/main.go
index 7487d29f..8d11ae82 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -74,6 +74,7 @@ func Main(args []string) error {
 			cmdWarmup(),
 			cmdRmr(),
 			cmdSync(),
+			cmdDoctor(),
 		},
 	}
 
diff --git a/cmd/objbench.go b/cmd/objbench.go
index 0b09252a..90415514 100644
--- a/cmd/objbench.go
+++ b/cmd/objbench.go
@@ -153,7 +153,11 @@ func objbench(ctx *cli.Context) error {
 		failed = fmt.Sprintf("%s%dm%s%s", COLOR_SEQ, RED, failed, RESET_SEQ)
 	}
 
-	if !ctx.IsSet("skip-functional-tests") {
+	if ctx.Bool("skip-functional-tests") {
+		if err := blob.Create(); err != nil {
+			return fmt.Errorf("can't create bucket: %s", err)
+		}
+	} else {
 		var result [][]string
 		result = append(result, []string{"CATEGORY", "TEST", "RESULT"})
 		fmt.Println("Start Functional Testing ...")
diff --git a/cmd/sync.go b/cmd/sync.go
index 930d8f25..69b294a2 100644
--- a/cmd/sync.go
+++ b/cmd/sync.go
@@ -313,8 +313,16 @@ func doSync(c *cli.Context) error {
 	go func() { _ = http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", config.HTTPPort), nil) }()
 
 	// Windows support `\` and `/` as its separator, Unix only use `/`
-	srcURL := strings.Replace(c.Args().Get(0), "\\", "/", -1)
-	dstURL := strings.Replace(c.Args().Get(1), "\\", "/", -1)
+	srcURL := c.Args().Get(0)
+	dstURL := c.Args().Get(1)
+	if runtime.GOOS == "windows" {
+		if !strings.Contains(srcURL, "://") {
+			srcURL = strings.Replace(srcURL, "\\", "/", -1)
+		}
+		if !strings.Contains(dstURL, "://") {
+			dstURL = strings.Replace(dstURL, "\\", "/", -1)
+		}
+	}
 	if strings.HasSuffix(srcURL, "/") != strings.HasSuffix(dstURL, "/") {
 		logger.Fatalf("SRC and DST should both end with path separator or not!")
 	}
diff --git a/docs/en/administration/fault_diagnosis_and_analysis.md b/docs/en/administration/fault_diagnosis_and_analysis.md
index 1d2f1128..5e26f891 100644
--- a/docs/en/administration/fault_diagnosis_and_analysis.md
+++ b/docs/en/administration/fault_diagnosis_and_analysis.md
@@ -149,6 +149,14 @@ By default, pprof listens on port numbers ranging from 6060 to 6099. That's why
 - Goroutine stack information: `http://localhost:<port>/debug/pprof/goroutine?debug=1`
 - CPU performance statistics: `http://localhost:<port>/debug/pprof/profile?seconds=30`
 - Memory allocation statistics: `http://localhost:<port>/debug/pprof/heap`
+- 
+:::tip
+You can also use the doctor command to automatically collect these runtime information and save it locally. By default, it is saved to the doctor directory under the current directory, for example:
+```bash
+juicefs doctor --collect-pprof /mnt/jfs
+```
+For more information about the doctor command, see [command reference](https://juicefs.com/docs/community/command_reference#juicefs-doctor)
+:::
 
 To make it easier to analyze this runtime information, you can save it locally, e.g.:
 
diff --git a/docs/en/community/adopters.md b/docs/en/community/adopters.md
index 1d468ebb..a0d9d2b3 100644
--- a/docs/en/community/adopters.md
+++ b/docs/en/community/adopters.md
@@ -6,33 +6,39 @@ slug: /adopters
 
 # JuiceFS Adopters
 
-| Company/Team                                                                            | Environment | Use Cases                                                                           |
-|:----------------------------------------------------------------------------------------|:------------|:------------------------------------------------------------------------------------|
-| [Xiaomi](https://www.mi.com)                                                            | Production  | AI                                                                                  |
-| [Li Auto Inc.](https://www.lixiang.com)                                                 | Production  | Big Data, AI                                                                        |
-| [Shopee](https://shopee.com)                                                            | Production  | Big Data ([User story](https://juicefs.com/en/blog/shopee-clickhouse-with-juicefs)) |
-| [Zhihu](https://www.zhihu.com)                                                          | Production  | Big Data                                                                            |
-| [Yaoxin Financing Re-Guarantee](https://www.yaoxinhd.com)                               | Production  | Big Data, File Sharing                                                              |
-| [Megvii](https://megvii.com)                                                            | Production  | AI                                                                                  |
-| [Piesat Information Technology Co., Ltd.](https://www.piesat.cn)                        | Production  | File Sharing                                                                        |
-| [Gene Way](https://www.geneway.cn)                                                      | Production  | File Sharing                                                                        |
-| [SAIC Motor](https://www.saicmotor.com/english/index.shtml)                             | Production  | AI                                                                                  |
-| [National Supercomputing Center in JiNan](https://www.nsccjn.cn/)                       | Production  | DevOps                                                                              |
-| [UniSound](https://www.unisound.com)                                                    | Production  | AI                                                                                  |
-| [NetEase Games](http://www.neteasegames.com)                                            | Production  | Big Data, File Sharing                                                              |
-| [Yimian](https://www.yimian.io)                                                         | Production  | Big Data                                                                            |
-| [Trip.com](https://www.trip.com)                                                        | Production  | Big Data, File Sharing                                                              |
-| [vivo](https://www.vivo.com)                                                            | Production  | AI, File Sharing                                                                    |
-| [Horizon Robotics](https://horizon.ai)                                                  | Production  | AI                                                                                  |
-| [SenseTime](https://www.sensetime.com/en)                                               | Production  | File Sharing                                                                        |
-| [DP Technology](https://www.dp.tech)                                                    | Production  | File Sharing                                                                        |
-| [Joyient](http://www.joyient.com)                                                       | Prodcution  | File Sharing, 3D Rendering                                                          |
-| [DJI](https://www.dji.com)                                                              | Production  | AI                                                                                  |
-| [China Mobile Cloud](https://ecloud.he.chinamobile.com)                                 | Testing     | Big Data                                                                            |
-| [Volcano Engine](https://www.volcengine.com)                                            | Testing     | File Sharing                                                                        |
-| [Dingdong Fresh](https://www.100.me)                                                    | Testing     | Big Data                                                                            |
-| [SF Express](https://www.sf-express.com)                                                | Testing     | Big Data, File Sharing                                                              |
-| [BIGO](https://bigo.tv)                                                                 | Testing     | AI                                                                                  |
-| [Inner Mongolia MENGSHANG Consumer Finance Co., Ltd.](https://www.mengshangxiaofei.com) | Testing     | File Sharing                                                                        |
+| Company/Team                                                                            | Environment | Use Cases                  | User Story                                                                                                                                                                                          |
+|-----------------------------------------------------------------------------------------|-------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Xiaomi](https://www.mi.com/global)                                                     | Production  | AI                         |                                                                                                                                                                                                     |
+| [Li Auto](https://www.lixiang.com/en)                                                   | Production  | Big Data, AI               |                                                                                                                                                                                                     |
+| [Shopee](https://shopee.com)                                                            | Production  | Big Data                   | [Shopee x JuiceFS: ClickHouse Cold and Hot Data Separation Storage Architecture and Practice](https://juicefs.com/en/blog/shopee-clickhouse-with-juicefs)                                           |
+| [Zhihu](https://www.zhihu.com)                                                          | Production  | Big Data                   |                                                                                                                                                                                                     |
+| [Yaoxin Financing Re-Guarantee](https://www.yaoxinhd.com)                               | Production  | Big Data, File Sharing     |                                                                                                                                                                                                     |
+| [Megvii](https://megvii.com)                                                            | Production  | AI                         |                                                                                                                                                                                                     |
+| [Piesat Information Technology Co., Ltd.](https://www.piesat.cn)                        | Production  | File Sharing               |                                                                                                                                                                                                     |
+| [Gene Way](https://www.geneway.cn)                                                      | Production  | File Sharing               |                                                                                                                                                                                                     |
+| [SAIC Motor](https://www.saicmotor.com/english)                                         | Production  | AI                         |                                                                                                                                                                                                     |
+| [National Supercomputing Center in JiNan](https://www.nsccjn.cn/)                       | Production  | DevOps                     |                                                                                                                                                                                                     |
+| [UniSound](https://www.unisound.com)                                                    | Production  | AI                         | [Unisound’s HPC Platform accelerates AI model training and development with JuiceFS](https://juicefs.com/en/blog/unisounds-hpc-platform-accelerates-ai-model-training-and-development-with-juicefs) |
+| [NetEase Games](https://www.neteasegames.com)                                           | Production  | Big Data, File Sharing     |                                                                                                                                                                                                     |
+| [Yimian](https://www.yimian.io)                                                         | Production  | Big Data                   |                                                                                                                                                                                                     |
+| [Trip.com](https://www.trip.com)                                                        | Production  | Big Data, File Sharing     |                                                                                                                                                                                                     |
+| [vivo](https://www.vivo.com)                                                            | Production  | AI, File Sharing           |                                                                                                                                                                                                     |
+| [Horizon Robotics](https://horizon.ai)                                                  | Production  | AI                         |                                                                                                                                                                                                     |
+| [SenseTime](https://www.sensetime.com/en)                                               | Production  | File Sharing               |                                                                                                                                                                                                     |
+| [DP Technology](https://www.dp.tech)                                                    | Production  | File Sharing               |                                                                                                                                                                                                     |
+| [Joyient](http://www.joyient.com)                                                       | Prodcution  | File Sharing, 3D Rendering |                                                                                                                                                                                                     |
+| [DJI](https://www.dji.com)                                                              | Production  | AI                         |                                                                                                                                                                                                     |
+| [Kingsoft Cloud](https://en.ksyun.com)                                                  | Production  | Big Data                   |                                                                                                                                                                                                     |
+| [Skyplatanus](https://www.kuaidianyuedu.com)                                            | Production  | AI，File Sharing           |                                                                                                                                                                                                     |
+| [Clobotics](https://clobotics.com)                                                      | Production  | AI                         |                                                                                                                                                                                                     |
+| [CVTE](http://www.cvte.com/en)                                                          | Production  | File Sharing               |                                                                                                                                                                                                     |
+| [NIO Auto](https://www.nio.com)                                                         | Production  | AI                         |                                                                                                                                                                                                     |
+| [China Mobile Cloud](https://ecloud.he.chinamobile.com)                                 | Testing     | Big Data                   |                                                                                                                                                                                                     |
+| [Volcano Engine](https://www.volcengine.com)                                            | Testing     | File Sharing               |                                                                                                                                                                                                     |
+| [Dingdong Fresh](https://www.100.me)                                                    | Testing     | Big Data                   |                                                                                                                                                                                                     |
+| [SF Express](https://www.sf-express.com)                                                | Testing     | Big Data, File Sharing     |                                                                                                                                                                                                     |
+| [BIGO](https://bigo.tv)                                                                 | Testing     | AI                         |                                                                                                                                                                                                     |
+| [Inner Mongolia MENGSHANG Consumer Finance Co., Ltd.](https://www.mengshangxiaofei.com) | Testing     | File Sharing               |                                                                                                                                                                                                     |
+| [Dmall](https://www.dmall.com/en)                                                       | Testing     | Big Data, File Sharing     |                                                                                                                                                                                                     |
 
 You are welcome to share your experience after using JuiceFS, either by submitting a Pull Request directly to this list, or by contacting us at hello@juicedata.io.
diff --git a/docs/en/guide/how_to_set_up_object_storage.md b/docs/en/guide/how_to_set_up_object_storage.md
index 0665b1c6..d661cda0 100644
--- a/docs/en/guide/how_to_set_up_object_storage.md
+++ b/docs/en/guide/how_to_set_up_object_storage.md
@@ -411,6 +411,10 @@ juicefs format \
     myjfs
 ```
 
+:::caution 
+Storj DCS [ListObjects](https://github.com/storj/gateway-st/blob/main/docs/s3-compatibility.md#listobjects) API is not fully S3 compatible (result list is not sorted), so some features of juicefs do not work. For example, `juicefs gc`, `juicefs fsck`, `juicefs sync`, `juicefs destroy`. And when using `juicefs mount`, you need to disable [automatic-backup](../administration/metadata_dump_load.md#automatic-backup) function by adding `--backup-meta 0`.
+:::
+
 ## Vultr Object Storage
 
 Vultr Object Storage is an S3-compatible storage, using `s3` for `--storage` option. The format of the option `--bucket` is `https://<bucket>.<region>.vultrobjects.com/`. For example:
diff --git a/docs/en/reference/command_reference.md b/docs/en/reference/command_reference.md
index 1bfab444..5398ebb4 100644
--- a/docs/en/reference/command_reference.md
+++ b/docs/en/reference/command_reference.md
@@ -40,6 +40,7 @@ COMMANDS:
      stats    Show real time performance statistics of JuiceFS
      profile  Show profiling of operations completed in JuiceFS
      info     Show internal information of a path or inode
+     doctor   Show information from multiple dimensions such as the operating environment and system logs
    SERVICE:
      mount    Mount a volume
      umount   Unmount a volume
@@ -1195,3 +1196,54 @@ skip sanity check and force destroy the volume (default: false)
 ```bash
 $ juicefs destroy redis://localhost e94d66a8-2339-4abd-b8d8-6812df737892
 ```
+
+### juicefs doctor
+
+#### Description
+
+It collects and displays information from multiple dimensions such as the operating environment and system logs to help better locate errors
+
+#### Synopsis
+
+```
+juicefs doctor [command options] MOUNTPOINT
+```
+
+#### Options
+
+`--out-dir value`<br />
+The output directory of the results, automatically created if the directory does not exist (default: ./doctor/)
+
+`--stats-sec value`<br />
+The number of seconds to sample .stats file (default: 5)
+
+`--collect-log`<br />
+enable log collection (default: false)
+
+`--limit value`<br />
+The number of log entries collected, from newest to oldest, if not specified, all entries will be collected
+
+`--collect-pprof`<br />
+enable pprof metrics collection (default: false)
+
+`--trace-sec value`<br />
+The number of seconds to sample trace metrics (default: 5)
+
+`--profile-sec value`<br />
+The number of seconds to sample profile metrics (default: 30)
+
+#### Examples
+
+```bash
+# Collect and display information about the mount point /mnt/jfs
+$ juicefs doctor /mnt/jfs
+
+# Specify the output directory as /var/log
+$ juicefs doctor --out-dir=/var/log /mnt/jfs
+
+# Enable log collection and get the last up to 1000 log entries
+$ juicefs doctor --out-dir=/var/log --collect-log --limit=1000 /mnt/jfs
+
+# Enable pprof metrics collection
+$ juicefs doctor --out-dir=/var/log --collect-log --limit=1000 --collect-pprof /mnt/jfs
+```
diff --git a/docs/en/tutorials/juicefs_on_k3s.md b/docs/en/tutorials/juicefs_on_k3s.md
index 6e3c72e5..3b63fc22 100644
--- a/docs/en/tutorials/juicefs_on_k3s.md
+++ b/docs/en/tutorials/juicefs_on_k3s.md
@@ -129,7 +129,7 @@ juicefs-sc             csi.juicefs.com         Retain          Immediate
 
 Next, deploy an Nginx Pod using a persistent storage declared by the JuiceFS storage class.
 
-### Depolyment
+### Deployment
 
 Create a configuration file, for example: `depolyment.yaml`
 
diff --git a/docs/zh_cn/administration/fault_diagnosis_and_analysis.md b/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
index 17ffdffb..42d1365c 100644
--- a/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
+++ b/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
@@ -163,6 +163,14 @@ curl 'http://localhost:<port>/debug/pprof/profile?seconds=30' > juicefs.cpu.pb.g
 curl 'http://localhost:<port>/debug/pprof/heap' > juicefs.heap.pb.gz
 ```
 
+:::tip 建议
+你也可以使用doctor命令自动收集这些运行时信息并保存到本地，默认保存到当前目录下的doctor目录中，例如：
+```bash
+juicefs doctor --collect-pprof /mnt/jfs
+```
+关于doctor命令的更多信息，请查看[命令参考](https://juicefs.com/docs/zh/community/command_reference#juicefs-doctor)
+:::
+
 如果你安装了 `go` 命令，那么可以通过 `go tool pprof` 命令直接分析，例如分析 CPU 性能统计：
 
 ```bash
diff --git a/docs/zh_cn/community/adopters.md b/docs/zh_cn/community/adopters.md
index 434aa345..45d33d17 100644
--- a/docs/zh_cn/community/adopters.md
+++ b/docs/zh_cn/community/adopters.md
@@ -6,34 +6,39 @@ slug: /adopters
 
 # JuiceFS 使用者
 
-| 公司／团队                                       | 使用环境   | 用例                                                                                             |
-|:-------------------------------------------------|:-----------|:-------------------------------------------------------------------------------------------------|
-| [小米](https://www.mi.com)                       | Production | AI                                                                                               |
-| [理想汽车](https://www.lixiang.com)              | Production | 大数据，AI（[案例](https://juicefs.com/zh-cn/blog/li-auto-with-juicefs)）                        |
-| [Shopee](https://shopee.com)                     | Production | 大数据（[案例](https://juicefs.com/zh-cn/blog/shopee-clickhouse-with-juicefs)）                  |
-| [知乎](https://www.zhihu.com)                    | Production | 大数据（[案例](https://juicefs.com/zh-cn/blog/zhihu-flink-with-juicefs)）                        |
-| [尧信](https://www.yaoxinhd.com)                 | Production | 大数据，共享文件存储                                                                             |
-| [旷视](https://megvii.com)                       | Production | AI                                                                                               |
-| [航天宏图](https://www.piesat.cn)                | Production | 共享文件存储                                                                                     |
-| [溯源精微](https://www.geneway.cn)               | Production | 共享文件存储                                                                                     |
-| [上汽集团](https://www.saicmotor.com/chinese/)   | Production | AI（[案例](https://juicefs.com/zh-cn/blog/performance-boost-3x-on-igear-platform)）              |
-| [国家超级计算济南中心](https://www.nsccjn.cn/)   | Production | DevOps                                                                                           |
-| [云知声](https://www.unisound.com)               | Production | AI（[案例](https://juicefs.com/zh-cn/blog/juicefs-support-ai-storage-at-unisound)）              |
-| [网易游戏](https://game.163.com)                 | Production | 大数据，共享文件存储                                                                             |
-| [一面数据](https://www.yimian.com.cn)            | Production | 大数据                                                                                           |
-| [携程旅行](https://www.ctrip.com)                | Production | 大数据，共享文件存储                                                                             |
-| [vivo](https://www.vivo.com)                     | Production | AI，共享文件存储                                                                                 |
-| [地平线](https://horizon.ai)                     | Production | AI                                                                                               |
-| [商汤科技](https://www.sensetime.com/cn)         | Production | 共享文件存储                                                                                     |
-| [深势科技](https://www.dp.tech)                  | Production | 共享文件存储（[案例](https://juicefs.com/zh-cn/blog/dptech-ai-storage-in-multi-cloud-practice)） |
-| [嘉谊互娱](http://www.joyient.com)               | Prodcution | 共享文件存储，渲染                                                                               |
-| [DJI 大疆创新](https://www.dji.com/cn)           | Production | AI                                                                                               |
-| [移动云](https://ecloud.he.chinamobile.com)      | Testing    | 大数据（[案例](https://juicefs.com/zh-cn/blog/juicefs-support-hbase-at-chinamobile-cloud)）      |
-| [火山引擎](https://www.volcengine.com)           | Testing    | 共享文件存储                                                                                     |
-| [叮咚买菜](https://www.100.me)                   | Testing    | 大数据                                                                                           |
-| [顺丰速运](https://www.sf-express.com)           | Testing    | 大数据，共享文件存储                                                                             |
-| [BIGO](https://bigo.tv)                          | Testing    | AI                                                                                               |
-| [蒙商消费金融](https://www.mengshangxiaofei.com) | Testing    | 共享文件存储                                                                                     |
-| [多点DMALL](https://www.dmall.com) | Testing    | 大数据，共享文件存储                                                                                    |
+| 公司／团队                                       | 使用环境   | 使用场景             | 案例                                                                                                                                                                                                  |
+|--------------------------------------------------|------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [小米](https://www.mi.com)                       | Production | AI                   |                                                                                                                                                                                                       |
+| [理想汽车](https://www.lixiang.com)              | Production | 大数据，AI           | [JuiceFS 在理想汽车的使用和展望](https://juicefs.com/zh-cn/blog/li-auto-with-juicefs)<br />[理想汽车：从 Hadoop 到云原生的演进与思考](https://juicefs.com/zh-cn/blog/liauto-case-hadoop-cloudnatrive) |
+| [Shopee](https://shopee.com)                     | Production | 大数据               | [Shopee x JuiceFS：ClickHouse 冷热数据分离存储架构与实践](https://juicefs.com/zh-cn/blog/shopee-clickhouse-with-juicefs)                                                                              |
+| [知乎](https://www.zhihu.com)                    | Production | 大数据               | [知乎 x JuiceFS：利用 JuiceFS 给 Flink 容器启动加速](https://juicefs.com/zh-cn/blog/zhihu-flink-with-juicefs)                                                                                         |
+| [尧信](https://www.yaoxinhd.com)                 | Production | 大数据，共享文件存储 |                                                                                                                                                                                                       |
+| [旷视](https://megvii.com)                       | Production | AI                   |                                                                                                                                                                                                       |
+| [航天宏图](https://www.piesat.cn)                | Production | 共享文件存储         |                                                                                                                                                                                                       |
+| [溯源精微](https://www.geneway.cn)               | Production | 共享文件存储         |                                                                                                                                                                                                       |
+| [上汽集团](https://www.saicmotor.com/chinese)    | Production | AI                   | [上汽云 x JuiceFS：iGear 用了这个小魔法，模型训练速度提升 300%](https://juicefs.com/zh-cn/blog/performance-boost-3x-on-igear-platform)                                                                |
+| [国家超级计算济南中心](https://www.nsccjn.cn)    | Production | DevOps               |                                                                                                                                                                                                       |
+| [云知声](https://www.unisound.com)               | Production | AI                   | [AI 场景存储优化：云知声超算平台基于 JuiceFS 的存储实践](https://juicefs.com/zh-cn/blog/juicefs-support-ai-storage-at-unisound)                                                                       |
+| [网易游戏](https://game.163.com)                 | Production | 大数据，共享文件存储 |                                                                                                                                                                                                       |
+| [一面数据](https://www.yimian.com.cn)            | Production | 大数据               | [一面数据：Hadoop 迁移云上架构设计与实践](https://juicefs.com/zh-cn/blog/yimiancase)                                                                                                                  |
+| [携程旅行](https://www.ctrip.com)                | Production | 大数据，共享文件存储 | [突破存储数据量限制，JuiceFS 在携程海量冷数据场景下的实践](https://juicefs.com/zh-cn/blog/xiecheng-case)                                                                                              |
+| [vivo](https://www.vivo.com)                     | Production | AI，共享文件存储     |                                                                                                                                                                                                       |
+| [地平线](https://horizon.ai)                     | Production | AI                   |                                                                                                                                                                                                       |
+| [商汤科技](https://www.sensetime.com/cn)         | Production | 共享文件存储         |                                                                                                                                                                                                       |
+| [深势科技](https://www.dp.tech)                  | Production | 共享文件存储         | [深势科技分享 AI 企业多云存储架构实践](https://juicefs.com/zh-cn/blog/dptech-ai-storage-in-multi-cloud-practice)                                                                                      |
+| [嘉谊互娱](http://www.joyient.com)               | Production | 共享文件存储，渲染   |                                                                                                                                                                                                       |
+| [DJI 大疆创新](https://www.dji.com/cn)           | Production | AI                   |                                                                                                                                                                                                       |
+| [金山云](https://www.ksyun.com)                  | Production | 大数据               |                                                                                                                                                                                                       |
+| [天桐互动](https://www.kuaidianyuedu.com)        | Production | AI，共享文件存储     |                                                                                                                                                                                                       |
+| [Clobotics](https://clobotics.com)               | Production | AI                   |                                                                                                                                                                                                       |
+| [视源股份](http://www.cvte.com)                  | Production | 共享文件存储         |                                                                                                                                                                                                       |
+| [蔚来汽车](https://www.nio.cn)                   | Production | AI                   |                                                                                                                                                                                                       |
+| [移动云](https://ecloud.he.chinamobile.com)      | Testing    | 大数据               | [移动云使用 JuiceFS 支持 Apache HBase 增效降本的探索](https://juicefs.com/zh-cn/blog/juicefs-support-hbase-at-chinamobile-cloud)                                                                      |
+| [火山引擎](https://www.volcengine.com)           | Testing    | 共享文件存储         |                                                                                                                                                                                                       |
+| [叮咚买菜](https://www.100.me)                   | Testing    | 大数据               |                                                                                                                                                                                                       |
+| [顺丰速运](https://www.sf-express.com)           | Testing    | 大数据，共享文件存储 |                                                                                                                                                                                                       |
+| [BIGO](https://bigo.tv)                          | Testing    | AI                   |                                                                                                                                                                                                       |
+| [蒙商消费金融](https://www.mengshangxiaofei.com) | Testing    | 共享文件存储         |                                                                                                                                                                                                       |
+| [多点 DMALL](https://www.dmall.com)              | Testing    | 大数据，共享文件存储 |                                                                                                                                                                                                       |
 
 欢迎你在使用 JuiceFS 后，向大家分享你的使用经验，可以直接向这个列表提交 Pull Request，或者联系我们 hello@juicedata.io。
diff --git a/docs/zh_cn/guide/how_to_set_up_object_storage.md b/docs/zh_cn/guide/how_to_set_up_object_storage.md
index 19c838e7..d479c497 100644
--- a/docs/zh_cn/guide/how_to_set_up_object_storage.md
+++ b/docs/zh_cn/guide/how_to_set_up_object_storage.md
@@ -411,6 +411,10 @@ juicefs format \
     myjfs
 ```
 
+:::caution 特别提示
+因为 Storj DCS 的 [ListObjects](https://github.com/storj/gateway-st/blob/main/docs/s3-compatibility.md#listobjects) API 并非完全 S3 兼容（返回结果没有实现排序功能），所以 juicefs 的部分功能无法使用，比如 `juicefs gc`，`juicefs fsck`，`juicefs sync`，`juicefs destroy`。另外，使用 `juicefs mount` 时需要关闭[元数据自动备份](../administration/metadata_dump_load.md#自动备份)功能，即加上 `--backup-meta 0`。
+:::
+
 ## Vultr 对象存储
 
 Vultr 的对象存储兼容 S3 API，存储类型使用 `s3`，`--bucket` 格式为 `https://<bucket>.<region>.vultrobjects.com/`。例如：
diff --git a/docs/zh_cn/reference/command_reference.md b/docs/zh_cn/reference/command_reference.md
index 2519f27c..e89b0fbd 100644
--- a/docs/zh_cn/reference/command_reference.md
+++ b/docs/zh_cn/reference/command_reference.md
@@ -40,6 +40,7 @@ COMMANDS:
      stats    Show real time performance statistics of JuiceFS
      profile  Show profiling of operations completed in JuiceFS
      info     Show internal information of a path or inode
+     doctor   Show information from multiple dimensions such as the operating environment and system logs
    SERVICE:
      mount    Mount a volume
      umount   Unmount a volume
@@ -1198,3 +1199,54 @@ juicefs destroy [command options] META-URL UUID
 ```bash
 $ juicefs destroy redis://localhost e94d66a8-2339-4abd-b8d8-6812df737892
 ```
+
+### juicefs doctor
+
+#### 描述
+
+从运行环境、系统日志等多个维度收集和展示信息，帮助更好地定位错误
+
+#### 使用
+
+```
+juicefs doctor [command options] MOUNTPOINT
+```
+
+#### 选项
+
+`--out-dir value`<br />
+结果输出目录，若目录不存在则自动创建 (默认：./doctor/)
+
+`--stats-sec value`<br />
+.stats文件采样秒数 (默认：5)
+
+`--collect-log`<br />
+启用日志收集 (默认：false)
+
+`--limit value`<br />
+收集的日志条目数，从新到旧，若不指定则收集全部条目
+
+`--collect-pprof`<br />
+启用pprof指标收集 (默认：false)
+
+`--trace-sec value`<br />
+trace指标采样秒数 (默认：5)
+
+`--profile-sec value`<br />
+profile指标采样秒数 (默认：30)
+
+#### 示例
+
+```bash
+# 收集并展示挂载点 /mnt/jfs 的各类信息
+$ juicefs doctor /mnt/jfs
+
+# 指定输出目录为 /var/log
+$ juicefs doctor --out-dir=/var/log /mnt/jfs
+
+# 启用日志收集，并收集最后1000条日志条目
+$ juicefs doctor --out-dir=/var/log --collect-log --limit=1000 /mnt/jfs
+
+# 启用pprof指标收集
+$ juicefs doctor --out-dir=/var/log --collect-log --limit=1000 --collect-pprof /mnt/jfs
+```
diff --git a/docs/zh_cn/tutorials/juicefs_on_k3s.md b/docs/zh_cn/tutorials/juicefs_on_k3s.md
index 5093e48f..6d9d9a11 100644
--- a/docs/zh_cn/tutorials/juicefs_on_k3s.md
+++ b/docs/zh_cn/tutorials/juicefs_on_k3s.md
@@ -135,7 +135,7 @@ juicefs-sc             csi.juicefs.com         Retain          Immediate
 
 接下来部署一个 Nginx Pod，使用 JuiceFS 存储类声明的持久化存储。
 
-### Depolyment
+### Deployment
 
 创建一个配置文件，例如：`depolyment.yaml`
 
diff --git a/go.mod b/go.mod
index 9750ee86..281d3cf3 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,6 @@ require (
 	github.com/go-redis/redis/v8 v8.11.5
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/gofrs/flock v0.8.1
-	github.com/golang/protobuf v1.5.2
 	github.com/google/btree v1.0.1
 	github.com/google/gops v0.3.22
 	github.com/google/uuid v1.3.0
@@ -37,7 +36,7 @@ require (
 	github.com/ks3sdklib/aws-sdk-go v1.1.4
 	github.com/lib/pq v1.8.0
 	github.com/mattn/go-isatty v0.0.14
-	github.com/mattn/go-sqlite3 v2.0.1+incompatible
+	github.com/mattn/go-sqlite3 v1.14.15
 	github.com/minio/cli v1.22.0
 	github.com/minio/minio v0.0.0-20210206053228-97fe57bba92c
 	github.com/minio/minio-go v6.0.14+incompatible
@@ -70,6 +69,7 @@ require (
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
 	golang.org/x/text v0.3.7
 	google.golang.org/api v0.70.0
+	google.golang.org/protobuf v1.27.1
 	gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216
 	xorm.io/xorm v1.0.7
 )
@@ -85,6 +85,7 @@ require (
 	github.com/VividCortex/ewma v1.2.0 // indirect
 	github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
 	github.com/alecthomas/participle v0.2.1 // indirect
+	github.com/apple/foundationdb/bindings/go v0.0.0-20211207225159-47b9a81d1c10
 	github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
 	github.com/bcicen/jstream v1.0.1 // indirect
 	github.com/beevik/ntp v0.3.0 // indirect
@@ -122,6 +123,7 @@ require (
 	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/golang/snappy v0.0.3 // indirect
 	github.com/google/flatbuffers v1.12.1 // indirect
 	github.com/google/go-cmp v0.5.7 // indirect
@@ -151,7 +153,7 @@ require (
 	github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
 	github.com/jcmturner/rpc/v2 v2.0.3 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
-	github.com/json-iterator/go v1.1.11 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/jtolds/gls v4.20.0+incompatible // indirect
 	github.com/klauspost/compress v1.13.4 // indirect
 	github.com/klauspost/cpuid v1.3.1 // indirect
@@ -174,7 +176,7 @@ require (
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.4.3 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
-	github.com/modern-go/reflect2 v1.0.1 // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/montanaflynn/stats v0.5.0 // indirect
 	github.com/mozillazg/go-httpheader v0.2.1 // indirect
 	github.com/ncw/directio v1.0.5 // indirect
@@ -223,7 +225,6 @@ require (
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6 // indirect
 	google.golang.org/grpc v1.45.0 // indirect
-	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/ini.v1 v1.57.0 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
 	gopkg.in/square/go-jose.v2 v2.3.1 // indirect
@@ -231,7 +232,7 @@ require (
 	xorm.io/builder v0.3.7 // indirect
 )
 
-replace github.com/minio/minio v0.0.0-20210206053228-97fe57bba92c => github.com/juicedata/minio v0.0.0-20220613143934-cee0571a1b03
+replace github.com/minio/minio v0.0.0-20210206053228-97fe57bba92c => github.com/juicedata/minio v0.0.0-20220913070254-47325cb1d5fe
 
 replace github.com/hanwen/go-fuse/v2 v2.1.1-0.20210611132105-24a1dfe6b4f8 => github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343
 
diff --git a/go.sum b/go.sum
index aa49fa07..8ce61717 100644
--- a/go.sum
+++ b/go.sum
@@ -148,6 +148,8 @@ github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQY
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apple/foundationdb/bindings/go v0.0.0-20211207225159-47b9a81d1c10 h1:xU6bzJilZ630rLUhRsqWgJjSl2PCn5uLrehoG6ntwls=
+github.com/apple/foundationdb/bindings/go v0.0.0-20211207225159-47b9a81d1c10/go.mod h1:w63jdZTFCtvdjsUj5yrdKgjxaAD5uXQX6hJ7EaiLFRs=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
@@ -592,8 +594,9 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
 github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@@ -602,8 +605,8 @@ github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343 h1:fbMz7gSl
 github.com/juicedata/go-fuse/v2 v2.1.1-0.20220822092405-ece777deb343/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
 github.com/juicedata/godaemon v0.0.0-20210629045518-3da5144a127d h1:kpQMvNZJKGY3PTt7OSoahYc4nM0HY67SvK0YyS0GLwA=
 github.com/juicedata/godaemon v0.0.0-20210629045518-3da5144a127d/go.mod h1:dlxKkLh3qAIPtgr2U/RVzsZJDuXA1ffg+Njikfmhvgw=
-github.com/juicedata/minio v0.0.0-20220613143934-cee0571a1b03 h1:+0jiEkr6qE5Zm2+LilAv5pV/+V+V1XZ5KUv0pmXZHhQ=
-github.com/juicedata/minio v0.0.0-20220613143934-cee0571a1b03/go.mod h1:ILQUt98Xz3GF9KgJKeGQ81c0RFaRSrUZmZ8oSb/7QxU=
+github.com/juicedata/minio v0.0.0-20220913070254-47325cb1d5fe h1:BcKE03BrzklTWcU9RtLsjahnXPhEKBF/xac/Q1flnVU=
+github.com/juicedata/minio v0.0.0-20220913070254-47325cb1d5fe/go.mod h1:8oMBmyEWA8aYwMwO7eUNOjvVNOhDeqDlio2RSv6T/4Q=
 github.com/juicedata/mpb/v7 v7.0.4-0.20220719014258-68df1356cfba h1:YSCPvyONPDp/ivKgRanFpNEHh3N5/0UsKmwbbKQIuGE=
 github.com/juicedata/mpb/v7 v7.0.4-0.20220719014258-68df1356cfba/go.mod h1:NXGsfPGx6G2JssqvEcULtDqUrxuuYs4llpv8W6ZUpzk=
 github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
@@ -682,8 +685,8 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
-github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
-github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
+github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -733,8 +736,9 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
 github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk=
 github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go
index 2f0336cc..f057d5bc 100644
--- a/pkg/fuse/fuse.go
+++ b/pkg/fuse/fuse.go
@@ -471,6 +471,9 @@ func Serve(v *vfs.VFS, options string, xattrs bool) error {
 	if err != nil {
 		return fmt.Errorf("fuse: %s", err)
 	}
+	v.InvalidateEntry = func(parent Ino, name string) syscall.Errno {
+		return syscall.Errno(fssrv.EntryNotify(uint64(parent), name))
+	}
 
 	fssrv.Serve()
 	return nil
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index 5e22b68e..64c5fc10 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -20,6 +20,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"path"
+	"path/filepath"
 	"runtime"
 	"sort"
 	"strings"
@@ -1045,49 +1046,137 @@ func (m *baseMeta) GetPaths(ctx Context, inode Ino) []string {
 	return paths
 }
 
-func (m *baseMeta) Check(ctx Context, fpath string, repair bool) (st syscall.Errno) {
+func (m *baseMeta) countDirNlink(ctx Context, inode Ino) (uint32, syscall.Errno) {
+	var entries []*Entry
+	if st := m.en.doReaddir(ctx, inode, 0, &entries, -1); st != 0 {
+		logger.Errorf("readdir inode %d: %s", inode, st)
+		return 0, st
+	}
+	var dirCounter uint32 = 2
+	for _, e := range entries {
+		if e.Attr.Typ == TypeDirectory {
+			dirCounter++
+		}
+	}
+	return dirCounter, 0
+}
+
+type metaWalkFunc func(ctx Context, inode Ino, path string, attr *Attr)
+
+func (m *baseMeta) walk(ctx Context, inode Ino, path string, attr *Attr, walkFn metaWalkFunc) syscall.Errno {
+	walkFn(ctx, inode, path, attr)
+	var entries []*Entry
+	st := m.en.doReaddir(ctx, inode, 1, &entries, -1)
+	if st != 0 {
+		logger.Errorf("list %s: %s", path, st)
+		return st
+	}
+	for _, entry := range entries {
+		if !entry.Attr.Full {
+			entry.Attr.Parent = inode
+		}
+		if st := m.walk(ctx, entry.Inode, filepath.Join(path, string(entry.Name)), entry.Attr, walkFn); st != 0 {
+			return st
+		}
+	}
+	return 0
+}
+
+func (m *baseMeta) Check(ctx Context, fpath string, repair bool, recursive bool) (st syscall.Errno) {
 	var attr Attr
-	var inode Ino
-	var parent Ino = 1
-	ps := strings.Split(fpath, "/")
+	var inode = RootInode
+	var parent = RootInode
+	attr.Typ = TypeDirectory
+	ps := strings.FieldsFunc(fpath, func(r rune) bool {
+		return r == '/'
+	})
 	for i, name := range ps {
-		if len(name) == 0 {
-			continue
-		}
+		parent = inode
 		if st = m.Lookup(ctx, parent, name, &inode, &attr); st != 0 {
 			logger.Errorf("Lookup parent %d name %s: %s", parent, name, st)
 			return
 		}
-		if attr.Full {
-			parent = inode
-			continue
-		}
-
-		// missing attribute
-		p := "/" + path.Join(ps[:i+1]...)
-		if attr.Typ != TypeDirectory { // TODO: determine file size?
-			logger.Errorf("Path %s (inode %d type %d) cannot be auto-repaired, you have to repair it manually or remove it", p, inode, attr.Typ)
-		} else if !repair {
-			logger.Warnf("Path %s (inode %d) can be repaired, please re-check with 'repair' enabled", p, inode)
-		} else { // repair directory inode
-			now := time.Now().Unix()
-			attr.Mode = 0644
-			attr.Uid = ctx.Uid()
-			attr.Gid = ctx.Gid()
-			attr.Atime = now
-			attr.Mtime = now
-			attr.Ctime = now
-			attr.Length = 4 << 10
-			attr.Parent = parent
-			if st = m.en.doRepair(ctx, inode, &attr); st == 0 {
-				logger.Infof("Path %s (inode %d) is successfully repaired", p, inode)
+		if !attr.Full && i < len(ps)-1 {
+			// missing attribute
+			p := "/" + path.Join(ps[:i+1]...)
+			if attr.Typ != TypeDirectory { // TODO: determine file size?
+				logger.Warnf("Attribute of %s (inode %d type %d) is missing and cannot be auto-repaired, please repair it manually or remove it", p, inode, attr.Typ)
 			} else {
-				logger.Errorf("Repair path %s inode %d: %s", p, inode, st)
+				logger.Warnf("Attribute of %s (inode %d) is missing, please re-run with '--path %s --repair' to fix it", p, inode, p)
 			}
 		}
-		return // handle one missing inode at a time
 	}
-	logger.Infof("Path %s inode %d is valid", fpath, parent)
+	if !attr.Full {
+		attr.Parent = parent
+	}
+
+	type node struct {
+		inode Ino
+		path  string
+		attr  *Attr
+	}
+	nodes := make(chan *node, 1000)
+	go func() {
+		defer close(nodes)
+		if recursive {
+			st = m.walk(ctx, inode, fpath, &attr, func(ctx Context, inode Ino, path string, attr *Attr) {
+				nodes <- &node{inode, path, attr}
+			})
+		} else {
+			nodes <- &node{inode, fpath, &attr}
+		}
+	}()
+
+	var wg sync.WaitGroup
+	for i := 0; i < 20; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			for e := range nodes {
+				inode := e.inode
+				path := e.path
+				attr := e.attr
+				if attr.Typ != TypeDirectory {
+					// TODO
+					continue
+				}
+				if attr.Full {
+					nlink, st := m.countDirNlink(ctx, inode)
+					if st != 0 {
+						logger.Errorf("Count nlink for inode %d: %s", inode, st)
+						continue
+					}
+					if attr.Nlink == nlink {
+						continue
+					}
+					logger.Warnf("nlink of %s should be %d, but got %d", path, nlink, attr.Nlink)
+				} else {
+					logger.Warnf("attribute of %s is missing", path)
+				}
+
+				if repair {
+					if !attr.Full {
+						now := time.Now().Unix()
+						attr.Mode = 0644
+						attr.Uid = ctx.Uid()
+						attr.Gid = ctx.Gid()
+						attr.Atime = now
+						attr.Mtime = now
+						attr.Ctime = now
+						attr.Length = 4 << 10
+					}
+					if st1 := m.en.doRepair(ctx, inode, attr); st1 == 0 {
+						logger.Infof("Path %s (inode %d) is successfully repaired", path, inode)
+					} else {
+						logger.Errorf("Repair path %s inode %d: %s", path, inode, st1)
+					}
+				} else {
+					logger.Warnf("Path %s (inode %d) can be repaired, please re-run with '--path %s --repair' to fix it", path, inode, path)
+				}
+			}
+		}()
+	}
+	wg.Wait()
 	return
 }
 
diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go
index c077179f..b3cd3d57 100644
--- a/pkg/meta/interface.go
+++ b/pkg/meta/interface.go
@@ -351,7 +351,7 @@ type Meta interface {
 	// GetPaths returns all paths of an inode
 	GetPaths(ctx Context, inode Ino) []string
 	// Check integrity of an absolute path and repair it if asked
-	Check(ctx Context, fpath string, repair bool) syscall.Errno
+	Check(ctx Context, fpath string, repair bool, recursive bool) syscall.Errno
 
 	// OnMsg add a callback for the given message type.
 	OnMsg(mtype uint32, cb MsgCallback)
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 6eb1c1d5..b1be8fbd 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -1777,7 +1777,7 @@ func (m *redisMeta) doReaddir(ctx Context, inode Ino, plus uint8, entries *[]*En
 		return errno(err)
 	}
 
-	if plus != 0 {
+	if plus != 0 && len(*entries) != 0 {
 		fillAttr := func(es []*Entry) error {
 			var keys = make([]string, len(es))
 			for i, e := range es {
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index c5c4e2ec..7157ae8f 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -476,18 +476,22 @@ func (m *dbMeta) getSession(row interface{}, detail bool) (*Session, error) {
 
 func (m *dbMeta) GetSession(sid uint64, detail bool) (s *Session, err error) {
 	err = m.roTxn(func(ses *xorm.Session) error {
-		row := session2{Sid: sid}
-		if ok, err := ses.Get(&row); err != nil {
+		if ok, err := ses.IsTableExist(&session2{}); err != nil {
 			return err
 		} else if ok {
-			s, err = m.getSession(&row, detail)
-			return err
+			row := session2{Sid: sid}
+			if ok, err = ses.Get(&row); err != nil {
+				return err
+			} else if ok {
+				s, err = m.getSession(&row, detail)
+				return err
+			}
 		}
 		if ok, err := ses.IsTableExist(&session{}); err != nil {
 			return err
 		} else if ok {
 			row := session{Sid: sid}
-			if ok, err := ses.Get(&row); err != nil {
+			if ok, err = ses.Get(&row); err != nil {
 				return err
 			} else if ok {
 				s, err = m.getSession(&row, detail)
@@ -502,19 +506,22 @@ func (m *dbMeta) GetSession(sid uint64, detail bool) (s *Session, err error) {
 func (m *dbMeta) ListSessions() ([]*Session, error) {
 	var sessions []*Session
 	err := m.roTxn(func(ses *xorm.Session) error {
-		var rows []session2
-		err := ses.Find(&rows)
-		if err != nil {
+		if ok, err := ses.IsTableExist(&session2{}); err != nil {
 			return err
-		}
-		sessions = make([]*Session, 0, len(rows))
-		for i := range rows {
-			s, err := m.getSession(&rows[i], false)
-			if err != nil {
-				logger.Errorf("get session: %s", err)
-				continue
+		} else if ok {
+			var rows []session2
+			if err = ses.Find(&rows); err != nil {
+				return err
+			}
+			sessions = make([]*Session, 0, len(rows))
+			for i := range rows {
+				s, err := m.getSession(&rows[i], false)
+				if err != nil {
+					logger.Errorf("get session: %s", err)
+					continue
+				}
+				sessions = append(sessions, s)
 			}
-			sessions = append(sessions, s)
 		}
 		if ok, err := ses.IsTableExist(&session{}); err != nil {
 			logger.Errorf("Check legacy session table: %s", err)
@@ -2615,8 +2622,9 @@ func (m *dbMeta) doRepair(ctx Context, inode Ino, attr *Attr) syscall.Errno {
 		Atime:  attr.Atime * 1e6,
 		Mtime:  attr.Mtime * 1e6,
 		Ctime:  attr.Ctime * 1e6,
-		Length: 4 << 10,
+		Length: attr.Length,
 		Parent: attr.Parent,
+		Nlink:  attr.Nlink,
 	}
 	return errno(m.txn(func(s *xorm.Session) error {
 		n.Nlink = 2
@@ -2629,7 +2637,15 @@ func (m *dbMeta) doRepair(ctx Context, inode Ino, attr *Attr) syscall.Errno {
 				n.Nlink++
 			}
 		}
-		return mustInsert(s, n)
+		ok, err := s.ForUpdate().Get(&node{Inode: inode})
+		if err == nil {
+			if ok {
+				_, err = s.Update(n, &node{Inode: inode})
+			} else {
+				err = mustInsert(s, n)
+			}
+		}
+		return err
 	}, inode))
 }
 
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index d5f9dfe3..e9ebac8f 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -1592,7 +1592,7 @@ func (m *kvMeta) doReaddir(ctx Context, inode Ino, plus uint8, entries *[]*Entry
 		})
 	}
 
-	if plus != 0 {
+	if plus != 0 && len(*entries) != 0 {
 		fillAttr := func(es []*Entry) error {
 			var keys = make([][]byte, len(es))
 			for i, e := range es {
@@ -2224,7 +2224,7 @@ func (m *kvMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, sh
 func (m *kvMeta) doRepair(ctx Context, inode Ino, attr *Attr) syscall.Errno {
 	return errno(m.txn(func(tx kvTxn) error {
 		attr.Nlink = 2
-		_ = tx.scanValues(m.entryKey(inode, ""), 0, func(k, v []byte) bool {
+		_ = tx.scanValues(m.entryKey(inode, ""), -1, func(k, v []byte) bool {
 			typ, _ := m.parseEntry(v)
 			if typ == TypeDirectory {
 				attr.Nlink++
diff --git a/pkg/meta/tkv_fdb.go b/pkg/meta/tkv_fdb.go
new file mode 100644
index 00000000..dca04aed
--- /dev/null
+++ b/pkg/meta/tkv_fdb.go
@@ -0,0 +1,185 @@
+//go:build fdb
+// +build fdb
+
+/*
+ * JuiceFS, Copyright 2022 Juicedata, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package meta
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/apple/foundationdb/bindings/go/src/fdb"
+)
+
+func init() {
+	Register("fdb", newKVMeta)
+	drivers["fdb"] = newFdbClient
+}
+
+type fdbTxn struct {
+	fdb.Transaction
+}
+
+type fdbClient struct {
+	client fdb.Database
+}
+
+func newFdbClient(addr string) (tkvClient, error) {
+	err := fdb.APIVersion(630)
+	if err != nil {
+		return nil, fmt.Errorf("set API version: %s", err)
+	}
+	u, err := url.Parse("fdb://" + addr)
+	if err != nil {
+		return nil, err
+	}
+	db, err := fdb.OpenDatabase(u.Path)
+	if err != nil {
+		return nil, fmt.Errorf("open database: %s", err)
+	}
+	return withPrefix(&fdbClient{db}, append([]byte(u.Query().Get("prefix")), 0xFD)), nil
+}
+
+func (c *fdbClient) name() string {
+	return "fdb"
+}
+
+func (c *fdbClient) txn(f func(kvTxn) error) error {
+	_, err := c.client.Transact(func(t fdb.Transaction) (interface{}, error) {
+		e := f(&fdbTxn{t})
+		return nil, e
+	})
+	return err
+}
+
+func (c *fdbClient) scan(prefix []byte, handler func(key, value []byte)) error {
+	_, err := c.client.ReadTransact(func(t fdb.ReadTransaction) (interface{}, error) {
+		snapshot := t.Snapshot()
+		iter := snapshot.GetRange(
+			fdb.KeyRange{Begin: fdb.Key(prefix), End: fdb.Key(nextKey(prefix))},
+			fdb.RangeOptions{Mode: fdb.StreamingModeWantAll},
+		).Iterator()
+		for iter.Advance() {
+			r := iter.MustGet()
+			handler(r.Key, r.Value)
+		}
+		return nil, nil
+	})
+	return err
+}
+
+func (c *fdbClient) reset(prefix []byte) error {
+	_, err := c.client.Transact(func(t fdb.Transaction) (interface{}, error) {
+		t.ClearRange(fdb.KeyRange{
+			Begin: fdb.Key(prefix),
+			End:   fdb.Key(nextKey(prefix)),
+		})
+		return nil, nil
+	})
+	return err
+}
+
+func (c *fdbClient) close() error {
+	// c = &fdbClient{}
+	return nil
+}
+
+func (c *fdbClient) shouldRetry(err error) bool {
+	return false
+}
+
+func (tx *fdbTxn) get(key []byte) []byte {
+	return tx.Get(fdb.Key(key)).MustGet()
+}
+
+func (tx *fdbTxn) gets(keys ...[]byte) [][]byte {
+	ret := make([][]byte, len(keys))
+	for i, key := range keys {
+		val := tx.Get(fdb.Key(key)).MustGet()
+		ret[i] = val
+	}
+	return ret
+}
+
+func (tx *fdbTxn) range0(begin, end []byte) *fdb.RangeIterator {
+	return tx.GetRange(
+		fdb.KeyRange{Begin: fdb.Key(begin), End: fdb.Key(end)},
+		fdb.RangeOptions{Mode: fdb.StreamingModeWantAll},
+	).Iterator()
+}
+
+func (tx *fdbTxn) scanRange(begin, end []byte) map[string][]byte {
+	ret := make(map[string][]byte)
+	iter := tx.range0(begin, end)
+	for iter.Advance() {
+		r := iter.MustGet()
+		ret[string(r.Key)] = r.Value
+	}
+	return ret
+}
+
+func (tx *fdbTxn) scanKeys(prefix []byte) [][]byte {
+	ret := make([][]byte, 0)
+	iter := tx.range0(prefix, nextKey(prefix))
+	for iter.Advance() {
+		ret = append(ret, iter.MustGet().Key)
+	}
+	return ret
+}
+
+func (tx *fdbTxn) scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte {
+	ret := make(map[string][]byte)
+	iter := tx.range0(prefix, nextKey(prefix))
+	for iter.Advance() {
+		r := iter.MustGet()
+		if filter == nil || filter(r.Key, r.Value) {
+			ret[string(r.Key)] = r.Value
+			if limit > 0 {
+				if limit--; limit == 0 {
+					break
+				}
+			}
+		}
+	}
+	return ret
+}
+
+func (tx *fdbTxn) exist(prefix []byte) bool {
+	iter := tx.range0(prefix, nextKey(prefix))
+	return iter.Advance()
+}
+
+func (tx *fdbTxn) set(key, value []byte) {
+	tx.Set(fdb.Key(key), value)
+}
+
+func (tx *fdbTxn) append(key []byte, value []byte) []byte {
+	tx.AppendIfFits(fdb.Key(key), fdb.Key(value))
+	return tx.Get(fdb.Key(key)).MustGet()
+}
+
+func (tx *fdbTxn) incrBy(key []byte, value int64) int64 {
+	tx.Add(fdb.Key(key), packCounter(value))
+	return parseCounter(tx.Get(fdb.Key(key)).MustGet())
+}
+
+func (tx *fdbTxn) dels(keys ...[]byte) {
+	for _, key := range keys {
+		tx.Clear(fdb.Key(key))
+	}
+}
diff --git a/pkg/meta/tkv_mem.go b/pkg/meta/tkv_mem.go
index cb7299de..869554b3 100644
--- a/pkg/meta/tkv_mem.go
+++ b/pkg/meta/tkv_mem.go
@@ -1,6 +1,3 @@
-//go:build !fdb
-// +build !fdb
-
 /*
  * JuiceFS, Copyright 2021 Juicedata, Inc.
  *
diff --git a/pkg/object/etcd.go b/pkg/object/etcd.go
index 914a668a..db738c7c 100644
--- a/pkg/object/etcd.go
+++ b/pkg/object/etcd.go
@@ -156,8 +156,8 @@ func buildTlsConfig(u *url.URL) (*tls.Config, error) {
 }
 
 func newEtcd(addr, user, passwd, token string) (ObjectStorage, error) {
-	if !strings.Contains(addr, "://") {
-		addr = "http://" + addr
+	if !strings.HasPrefix(addr, "etcd://") {
+		addr = "etcd://" + addr
 	}
 	u, err := url.Parse(addr)
 	if err != nil {
diff --git a/pkg/object/prefix.go b/pkg/object/prefix.go
index cbc467cd..c0acc170 100644
--- a/pkg/object/prefix.go
+++ b/pkg/object/prefix.go
@@ -28,7 +28,7 @@ type withPrefix struct {
 	prefix string
 }
 
-// WithPrefix retuns a object storage that add a prefix to keys.
+// WithPrefix return an object storage that add a prefix to keys.
 func WithPrefix(os ObjectStorage, prefix string) ObjectStorage {
 	return &withPrefix{os, prefix}
 }
diff --git a/pkg/object/sql.go b/pkg/object/sql.go
index 62fbe097..980c9da4 100644
--- a/pkg/object/sql.go
+++ b/pkg/object/sql.go
@@ -199,22 +199,22 @@ func newSQLStore(driver, addr, user, password string) (ObjectStorage, error) {
 	return &sqlStore{DefaultObjectStorage{}, engine, addr}, nil
 }
 
+func removeScheme(addr string) string {
+	p := strings.Index(addr, "://")
+	if p > 0 {
+		addr = addr[p+3:]
+	}
+	return addr
+}
+
 func init() {
 	Register("sqlite3", func(addr, user, pass, token string) (ObjectStorage, error) {
-		p := strings.Index(addr, "://")
-		if p > 0 {
-			addr = addr[p+3:]
-		}
-		return newSQLStore("sqlite3", addr, user, pass)
+		return newSQLStore("sqlite3", removeScheme(addr), user, pass)
 	})
 	Register("mysql", func(addr, user, pass, token string) (ObjectStorage, error) {
-		p := strings.Index(addr, "://")
-		if p > 0 {
-			addr = addr[p+3:]
-		}
-		return newSQLStore("mysql", addr, user, pass)
+		return newSQLStore("mysql", removeScheme(addr), user, pass)
 	})
 	Register("postgres", func(addr, user, pass, token string) (ObjectStorage, error) {
-		return newSQLStore("postgres", addr, user, pass)
+		return newSQLStore("postgres", removeScheme(addr), user, pass)
 	})
 }
diff --git a/pkg/object/tikv.go b/pkg/object/tikv.go
index 32c6a181..0b9a86f9 100644
--- a/pkg/object/tikv.go
+++ b/pkg/object/tikv.go
@@ -127,7 +127,10 @@ func newTiKV(endpoint, accesskey, secretkey, token string) (ObjectStorage, error
 	l, prop, _ := plog.InitLogger(&plog.Config{Level: plvl})
 	plog.ReplaceGlobals(l, prop)
 
-	tUrl, err := url.Parse("tikv://" + endpoint)
+	if !strings.HasPrefix(endpoint, "tikv://") {
+		endpoint = "tikv://" + endpoint
+	}
+	tUrl, err := url.Parse(endpoint)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/object/ufile.go b/pkg/object/ufile.go
index e5cdd707..6d446b2e 100644
--- a/pkg/object/ufile.go
+++ b/pkg/object/ufile.go
@@ -245,7 +245,7 @@ func (u *ufile) CreateMultipartUpload(key string) (*MultipartUpload, error) {
 }
 
 func (u *ufile) UploadPart(key string, uploadID string, num int, data []byte) (*Part, error) {
-	// UFile require the PartNumber to start from 0 (continious)
+	// UFile require the PartNumber to start from 0 (continuous)
 	num--
 	path := fmt.Sprintf("%s?uploadId=%s&partNumber=%d", key, uploadID, num)
 	resp, err := u.request("PUT", path, bytes.NewReader(data), nil)
diff --git a/pkg/utils/utils_darwin.go b/pkg/utils/utils_darwin.go
index cf77dfaf..8363d8cb 100644
--- a/pkg/utils/utils_darwin.go
+++ b/pkg/utils/utils_darwin.go
@@ -16,4 +16,38 @@
 
 package utils
 
+import (
+	"fmt"
+	"os/exec"
+)
+
 func GetKernelVersion() (major, minor int) { return }
+
+func GetSysInfo() (string, error) {
+	var (
+		kernel    string
+		osVersion []byte
+		hardware  []byte
+		err       error
+	)
+
+	if kernel, err = GetKernelInfo(); err != nil {
+		return "", fmt.Errorf("failed to execute command `uname`: %s", err)
+	}
+
+	if osVersion, err = exec.Command("sw_vers").Output(); err != nil {
+		return "", fmt.Errorf("failed to execute command `sw_vers`: %s", err)
+	}
+
+	if hardware, err = exec.Command("system_profiler", "SPMemoryDataType", "SPStorageDataType").Output(); err != nil {
+		return "", fmt.Errorf("failed to execute command `system_profiler`: %s", err)
+	}
+
+	return fmt.Sprintf(`
+Kernel: 
+%s
+OS: 
+%s
+Hardware: 
+%s`, kernel, string(osVersion), string(hardware)), nil
+}
diff --git a/pkg/utils/utils_linux.go b/pkg/utils/utils_linux.go
index ae18b41f..0dd29977 100644
--- a/pkg/utils/utils_linux.go
+++ b/pkg/utils/utils_linux.go
@@ -17,6 +17,9 @@
 package utils
 
 import (
+	"fmt"
+	"os"
+	"os/exec"
 	"strconv"
 	"strings"
 	"syscall"
@@ -43,3 +46,30 @@ func GetKernelVersion() (major, minor int) {
 	}
 	return
 }
+
+func GetSysInfo() (string, error) {
+	var (
+		kernel    []byte
+		osVersion []byte
+		err       error
+	)
+
+	procPath := "/proc/version"
+	if _, err := os.Stat(procPath); err == nil {
+		kernel, err = exec.Command("cat", procPath).Output()
+		if err != nil {
+			return "", fmt.Errorf("failed to execute command `cat %s`: %s", procPath, err)
+		}
+	}
+
+	osVersion, err = exec.Command("lsb_release", "-a").Output()
+	if err != nil {
+		return "", fmt.Errorf("failed to execute command `lsb_release`: %s", err)
+	}
+
+	return fmt.Sprintf(`
+Kernel: 
+%s
+OS: 
+%s`, kernel, osVersion), nil
+}
diff --git a/pkg/utils/utils_unix.go b/pkg/utils/utils_unix.go
index f4ae4a80..821b64e3 100644
--- a/pkg/utils/utils_unix.go
+++ b/pkg/utils/utils_unix.go
@@ -21,6 +21,8 @@ package utils
 
 import (
 	"os"
+	"os/exec"
+	"strings"
 	"syscall"
 )
 
@@ -45,3 +47,15 @@ func GetDev(fpath string) int { // ID of device containing file
 	}
 	return -1
 }
+
+func GetKernelInfo() (string, error) {
+	kernel, err := exec.Command("uname", "-a").Output()
+	if err != nil {
+		return "", err
+	}
+
+	// Ignore hostname information
+	tmp := strings.Split(string(kernel), " ")
+	result := strings.Join(append(tmp[:1], tmp[2:]...), " ")
+	return result, nil
+}
diff --git a/pkg/utils/utils_windows.go b/pkg/utils/utils_windows.go
index 32046412..49073b3f 100644
--- a/pkg/utils/utils_windows.go
+++ b/pkg/utils/utils_windows.go
@@ -17,7 +17,9 @@
 package utils
 
 import (
+	"fmt"
 	"os"
+	"os/exec"
 
 	"golang.org/x/sys/windows"
 )
@@ -40,3 +42,11 @@ func GetFileInode(path string) (uint64, error) {
 func GetKernelVersion() (major, minor int) { return }
 
 func GetDev(fpath string) int { return -1 }
+
+func GetSysInfo() (string, error) {
+	sysInfo, err := exec.Command("systeminfo").Output()
+	if err != nil {
+		return "", fmt.Errorf("Failed to execute command `systeminfo`: %s", err)
+	}
+	return string(sysInfo), nil
+}
diff --git a/pkg/vfs/internal.go b/pkg/vfs/internal.go
index b738654d..f445a75d 100644
--- a/pkg/vfs/internal.go
+++ b/pkg/vfs/internal.go
@@ -251,11 +251,11 @@ func (v *VFS) handleInternalMsg(ctx meta.Context, cmd uint32, r *utils.Buffer, d
 	switch cmd {
 	case meta.Rmr:
 		done := make(chan struct{})
+		inode := Ino(r.Get64())
+		name := string(r.Get(int(r.Get8())))
 		var count uint64
 		var st syscall.Errno
 		go func() {
-			inode := Ino(r.Get64())
-			name := string(r.Get(int(r.Get8())))
 			st = v.Meta.Remove(ctx, inode, name, &count)
 			if st != 0 {
 				logger.Errorf("remove %d/%s: %s", inode, name, st)
@@ -263,6 +263,11 @@ func (v *VFS) handleInternalMsg(ctx meta.Context, cmd uint32, r *utils.Buffer, d
 			close(done)
 		}()
 		writeProgress(&count, nil, data, done)
+		if st == 0 && v.InvalidateEntry != nil {
+			if st = v.InvalidateEntry(inode, name); st != 0 {
+				logger.Errorf("Invalidate entry %d/%s: %s", inode, name, st)
+			}
+		}
 		*data = append(*data, uint8(st))
 	case meta.Info:
 		var summary meta.Summary
diff --git a/pkg/vfs/reader.go b/pkg/vfs/reader.go
index c9606171..9acdb767 100644
--- a/pkg/vfs/reader.go
+++ b/pkg/vfs/reader.go
@@ -684,7 +684,7 @@ func NewDataReader(conf *Config, m meta.Meta, store chunk.ChunkStore) DataReader
 	var readAheadTotal = 256 << 20
 	var readAheadMax = conf.Chunk.BlockSize * 8
 	if conf.Chunk.BufferSize > 0 {
-		readAheadTotal = conf.Chunk.BufferSize * 8 / 10 // 80% of total buffer
+		readAheadTotal = conf.Chunk.BufferSize / 10 * 8 // 80% of total buffer
 	}
 	if conf.Chunk.Readahead > 0 {
 		readAheadMax = conf.Chunk.Readahead
diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go
index 77bea8d5..77bb219b 100644
--- a/pkg/vfs/vfs.go
+++ b/pkg/vfs/vfs.go
@@ -744,10 +744,8 @@ func (v *VFS) Flush(ctx Context, ino Ino, fh uint64, lockOwner uint64) (err sysc
 	}
 
 	if h.writer != nil {
-		if !h.Wlock(ctx) {
+		for !h.Wlock(ctx) {
 			h.cancelOp(ctx.Pid())
-			err = syscall.EINTR
-			return
 		}
 
 		err = h.writer.Flush(ctx)
@@ -904,11 +902,12 @@ func (v *VFS) RemoveXattr(ctx Context, ino Ino, name string) (err syscall.Errno)
 var logger = utils.GetLogger("juicefs")
 
 type VFS struct {
-	Conf   *Config
-	Meta   meta.Meta
-	Store  chunk.ChunkStore
-	reader DataReader
-	writer DataWriter
+	Conf            *Config
+	Meta            meta.Meta
+	Store           chunk.ChunkStore
+	InvalidateEntry func(parent meta.Ino, name string) syscall.Errno
+	reader          DataReader
+	writer          DataWriter
 
 	handles map[Ino][]*handle
 	hanleM  sync.Mutex
diff --git a/pkg/vfs/writer.go b/pkg/vfs/writer.go
index c6e6f15e..8ff2ecd7 100644
--- a/pkg/vfs/writer.go
+++ b/pkg/vfs/writer.go
@@ -357,7 +357,7 @@ func (f *fileWriter) flush(ctx meta.Context, writeback bool) syscall.Errno {
 				}
 			}
 		}
-		if f.flushcond.WaitWithTimeout(time.Second*3) && ctx.Canceled() {
+		if f.flushcond.WaitWithTimeout(time.Second*3) && ctx.Canceled() && time.Since(s) > f.w.conf.Chunk.PutTimeout*2 {
 			logger.Warnf("flush %d interrupted after %d", f.inode, time.Since(s))
 			err = syscall.EINTR
 			break
@@ -412,6 +412,7 @@ type dataWriter struct {
 	sync.Mutex
 	m          meta.Meta
 	store      chunk.ChunkStore
+	conf       *Config
 	reader     DataReader
 	blockSize  int
 	bufferSize int64
@@ -424,6 +425,7 @@ func NewDataWriter(conf *Config, m meta.Meta, store chunk.ChunkStore, reader Dat
 		m:          m,
 		store:      store,
 		reader:     reader,
+		conf:       conf,
 		blockSize:  conf.Chunk.BlockSize,
 		bufferSize: int64(conf.Chunk.BufferSize),
 		files:      make(map[Ino]*fileWriter),
diff --git a/sdk/java/libjfs/bridge.go b/sdk/java/libjfs/bridge.go
index 337df520..8b1877ec 100644
--- a/sdk/java/libjfs/bridge.go
+++ b/sdk/java/libjfs/bridge.go
@@ -27,11 +27,11 @@ import (
 	"sort"
 	"time"
 
-	"github.com/golang/protobuf/proto"
 	"github.com/prometheus/client_golang/prometheus"
 	dto "github.com/prometheus/client_model/go"
 	"github.com/prometheus/common/expfmt"
 	"github.com/prometheus/common/model"
+	"google.golang.org/protobuf/proto"
 )
 
 const (
diff --git a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
index df3b6943..4df231dd 100644
--- a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
+++ b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
@@ -53,6 +53,7 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.*;
 import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
 import java.util.*;
@@ -62,6 +63,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.jar.JarFile;
 import java.util.stream.Collectors;
 import java.util.zip.GZIPInputStream;
+import java.util.zip.ZipEntry;
 
 /****************************************************************
  * Implement the FileSystem API for JuiceFS
@@ -93,7 +95,7 @@ public class JuiceFileSystemImpl extends FileSystem {
   private boolean metricsEnable = false;
 
   /*
-   * hadoop compability
+   * hadoop compatibility
    */
   private boolean withStreamCapability;
   // constructor for BufferedFSOutputStreamWithStreamCapabilities
@@ -532,40 +534,40 @@ public class JuiceFileSystemImpl extends FileSystem {
 
     File libFile = new File(dir, name);
 
-    URL res = null;
-    String jarPath;
-    try {
-      jarPath = JuiceFileSystemImpl.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
-    } catch (URISyntaxException e) {
-      throw new IOException("get jar path failed", e);
-    }
-    Enumeration<URL> resources = JuiceFileSystemImpl.class.getClassLoader().getResources(resource);
-    while (resources.hasMoreElements()) {
-      res = resources.nextElement();
-      if (URI.create(res.getPath()).getPath().startsWith(jarPath)) {
-        break;
-      }
-    }
-    if (res == null) {
+    InputStream ins;
+    long soTime;
+    URL location = JuiceFileSystemImpl.class.getProtectionDomain().getCodeSource().getLocation();
+    if (location == null) {
       // jar may changed
       return libjfsLibraryLoader.load(libFile.getAbsolutePath());
     }
-    URLConnection conn;
+    URLConnection con;
     try {
-      conn = res.openConnection();
+      con = location.openConnection();
     } catch (FileNotFoundException e) {
       // jar may changed
       return libjfsLibraryLoader.load(libFile.getAbsolutePath());
     }
-
-    long soTime = conn.getLastModified();
-    if (res.getProtocol().equalsIgnoreCase("jar")) {
-      soTime = new JarFile(jarPath).getJarEntry(resource)
-              .getLastModifiedTime()
-              .toMillis();
+    if (location.getProtocol().equals("jar") && (con instanceof JarURLConnection)) {
+      LOG.debug("juicefs-hadoop.jar is a nested jar");
+      JarURLConnection connection = (JarURLConnection) con;
+      JarFile jfsJar = connection.getJarFile();
+      ZipEntry entry = jfsJar.getJarEntry(resource);
+      soTime = entry.getLastModifiedTime().toMillis();
+      ins = jfsJar.getInputStream(entry);
+    } else {
+      String jarPath = URLDecoder.decode(location.getPath(), Charset.defaultCharset().name());
+      if (jarPath.endsWith(".jar")) {
+        JarFile jfsJar = new JarFile(jarPath);
+        ZipEntry entry = jfsJar.getJarEntry(resource);
+        soTime = entry.getLastModifiedTime().toMillis();
+        ins = jfsJar.getInputStream(entry);
+      } else { // for debug: sdk/java/target/classes
+        soTime = con.getLastModified();
+        ins = JuiceFileSystemImpl.class.getClassLoader().getResourceAsStream(resource);
+      }
     }
 
-    InputStream ins = conn.getInputStream();
     synchronized (JuiceFileSystemImpl.class) {
       if (!libFile.exists() || libFile.lastModified() < soTime) {
         // try the name for current user
diff --git a/sdk/java/src/main/java/io/juicefs/bench/NNBench.java b/sdk/java/src/main/java/io/juicefs/bench/NNBench.java
index f90036a2..bae20c79 100644
--- a/sdk/java/src/main/java/io/juicefs/bench/NNBench.java
+++ b/sdk/java/src/main/java/io/juicefs/bench/NNBench.java
@@ -380,8 +380,8 @@ public class NNBench extends Main.Command {
             "                      Files per thread: " + numberOfFiles,
             "            Successful file operations: " + threadsPerMap * numberOfFiles,
             "",
-            "                           TPS: Create: " + (int) (totalTimeTPS),
-            "                  Avg Lat (ms): Create: " + String.format("%.2f", (double) (end - start) / (threadsPerMap * numberOfFiles)),
+            "                                   TPS: " + (int) (totalTimeTPS),
+            "                          Avg Lat (ms): " + String.format("%.2f", (double) (end - start) / (threadsPerMap * numberOfFiles)),
             "",
             "           RAW DATA: Job Duration (ms): " + (end - start),
             ""};


Test patch:
diff --git a/.github/scripts/testVersionCompatible.py b/.github/scripts/testVersionCompatible.py
index fc2b8778..990e4666 100644
--- a/.github/scripts/testVersionCompatible.py
+++ b/.github/scripts/testVersionCompatible.py
@@ -1,9 +1,11 @@
 import json
 import os
+from pickle import FALSE
 import platform
 import shutil
 import subprocess
 import sys
+from termios import TIOCPKT_DOSTOP
 import time
 import unittest
 from xmlrpc.client import boolean
@@ -11,92 +13,42 @@ from hypothesis.stateful import rule, precondition, RuleBasedStateMachine
 from hypothesis import assume, strategies as st
 from packaging import version
 from minio import Minio
-from utils import flush_meta, clear_storage, clear_cache, run_jfs_cmd, run_cmd, is_readonly, get_upload_delay_seconds, get_stage_blocks, write_data, write_block
+import uuid
+from utils import *
 
 class JuicefsMachine(RuleBasedStateMachine):
     MIN_CLIENT_VERSIONS = ['0.0.1', '0.0.17','1.0.0-beta1', '1.0.0-rc1']
     MAX_CLIENT_VERSIONS = ['1.1.0', '1.2.0', '2.0.0']
-    # JFS_BIN = ['./juicefs-1.0.0-beta1', './juicefs-1.0.0-beta2', './juicefs-1.0.0-beta3', './juicefs-1.0.0-rc1', './juicefs-1.0.0-rc2','./juicefs-1.0.0-rc3','./juicefs']
-    # juicefs_version = subprocess.check_output(['./juicefs', 'version']).decode().split()[-1].split('+')[0]
-    # os.environ['NEW_JFS_BIN'] = f'./juicefs-{juicefs_version}'
     JFS_BINS = ['./'+os.environ.get('OLD_JFS_BIN'), './'+os.environ.get('NEW_JFS_BIN')]
-    # JFS_BINS = ['./juicefs-1.0.0-rc2',  './juicefs-1.1.0-dev']
-    META_URLS = [os.environ.get('META_URL')]
-    STORAGES = [os.environ.get('STORAGE')]
-    # META_URL = 'badger://abc.db'
+    meta_dict = {'redis':'redis://localhost/1', 'mysql':'mysql://root:root@(127.0.0.1)/test', 'postgres':'postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable', \
+        'tikv':'tikv://127.0.0.1:2379', 'badger':'badger://badger-data', 'mariadb': 'mysql://root:root@(127.0.0.1)/test', \
+            'sqlite3': 'sqlite3://test.db'}
+    META_URL = meta_dict[os.environ.get('META')]
+    STORAGE = os.environ.get('STORAGE')
     MOUNT_POINT = '/tmp/sync-test/'
     VOLUME_NAME = 'test-volume'
-    valid_file_name = st.text(st.characters(max_codepoint=1000, blacklist_categories=('Cc', 'Cs')), min_size=2).map(lambda s: s.strip()).filter(lambda s: len(s) > 0)
+    # valid_file_name = st.text(st.characters(max_codepoint=1000, blacklist_categories=('Cc', 'Cs')), min_size=2).map(lambda s: s.strip()).filter(lambda s: len(s) > 0)
 
     def __init__(self):
         super(JuicefsMachine, self).__init__()
-        print('\n__init__')
+        self.run_id = uuid.uuid4().hex
+        print(f'\ninit with run_id: {self.run_id}')
         with open('command.log', 'a') as f:
-            f.write('init------------------------------------\n')
+            f.write(f'init with run_id: {self.run_id}\n')
         self.formatted = False
         self.mounted = False
-        self.meta_url = None
+        # mount at least once, see ref: https://github.com/juicedata/juicefs/issues/2717
+        self.mounted_by = []
         self.formatted_by = ''
-        run_cmd(f'mc alias set myminio http://localhost:9000 minioadmin minioadmin')
+        self.dumped_by = ''
+        if JuicefsMachine.META_URL.startswith('badger://'):
+            # change url for each run
+            JuicefsMachine.META_URL = f'badger://badger-{uuid.uuid4().hex}'
+        if JuicefsMachine.STORAGE == 'minio':
+            run_cmd(f'mc alias set myminio http://localhost:9000 minioadmin minioadmin')
         if os.path.isfile('dump.json'):
             os.remove('dump.json')
-
-    @rule(
-        juicefs=st.sampled_from(JFS_BINS),
-        capacity=st.integers(min_value=0, max_value=1024), 
-        inodes=st.integers(min_value=1024*1024, max_value=1024*1024*1024),
-        change_bucket=st.booleans(), 
-        change_aksk=st.booleans(), 
-        encrypt_secret = st.booleans(), 
-        trash_days =  st.integers(min_value=0, max_value=10000),
-        min_client_version = st.sampled_from(MIN_CLIENT_VERSIONS), 
-        max_client_version = st.sampled_from(MAX_CLIENT_VERSIONS), 
-        force = st.booleans(),
-    )
-    @precondition(lambda self: self.formatted)
-    def config(self, juicefs, capacity, inodes, change_bucket, change_aksk, encrypt_secret, trash_days, min_client_version, max_client_version, force):
-        assume (self.is_supported_version(juicefs))
-        assume(run_cmd(f'{juicefs} --help | grep destroy') == 0)
-        print('start config')
-        options = [juicefs, 'config', self.meta_url]
-        options.extend(['--trash-days', str(trash_days)])
-        options.extend(['--capacity', str(capacity)])
-        options.extend(['--inodes', str(inodes)])
-        assert version.parse(min_client_version) <= version.parse(max_client_version)
-        options.extend(['--min-client-version', min_client_version])
-        options.extend(['--max-client-version', max_client_version])
-        output = subprocess.check_output([juicefs, 'status', self.meta_url])
-        storage = json.loads(output.decode().replace("'", '"'))['Setting']['Storage']
-        
-        if change_bucket:
-            if storage == 'file':
-                options.extend(['--bucket', os.path.expanduser('~/.juicefs/local2')])
-            elif storage == 'minio': 
-                c = Minio('localhost:9000', access_key='minioadmin', secret_key='minioadmin', secure=False)
-                if not c.bucket_exists('test-bucket2'):
-                    run_cmd('mc mb myminio/test-bucket2')
-                    # assert c.bucket_exists('test-bucket2')
-                options.extend(['--bucket', 'http://localhost:9000/test-bucket2'])
-        if change_aksk and storage == 'minio':
-            output = subprocess.check_output('mc admin user list myminio'.split())
-            if not output:
-                run_cmd('mc admin user add myminio juicedata 12345678')
-                run_cmd('mc admin policy set myminio consoleAdmin user=juicedata')
-            options.extend(['--access-key', 'juicedata'])
-            options.extend(['--secret-key', '12345678'])
-        if encrypt_secret and version.parse('-'.join(juicefs.split('-')[1:])) >= version.parse('1.0.0-rc2'):
-            options.append('--encrypt-secret')
-        options.append('--force')
-        run_jfs_cmd(options)
-        if change_bucket:
-            # change bucket back to avoid fsck fail.
-            if storage == 'file':
-                run_jfs_cmd([juicefs, 'config', self.meta_url, '--bucket', os.path.expanduser('~/.juicefs/local')])
-            elif storage == 'minio':
-                run_jfs_cmd([juicefs, 'config', self.meta_url, '--bucket', 'http://localhost:9000/test-bucket'])
-        print('config succeed')
-    
-    
+        os.environ['PGPASSWORD'] = 'postgres'
 
     @rule(
           juicefs=st.sampled_from(JFS_BINS),
@@ -105,19 +57,18 @@ class JuicefsMachine(RuleBasedStateMachine):
           inodes=st.integers(min_value=1024*1024, max_value=1024*1024*1024),
           compress=st.sampled_from(['lz4', 'zstd', 'none']),
           shards=st.integers(min_value=0, max_value=1),
-          storage=st.sampled_from(STORAGES), 
+          storage=st.just(STORAGE), 
           encrypt_rsa_key = st.booleans(), 
           encrypt_algo = st.sampled_from(['aes256gcm-rsa','chacha20-rsa']),
           trash_days=st.integers(min_value=0, max_value=10000), 
           hash_prefix=st.booleans(), 
           force = st.booleans(), 
-          no_update = st.booleans(),
-          meta_url=st.sampled_from(META_URLS),
+          no_update = st.booleans()
           )
-    def format(self, juicefs, block_size, capacity, inodes, compress, shards, storage, encrypt_rsa_key, encrypt_algo, trash_days, hash_prefix, force, no_update, meta_url):
-        assume (self.is_supported_version(juicefs))
+    def format(self, juicefs, block_size, capacity, inodes, compress, shards, storage, encrypt_rsa_key, encrypt_algo, trash_days, hash_prefix, force, no_update):
+        assume (self.greater_than_version_formatted(juicefs))
         print('start format')
-        options = [juicefs, 'format',  meta_url, JuicefsMachine.VOLUME_NAME]
+        options = [juicefs, 'format',  JuicefsMachine.META_URL, JuicefsMachine.VOLUME_NAME]
         if not self.formatted:
             options.extend(['--block-size', str(block_size)])
             options.extend(['--compress', compress])
@@ -143,14 +94,28 @@ class JuicefsMachine(RuleBasedStateMachine):
                 options.extend(['--encrypt-algo', encrypt_algo])
         
         if storage == 'minio':
-            bucket = 'http://localhost:9000/test-bucket'
+            bucket = 'http://localhost:9000/testbucket'
             options.extend(['--bucket', bucket])
             options.extend(['--access-key', 'minioadmin'])
             options.extend(['--secret-key', 'minioadmin'])
+            if self.formatted and version.parse('-'.join(juicefs.split('-')[1:])) <= version.parse('1.0.0-rc1'):
+                # use the latest version to change secret-key because rc1 has a bug for secret-key
+                options[0] = JuicefsMachine.JFS_BINS[1]
         elif storage == 'file':
             bucket = os.path.expanduser('~/.juicefs/local/')
             options.extend(['--bucket', bucket])
+        elif storage == 'mysql':
+            bucket = '(localhost:3306)/testbucket'
+            options.extend(['--bucket', bucket])
+            options.extend(['--access-key', 'root'])
+            options.extend(['--secret-key', 'root'])
+        elif storage == 'postgres':
+            bucket = 'localhost:5432/testbucket?sslmode=disable'
+            options.extend(['--bucket', bucket])
+            options.extend(['--access-key', 'postgres'])
+            options.extend(['--secret-key', 'postgres'])
         else:
+            print(f'storage is {storage}')
             raise Exception(f'storage value error: {storage}')
 
         if not self.formatted:
@@ -158,28 +123,108 @@ class JuicefsMachine(RuleBasedStateMachine):
                 run_cmd('umount %s'%JuicefsMachine.MOUNT_POINT)
                 print(f'umount {JuicefsMachine.MOUNT_POINT} succeed')
             clear_storage(storage, bucket, JuicefsMachine.VOLUME_NAME)
-            flush_meta(meta_url)
+            flush_meta(JuicefsMachine.META_URL)
         print(f'format options: {" ".join(options)}' )
         run_jfs_cmd(options)
-        self.meta_url = meta_url
         self.formatted = True
         self.formatted_by = juicefs
         print('format succeed')
 
+
+    @rule(
+        juicefs=st.sampled_from(JFS_BINS),
+        capacity=st.integers(min_value=0, max_value=1024), 
+        inodes=st.integers(min_value=1024*1024, max_value=1024*1024*1024),
+        change_bucket=st.booleans(), 
+        change_aksk=st.booleans(), 
+        encrypt_secret = st.booleans(), 
+        trash_days =  st.integers(min_value=0, max_value=10000),
+        min_client_version = st.sampled_from(MIN_CLIENT_VERSIONS), 
+        max_client_version = st.sampled_from(MAX_CLIENT_VERSIONS), 
+        force = st.booleans(),
+    )
+    @precondition(lambda self: self.formatted)
+    def config(self, juicefs, capacity, inodes, change_bucket, change_aksk, encrypt_secret, trash_days, min_client_version, max_client_version, force):
+        assume (self.greater_than_version_formatted(juicefs))
+        assume(run_cmd(f'{juicefs} --help | grep config') == 0)
+        print('start config')
+        options = [juicefs, 'config', JuicefsMachine.META_URL]
+        options.extend(['--trash-days', str(trash_days)])
+        options.extend(['--capacity', str(capacity)])
+        options.extend(['--inodes', str(inodes)])
+        assert version.parse(min_client_version) <= version.parse(max_client_version)
+        if run_cmd(f'{juicefs} config --help | grep min-client-version') == 0:
+            options.extend(['--min-client-version', min_client_version])
+        if run_cmd(f'{juicefs} config --help | grep max-client-version') == 0:
+            options.extend(['--max-client-version', max_client_version])
+        storage = get_storage(juicefs, JuicefsMachine.META_URL)
+        
+        if change_bucket:
+            if storage == 'file':
+                options.extend(['--bucket', os.path.expanduser('~/.juicefs/local2')])
+            elif storage == 'minio': 
+                c = Minio('localhost:9000', access_key='minioadmin', secret_key='minioadmin', secure=False)
+                if not c.bucket_exists('testbucket2'):
+                    run_cmd('mc mb myminio/testbucket2')
+                    # assert c.bucket_exists('testbucket2')
+                options.extend(['--bucket', 'http://localhost:9000/testbucket2'])
+            elif storage == 'mysql':
+                create_mysql_db('mysql://root:root@(localhost:3306)/testbucket2')
+                options.extend(['--bucket', '(localhost:3306)/testbucket2'])
+            elif storage == 'postgres':
+                create_postgres_db('postgres://postgres:postgres@localhost:5432/testbucket2?sslmode=disable')
+                options.extend(['--bucket', 'localhost:5432/testbucket2?sslmode=disable'])
+        if change_aksk and storage == 'minio':
+            output = subprocess.check_output('mc admin user list myminio'.split())
+            if not output:
+                run_cmd('mc admin user add myminio juicedata 12345678')
+                run_cmd('mc admin policy set myminio consoleAdmin user=juicedata')
+            options.extend(['--access-key', 'juicedata'])
+            options.extend(['--secret-key', '12345678'])
+            if version.parse('-'.join(juicefs.split('-')[1:])) <= version.parse('1.0.0-rc1'):
+                # use the latest version to set secret-key because rc1 has a bug for secret-key
+                options[0] = JuicefsMachine.JFS_BINS[1]
+        if encrypt_secret and run_cmd(f'{juicefs} config --help | grep encrypt-secret') == 0:
+            # 0.17.5 store the secret without encrypt, ref: https://github.com/juicedata/juicefs/issues/2721
+            #if version.parse('-'.join(juicefs.split('-')[1:])) > version.parse('0.17.5'):
+            options.append('--encrypt-secret')
+        options.append('--force')
+        run_jfs_cmd(options)
+        if change_bucket:
+            # change bucket back to avoid fsck fail.
+            if storage == 'file':
+                run_jfs_cmd([juicefs, 'config', JuicefsMachine.META_URL, '--bucket', os.path.expanduser('~/.juicefs/local')])
+            elif storage == 'minio':
+                run_jfs_cmd([juicefs, 'config', JuicefsMachine.META_URL, '--bucket', 'http://localhost:9000/testbucket'])
+            elif storage == 'mysql':
+                run_jfs_cmd([juicefs, 'config', JuicefsMachine.META_URL, '--bucket', '(localhost:3306)/testbucket'])
+            elif storage == 'postgres':
+                run_jfs_cmd([juicefs, 'config', JuicefsMachine.META_URL, '--bucket', 'localhost:5432/testbucket?sslmode=disable'])
+        self.formatted_by = juicefs
+        print('config succeed')
+
+
     @rule(juicefs=st.sampled_from(JFS_BINS))
     @precondition(lambda self: self.formatted )
     def status(self, juicefs):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
         print('start status')
-        output = subprocess.check_output([juicefs, 'status', self.meta_url])
-        print(f'status output: {output.decode()}')
-        uuid = json.loads(output.decode().replace("'", '"'))['Setting']['UUID']
+        output = subprocess.run([juicefs, 'status', JuicefsMachine.META_URL], check=True, stdout=subprocess.PIPE).stdout.decode()
+        if 'get timestamp too slow' in output: 
+            # remove the first line caust it is tikv log message
+            output = '\n'.join(output.split('\n')[1:])
+        print(f'status output: {output}')
+        try:
+            uuid = json.loads(output.replace("'", '"'))['Setting']['UUID']
+        except:
+            raise Exception(f'parse uuid failed, output: {output}')
         assert len(uuid) != 0
-        if self.mounted and not is_readonly(JuicefsMachine.MOUNT_POINT):
-            sessions = json.loads(output.decode().replace("'", '"'))['Sessions']
+        if self.mounted and not is_readonly(JuicefsMachine.MOUNT_POINT) and self.greater_than_version_mounted(juicefs):
+            sessions = json.loads(output.replace("'", '"'))['Sessions']
             assert len(sessions) != 0 
         print('status succeed')
 
+
     @rule(juicefs=st.sampled_from(JFS_BINS), 
         no_syslog=st.booleans(),
         other_fuse_options=st.lists(st.sampled_from(['debug', 'allow_other', 'writeback_cache']), unique=True), 
@@ -198,16 +243,16 @@ class JuicefsMachine(RuleBasedStateMachine):
         prefetch=st.integers(min_value=0, max_value=100), 
         writeback=st.booleans(),
         upload_delay=st.sampled_from([0, 2]), 
-        cache_dir=valid_file_name,
+        cache_dir=st.sampled_from(['cache1', 'cache2']),
         cache_size=st.integers(min_value=0, max_value=1024000), 
         free_space_ratio=st.floats(min_value=0.1, max_value=0.5), 
         cache_partial_only=st.booleans(),
-        backup_meta=st.integers(min_value=300, max_value=3600),
+        backup_meta=st.integers(min_value=300, max_value=1000),
         heartbeat=st.integers(min_value=5, max_value=12), 
         read_only=st.booleans(),
         no_bgjob=st.booleans(),
         open_cache=st.integers(min_value=0, max_value=100),
-        sub_dir=valid_file_name,
+        sub_dir=st.sampled_from(['dir1', 'dir2']),
         metrics=st.sampled_from(['127.0.0.1:9567', '127.0.0.1:9568']), 
         consul=st.sampled_from(['127.0.0.1:8500', '127.0.0.1:8501']), 
         no_usage_report=st.booleans(),
@@ -217,7 +262,9 @@ class JuicefsMachine(RuleBasedStateMachine):
         get_timeout, put_timeout, io_retries, max_uploads, max_deletes, buffer_size, upload_limit, download_limit, prefetch, 
         writeback, upload_delay, cache_dir, cache_size, free_space_ratio, cache_partial_only, backup_meta, heartbeat, read_only,
         no_bgjob, open_cache, sub_dir, metrics, consul, no_usage_report):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
+        if JuicefsMachine.META_URL.startswith('badger://'):
+            assume(not self.mounted)
         retry = 3
         while os.path.exists(f'{JuicefsMachine.MOUNT_POINT}/.accesslog') and retry > 0:
             os.system(f'umount {JuicefsMachine.MOUNT_POINT}')
@@ -227,7 +274,7 @@ class JuicefsMachine(RuleBasedStateMachine):
             print(f'FATAL: umount {JuicefsMachine.MOUNT_POINT} failed.')
         assume(not os.path.exists(f'{JuicefsMachine.MOUNT_POINT}/.accesslog'))
         print('start mount')
-        options = [juicefs, 'mount', '-d',  self.meta_url, JuicefsMachine.MOUNT_POINT]
+        options = [juicefs, 'mount', '-d',  JuicefsMachine.META_URL, JuicefsMachine.MOUNT_POINT]
         if no_syslog:
             options.append('--no-syslog')
         options.extend(['--log', os.path.expanduser(f'~/.juicefs/juicefs.log')])
@@ -247,31 +294,42 @@ class JuicefsMachine(RuleBasedStateMachine):
         options.extend(['--put-timeout', str(put_timeout)])
         options.extend(['--io-retries', str(io_retries)])
         options.extend(['--max-uploads', str(max_uploads)])
-        options.extend(['--max-deletes', str(max_deletes)])
+        if run_cmd(f'{juicefs} mount --help | grep max-deletes') == 0:
+            options.extend(['--max-deletes', str(max_deletes)])
         options.extend(['--buffer-size', str(buffer_size)])
         options.extend(['--upload-limit', str(upload_limit)])
         options.extend(['--download-limit', str(download_limit)])
         options.extend(['--prefetch', str(prefetch)])
         if writeback:
             options.append('--writeback')
+        upload_delay = str(upload_delay)
+        if version.parse('-'.join(juicefs.split('-')[1:])) <= version.parse('1.0.0-beta2'):
+            upload_delay = upload_delay + 's'
         options.extend(['--upload-delay', str(upload_delay)])
         options.extend(['--cache-dir', os.path.expanduser(f'~/.juicefs/{cache_dir}')])
         options.extend(['--cache-size', str(cache_size)])
         options.extend(['--free-space-ratio', str(free_space_ratio)])
         if cache_partial_only:
             options.append('--cache-partial-only')
-        options.extend(['--backup-meta', str(backup_meta)])
-        options.extend(['--heartbeat', str(heartbeat)])
+        backup_meta = str(backup_meta)
+        if version.parse('-'.join(juicefs.split('-')[1:])) <= version.parse('1.0.0-beta2'):
+            backup_meta = '1h0m0s'
+        if run_cmd(f'{juicefs} mount --help | grep backup-meta') == 0:
+            options.extend(['--backup-meta', backup_meta])
+        if run_cmd(f'{juicefs} mount --help | grep heartbeat') == 0:
+            options.extend(['--heartbeat', str(heartbeat)])
         if read_only:
             options.append('--read-only')
-        if no_bgjob:
+        if no_bgjob and run_cmd(f'{juicefs} mount --help | grep no-bgjob') == 0:
             options.append('--no-bgjob')
 
         options.extend(['--open-cache', str(open_cache)])
         print('TODO: subdir')
         # options.extend('--subdir', str(sub_dir))
-        options.extend(['--metrics', str(metrics)])
-        options.extend(['--consul', str(consul)])
+        if not is_port_in_use( int(metrics.split(':')[1])):
+            options.extend(['--metrics', str(metrics)])
+        # if run_cmd(f'{juicefs} mount --help | grep consul') == 0:
+        #     options.extend(['--consul', str(consul)])
         if no_usage_report:
             options.append('--no-usage-report')
         if os.path.exists(JuicefsMachine.MOUNT_POINT):
@@ -284,19 +342,63 @@ class JuicefsMachine(RuleBasedStateMachine):
             inode = subprocess.check_output(f'stat -f %i {JuicefsMachine.MOUNT_POINT}'.split())
         print(f'inode number: {inode}')
         assert(inode.decode()[:-1] == '1')
-        output = subprocess.check_output([juicefs, 'status', self.meta_url])
+        output = subprocess.run([juicefs, 'status', JuicefsMachine.META_URL], check=True, stdout=subprocess.PIPE).stdout.decode()
+        if 'get timestamp too slow' in output: 
+            # remove the first line caust it is tikv log message
+            output = '\n'.join(output.split('\n')[1:])
         print(f'status output: {output}')
-        sessions = json.loads(output.decode().replace("'", '"'))['Sessions']
+        sessions = json.loads(output.replace("'", '"'))['Sessions']
         if not read_only: 
             assert len(sessions) != 0 
         self.mounted = True
+        if not read_only:
+            self.mounted_by.append(juicefs)
         print('mount succeed')
 
+    @rule(juicefs=st.sampled_from(JFS_BINS), 
+        file_name=st.just('file_to_info'), 
+        data = st.binary())
+    @precondition(lambda self: self.formatted and self.mounted )
+    def info(self, juicefs, file_name, data):
+        assume (self.greater_than_version_formatted(juicefs))
+        assume (self.greater_than_version_mounted(juicefs))
+        assume(not is_readonly(f'{JuicefsMachine.MOUNT_POINT}'))
+        assert(os.path.exists(f'{JuicefsMachine.MOUNT_POINT}/.accesslog'))
+        print('start info')
+        path = JuicefsMachine.MOUNT_POINT+file_name
+        write_data(JuicefsMachine.MOUNT_POINT, path, data)
+        options = [juicefs, 'info', path]
+        run_jfs_cmd(options)
+        print('info succeed')
+
+    @rule(juicefs=st.sampled_from(JFS_BINS), 
+    file_name=st.just('file_to_rmr'))
+    @precondition(lambda self: self.formatted and self.mounted )
+    def rmr(self, juicefs, file_name):
+        assume (self.greater_than_version_formatted(juicefs))
+        assume (self.greater_than_version_mounted(juicefs))
+        assume(not is_readonly(f'{JuicefsMachine.MOUNT_POINT}'))
+        # ref: https://github.com/juicedata/juicefs/pull/2776
+        assume(version.parse('-'.join(juicefs.split('-')[1:])) >= version.parse('1.1.0-dev'))
+        # TODO: should test upload delay.
+        assume(get_upload_delay_seconds(JuicefsMachine.MOUNT_POINT) == 0)
+        assert(os.path.exists(f'{JuicefsMachine.MOUNT_POINT}/.accesslog'))
+        print('start rmr')
+        path = f'{JuicefsMachine.MOUNT_POINT}{file_name}'
+        write_block(JuicefsMachine.MOUNT_POINT, path, 1048576, 3)
+        assert(os.path.exists(path))
+        run_cmd(f'stat {path}')
+        options = [juicefs, 'rmr', path]
+        run_jfs_cmd(options)
+        # TODO: should uncomment the assert
+        # assert(not os.path.exists(path))
+        print('rmr succeed')
+
     @rule(juicefs=st.sampled_from(JFS_BINS), 
     force=st.booleans())
     @precondition(lambda self: self.mounted)
     def umount(self, juicefs, force):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
         print('start umount')
         options = [juicefs, 'umount', JuicefsMachine.MOUNT_POINT]
         # don't force umount because it may not unmounted succeed.
@@ -309,20 +411,28 @@ class JuicefsMachine(RuleBasedStateMachine):
     @rule(juicefs=st.sampled_from(JFS_BINS))
     @precondition(lambda self: self.formatted and not self.mounted)
     def destroy(self, juicefs):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
         assume(run_cmd(f'{juicefs} --help | grep destroy') == 0)
         print('start destroy')
-        output = subprocess.check_output([juicefs, 'status', self.meta_url])
-        uuid = json.loads(output.decode().replace("'", '"'))['Setting']['UUID']
+        output = subprocess.run([juicefs, 'status', JuicefsMachine.META_URL], check=True, stdout=subprocess.PIPE).stdout.decode()
+        if 'get timestamp too slow' in output: 
+            # remove the first line caust it is tikv log message
+            output = '\n'.join(output.split('\n')[1:]) 
+        print(f'status output: {output}')
+        uuid = json.loads(output.replace("'", '"'))['Setting']['UUID']
+        print(f'uuid is: {uuid}')
         assert len(uuid) != 0
-        options = [juicefs, 'destroy', self.meta_url, uuid]
+        options = [juicefs, 'destroy', JuicefsMachine.META_URL, uuid]
         options.append('--force')
         run_jfs_cmd(options)
         self.formatted = False
         self.mounted = False
+        self.mounted_by = []
+        self.formatted_by = ''
         print('destroy succeed')
 
-    @rule(file_name=valid_file_name, data=st.binary() )
+    @rule(file_name=st.sampled_from(['myfile1', 'myfile2']), 
+        data=st.binary() )
     @precondition(lambda self: self.mounted )
     def write_and_read(self, file_name, data):
         assume(not is_readonly(f'{JuicefsMachine.MOUNT_POINT}'))
@@ -338,36 +448,48 @@ class JuicefsMachine(RuleBasedStateMachine):
     @rule(juicefs = st.sampled_from(JFS_BINS))
     @precondition(lambda self: self.formatted )
     def dump(self, juicefs):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
+        # check this because of: https://github.com/juicedata/juicefs/issues/2717
+        assume(juicefs in self.mounted_by)
         print('start dump')
-        run_jfs_cmd([juicefs, 'dump', self.meta_url, 'dump.json'])
+        run_jfs_cmd([juicefs, 'dump', JuicefsMachine.META_URL, 'dump.json'])
+        self.dumped_by = juicefs
         print('dump succeed')
 
     @rule(juicefs = st.sampled_from(JFS_BINS))
     @precondition(lambda self: self.formatted and os.path.exists('dump.json'))
     def load(self, juicefs):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
+        assume (self.greater_than_version_dumped(juicefs))
         print('start load')
         if os.path.exists(JuicefsMachine.MOUNT_POINT) and os.path.exists(JuicefsMachine.MOUNT_POINT+'.accesslog'):
             run_cmd('umount %s'%JuicefsMachine.MOUNT_POINT)
             print(f'umount {JuicefsMachine.MOUNT_POINT} succeed')
             self.mounted = False
-        flush_meta(self.meta_url)
-        run_jfs_cmd([juicefs, 'load', self.meta_url, 'dump.json'])
+        flush_meta(JuicefsMachine.META_URL)
+        run_jfs_cmd([juicefs, 'load', JuicefsMachine.META_URL, 'dump.json'])
         print('load succeed')
-        options = [juicefs, 'config', self.meta_url]
-        options.extend(['--access-key', 'minioadmin', '--secret-key', 'minioadmin'])
-        if version.parse('-'.join(juicefs.split('-')[1:])) >= version.parse('1.0.0-rc2'):
-            options.append('--encrypt-secret')
-        run_jfs_cmd(options)
+        options = [juicefs, 'config', JuicefsMachine.META_URL]
+        if version.parse('-'.join(juicefs.split('-')[1:])) <= version.parse('1.0.0-rc1'):
+            # use the latest version to change secret-key because rc1 has a bug for secret-key
+            options[0] = JuicefsMachine.JFS_BINS[1]
+        storage = get_storage(juicefs, JuicefsMachine.META_URL)
+        if storage == 'minio':
+            run_jfs_cmd([JuicefsMachine.JFS_BINS[1], 'config', JuicefsMachine.META_URL, '--access-key', 'minioadmin', '--secret-key', 'minioadmin'])
+        elif storage == 'mysql':
+            run_jfs_cmd([JuicefsMachine.JFS_BINS[1], 'config', JuicefsMachine.META_URL, '--access-key', 'root', '--secret-key', 'root'])
+        elif storage == 'postgres':
+            run_jfs_cmd([JuicefsMachine.JFS_BINS[1], 'config', JuicefsMachine.META_URL, '--access-key', 'postgres', '--secret-key', 'postgres'])
+        
         os.remove('dump.json')
 
     @rule(juicefs=st.sampled_from(JFS_BINS))
     @precondition(lambda self: self.formatted)
     def fsck(self, juicefs):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
+        assume(juicefs in self.mounted_by)
         print('start fsck')
-        run_jfs_cmd([juicefs, 'fsck', self.meta_url])
+        run_jfs_cmd([juicefs, 'fsck', JuicefsMachine.META_URL])
         print('fsck succeed')
 
     @rule(juicefs=st.sampled_from(JFS_BINS),
@@ -378,7 +500,7 @@ class JuicefsMachine(RuleBasedStateMachine):
      threads=st.integers(min_value=1, max_value=100))
     @precondition(lambda self: self.mounted and False)
     def bench(self, juicefs, block_size, big_file_size, small_file_size, small_file_count, threads):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
         assert(os.path.exists(f'{JuicefsMachine.MOUNT_POINT}/.accesslog'))
         print('start bench')
         run_cmd(f'df | grep {JuicefsMachine.MOUNT_POINT}')
@@ -401,7 +523,8 @@ class JuicefsMachine(RuleBasedStateMachine):
         directory = st.booleans() )
     @precondition(lambda self: self.mounted)
     def warmup(self, juicefs, threads, background, from_file, directory):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
+        assume (self.greater_than_version_mounted(juicefs))
         assume(not is_readonly(f'{JuicefsMachine.MOUNT_POINT}'))
         assert(os.path.exists(f'{JuicefsMachine.MOUNT_POINT}/.accesslog'))
         print('start warmup')
@@ -427,9 +550,9 @@ class JuicefsMachine(RuleBasedStateMachine):
             if directory:
                 options.append(JuicefsMachine.MOUNT_POINT)
             else:
-                write_block(JuicefsMachine.MOUNT_POINT, f'{JuicefsMachine.MOUNT_POINT}/bigfile', 1048576, 100)
-                assert os.path.exists(f'{JuicefsMachine.MOUNT_POINT}/bigfile')
-                options.append(f'{JuicefsMachine.MOUNT_POINT}/bigfile')
+                write_block(JuicefsMachine.MOUNT_POINT, f'{JuicefsMachine.MOUNT_POINT}/file_to_warmup', 1048576, 100)
+                assert os.path.exists(f'{JuicefsMachine.MOUNT_POINT}/file_to_warmup')
+                options.append(f'{JuicefsMachine.MOUNT_POINT}/file_to_warmup')
                 
         run_jfs_cmd(options)
         # print(output)
@@ -444,9 +567,10 @@ class JuicefsMachine(RuleBasedStateMachine):
         threads=st.integers(min_value=1, max_value=100) )
     @precondition(lambda self: self.formatted)
     def gc(self, juicefs, compact, delete, threads):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
+        assume(juicefs in self.mounted_by)
         print('start gc')
-        options = [juicefs, 'gc', self.meta_url]
+        options = [juicefs, 'gc', JuicefsMachine.META_URL]
         if compact:
             options.append('--compact')
         if delete:
@@ -457,10 +581,9 @@ class JuicefsMachine(RuleBasedStateMachine):
         print('gc succeed')
 
 
-    valid_file_name = st.text(st.characters(max_codepoint=1000, blacklist_categories=('Cc', 'Cs')), min_size=2).map(lambda s: s.strip()).filter(lambda s: len(s) > 0)
     @rule(juicefs=st.sampled_from(JFS_BINS), 
-        get_timeout=st.integers(min_value=30, max_value=60), 
-        put_timeout=st.integers(min_value=30, max_value=60), 
+        get_timeout=st.integers(min_value=30, max_value=59), 
+        put_timeout=st.integers(min_value=30, max_value=59), 
         io_retries=st.integers(min_value=5, max_value=15), 
         max_uploads=st.integers(min_value=1, max_value=100), 
         max_deletes=st.integers(min_value=1, max_value=100), 
@@ -468,13 +591,13 @@ class JuicefsMachine(RuleBasedStateMachine):
         upload_limit=st.integers(min_value=0, max_value=1000), 
         download_limit=st.integers(min_value=0, max_value=1000), 
         prefetch=st.integers(min_value=0, max_value=100), 
-        writeback=st.booleans(),
-        upload_delay=st.integers(min_value=0, max_value=60), 
-        cache_dir=valid_file_name,
+        writeback=st.just(False),
+        upload_delay=st.sampled_from([0, 2]), 
+        cache_dir=st.sampled_from(['cache1', 'cache2']),
         cache_size=st.integers(min_value=0, max_value=1024000), 
         free_space_ratio=st.floats(min_value=0.1, max_value=0.5), 
         cache_partial_only=st.booleans(),
-        backup_meta=st.integers(min_value=300, max_value=3600),
+        backup_meta=st.integers(min_value=300, max_value=1000),
         heartbeat=st.integers(min_value=5, max_value=30), 
         read_only=st.booleans(),
         no_bgjob=st.booleans(),
@@ -482,7 +605,7 @@ class JuicefsMachine(RuleBasedStateMachine):
         attr_cache=st.integers(min_value=1, max_value=10), 
         entry_cache=st.integers(min_value=1, max_value=10), 
         dir_entry_cache=st.integers(min_value=1, max_value=10), 
-        access_log=valid_file_name,
+        access_log=st.sampled_from(['accesslog1', 'accesslog2']),
         no_banner=st.booleans(),
         multi_buckets=st.booleans(), 
         keep_etag=st.booleans(),
@@ -490,7 +613,7 @@ class JuicefsMachine(RuleBasedStateMachine):
         metrics=st.sampled_from(['127.0.0.1:9567', '127.0.0.1:9568']), 
         consul=st.sampled_from(['127.0.0.1:8500', '127.0.0.1:8501']), 
         no_usage_report=st.booleans(),
-        sub_dir=valid_file_name,
+        sub_dir=st.sampled_from(['dir1', 'dir2']),
         port=st.integers(min_value=9001, max_value=10000)
     )
     @precondition(lambda self: self.formatted )
@@ -498,12 +621,14 @@ class JuicefsMachine(RuleBasedStateMachine):
         download_limit, prefetch, writeback, upload_delay, cache_dir, cache_size, free_space_ratio, cache_partial_only, 
         backup_meta,heartbeat, read_only, no_bgjob, open_cache, attr_cache, entry_cache, dir_entry_cache, access_log, 
         no_banner, multi_buckets, keep_etag, umask, metrics, consul, no_usage_report, sub_dir, port):
-        assume (self.is_supported_version(juicefs))
-        assume(self.is_port_in_use(port))
+        assume (self.greater_than_version_formatted(juicefs))
+        assume(not is_port_in_use(port))
+        if JuicefsMachine.META_URL.startswith('badger://'):
+            assume(not self.mounted)
         print('start gateway')
         os.environ['MINIO_ROOT_USER'] = 'admin'
         os.environ['MINIO_ROOT_PASSWORD'] = '12345678'
-        options = [juicefs, 'gateway', self.meta_url, f'localhost:{port}']
+        options = [juicefs, 'gateway', JuicefsMachine.META_URL, f'localhost:{port}']
         
         options.extend(['--attr-cache', str(attr_cache)])
         options.extend(['--entry-cache', str(entry_cache)])
@@ -512,39 +637,51 @@ class JuicefsMachine(RuleBasedStateMachine):
         options.extend(['--put-timeout', str(put_timeout)])
         options.extend(['--io-retries', str(io_retries)])
         options.extend(['--max-uploads', str(max_uploads)])
-        options.extend(['--max-deletes', str(max_deletes)])
+        if run_cmd(f'{juicefs} gateway --help | grep max-deletes') == 0:
+            options.extend(['--max-deletes', str(max_deletes)])
         options.extend(['--buffer-size', str(buffer_size)])
         options.extend(['--upload-limit', str(upload_limit)])
         options.extend(['--download-limit', str(download_limit)])
         options.extend(['--prefetch', str(prefetch)])
         if writeback:
             options.append('--writeback')
-        options.extend(['--upload-delay', str(upload_delay)])
+        upload_delay = str(upload_delay)
+        if version.parse('-'.join(juicefs.split('-')[1:])) <= version.parse('1.0.0-beta2'):
+            upload_delay = upload_delay + 's'
+        options.extend(['--upload-delay', upload_delay])
         options.extend(['--cache-dir', os.path.expanduser(f'~/.juicefs/{cache_dir}')])
         options.extend(['--access-log', os.path.expanduser(f'~/.juicefs/{access_log}')])
         options.extend(['--cache-size', str(cache_size)])
         options.extend(['--free-space-ratio', str(free_space_ratio)])
         if cache_partial_only:
             options.append('--cache-partial-only')
-        options.extend(['--backup-meta', str(backup_meta)])
-        options.extend(['--heartbeat', str(heartbeat)])
+        backup_meta = str(backup_meta)
+        if version.parse('-'.join(juicefs.split('-')[1:])) <= version.parse('1.0.0-beta2'):
+            backup_meta = '1h0m0s'
+        if run_cmd(f'{juicefs} gateway --help | grep backup-meta') == 0:
+            options.extend(['--backup-meta', backup_meta])
+        if run_cmd(f'{juicefs} gateway --help | grep heartbeat') == 0:
+            options.extend(['--heartbeat', str(heartbeat)])
         if read_only:
             options.append('--read-only')
-        if no_bgjob:
+        if no_bgjob and run_cmd(f'{juicefs} gateway --help | grep no-bgjob') == 0:
             options.append('--no-bgjob')
         if no_banner:
             options.append('--no-banner')
-        if multi_buckets:
+        if multi_buckets and run_cmd(f'{juicefs} gateway --help | grep multi-buckets') == 0:
             options.append('--multi-buckets')
-        if keep_etag:
+        if keep_etag and run_cmd(f'{juicefs} gateway --help | grep keep-etag') == 0:
             options.append('--keep-etag')
-        options.extend(['--umask', umask])
+        if run_cmd(f'{juicefs} gateway --help | grep umask') == 0:
+            options.extend(['--umask', umask])
 
         options.extend(['--open-cache', str(open_cache)])
         print(f'TODO: subdir:{sub_dir}')
         # options.extend('--subdir', str(sub_dir))
-        options.extend(['--metrics', str(metrics)])
-        options.extend(['--consul', str(consul)])
+        if not is_port_in_use( int(metrics.split(':')[1])):
+            options.extend(['--metrics', str(metrics)])
+        # if run_cmd(f'{juicefs} mount --help | grep consul') == 0:
+        #     options.extend(['--consul', str(consul)])
         if no_usage_report:
             options.append('--no-usage-report')
 
@@ -558,24 +695,32 @@ class JuicefsMachine(RuleBasedStateMachine):
         port=st.integers(min_value=10001, max_value=11000)) 
     @precondition(lambda self: self.formatted )
     def webdav(self, juicefs, port):
-        assume (self.is_supported_version(juicefs))
+        assume (self.greater_than_version_formatted(juicefs))
+        assert version.parse('-'.join(juicefs.split('-')[1:])) >=  version.parse('-'.join(self.formatted_by.split('-')[1:]))
+        assume (not is_port_in_use(port))
+        if JuicefsMachine.META_URL.startswith('badger://'):
+            assume(not self.mounted)
         print('start webdav')
-        if self.is_port_in_use(port):
-            return 
-        options = [juicefs, 'webdav', self.meta_url, f'localhost:{port}']
+        
+        options = [juicefs, 'webdav', JuicefsMachine.META_URL, f'localhost:{port}']
         proc = subprocess.Popen(options)
         time.sleep(2.0)
         subprocess.Popen.kill(proc)
         print('webdav succeed')
 
-    def is_port_in_use(self, port: int) -> bool:
-        import socket
-        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-            return s.connect_ex(('localhost', port)) == 0
-
-    def is_supported_version(self, ver):
+    def greater_than_version_formatted(self, ver):
         return version.parse('-'.join(ver.split('-')[1:])) >=  version.parse('-'.join(self.formatted_by.split('-')[1:]))
 
+    def greater_than_version_dumped(self, ver):
+        return version.parse('-'.join(ver.split('-')[1:])) >=  version.parse('-'.join(self.dumped_by.split('-')[1:]))
+
+    def greater_than_version_mounted(self, ver):
+        for mounted_version in self.mounted_by:
+            if version.parse('-'.join(ver.split('-')[1:])) <  version.parse('-'.join(mounted_version.split('-')[1:])):
+                return False
+        return True
+
+
 TestJuiceFS = JuicefsMachine.TestCase
 
 if __name__ == "__main__":
diff --git a/.github/workflows/aitests.yml b/.github/workflows/aitests.yml
index e4e06661..ff08ea55 100644
--- a/.github/workflows/aitests.yml
+++ b/.github/workflows/aitests.yml
@@ -39,9 +39,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/aitests_limitcache.yml b/.github/workflows/aitests_limitcache.yml
index cc47af3b..e21ae279 100644
--- a/.github/workflows/aitests_limitcache.yml
+++ b/.github/workflows/aitests_limitcache.yml
@@ -39,9 +39,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/elastictest.yml b/.github/workflows/elastictest.yml
index 47f4e5b9..1d37a83d 100644
--- a/.github/workflows/elastictest.yml
+++ b/.github/workflows/elastictest.yml
@@ -39,9 +39,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
             
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/integrationtests.yml b/.github/workflows/integrationtests.yml
index 13eabd79..83239e0f 100644
--- a/.github/workflows/integrationtests.yml
+++ b/.github/workflows/integrationtests.yml
@@ -41,9 +41,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/mdtest.yml b/.github/workflows/mdtest.yml
index 677d38d0..70579010 100644
--- a/.github/workflows/mdtest.yml
+++ b/.github/workflows/mdtest.yml
@@ -45,9 +45,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/mysqltest_bigtable.yml b/.github/workflows/mysqltest_bigtable.yml
index 40802f8d..5a5969b0 100644
--- a/.github/workflows/mysqltest_bigtable.yml
+++ b/.github/workflows/mysqltest_bigtable.yml
@@ -38,9 +38,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/mysqltest_smalltable.yml b/.github/workflows/mysqltest_smalltable.yml
index 1b8da2a5..828f3b70 100644
--- a/.github/workflows/mysqltest_smalltable.yml
+++ b/.github/workflows/mysqltest_smalltable.yml
@@ -38,9 +38,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/pjdfstest.yml b/.github/workflows/pjdfstest.yml
index e3a34df7..f8e5cf32 100644
--- a/.github/workflows/pjdfstest.yml
+++ b/.github/workflows/pjdfstest.yml
@@ -45,9 +45,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: make juicefs
diff --git a/.github/workflows/sdktest.yml b/.github/workflows/sdktest.yml
index 0e723813..22ad1627 100644
--- a/.github/workflows/sdktest.yml
+++ b/.github/workflows/sdktest.yml
@@ -43,9 +43,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Build linux target
         run: |
diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml
index ba3913f1..379a99bc 100644
--- a/.github/workflows/unittests.yml
+++ b/.github/workflows/unittests.yml
@@ -54,9 +54,9 @@ jobs:
           path: |
             ~/.cache/go-build
             ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+          key: ${{ runner.os }}-go
           restore-keys: |
-            ${{ runner.os }}-go-
+            ${{ runner.os }}-go
 
       - name: Install Packages
         run: |
@@ -73,6 +73,8 @@ jobs:
           wget -O /home/travis/.m2/rclone-v1.57.0-linux-amd64.zip --no-check-certificate https://downloads.rclone.org/v1.57.0/rclone-v1.57.0-linux-amd64.zip
           wget -O /home/travis/.m2/litmus-0.13.tar.gz http://www.webdav.org/neon/litmus/litmus-0.13.tar.gz
           wget -O /home/travis/.m2/etcd-v3.5.2-linux-amd64.tar.gz  https://github.com/etcd-io/etcd/releases/download/v3.5.2/etcd-v3.5.2-linux-amd64.tar.gz
+          wget -O /home/travis/.m2/foundationdb-clients_6.3.23-1_amd64.deb https://github.com/apple/foundationdb/releases/download/6.3.23/foundationdb-clients_6.3.23-1_amd64.deb
+          wget -O /home/travis/.m2/foundationdb-server_6.3.23-1_amd64.deb https://github.com/apple/foundationdb/releases/download/6.3.23/foundationdb-server_6.3.23-1_amd64.deb
           unzip /home/travis/.m2/rclone-v1.57.0-linux-amd64.zip -d /home/travis/.m2/
           tar -zxvf /home/travis/.m2/litmus-0.13.tar.gz -C /home/travis/.m2/
           cd  /home/travis/.m2/litmus-0.13/ && ./configure && make && cd -
@@ -98,6 +100,7 @@ jobs:
           source /home/runner/.bash_profile
           tiup -v
           nohup tiup playground --mode tikv-slim >> output.log 2>&1 &
+          sudo dpkg -i /home/travis/.m2/foundationdb-clients_6.3.23-1_amd64.deb /home/travis/.m2/foundationdb-server_6.3.23-1_amd64.deb
           docker run -d -p 9000:9000 -p 9001:9001 -e "MINIO_ROOT_USER=testUser" -e "MINIO_ROOT_PASSWORD=testUserPassword" quay.io/minio/minio:RELEASE.2022-01-25T19-56-04Z server /data --console-address ":9001"
           go install github.com/minio/mc@RELEASE.2022-01-07T06-01-38Z && mc config host add local http://127.0.0.1:9000 testUser testUserPassword && mc mb local/testbucket
           nohup /home/travis/.m2/rclone-v1.57.0-linux-amd64/rclone serve webdav local --addr 127.0.0.1:9007 >> rclone.log 2>&1 &
@@ -109,7 +112,7 @@ jobs:
           chmod +x travis-setup-hdfs.sh
           sudo lsof -i :8020 || true
           ./travis-setup-hdfs.sh
-          for i in {2222,3306,5432,8020,9000,9007} ; do echo "lsof port:"$i;sudo lsof -i :$i;if [ $? != 0 ];then sleep 5; else continue; fi;sudo lsof -i :$i; if [ $? != 0 ];then echo "service not ready, port:"$i; exit 1;fi; done
+          for i in {2222,3306,5432,8020,9000,9007,4500} ; do echo "lsof port:"$i;sudo lsof -i :$i;if [ $? != 0 ];then sleep 5; else continue; fi;sudo lsof -i :$i; if [ $? != 0 ];then echo "service not ready, port:"$i; exit 1;fi; done
           sudo lsof -i :2379;if [ $? != 0 ];then echo "tikv is not ready";cat output.log;exit 1; fi
 
 
@@ -120,6 +123,8 @@ jobs:
           make test
           cat cov1.out >> coverage.txt
           cat cov2.out >> coverage.txt
+          make test.fdb
+          cat cov3.out >> coverage.txt
           sudo ./juicefs umount /jfs || sudo lsof /jfs && sudo ./juicefs umount --force /jfs || true
           sudo ./juicefs gc localhost
           sudo ./juicefs fsck localhost
diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go
new file mode 100644
index 00000000..c1d9532b
--- /dev/null
+++ b/cmd/doctor_test.go
@@ -0,0 +1,66 @@
+/*
+ * JuiceFS, Copyright 2022 Juicedata, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"fmt"
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDoctor(t *testing.T) {
+	mountTemp(t, nil, nil, nil)
+	defer umountTemp(t)
+	Convey("TestDoctor", t, func() {
+		Convey("Mount point does not exist", func() {
+			mp := "/jfs/test/mp"
+			So(Main([]string{"", "doctor", mp}), ShouldNotBeNil)
+		})
+
+		Convey("Directory is not a mount point", func() {
+			mp := "./"
+			So(Main([]string{"", "doctor", mp}), ShouldNotBeNil)
+		})
+
+	})
+
+	Convey("TestDoctor_OutDir", t, func() {
+		Convey("Specify a file as out dir", func() {
+			So(Main([]string{"", "doctor", "--out-dir", "./doctor_test.go", testMountPoint}), ShouldNotBeNil)
+		})
+	})
+
+	Convey("TestDoctor_LogArg", t, func() {
+		cases := []struct {
+			arg string
+			val string
+		}{
+			{"--log /var/log/jfs.log", "/var/log/jfs.log"},
+			{"--log=/var/log/jfs.log", "/var/log/jfs.log"},
+			{"--log   =   /var/log/jfs.log", "/var/log/jfs.log"},
+			{"--log=    /var/log/jfs.log", "/var/log/jfs.log"},
+			{"--log    =/var/log/jfs.log", "/var/log/jfs.log"},
+			{"--log      /var/log/jfs.log", "/var/log/jfs.log"},
+		}
+		for i, c := range cases {
+			Convey(fmt.Sprintf("valid log arg %d", i), func() {
+				So(logArg.FindStringSubmatch(c.arg)[2] == c.val, ShouldBeTrue)
+			})
+		}
+	})
+}
diff --git a/pkg/meta/base_test.go b/pkg/meta/base_test.go
index 557eaee9..af0b451b 100644
--- a/pkg/meta/base_test.go
+++ b/pkg/meta/base_test.go
@@ -30,7 +30,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/go-redis/redis/v8"
 	"github.com/juicedata/juicefs/pkg/utils"
+	"xorm.io/xorm"
 )
 
 func TestRedisClient(t *testing.T) {
@@ -76,6 +78,7 @@ func testMeta(t *testing.T, m Meta) {
 	testOpenCache(t, m)
 	base.conf.CaseInsensi = true
 	testCaseIncensi(t, m)
+	testCheckAndRepair(t, m)
 	base.conf.ReadOnly = true
 	testReadOnly(t, m)
 }
@@ -1584,3 +1587,145 @@ func testAttrFlags(t *testing.T, m Meta) {
 		t.Fatalf("copy_file_range f: %s", st)
 	}
 }
+
+func setAttr(t *testing.T, m Meta, inode Ino, attr *Attr) {
+	var err error
+	switch m := m.(type) {
+	case *redisMeta:
+		err = m.txn(Background, func(tx *redis.Tx) error {
+			return tx.Set(Background, m.inodeKey(inode), m.marshal(attr), 0).Err()
+		}, m.inodeKey(inode))
+	case *dbMeta:
+		err = m.txn(func(s *xorm.Session) error {
+			_, err = s.ID(inode).AllCols().Update(&node{
+				Inode:  inode,
+				Type:   attr.Typ,
+				Flags:  attr.Flags,
+				Mode:   attr.Mode,
+				Uid:    attr.Uid,
+				Gid:    attr.Gid,
+				Mtime:  attr.Mtime * 1e6,
+				Ctime:  attr.Ctime * 1e6,
+				Atime:  attr.Atime * 1e6,
+				Nlink:  attr.Nlink,
+				Length: attr.Length,
+				Rdev:   attr.Rdev,
+				Parent: attr.Parent,
+			})
+			return err
+		})
+	case *kvMeta:
+		err = m.txn(func(tx kvTxn) error {
+			tx.set(m.inodeKey(inode), m.marshal(attr))
+			return nil
+		})
+	}
+	if err != nil {
+		t.Fatalf("setAttr: %v", err)
+	}
+}
+
+func testCheckAndRepair(t *testing.T, m Meta) {
+	var checkInode, d1Inode, d2Inode, d3Inode, d4Inode Ino
+	dirAttr := &Attr{Mode: 0644, Full: true, Typ: TypeDirectory, Nlink: 3}
+	if st := m.Mkdir(Background, RootInode, "check", 0640, 022, 0, &checkInode, dirAttr); st != 0 {
+		t.Fatalf("mkdir: %s", st)
+	}
+	if st := m.Mkdir(Background, checkInode, "d1", 0640, 022, 0, &d1Inode, dirAttr); st != 0 {
+		t.Fatalf("mkdir: %s", st)
+	}
+	if st := m.Mkdir(Background, d1Inode, "d2", 0640, 022, 0, &d2Inode, dirAttr); st != 0 {
+		t.Fatalf("mkdir: %s", st)
+	}
+	if st := m.Mkdir(Background, d2Inode, "d3", 0640, 022, 0, &d3Inode, dirAttr); st != 0 {
+		t.Fatalf("mkdir: %s", st)
+	}
+	if st := m.Mkdir(Background, d3Inode, "d4", 0640, 022, 0, &d4Inode, dirAttr); st != 0 {
+		t.Fatalf("mkdir: %s", st)
+	}
+
+	if st := m.GetAttr(Background, checkInode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	dirAttr.Nlink = 0
+	setAttr(t, m, checkInode, dirAttr)
+
+	if st := m.GetAttr(Background, d1Inode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	dirAttr.Nlink = 0
+	setAttr(t, m, d1Inode, dirAttr)
+
+	if st := m.GetAttr(Background, d2Inode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	dirAttr.Nlink = 0
+	setAttr(t, m, d2Inode, dirAttr)
+
+	if st := m.GetAttr(Background, d3Inode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	dirAttr.Nlink = 0
+	setAttr(t, m, d3Inode, dirAttr)
+
+	if st := m.GetAttr(Background, d4Inode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	dirAttr.Full = false
+	dirAttr.Nlink = 0
+	setAttr(t, m, d4Inode, dirAttr)
+
+	if st := m.Check(Background, "/check", false, false); st != 0 {
+		t.Fatalf("check: %s", st)
+	}
+	if st := m.GetAttr(Background, checkInode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	if dirAttr.Nlink != 0 {
+		t.Fatalf("checkInode nlink should is 0 now: %d", dirAttr.Nlink)
+	}
+
+	if st := m.Check(Background, "/check", true, false); st != 0 {
+		t.Fatalf("check: %s", st)
+	}
+	if st := m.GetAttr(Background, checkInode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	if dirAttr.Nlink != 3 || dirAttr.Parent != RootInode {
+		t.Fatalf("checkInode nlink should is 3 now: %d", dirAttr.Nlink)
+	}
+
+	if st := m.Check(Background, "/check/d1/d2", true, false); st != 0 {
+		t.Fatalf("check: %s", st)
+	}
+	if st := m.GetAttr(Background, d2Inode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	if dirAttr.Nlink != 3 || dirAttr.Parent != d1Inode {
+		t.Fatalf("d2Inode nlink should is 3 now: %d", dirAttr.Nlink)
+	}
+	if st := m.GetAttr(Background, d1Inode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	if dirAttr.Nlink != 0 || dirAttr.Parent != checkInode {
+		t.Fatalf("d1Inode nlink should is 0 now: %d", dirAttr.Nlink)
+	}
+
+	if st := m.Check(Background, "/", true, true); st != 0 {
+		t.Fatalf("check: %s", st)
+	}
+	for _, ino := range []Ino{checkInode, d1Inode, d2Inode, d3Inode} {
+		if st := m.GetAttr(Background, ino, dirAttr); st != 0 {
+			t.Fatalf("getattr: %s", st)
+		}
+		if !dirAttr.Full || dirAttr.Nlink != 3 {
+			t.Fatalf("nlink should is 3 now: %d", dirAttr.Nlink)
+		}
+	}
+	if st := m.GetAttr(Background, d4Inode, dirAttr); st != 0 {
+		t.Fatalf("getattr: %s", st)
+	}
+	if !dirAttr.Full || dirAttr.Nlink != 2 || dirAttr.Parent != d3Inode {
+		t.Fatalf("d4Inode attr: %+v", *dirAttr)
+	}
+}
diff --git a/pkg/meta/tkv_fdb_test.go b/pkg/meta/tkv_fdb_test.go
new file mode 100644
index 00000000..dbd32fc5
--- /dev/null
+++ b/pkg/meta/tkv_fdb_test.go
@@ -0,0 +1,41 @@
+//go:build fdb
+// +build fdb
+
+/*
+ * JuiceFS, Copyright 2022 Juicedata, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//nolint:errcheck
+package meta
+
+import (
+	"testing"
+)
+
+func TestFdbClient(t *testing.T) {
+	m, err := newKVMeta("fdb", "/etc/foundationdb/fdb.cluster?prefix=test2", &Config{})
+	if err != nil {
+		t.Fatalf("create meta: %s", err)
+	}
+	testMeta(t, m)
+}
+
+func TestFdb(t *testing.T) {
+	c, err := newFdbClient("/etc/foundationdb/fdb.cluster?prefix=test1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	testTKV(t, c)
+}
diff --git a/pkg/sync/sync_test.go b/pkg/sync/sync_test.go
index 7606783f..e64d301d 100644
--- a/pkg/sync/sync_test.go
+++ b/pkg/sync/sync_test.go
@@ -131,7 +131,7 @@ func TestSync(t *testing.T) {
 	if err := Sync(a, b, config); err != nil {
 		t.Fatalf("sync: %s", err)
 	}
-	// No copy occured
+	// No copy occurred
 	if c := copied.Current(); c != 0 {
 		t.Fatalf("should copy 0 keys, but got %d", c)
 	}