Repository: juicedata/juicefs
Pull request #2257 (2022-06-17 03:02:36)

CI:fix clickhouse test err
add a local directory test 


Patch:
diff --git a/.github/actions/gateway/action.yml b/.github/actions/gateway/action.yml
index 3ce20ef8..180424af 100644
--- a/.github/actions/gateway/action.yml
+++ b/.github/actions/gateway/action.yml
@@ -54,6 +54,7 @@ runs:
       shell: bash
 
     - name: Init
+      shell: bash
       run: | 
         set -x
         mp=${{inputs.mount_point}}
@@ -66,19 +67,12 @@ runs:
         fi
         test -d /var/jfs/$volume && rm -rf /var/jfs/$volume || true
         echo finished
+      
+    - name: create database
       shell: bash
-
-    - name: Test
-      run: |
+      run: | 
         meta_url=${{inputs.meta_url}}
-        file_size=${{inputs.file_size}}
-        file_count=${{inputs.file_count}}
-        threads=${{inputs.threads}}
         isolation_level="${{inputs.isolation_level}}"
-        mp=${{inputs.mount_point}}
-        volume=${{inputs.volume_name}}
-        echo meta_url is: $meta_url, file_size is $file_size, file_count is $file_count, isolation_level is $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}')
@@ -94,13 +88,19 @@ runs:
           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
-        
+        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 &
+          ./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 &
@@ -109,14 +109,24 @@ runs:
           #sudo rm /etc/nginx/sites-enabled/default
           #sudo systemctl restart nginx
         fi
-        set -x 
+      
+    - 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        
+        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 &
@@ -124,8 +134,6 @@ runs:
         ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:8080/$volume/data/ --no-https -p $threads &
         pids+=($!)
         wait "${pids[@]}"
-        # mkdir emptydir
-        # ./juicefs sync emptydir/ s3://minioadmin:minioadmin@localhost:8080/$volume/ --delete-dst --no-https -p $threads
         rm -rf $HOME/.juicefs/cache/ || true
         # ./mc alias set minio http://localhost:9000 minioadmin minioadmin --api S3v4
         # ./mc mb minio/$volume
@@ -133,11 +141,36 @@ runs:
         end=`date +%s`
         time=$((end-start))
         echo time cost is: $time second
-        # stop juicefs gateway
         killall juicefs 
         sleep 3
         ./juicefs mount -d $meta_url $mp
         diff -ur data/ $mp/data/ 
         echo "diff succeed"
-        rm $mp/data/ -rf
+        ./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
\ No newline at end of file
diff --git a/.github/actions/vdbench/action.yml b/.github/actions/vdbench/action.yml
new file mode 100644
index 00000000..c1d74e9f
--- /dev/null
+++ b/.github/actions/vdbench/action.yml
@@ -0,0 +1,102 @@
+name: 'Vdbench Action'
+description: 'Vdbench Action'
+inputs:
+  meta_url:  
+    description: 'meta url'
+    required: true
+    default: 'redis://localhost/1'
+  storage:  
+    description: 'storage'
+    required: true
+    default: 'minio'
+  bucket:  
+    description: 'bucket'
+    required: true
+    default: 'http://localhost:9000/vdbench-test'
+  access_key:  
+    description: 'access_key'
+    required: true
+    default: 'minioadmin'
+  secret_key:  
+    description: 'secret_key'
+    required: true
+    default: 'minioadmin'
+  volume_name: 
+    description: 'volume name'
+    required: true
+    default: 'vdbench-test'
+  mount_point: 
+    description: 'mount_point'
+    required: true
+    default: '/tmp/vdbench'
+  version_count:
+    description: 'how many version to test besides dev'
+    required: true
+    default: 1
+  mysql_password:
+    description: 'mysql password'
+    required: true
+    default: ''
+  PYROSCOPE_AUTH_TOKEN:
+    description: 'PYROSCOPE_AUTH_TOKEN'
+    required: true
+    default: ''
+
+runs:
+  using: "composite"
+  steps:
+    - name: Install tools
+      shell: bash
+      run: |
+        wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
+        chmod +x mc 
+        wget -q https://s.juicefs.com/static/bench/vdbench50407.zip
+        unzip vdbench50407.zip -d vdbench
+        [[ ${{ inputs.meta_url }} == redis://* ]] && docker run --name my-redis -p 6379:6379 -d redis
+        [[ ${{ inputs.storage }} == minio ]] && 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: Build linux target
+      shell: bash
+      run: |
+        make juicefs
+      
+    - name: vdbench-big 
+      shell: bash
+      run: | 
+        ./juicefs format ${{inputs.meta_url}} vdbench-big
+        ./juicefs mount -d ${{inputs.meta_url}} ${{ inputs.mount_point }}
+        vdbench/vdbench -f ./.github/workflows/resources/vdbench_big_file.conf
+
+    - uses: actions/upload-artifact@v3
+      with:
+        name: output-big
+        path: output
+
+    - name: clean
+      shell: bash
+      run: |
+        rm /var/jfsCache/ -rf || true
+        if [ -d ${{ inputs.mount_point }} ]; then
+          ./juicefs umount ${{ inputs.mount_point }} || true
+        fi
+        uuid=$(./juicefs status ${{ inputs.meta_url }} | grep UUID | cut -d '"' -f 4) || true
+        if [ -n "$uuid" ];then
+          sudo ./juicefs destroy --force ${{ inputs.meta_url }} $uuid
+        fi
+
+    - name: vdbench-small
+      shell: bash
+      run: |
+        ./juicefs format ${{inputs.meta_url}} vdbench-small
+        ./juicefs mount -d ${{inputs.meta_url}} ${{ inputs.mount_point }}        
+        vdbench/vdbench -f ./.github/workflows/resources/vdbench_small_file.conf
+
+    - uses: actions/upload-artifact@v3
+      with:
+        name: output-small
+        path: output
diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
index a2ca50b5..1a7b56d6 100644
--- a/.github/workflows/config.yml
+++ b/.github/workflows/config.yml
@@ -3,7 +3,6 @@ name: "config-test"
 on:
   push:
     branches: 
-      - main
       - release-**
     paths-ignore:
       - 'docs/**'
@@ -11,7 +10,6 @@ on:
   pull_request:
     #The branches below must be a subset of the branches above
     branches: 
-      - main
       - release-**
     paths-ignore:
       - 'docs/**'
@@ -35,28 +33,24 @@ jobs:
           # Maps port 6379 on service container to the host
           - 6379:6379
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v2
-        with:
-          go-version: '1.17.x'
-
       - name: Checkout
         uses: actions/checkout@v2
         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     
+          sleep 5
+          
       - name: Build linux target
         run: make juicefs
 
-      - 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     
-                     
       - name: Download tool
         run: | 
           wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
diff --git a/.github/workflows/encoding_mysql.yml b/.github/workflows/encoding_mysql.yml
index 9b4da4bf..e1ea28b3 100644
--- a/.github/workflows/encoding_mysql.yml
+++ b/.github/workflows/encoding_mysql.yml
@@ -3,7 +3,6 @@ name: "mysql-encoding-test"
 on:
   push:
     branches: 
-      - main
       - release-**
     paths-ignore:
       - 'docs/**'
@@ -11,7 +10,6 @@ on:
   pull_request:
     #The branches below must be a subset of the branches above
     branches: 
-      - main
       - release-**
     paths-ignore:
       - 'docs/**'
diff --git a/.github/workflows/fio_benchmark.yml b/.github/workflows/fio_benchmark.yml
index b3875a9d..fc4eeb5d 100644
--- a/.github/workflows/fio_benchmark.yml
+++ b/.github/workflows/fio_benchmark.yml
@@ -1,13 +1,6 @@
 name: "fio-benchmark"
 
 on:
-  push:
-    branches: 
-      - release-**
-  pull_request:
-    #The branches below must be a subset of the branches above
-    branches: 
-      - release-**
   schedule:
     - cron:  '0 0 * * *'
   workflow_dispatch:
diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml
index 58b2119a..27ce8da9 100644
--- a/.github/workflows/gateway.yml
+++ b/.github/workflows/gateway.yml
@@ -38,7 +38,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "redis://localhost/1"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 5
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -66,7 +66,7 @@ jobs:
         with:
           meta_url: "redis://localhost/1"
           file_size: "8K"
-          file_count: 10000
+          file_count: 2500
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1    
@@ -81,7 +81,7 @@ jobs:
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
           file_size: "8K"
-          file_count: 10000
+          file_count: 2500
           isolation_level: "read uncommitted"    
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -97,7 +97,7 @@ jobs:
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
           file_size: "8K"
-          file_count: 10000
+          file_count: 2500
           isolation_level: "read committed"    
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -113,7 +113,7 @@ jobs:
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
           file_size: "8K"
-          file_count: 10000
+          file_count: 2500
           isolation_level: "repeatable read"   
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -129,7 +129,7 @@ jobs:
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
           file_size: "8K"
-          file_count: 10000
+          file_count: 2500
           isolation_level: "serializable"     
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -144,7 +144,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 5
           isolation_level: "read uncommitted"
       - name: Setup upterm session
@@ -160,7 +160,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 5
           isolation_level: "read committed"
       - name: Setup upterm session
@@ -176,7 +176,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 5
           isolation_level: "repeatable read"
       - name: Setup upterm session
@@ -192,7 +192,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 5
           isolation_level: "serializable"
       - name: Setup upterm session
@@ -210,7 +210,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/test"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 5
           isolation_level: "read committed"
       - name: Setup upterm session
@@ -226,7 +226,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "badger://badger-data"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 1
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -242,7 +242,7 @@ jobs:
         with:
           meta_url: "badger://badger-data"
           file_size: "8K"
-          file_count: 10000
+          file_count: 2500
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1    
@@ -258,7 +258,7 @@ jobs:
         with:
           meta_url: "sqlite3://test.db"
           file_size: "8K"
-          file_count: 10000    
+          file_count: 2500    
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1    
@@ -272,8 +272,8 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "sqlite3://test.db"
-          file_size: "200M"
-          file_count: 10    
+          file_size: "100M"
+          file_count: 5    
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1    
@@ -300,7 +300,7 @@ jobs:
         with:
           meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
           file_size: "8K"
-          file_count: 10000
+          file_count: 2500
           threads: 10    
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -327,7 +327,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 3
           threads: 10
           isolation_level: "read committed"
@@ -357,8 +357,8 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
-          file_size: "200M"
-          file_count: 10
+          file_size: "100M"
+          file_count: 5
           threads: 10
           isolation_level: "repeatable read"
 
@@ -387,8 +387,8 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "postgres://postgres:postgres@127.0.0.1:5432/test?sslmode=disable"
-          file_size: "200M"
-          file_count: 10
+          file_size: "100M"
+          file_count: 5
           threads: 10
           isolation_level: "serializable"
 
@@ -411,8 +411,8 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "mysql://root:@\\(127.0.0.1:4000\\)/test"
-          file_size: "200M"
-          file_count: 10
+          file_size: "100M"
+          file_count: 5
           isolation_level: "read committed"
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -432,8 +432,8 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "mysql://root:@\\(127.0.0.1:4000\\)/test"
-          file_size: "200M"
-          file_count: 10
+          file_size: "100M"
+          file_count: 5
           isolation_level: "repeatable read"
       - name: Setup upterm session
         if: ${{ failure() }}
@@ -449,7 +449,7 @@ jobs:
         with:
           meta_url: "etcd://localhost:2379/jfs"
           file_size: "8K"
-          file_count: 10000    
+          file_count: 2500    
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1    
@@ -464,7 +464,7 @@ jobs:
         with:
           meta_url: "etcd://localhost:2379/jfs"
           file_size: "200M"
-          file_count: 10    
+          file_count: 5    
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1    
@@ -540,7 +540,7 @@ jobs:
         with:
           meta_url: "tikv://127.0.0.1:2379"
           file_size: "8K"
-          file_count: 10000
+          file_count: 2500
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1
@@ -561,7 +561,7 @@ jobs:
         uses: ./.github/actions/gateway
         with:
           meta_url: "tikv://127.0.0.1:2379"
-          file_size: "200M"
+          file_size: "100M"
           file_count: 5
       - name: Setup upterm session
         if: ${{ failure() }}
diff --git a/.github/workflows/gc.yml b/.github/workflows/gc.yml
index 99e337b1..cc3b6056 100644
--- a/.github/workflows/gc.yml
+++ b/.github/workflows/gc.yml
@@ -3,14 +3,12 @@ name: "gc-test"
 on:
   push:
     branches:
-      - 'main'
       - 'release-**'
     paths-ignore:
       - 'docs/**'
   pull_request:
     #The branches below must be a subset of the branches above
     branches:
-      - 'main'
       - 'release-**'
     paths-ignore:
       - 'docs/**'
diff --git a/.github/workflows/list.yml b/.github/workflows/list.yml
index 71d5e6cc..dc3040da 100644
--- a/.github/workflows/list.yml
+++ b/.github/workflows/list.yml
@@ -1,17 +1,17 @@
 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/**'
+#  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:
diff --git a/.github/workflows/load.yml b/.github/workflows/load.yml
index c0b208a0..3b42bce4 100644
--- a/.github/workflows/load.yml
+++ b/.github/workflows/load.yml
@@ -1,17 +1,5 @@
 name: "load-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 3 * * *'
   workflow_dispatch:
@@ -39,13 +27,13 @@ jobs:
           fetch-depth: 1
       - name: download data
         run: |
-          wget -q https://s.juicefs.com/static/meta_with_4M_Empty_files.json.gz
-          gzip -dk  meta_with_4M_Empty_files.json.gz
+          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: "meta_with_4M_Empty_files.json"            
+          load_file: "2M_emtpy_files.dump"            
           
   load-to-mysql:
     runs-on: ubuntu-latest
@@ -53,8 +41,8 @@ jobs:
       - uses: actions/checkout@v3
       - name: download data
         run: |
-          wget -q https://s.juicefs.com/static/meta_with_4M_Empty_files.json.gz
-          gzip -dk  meta_with_4M_Empty_files.json.gz
+          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
@@ -63,7 +51,7 @@ jobs:
         uses: ./.github/actions/load
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/sync_test" 
-          load_file: "meta_with_4M_Empty_files.json"      
+          load_file: "2M_emtpy_files.dump"      
             
   load-to-redis:
     runs-on: ubuntu-latest
@@ -84,13 +72,13 @@ jobs:
       - uses: actions/checkout@v3
       - name: download data
         run: |
-          wget -q https://s.juicefs.com/static/meta_with_4M_Empty_files.json.gz
-          gzip -dk  meta_with_4M_Empty_files.json.gz
+          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: "meta_with_4M_Empty_files.json"      
+          load_file: "2M_emtpy_files.dump"      
 
   load-to-sqlite3:
     runs-on: ubuntu-latest   
@@ -98,13 +86,13 @@ jobs:
       - uses: actions/checkout@v3
       - name: download data
         run: |
-          wget -q https://s.juicefs.com/static/meta_with_4M_Empty_files.json.gz
-          gzip -dk  meta_with_4M_Empty_files.json.gz
+          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: sqlite3://sync-test.db
-          load_file: "meta_with_4M_Empty_files.json"      
+          load_file: "2M_emtpy_files.dump"      
     
   load-to-tikv:
     runs-on: ubuntu-latest
@@ -112,8 +100,8 @@ jobs:
       - uses: actions/checkout@v3
       - name: download data
         run: |
-          wget -q https://s.juicefs.com/static/meta_with_4M_Empty_files.json.gz
-          gzip -dk  meta_with_4M_Empty_files.json.gz
+          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
@@ -125,7 +113,7 @@ jobs:
         uses: ./.github/actions/load
         with:
           meta_url: "tikv://127.0.0.1:2379/load_test"
-          load_file: "meta_with_4M_Empty_files.json"
+          load_file: "2M_emtpy_files.dump"
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1
@@ -136,14 +124,14 @@ jobs:
       - uses: actions/checkout@v3
       - name: download data
         run: |
-          wget -q https://s.juicefs.com/static/meta_with_4M_Empty_files.json.gz
-          gzip -dk  meta_with_4M_Empty_files.json.gz
+          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
         with:
           meta_url: "badger://load_test"
-          load_file: "meta_with_4M_Empty_files.json"    
+          load_file: "2M_emtpy_files.dump"    
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1
@@ -154,8 +142,8 @@ jobs:
       - uses: actions/checkout@v3
       - name: download data
         run: |
-          wget -q https://s.juicefs.com/static/meta_with_4M_Empty_files.json.gz
-          gzip -dk  meta_with_4M_Empty_files.json.gz
+          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
@@ -163,7 +151,7 @@ jobs:
         uses: ./.github/actions/load
         with:
           meta_url: "mysql://root:root@\\(127.0.0.1\\)/load_test"
-          load_file: "meta_with_4M_Empty_files.json"   
+          load_file: "2M_emtpy_files.dump"   
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1
@@ -174,8 +162,8 @@ jobs:
       - uses: actions/checkout@v3
       - name: download data
         run: |
-          wget -q https://s.juicefs.com/static/meta_with_4M_Empty_files.json.gz
-          gzip -dk  meta_with_4M_Empty_files.json.gz
+          wget -q https://s.juicefs.com/static/bench/2M_emtpy_files.dump.gz
+          gzip -dk  2M_emtpy_files.dump.gz
       - name: install 
         run: | 
           curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
@@ -187,7 +175,7 @@ jobs:
         uses: ./.github/actions/load
         with:
           meta_url: "mysql://root:@\\(127.0.0.1:4000\\)/load_test"
-          load_file: "meta_with_4M_Empty_files.json"   
+          load_file: "2M_emtpy_files.dump"   
       - name: Setup upterm session
         if: ${{ failure() }}
         uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/resources/vdbench_big_file.conf b/.github/workflows/resources/vdbench_big_file.conf
new file mode 100644
index 00000000..86267c45
--- /dev/null
+++ b/.github/workflows/resources/vdbench_big_file.conf
@@ -0,0 +1,7 @@
+fsd=fsd1,anchor=/tmp/vdbench/vdbench-big,depth=1,width=1,files=4,size=1g,openflags=o_direct
+
+fwd=fwd1,fsd=fsd1,operation=write,xfersize=1m,fileio=sequential,fileselect=sequential,threads=4
+fwd=fwd2,fsd=fsd1,operation=read,xfersize=1m,fileio=sequential,fileselect=sequential,threads=4
+
+rd=rd1,fwd=fwd1,fwdrate=max,format=restart,elapsed=10,interval=1
+rd=rd2,fwd=fwd2,fwdrate=max,format=restart,elapsed=10,interval=1
\ No newline at end of file
diff --git a/.github/workflows/resources/vdbench_small_file.conf b/.github/workflows/resources/vdbench_small_file.conf
new file mode 100644
index 00000000..55411292
--- /dev/null
+++ b/.github/workflows/resources/vdbench_small_file.conf
@@ -0,0 +1,9 @@
+fsd=fsd1,anchor=/tmp/vdbench/vdbench-small,depth=4,width=10,files=10,size=128k,openflags=o_direct
+
+fwd=fwd1,fsd=fsd1,operation=write,xfersize=128k,fileio=random,fileselect=random,threads=4
+fwd=fwd2,fsd=fsd1,operation=read,xfersize=128k,fileio=random,fileselect=random,threads=4
+fwd=fwd3,fsd=fsd1,rdpct=70,xfersize=128k,fileio=random,fileselect=random,threads=4
+
+rd=rd1,fwd=fwd1,fwdrate=max,format=restart,elapsed=60,interval=1
+rd=rd2,fwd=fwd2,fwdrate=max,format=restart,elapsed=60,interval=1
+rd=rd3,fwd=fwd3,fwdrate=max,format=restart,elapsed=60,interval=1
\ No newline at end of file
diff --git a/.github/workflows/rmdirectory.yml b/.github/workflows/rmdirectory.yml
deleted file mode 100644
index c796d071..00000000
--- a/.github/workflows/rmdirectory.yml
+++ /dev/null
@@ -1,242 +0,0 @@
-name: "rm-directory"
-
-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-directory:
-    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:
-          - 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_4M_EMPTY_FILE_RM.json.gz* >META_4M_EMPTY_FILE_RM.json.gz
-          gzip -dk  META_4M_EMPTY_FILE_RM.json.gz
-          ls -lh
-          mv META_4M_EMPTY_FILE_RM.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_4M_EMPTY_FILE_RM.json"
-      - name: Rm 400w Files in a Directory of 400 Children Directory
-        run: |
-          cd /tmp/juicefs-load-test/
-          sudo chmod 777 mkdir
-          echo "rm 400w files in a directory of 400 children directory"
-          git clone https://github.com/juicedata/juicefs.git
-          cd juicefs
-          make
-          sudo ./juicefs rmr ../mkdir/
-          cd ..
-          ls -lah |wc -l | grep 8
-      - name: Log
-        if: ${{ failure() }}
-        run: |
-          sudo tail -n 100 /var/log/juicefs.log
-      - 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() }}
-        uses: lhotari/action-upterm@v1
-
-
-  mysql-directory:
-    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_4M_EMPTY_FILE_RM.json.gz* >META_4M_EMPTY_FILE_RM.json.gz
-          gzip -dk  META_4M_EMPTY_FILE_RM.json.gz
-          ls -lh
-          mv META_4M_EMPTY_FILE_RM.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_4M_EMPTY_FILE_RM.json"
-      - name: Rm 400w Files in a Directory of 400 Children Directory
-        run: |
-          cd /tmp/juicefs-load-test/
-          sudo chmod 777 mkdir
-          echo "rm 400w files in a directory of 400 children directory"
-          git clone https://github.com/juicedata/juicefs.git
-          cd juicefs
-          make
-          sudo ./juicefs rmr ../mkdir/
-          cd ..
-          ls -lah |wc -l | grep 8
-      - name: Log
-        if: ${{ failure() }}
-        run: |
-          sudo tail -n 100 /var/log/juicefs.log
-      - 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() }}
-        uses: lhotari/action-upterm@v1
-
-
-  redis-directory:
-    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
-        with:
-          fetch-depth: 1
-      - name: Init
-        run: |
-          sudo chmod 777 /var
-          git clone https://github.com/sanwan/actionfile.git
-          cd actionfile
-          cat META_4M_EMPTY_FILE_RM.json.gz* >META_4M_EMPTY_FILE_RM.json.gz
-          gzip -dk  META_4M_EMPTY_FILE_RM.json.gz
-          ls -lh
-          mv META_4M_EMPTY_FILE_RM.json ../
-          cd ../
-          ls -lh
-      - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: redis://127.0.0.1:6379/1
-          load_file: "META_4M_EMPTY_FILE_RM.json"
-      - name: Rm 400w Files in a Directory of 400 Children Directory
-        run: |
-          cd /tmp/juicefs-load-test/
-          sudo chmod 777 mkdir
-          echo "rm 400w files in a directory of 400 children directory"
-          git clone https://github.com/juicedata/juicefs.git
-          cd juicefs
-          make
-          sudo ./juicefs rmr ../mkdir/
-          cd ..
-          ls -lah |wc -l | grep 8
-      - name: Log
-        if: ${{ failure() }}
-        run: |
-          sudo tail -n 100 /var/log/juicefs.log
-      - 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() }}
-        uses: lhotari/action-upterm@v1
-
-  sqlite3-directory:
-    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
-          git clone https://github.com/sanwan/actionfile.git
-          cd actionfile
-          cat META_4M_EMPTY_FILE_RM.json.gz* >META_4M_EMPTY_FILE_RM.json.gz
-          gzip -dk  META_4M_EMPTY_FILE_RM.json.gz
-          ls -lh
-          mv META_4M_EMPTY_FILE_RM.json ../
-          cd ../
-          ls -lh
-      - name: Load
-        uses: ./.github/actions/load
-        with:
-          meta_url: sqlite3://sync-test.db
-          load_file: "META_4M_EMPTY_FILE_RM.json"
-      - name: Rm 400w Files in a Directory of 400 Children Directory
-        run: |
-          cd /tmp/juicefs-load-test/
-          sudo chmod 777 mkdir
-          echo "rm 400w files in a directory of 400 children directory"
-          git clone https://github.com/juicedata/juicefs.git
-          cd juicefs
-          make
-          sudo ./juicefs rmr ../mkdir/
-          cd ..
-          ls -lah |wc -l | grep 8
-      - name: Log
-        if: ${{ failure() }}
-        run: |
-          sudo tail -n 100 /var/log/juicefs.log
-      - 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() }}
-        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/rmfiles.yml b/.github/workflows/rmfiles.yml
index 112832b7..0badbac8 100644
--- a/.github/workflows/rmfiles.yml
+++ b/.github/workflows/rmfiles.yml
@@ -1,17 +1,17 @@
 name: "rm-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/**'
+#  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:
@@ -42,38 +42,25 @@ jobs:
           sudo chmod 777 /var
           git clone https://github.com/sanwan/actionfile.git
           cd actionfile
-          cat META_4M_EMPTY_FILE_RM.json.gz* >META_4M_EMPTY_FILE_RM.json.gz
-          gzip -dk  META_4M_EMPTY_FILE_RM.json.gz
+          cat META_50W_EMPTY_FILE.json* >META_50W_EMPTY_FILE.json
           ls -lh
-          mv META_4M_EMPTY_FILE_RM.json ../
+          mv META_50W_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_4M_EMPTY_FILE_RM.json"
-      - name: Rm 100w Files in Depth of 1 Directory
+          load_file: "META_50W_EMPTY_FILE.json"
+      - name: Rm 50w Files
         run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          sudo chmod 777 ../mkdir
-          echo "rm 100w files in depth of 1 directory"
+          cd /tmp/juicefs-load-test/
+          sudo chmod 777 createfile/
+          echo "rm 50w files"
           git clone https://github.com/juicedata/juicefs.git
           cd juicefs
           make
-          sudo ./juicefs rmr ../aaa/
-      - name: Rm 100w Files in Depth of 10 Directory
-        run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          echo "rm 100w files in depth of 10 directory"
-          cd juicefs
-          sudo ./juicefs rmr ../aaa0/
-      - name: Rm 200w Files in Depth of 20 Directory
-        run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          echo "rm 200w files in depth of 20 directory"
-          cd juicefs
-          sudo ./juicefs rmr ../aaa2/
+          sudo ./juicefs rmr ../createfile/
       - name: Log
         if: ${{ failure() }}
         run: |
@@ -103,38 +90,25 @@ jobs:
           sudo /etc/init.d/mysql start
           git clone https://github.com/sanwan/actionfile.git
           cd actionfile
-          cat META_4M_EMPTY_FILE_RM.json.gz* >META_4M_EMPTY_FILE_RM.json.gz
-          gzip -dk  META_4M_EMPTY_FILE_RM.json.gz
+          cat META_50W_EMPTY_FILE.json* >META_50W_EMPTY_FILE.json
           ls -lh
-          mv META_4M_EMPTY_FILE_RM.json ../
+          mv META_50W_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_4M_EMPTY_FILE_RM.json"
-      - name: Rm 100w Files in Depth of 1 Directory
+          load_file: "META_50W_EMPTY_FILE.json"
+      - name: Rm 50w Files
         run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          sudo chmod 777 ../mkdir
-          echo "rm 100w files in depth of 1 directory"
+          cd /tmp/juicefs-load-test/
+          sudo chmod 777 createfile/
+          echo "rm 50w files"
           git clone https://github.com/juicedata/juicefs.git
           cd juicefs
           make
-          sudo ./juicefs rmr ../aaa/
-      - name: Rm 100w Files in Depth of 10 Directory
-        run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          echo "rm 100w files in depth of 10 directory"
-          cd juicefs
-          sudo ./juicefs rmr ../aaa0/
-      - name: Rm 200w Files in Depth of 20 Directory
-        run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          echo "rm 200w files in depth of 20 directory"
-          cd juicefs
-          sudo ./juicefs rmr ../aaa2/
+          sudo ./juicefs rmr ../createfile/
       - name: Log
         if: ${{ failure() }}
         run: |
@@ -176,38 +150,25 @@ jobs:
           sudo chmod 777 /var
           git clone https://github.com/sanwan/actionfile.git
           cd actionfile
-          cat META_4M_EMPTY_FILE_RM.json.gz* >META_4M_EMPTY_FILE_RM.json.gz
-          gzip -dk  META_4M_EMPTY_FILE_RM.json.gz
+          cat META_50W_EMPTY_FILE.json* >META_50W_EMPTY_FILE.json
           ls -lh
-          mv META_4M_EMPTY_FILE_RM.json ../
+          mv META_50W_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_4M_EMPTY_FILE_RM.json"
-      - name: Rm 100w Files in Depth of 1 Directory
+          load_file: "META_50W_EMPTY_FILE.json"
+      - name: Rm 50w Files
         run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          sudo chmod 777 ../mkdir
-          echo "rm 100w files in depth of 1 directory"
+          cd /tmp/juicefs-load-test/
+          sudo chmod 777 createfile/
+          echo "rm 50w files"
           git clone https://github.com/juicedata/juicefs.git
           cd juicefs
           make
-          sudo ./juicefs rmr ../aaa/
-      - name: Rm 100w Files in Depth of 10 Directory
-        run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          echo "rm 100w files in depth of 10 directory"
-          cd juicefs
-          sudo ./juicefs rmr ../aaa0/
-      - name: Rm 200w Files in Depth of 20 Directory
-        run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          echo "rm 200w files in depth of 20 directory"
-          cd juicefs
-          sudo ./juicefs rmr ../aaa2/
+          sudo ./juicefs rmr ../createfile/
       - name: Log
         if: ${{ failure() }}
         run: |
@@ -236,38 +197,25 @@ jobs:
           sudo chmod 777 /var
           git clone https://github.com/sanwan/actionfile.git
           cd actionfile
-          cat META_4M_EMPTY_FILE_RM.json.gz* >META_4M_EMPTY_FILE_RM.json.gz
-          gzip -dk  META_4M_EMPTY_FILE_RM.json.gz
+          cat META_50W_EMPTY_FILE.json* >META_50W_EMPTY_FILE.json
           ls -lh
-          mv META_4M_EMPTY_FILE_RM.json ../
+          mv META_50W_EMPTY_FILE.json ../
           cd ../
           ls -lh
       - name: Load
         uses: ./.github/actions/load
         with:
           meta_url: sqlite3://sync-test.db
-          load_file: "META_4M_EMPTY_FILE_RM.json"
-      - name: Rm 100w Files in Depth of 1 Directory
+          load_file: "META_50W_EMPTY_FILE.json"
+      - name: Rm 50w Files
         run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          sudo chmod 777 ../mkdir
-          echo "rm 100w files in depth of 1 directory"
+          cd /tmp/juicefs-load-test/
+          sudo chmod 777 createfile/
+          echo "rm 50w files"
           git clone https://github.com/juicedata/juicefs.git
           cd juicefs
           make
-          sudo ./juicefs rmr ../aaa/
-      - name: Rm 100w Files in Depth of 10 Directory
-        run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          echo "rm 100w files in depth of 10 directory"
-          cd juicefs
-          sudo ./juicefs rmr ../aaa0/
-      - name: Rm 200w Files in Depth of 20 Directory
-        run: |
-          cd /tmp/juicefs-load-test/mkdir/
-          echo "rm 200w files in depth of 20 directory"
-          cd juicefs
-          sudo ./juicefs rmr ../aaa2/
+          sudo ./juicefs rmr ../createfile/
       - name: Log
         if: ${{ failure() }}
         run: |
diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index 24f0ccd6..6c069d32 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -3,14 +3,12 @@ name: "sync-test"
 on:
   push:
     branches:
-      - 'main'
       - 'release-**'
     paths-ignore:
       - 'docs/**'
   pull_request:
     #The branches below must be a subset of the branches above
     branches:
-      - 'main'
       - 'release-**'
     paths-ignore:
       - 'docs/**'
diff --git a/.github/workflows/sysbench_smallfile_randrw.yml b/.github/workflows/sysbench_smallfile_randrw.yml
index ca00dbd0..c2c41e32 100644
--- a/.github/workflows/sysbench_smallfile_randrw.yml
+++ b/.github/workflows/sysbench_smallfile_randrw.yml
@@ -1,17 +1,17 @@
 name: "sysbench"
 
 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/**'
+#  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:
diff --git a/.github/workflows/sysbench_smallfile_seqrw.yml b/.github/workflows/sysbench_smallfile_seqrw.yml
index c96e65bc..e67366ee 100644
--- a/.github/workflows/sysbench_smallfile_seqrw.yml
+++ b/.github/workflows/sysbench_smallfile_seqrw.yml
@@ -1,17 +1,17 @@
 name: "sysbench"
 
 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/**'
+#  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:
diff --git a/.github/workflows/tpcds.yml b/.github/workflows/tpcds.yml
index f519eb4f..30927c29 100644
--- a/.github/workflows/tpcds.yml
+++ b/.github/workflows/tpcds.yml
@@ -1,17 +1,17 @@
 name: tpcds
 
 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/**'
+#  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:
diff --git a/.github/workflows/tpch_clickhouse.yml b/.github/workflows/tpch_clickhouse.yml
index b214da54..dc9a60d7 100644
--- a/.github/workflows/tpch_clickhouse.yml
+++ b/.github/workflows/tpch_clickhouse.yml
@@ -256,6 +256,11 @@ jobs:
                                          C_NATION ASC;"
           df -lh
 
+      - name: Log
+        if: ${{ always() }}
+        run: |
+          sudo tail -n 100 /var/log/juicefs.log
+
 
       - name: Send Slack Notification
         if: ${{ failure() }}
diff --git a/.github/workflows/tpch_clickhouse_local.yml b/.github/workflows/tpch_clickhouse_local.yml
new file mode 100644
index 00000000..a6903ff7
--- /dev/null
+++ b/.github/workflows/tpch_clickhouse_local.yml
@@ -0,0 +1,256 @@
+name: "tpch_clickhouse_local"
+
+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:  '59 * * * *'
+
+  workflow_dispatch:
+
+jobs:
+  tpchch_local:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: '1.17.x'
+
+      - name: Set up Docker
+        uses: docker-practice/actions-setup-docker@master
+
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+
+
+      - name: Install ClickHouse
+        run: |
+          sudo apt-get install -y apt-transport-https ca-certificates dirmngr
+          echo "deb https://packages.clickhouse.com/deb stable main" | sudo tee /etc/apt/sources.list.d/clickhouse.list
+          sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 8919F6BD2B48D754
+          sudo apt-get update
+          sudo DEBIAN_FRONTEND=noninteractive  apt-get install -y  clickhouse-server clickhouse-client
+
+
+      - name: Change Datadir
+        run: |
+          sudo mkdir -p /data/jfs/
+          sudo chmod -R 777 /data/
+          sudo chmod 777 /etc/clickhouse-server/
+          sudo chmod 777 /etc/clickhouse-server/config.xml
+          sudo sed -i "s?<path>/var/lib/clickhouse/</path>?<path>/data/jfs/</path>?" /etc/clickhouse-server/config.xml
+          sudo service clickhouse-server start
+          sudo service clickhouse-server status
+          ls -lah /data/jfs/
+
+
+      - name: Generate Data
+        run: |
+          cd /data/jfs/
+          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
+          mv actionfile/*.tbl .
+          rm lineorder.tbl.gz*
+          ls -lrth
+          sudo du -sh /data/jfs/
+          df -lh
+
+
+      - name: Create Table
+        run: |
+          clickhouse-client --query "CREATE TABLE customer
+                                     (
+                                             C_CUSTKEY       UInt32,
+                                             C_NAME          String,
+                                             C_ADDRESS       String,
+                                             C_CITY          LowCardinality(String),
+                                             C_NATION        LowCardinality(String),
+                                             C_REGION        LowCardinality(String),
+                                             C_PHONE         String,
+                                             C_MKTSEGMENT    LowCardinality(String)
+                                     )
+                                     ENGINE = MergeTree ORDER BY (C_CUSTKEY);"
+          clickhouse-client --query "CREATE TABLE lineorder
+                                     (
+                                         LO_ORDERKEY             UInt32,
+                                         LO_LINENUMBER           UInt8,
+                                         LO_CUSTKEY              UInt32,
+                                         LO_PARTKEY              UInt32,
+                                         LO_SUPPKEY              UInt32,
+                                         LO_ORDERDATE            Date,
+                                         LO_ORDERPRIORITY        LowCardinality(String),
+                                         LO_SHIPPRIORITY         UInt8,
+                                         LO_QUANTITY             UInt8,
+                                         LO_EXTENDEDPRICE        UInt32,
+                                         LO_ORDTOTALPRICE        UInt32,
+                                         LO_DISCOUNT             UInt8,
+                                         LO_REVENUE              UInt32,
+                                         LO_SUPPLYCOST           UInt32,
+                                         LO_TAX                  UInt8,
+                                         LO_COMMITDATE           Date,
+                                         LO_SHIPMODE             LowCardinality(String)
+                                     )
+                                     ENGINE = MergeTree PARTITION BY toYear(LO_ORDERDATE) ORDER BY (LO_ORDERDATE, LO_ORDERKEY);"
+          clickhouse-client --query "CREATE TABLE part
+                                     (
+                                             P_PARTKEY       UInt32,
+                                             P_NAME          String,
+                                             P_MFGR          LowCardinality(String),
+                                             P_CATEGORY      LowCardinality(String),
+                                             P_BRAND         LowCardinality(String),
+                                             P_COLOR         LowCardinality(String),
+                                             P_TYPE          LowCardinality(String),
+                                             P_SIZE          UInt8,
+                                             P_CONTAINER     LowCardinality(String)
+                                     )
+                                     ENGINE = MergeTree ORDER BY P_PARTKEY;"
+          clickhouse-client --query "CREATE TABLE supplier
+                                     (
+                                             S_SUPPKEY       UInt32,
+                                             S_NAME          String,
+                                             S_ADDRESS       String,
+                                             S_CITY          LowCardinality(String),
+                                             S_NATION        LowCardinality(String),
+                                             S_REGION        LowCardinality(String),
+                                             S_PHONE         String
+                                     )
+                                     ENGINE = MergeTree ORDER BY S_SUPPKEY;"
+
+
+      - name: Import Data
+        run: |
+          cd /data/jfs/chdata/
+          clickhouse-client --query "INSERT INTO customer FORMAT CSV" < customer.tbl
+          clickhouse-client --query "INSERT INTO part FORMAT CSV" < part.tbl
+          clickhouse-client --query "INSERT INTO supplier FORMAT CSV" < supplier.tbl
+          time clickhouse-client --query "INSERT INTO lineorder FORMAT CSV" < lineorder.tbl
+          df -lh
+
+
+      - name: ClickHouse Test
+        run: |
+          clickhouse-client --query "SET max_memory_usage = 20000000000;"
+          time clickhouse-client --query "CREATE TABLE lineorder_flat
+                                     ENGINE = MergeTree
+                                     PARTITION BY toYear(LO_ORDERDATE)
+                                     ORDER BY (LO_ORDERDATE, LO_ORDERKEY) AS
+                                     SELECT
+                                         l.LO_ORDERKEY AS LO_ORDERKEY,
+                                         l.LO_LINENUMBER AS LO_LINENUMBER,
+                                         l.LO_CUSTKEY AS LO_CUSTKEY,
+                                         l.LO_PARTKEY AS LO_PARTKEY,
+                                         l.LO_SUPPKEY AS LO_SUPPKEY,
+                                         l.LO_ORDERDATE AS LO_ORDERDATE,
+                                         l.LO_ORDERPRIORITY AS LO_ORDERPRIORITY,
+                                         l.LO_SHIPPRIORITY AS LO_SHIPPRIORITY,
+                                         l.LO_QUANTITY AS LO_QUANTITY,
+                                         l.LO_EXTENDEDPRICE AS LO_EXTENDEDPRICE,
+                                         l.LO_ORDTOTALPRICE AS LO_ORDTOTALPRICE,
+                                         l.LO_DISCOUNT AS LO_DISCOUNT,
+                                         l.LO_REVENUE AS LO_REVENUE,
+                                         l.LO_SUPPLYCOST AS LO_SUPPLYCOST,
+                                         l.LO_TAX AS LO_TAX,
+                                         l.LO_COMMITDATE AS LO_COMMITDATE,
+                                         l.LO_SHIPMODE AS LO_SHIPMODE,
+                                         c.C_NAME AS C_NAME,
+                                         c.C_ADDRESS AS C_ADDRESS,
+                                         c.C_CITY AS C_CITY,
+                                         c.C_NATION AS C_NATION,
+                                         c.C_REGION AS C_REGION,
+                                         c.C_PHONE AS C_PHONE,
+                                         c.C_MKTSEGMENT AS C_MKTSEGMENT,
+                                         s.S_NAME AS S_NAME,
+                                         s.S_ADDRESS AS S_ADDRESS,
+                                         s.S_CITY AS S_CITY,
+                                         s.S_NATION AS S_NATION,
+                                         s.S_REGION AS S_REGION,
+                                         s.S_PHONE AS S_PHONE,
+                                         p.P_NAME AS P_NAME,
+                                         p.P_MFGR AS P_MFGR,
+                                         p.P_CATEGORY AS P_CATEGORY,
+                                         p.P_BRAND AS P_BRAND,
+                                         p.P_COLOR AS P_COLOR,
+                                         p.P_TYPE AS P_TYPE,
+                                         p.P_SIZE AS P_SIZE,
+                                         p.P_CONTAINER AS P_CONTAINER
+                                     FROM lineorder AS l
+                                     INNER JOIN customer AS c ON c.C_CUSTKEY = l.LO_CUSTKEY
+                                     INNER JOIN supplier AS s ON s.S_SUPPKEY = l.LO_SUPPKEY
+                                     INNER JOIN part AS p ON p.P_PARTKEY = l.LO_PARTKEY;"
+          echo "Q1.1"
+          time clickhouse-client --query "SELECT sum(LO_EXTENDEDPRICE * LO_DISCOUNT) AS revenue
+                                     FROM lineorder_flat
+                                     WHERE toYear(LO_ORDERDATE) = 1993 AND LO_DISCOUNT BETWEEN 1 AND 3 AND LO_QUANTITY < 25;"
+          echo "Q1.2"
+          time clickhouse-client --query "SELECT sum(LO_EXTENDEDPRICE * LO_DISCOUNT) AS revenue
+                                     FROM lineorder_flat
+                                     WHERE toYYYYMM(LO_ORDERDATE) = 199401 AND LO_DISCOUNT BETWEEN 4 AND 6 AND LO_QUANTITY BETWEEN 26 AND 35;"
+          echo "Q2.1"
+          time clickhouse-client --query "SELECT
+                                         sum(LO_REVENUE),
+                                         toYear(LO_ORDERDATE) AS year,
+                                         P_BRAND
+                                     FROM lineorder_flat
+                                     WHERE P_CATEGORY = 'MFGR#12' AND S_REGION = 'AMERICA'
+                                     GROUP BY
+                                         year,
+                                         P_BRAND
+                                     ORDER BY
+                                         year,
+                                         P_BRAND;"
+          echo "Q3.1"
+          time clickhouse-client --query "SELECT
+                                         C_NATION,
+                                         S_NATION,
+                                         toYear(LO_ORDERDATE) AS year,
+                                         sum(LO_REVENUE) AS revenue
+                                     FROM lineorder_flat
+                                     WHERE C_REGION = 'ASIA' AND S_REGION = 'ASIA' AND year >= 1992 AND year <= 1997
+                                     GROUP BY
+                                         C_NATION,
+                                         S_NATION,
+                                         year
+                                     ORDER BY
+                                         year ASC,
+                                         revenue DESC;"
+          echo "Q4.1"
+          time clickhouse-client --query "SELECT
+                                         toYear(LO_ORDERDATE) AS year,
+                                         C_NATION,
+                                         sum(LO_REVENUE - LO_SUPPLYCOST) AS profit
+                                     FROM lineorder_flat
+                                     WHERE C_REGION = 'AMERICA' AND S_REGION = 'AMERICA' AND (P_MFGR = 'MFGR#1' OR P_MFGR = 'MFGR#2')
+                                     GROUP BY
+                                         year,
+                                         C_NATION
+                                     ORDER BY
+                                         year ASC,
+                                         C_NATION ASC;"
+          df -lh
+
+
+      - 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() }}
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/vdbench.yml b/.github/workflows/vdbench.yml
new file mode 100644
index 00000000..21175e35
--- /dev/null
+++ b/.github/workflows/vdbench.yml
@@ -0,0 +1,44 @@
+name: "vdbench"
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+  pull_request:
+    #The branches below must be a subset of the branches above
+    branches:
+      - 'main'
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+  schedule:
+    - cron: '0 0 * * *'
+  workflow_dispatch:
+  
+jobs:
+  vdbench:
+    runs-on: ubuntu-latest
+    steps:        
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+          
+      - name: Test
+        uses: ./.github/actions/vdbench
+        with:
+          meta_url: "redis://localhost/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 }}"          
+
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 77cd67a4..29e0e48f 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -4,11 +4,13 @@ on:
   push:
     branches:
     - main
+    - 'release-**'
     paths:
     - '**.go'
   pull_request:
     branches:
-    - main
+    - 'main'
+    - 'release-**'
     paths:
     - '**.yml'
     - '**.go'
diff --git a/cmd/config.go b/cmd/config.go
index ee2a102c..13941d09 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -71,6 +71,10 @@ $ juicefs config redis://localhost --min-client-version 1.0.0 --max-client-versi
 				Name:  "secret-key",
 				Usage: "secret key for object storage",
 			},
+			&cli.StringFlag{
+				Name:  "session-token",
+				Usage: "session token for object storage",
+			},
 			&cli.BoolFlag{
 				Name:  "encrypt-secret",
 				Usage: "encrypt the secret key if it was previously stored in plain format",
@@ -129,8 +133,8 @@ func config(ctx *cli.Context) error {
 	}
 
 	var quota, storage, trash, clientVer bool
-	var encrypted bool
 	var msg strings.Builder
+	encrypted := format.KeyEncrypted
 	for _, flag := range ctx.LocalFlagNames() {
 		switch flag {
 		case "capacity":
@@ -166,9 +170,17 @@ func config(ctx *cli.Context) error {
 			}
 		case "secret-key": // always update
 			msg.WriteString(fmt.Sprintf("%10s: updated\n", flag))
-			encrypted = format.KeyEncrypted
+			if err := format.Decrypt(); err != nil && strings.Contains(err.Error(), "secret was removed") {
+				logger.Warnf("decrypt secrets: %s", err)
+			}
 			format.SecretKey = ctx.String(flag)
-			format.KeyEncrypted = false
+			storage = true
+		case "session-token": // always update
+			msg.WriteString(fmt.Sprintf("%10s: updated\n", flag))
+			if err := format.Decrypt(); err != nil && strings.Contains(err.Error(), "secret was removed") {
+				logger.Warnf("decrypt secrets: %s", err)
+			}
+			format.SessionToken = ctx.String(flag)
 			storage = true
 		case "trash-days":
 			if new := ctx.Int(flag); new != format.TrashDays {
diff --git a/cmd/destroy.go b/cmd/destroy.go
index 8087e38a..34c53e93 100644
--- a/cmd/destroy.go
+++ b/cmd/destroy.go
@@ -145,7 +145,7 @@ func destroy(ctx *cli.Context) error {
 		fmt.Printf("data storage: %s\n", blob)
 		fmt.Printf("  used bytes: %d\n", totalSpace-availSpace)
 		fmt.Printf(" used inodes: %d\n", iused)
-		warn("The target volume will be destoried permanently, including:")
+		warn("The target volume will be permanently destroyed, including:")
 		warn("1. ALL objects in the data storage: %s", blob)
 		warn("2. ALL entries in the metadata engine: %s", utils.RemovePassword(uri))
 		if !userConfirmed() {
diff --git a/cmd/format.go b/cmd/format.go
index c9e1be66..16692bbd 100644
--- a/cmd/format.go
+++ b/cmd/format.go
@@ -139,6 +139,10 @@ Details: https://juicefs.com/docs/community/quick_start_guide`,
 				Name:  "secret-key",
 				Usage: "secret key for object storage (env SECRET_KEY)",
 			},
+			&cli.StringFlag{
+				Name:  "session-token",
+				Usage: "session token for object storage",
+			},
 			&cli.StringFlag{
 				Name:  "encrypt-rsa-key",
 				Usage: "a path to RSA private key (PEM)",
@@ -207,9 +211,9 @@ func createStorage(format meta.Format) (object.ObjectStorage, error) {
 		object.GetHttpClient().Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: tlsSkipVerify}
 	}
 	if format.Shards > 1 {
-		blob, err = object.NewSharded(strings.ToLower(format.Storage), format.Bucket, format.AccessKey, format.SecretKey, format.Shards)
+		blob, err = object.NewSharded(strings.ToLower(format.Storage), format.Bucket, format.AccessKey, format.SecretKey, format.SessionToken, format.Shards)
 	} else {
-		blob, err = object.CreateStorage(strings.ToLower(format.Storage), format.Bucket, format.AccessKey, format.SecretKey)
+		blob, err = object.CreateStorage(strings.ToLower(format.Storage), format.Bucket, format.AccessKey, format.SecretKey, format.SessionToken)
 	}
 	if err != nil {
 		return nil, err
@@ -353,8 +357,16 @@ func format(c *cli.Context) error {
 				format.AccessKey = c.String(flag)
 			case "secret-key":
 				encrypted = format.KeyEncrypted
+				if err := format.Decrypt(); err != nil && strings.Contains(err.Error(), "secret was removed") {
+					logger.Warnf("decrypt secrets: %s", err)
+				}
 				format.SecretKey = c.String(flag)
-				format.KeyEncrypted = false
+			case "session-key":
+				encrypted = format.KeyEncrypted
+				if err := format.Decrypt(); err != nil && strings.Contains(err.Error(), "secret was removed") {
+					logger.Warnf("decrypt secrets: %s", err)
+				}
+				format.SessionToken = c.String(flag)
 			case "trash-days":
 				format.TrashDays = c.Int(flag)
 			case "block-size":
@@ -374,21 +386,22 @@ func format(c *cli.Context) error {
 	} else if err.Error() == "database is not formatted" {
 		create = true
 		format = &meta.Format{
-			Name:        name,
-			UUID:        uuid.New().String(),
-			Storage:     c.String("storage"),
-			Bucket:      c.String("bucket"),
-			AccessKey:   c.String("access-key"),
-			SecretKey:   c.String("secret-key"),
-			EncryptKey:  loadEncrypt(c.String("encrypt-rsa-key")),
-			Shards:      c.Int("shards"),
-			HashPrefix:  c.Bool("hash-prefix"),
-			Capacity:    c.Uint64("capacity") << 30,
-			Inodes:      c.Uint64("inodes"),
-			BlockSize:   fixObjectSize(c.Int("block-size")),
-			Compression: c.String("compress"),
-			TrashDays:   c.Int("trash-days"),
-			MetaVersion: 1,
+			Name:         name,
+			UUID:         uuid.New().String(),
+			Storage:      c.String("storage"),
+			Bucket:       c.String("bucket"),
+			AccessKey:    c.String("access-key"),
+			SecretKey:    c.String("secret-key"),
+			SessionToken: c.String("session-token"),
+			EncryptKey:   loadEncrypt(c.String("encrypt-rsa-key")),
+			Shards:       c.Int("shards"),
+			HashPrefix:   c.Bool("hash-prefix"),
+			Capacity:     c.Uint64("capacity") << 30,
+			Inodes:       c.Uint64("inodes"),
+			BlockSize:    fixObjectSize(c.Int("block-size")),
+			Compression:  c.String("compress"),
+			TrashDays:    c.Int("trash-days"),
+			MetaVersion:  1,
 		}
 		if format.AccessKey == "" && os.Getenv("ACCESS_KEY") != "" {
 			format.AccessKey = os.Getenv("ACCESS_KEY")
@@ -398,6 +411,10 @@ func format(c *cli.Context) error {
 			format.SecretKey = os.Getenv("SECRET_KEY")
 			_ = os.Unsetenv("SECRET_KEY")
 		}
+		if format.SessionToken == "" && os.Getenv("SESSION_TOKEN") != "" {
+			format.SessionToken = os.Getenv("SESSION_TOKEN")
+			_ = os.Unsetenv("SESSION_TOKEN")
+		}
 	} else {
 		logger.Fatalf("Load metadata: %s", err)
 	}
diff --git a/cmd/gateway.go b/cmd/gateway.go
index a63a4c64..8df70225 100644
--- a/cmd/gateway.go
+++ b/cmd/gateway.go
@@ -26,6 +26,7 @@ import (
 	"time"
 
 	"github.com/juicedata/juicefs/pkg/chunk"
+	"github.com/juicedata/juicefs/pkg/fs"
 	"github.com/juicedata/juicefs/pkg/meta"
 	"github.com/juicedata/juicefs/pkg/vfs"
 
@@ -161,17 +162,17 @@ func (g *GateWay) Production() bool {
 func (g *GateWay) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
 	c := g.ctx
 	addr := c.Args().Get(0)
-	m, store, conf := initForSvc(c, "s3gateway", addr)
+	conf, jfs := initForSvc(c, "s3gateway", addr)
 
 	umask, err := strconv.ParseUint(c.String("umask"), 8, 16)
 	if err != nil {
 		logger.Fatalf("invalid umask %s: %s", c.String("umask"), err)
 	}
 
-	return jfsgateway.NewJFSGateway(conf, m, store, &jfsgateway.Config{MultiBucket: c.Bool("multi-buckets"), KeepEtag: c.Bool("keep-etag"), Mode: uint16(0777 &^ umask)})
+	return jfsgateway.NewJFSGateway(jfs, conf, &jfsgateway.Config{MultiBucket: c.Bool("multi-buckets"), KeepEtag: c.Bool("keep-etag"), Mode: uint16(0777 &^ umask)})
 }
 
-func initForSvc(c *cli.Context, mp string, metaUrl string) (meta.Meta, chunk.ChunkStore, *vfs.Config) {
+func initForSvc(c *cli.Context, mp string, metaUrl string) (*vfs.Config, *fs.FileSystem) {
 	removePassword(metaUrl)
 	metaConf := getMetaConf(c, mp, c.Bool("read-only"))
 	metaCli := meta.NewClient(metaUrl, metaConf)
@@ -208,6 +209,11 @@ func initForSvc(c *cli.Context, mp string, metaUrl string) (meta.Meta, chunk.Chu
 	vfsConf.DirEntryTimeout = time.Millisecond * time.Duration(c.Float64("dir-entry-cache")*1000)
 
 	initBackgroundTasks(c, vfsConf, metaConf, metaCli, blob, registerer, registry)
+	jfs, err := fs.NewFileSystem(vfsConf, metaCli, store)
+	if err != nil {
+		logger.Fatalf("Initialize failed: %s", err)
+	}
+	jfs.InitMetrics(registerer)
 
-	return metaCli, store, vfsConf
+	return vfsConf, jfs
 }
diff --git a/cmd/mount.go b/cmd/mount.go
index c8700851..9344396c 100644
--- a/cmd/mount.go
+++ b/cmd/mount.go
@@ -111,7 +111,7 @@ func exposeMetrics(c *cli.Context, m meta.Meta, registerer prometheus.Registerer
 		logger.Fatalf("metrics format error: %v", err)
 	}
 
-	meta.InitMetrics(registerer)
+	m.InitMetrics(registerer)
 	vfs.InitMetrics(registerer)
 	go metric.UpdateMetrics(m, registerer)
 	http.Handle("/metrics", promhttp.HandlerFor(
@@ -212,14 +212,16 @@ func daemonRun(c *cli.Context, addr string, vfsConf *vfs.Config, m meta.Meta) {
 			}
 		}
 	}
-	sqliteScheme := "sqlite3://"
-	if strings.HasPrefix(addr, sqliteScheme) {
-		path := addr[len(sqliteScheme):]
-		path2, err := filepath.Abs(path)
-		if err == nil && path2 != path {
-			for i, a := range os.Args {
-				if a == addr {
-					os.Args[i] = sqliteScheme + path2
+	embeddedSchemes := []string{"sqlite3://", "badger://"}
+	for _, es := range embeddedSchemes {
+		if strings.HasPrefix(addr, es) {
+			path := addr[len(es):]
+			path2, err := filepath.Abs(path)
+			if err == nil && path2 != path {
+				for i, a := range os.Args {
+					if a == addr {
+						os.Args[i] = es + path2
+					}
 				}
 			}
 		}
@@ -240,7 +242,7 @@ func getVfsConf(c *cli.Context, metaConf *meta.Config, format *meta.Format, chun
 		Chunk:      chunkConf,
 		BackupMeta: duration(c.String("backup-meta")),
 	}
-	if cfg.BackupMeta < time.Minute*5 {
+	if cfg.BackupMeta > 0 && cfg.BackupMeta < time.Minute*5 {
 		logger.Fatalf("backup-meta should not be less than 5 minutes: %s", cfg.BackupMeta)
 	}
 	return cfg
@@ -375,7 +377,7 @@ func NewReloadableStorage(format *meta.Format, reload func() (*meta.Format, erro
 				logger.Warnf("reload config: %s", err)
 				continue
 			}
-			if new.Storage != old.Storage || new.Bucket != old.Bucket || new.AccessKey != old.AccessKey || new.SecretKey != old.SecretKey {
+			if new.Storage != old.Storage || new.Bucket != old.Bucket || new.AccessKey != old.AccessKey || new.SecretKey != old.SecretKey || new.SessionToken != old.SessionToken {
 				logger.Infof("found new configuration: storage=%s bucket=%s ak=%s", new.Storage, new.Bucket, new.AccessKey)
 				newBlob, err := createStorage(*new)
 				if err != nil {
diff --git a/cmd/objbench.go b/cmd/objbench.go
index 26231904..1bb675e5 100644
--- a/cmd/objbench.go
+++ b/cmd/objbench.go
@@ -66,6 +66,10 @@ Details: https://juicefs.com/docs/community/performance_evaluation_guide#juicefs
 				Name:  "secret-key",
 				Usage: "secret key for object storage (env SECRET_KEY)",
 			},
+			&cli.StringFlag{
+				Name:  "session-token",
+				Usage: "session token for object storage",
+			},
 			&cli.UintFlag{
 				Name:  "block-size",
 				Value: 4096,
@@ -114,14 +118,17 @@ func objbench(ctx *cli.Context) error {
 			logger.Fatalf("%s should not be set to zero", name)
 		}
 	}
-	ak, sk := ctx.String("access-key"), ctx.String("secret-key")
+	ak, sk, token := ctx.String("access-key"), ctx.String("secret-key"), ctx.String("session-token")
 	if ak == "" {
 		ak = os.Getenv("ACCESS_KEY")
 	}
 	if sk == "" {
 		sk = os.Getenv("SECRET_KEY")
 	}
-	blobOrigin, err := object.CreateStorage(strings.ToLower(ctx.String("storage")), ctx.Args().First(), ak, sk)
+	if token == "" {
+		token = os.Getenv("SESSION_TOKEN")
+	}
+	blobOrigin, err := object.CreateStorage(strings.ToLower(ctx.String("storage")), ctx.Args().First(), ak, sk, token)
 	if err != nil {
 		logger.Fatalf("create storage failed: %v", err)
 	}
diff --git a/cmd/sync.go b/cmd/sync.go
index 64ee9206..930d8f25 100644
--- a/cmd/sync.go
+++ b/cmd/sync.go
@@ -232,7 +232,7 @@ func createSyncStorage(uri string, conf *sync.Config) (object.ObjectStorage, err
 				user = parts[0]
 				pass = parts[1]
 			}
-			return object.CreateStorage("sftp", uri, user, pass)
+			return object.CreateStorage("sftp", uri, user, pass, "")
 		}
 	}
 	u, err := url.Parse(uri)
@@ -267,7 +267,7 @@ func createSyncStorage(uri string, conf *sync.Config) (object.ObjectStorage, err
 		endpoint += u.Path
 	}
 
-	store, err := object.CreateStorage(name, endpoint, accessKey, secretKey)
+	store, err := object.CreateStorage(name, endpoint, accessKey, secretKey, "")
 	if err != nil {
 		return nil, fmt.Errorf("create %s %s: %s", name, endpoint, err)
 	}
diff --git a/cmd/webdav.go b/cmd/webdav.go
index 00267168..f23834c3 100644
--- a/cmd/webdav.go
+++ b/cmd/webdav.go
@@ -64,11 +64,7 @@ func webdav(c *cli.Context) error {
 	setup(c, 2)
 	metaUrl := c.Args().Get(0)
 	listenAddr := c.Args().Get(1)
-	m, store, conf := initForSvc(c, "webdav", metaUrl)
-	jfs, err := fs.NewFileSystem(conf, m, store)
-	if err != nil {
-		logger.Fatalf("initialize failed: %s", err)
-	}
+	_, jfs := initForSvc(c, "webdav", metaUrl)
 	fs.StartHTTPServer(jfs, listenAddr, c.Bool("gzip"), c.Bool("disallowList"))
-	return m.CloseSession()
+	return jfs.Meta().CloseSession()
 }
diff --git a/docs/en/reference/how_to_setup_object_storage.md b/docs/en/reference/how_to_setup_object_storage.md
index 3f8a25c8..01f20975 100644
--- a/docs/en/reference/how_to_setup_object_storage.md
+++ b/docs/en/reference/how_to_setup_object_storage.md
@@ -217,7 +217,7 @@ As you can see, there is no need to include authentication information in the co
 
 To use Azure Blob Storage as data storage of JuiceFS, please [check the documentation](https://docs.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage) to learn how to view the storage account name and access key, which correspond to the values ​​of the `--access-key` and `--secret-key` options, respectively.
 
-The `--bucket` option is set in the format `https://<container>.<endpoint>`, please replace `<container>` with the name of the actual blob container and `<endpoint>` with `blob.core.windows.net` (Azure Global) or `blob.core.chinacloudapi.cn` (Azure China). For example:
+The `--bucket` option is set in the format `https://<container>.<endpoint>`, please replace `<container>` with the name of the actual blob container and `<endpoint>` with `core.windows.net` (Azure Global) or `core.chinacloudapi.cn` (Azure China). For example:
 
 ```bash
 juicefs format \
diff --git a/docs/zh_cn/reference/how_to_setup_object_storage.md b/docs/zh_cn/reference/how_to_setup_object_storage.md
index f7901a7f..11aa2f01 100644
--- a/docs/zh_cn/reference/how_to_setup_object_storage.md
+++ b/docs/zh_cn/reference/how_to_setup_object_storage.md
@@ -221,7 +221,7 @@ $ juicefs format \
 
 使用 Azure Blob 存储作为 JuiceFS 的数据存储，请先 [查看文档](https://docs.microsoft.com/zh-cn/azure/storage/common/storage-account-keys-manage) 了解如何查看存储帐户的名称和密钥，它们分别对应 `--access-key` 和 `--secret-key` 选项的值。
 
-`--bucket` 选项的设置格式为 `https://<container>.<endpoint>`，请将其中的 `<container>` 替换为实际的 Blob 容器的名称，将 `<endpoint>` 替换为 `blob.core.windows.net`（Azure 全球）或 `blob.core.chinacloudapi.cn`（Azure 中国）。例如：
+`--bucket` 选项的设置格式为 `https://<container>.<endpoint>`，请将其中的 `<container>` 替换为实际的 Blob 容器的名称，将 `<endpoint>` 替换为 `core.windows.net`（Azure 全球）或 `core.chinacloudapi.cn`（Azure 中国）。例如：
 
 ```bash
 juicefs format \
diff --git a/pkg/chunk/cached_store.go b/pkg/chunk/cached_store.go
index 2909b9be..4bdf1b59 100644
--- a/pkg/chunk/cached_store.go
+++ b/pkg/chunk/cached_store.go
@@ -41,72 +41,6 @@ const SlowRequest = time.Second * time.Duration(10)
 
 var (
 	logger = utils.GetLogger("juicefs")
-
-	cacheHits = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "blockcache_hits",
-		Help: "read from cached block",
-	})
-	cacheMiss = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "blockcache_miss",
-		Help: "missed read from cached block",
-	})
-	cacheWrites = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "blockcache_writes",
-		Help: "written cached block",
-	})
-	cacheDrops = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "blockcache_drops",
-		Help: "dropped block",
-	})
-	cacheEvicts = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "blockcache_evicts",
-		Help: "evicted cache blocks",
-	})
-	cacheHitBytes = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "blockcache_hit_bytes",
-		Help: "read bytes from cached block",
-	})
-	cacheMissBytes = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "blockcache_miss_bytes",
-		Help: "missed bytes from cached block",
-	})
-	cacheWriteBytes = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "blockcache_write_bytes",
-		Help: "write bytes of cached block",
-	})
-	cacheReadHist = prometheus.NewHistogram(prometheus.HistogramOpts{
-		Name:    "blockcache_read_hist_seconds",
-		Help:    "read cached block latency distribution",
-		Buckets: prometheus.ExponentialBuckets(0.00001, 2, 20),
-	})
-	cacheWriteHist = prometheus.NewHistogram(prometheus.HistogramOpts{
-		Name:    "blockcache_write_hist_seconds",
-		Help:    "write cached block latency distribution",
-		Buckets: prometheus.ExponentialBuckets(0.00001, 2, 20),
-	})
-
-	objectReqsHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
-		Name:    "object_request_durations_histogram_seconds",
-		Help:    "Object requests latency distributions.",
-		Buckets: prometheus.ExponentialBuckets(0.01, 1.5, 25),
-	}, []string{"method"})
-	objectReqErrors = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "object_request_errors",
-		Help: "failed requests to object store",
-	})
-	objectDataBytes = prometheus.NewCounterVec(prometheus.CounterOpts{
-		Name: "object_request_data_bytes",
-		Help: "Object requests size in bytes.",
-	}, []string{"method"})
-
-	stageBlocks = prometheus.NewGauge(prometheus.GaugeOpts{
-		Name: "staging_blocks",
-		Help: "Number of blocks in the staging path.",
-	})
-	stageBlockBytes = prometheus.NewGauge(prometheus.GaugeOpts{
-		Name: "staging_block_bytes",
-		Help: "Total bytes of blocks in the staging path.",
-	})
 )
 
 type pendingItem struct {
@@ -197,9 +131,9 @@ func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err er
 			n, err = r.ReadAt(p, int64(boff))
 			_ = r.Close()
 			if err == nil {
-				cacheHits.Add(1)
-				cacheHitBytes.Add(float64(n))
-				cacheReadHist.Observe(time.Since(start).Seconds())
+				c.store.cacheHits.Add(1)
+				c.store.cacheHitBytes.Add(float64(n))
+				c.store.cacheReadHist.Observe(time.Since(start).Seconds())
 				return n, nil
 			}
 			if f, ok := r.(*os.File); ok {
@@ -209,8 +143,8 @@ func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err er
 		}
 	}
 
-	cacheMiss.Add(1)
-	cacheMissBytes.Add(float64(len(p)))
+	c.store.cacheMiss.Add(1)
+	c.store.cacheMissBytes.Add(float64(len(p)))
 
 	if c.store.seekable && boff > 0 && len(p) <= blockSize/4 {
 		if c.store.downLimit != nil {
@@ -228,13 +162,13 @@ func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err er
 		if used > SlowRequest {
 			logger.Infof("slow request: GET %s (%v, %.3fs)", key, err, used.Seconds())
 		}
-		objectDataBytes.WithLabelValues("GET").Add(float64(n))
-		objectReqsHistogram.WithLabelValues("GET").Observe(used.Seconds())
+		c.store.objectDataBytes.WithLabelValues("GET").Add(float64(n))
+		c.store.objectReqsHistogram.WithLabelValues("GET").Observe(used.Seconds())
 		c.store.fetcher.fetch(key)
 		if err == nil {
 			return n, nil
 		} else {
-			objectReqErrors.Add(1)
+			c.store.objectReqErrors.Add(1)
 		}
 	}
 
@@ -271,9 +205,9 @@ func (c *rChunk) delete(indx int) error {
 	if used > SlowRequest {
 		logger.Infof("slow request: DELETE %v (%v, %.3fs)", key, err, used.Seconds())
 	}
-	objectReqsHistogram.WithLabelValues("DELETE").Observe(used.Seconds())
+	c.store.objectReqsHistogram.WithLabelValues("DELETE").Observe(used.Seconds())
 	if err != nil {
-		objectReqErrors.Add(1)
+		c.store.objectReqErrors.Add(1)
 	}
 	return err
 }
@@ -414,10 +348,10 @@ func (store *cachedStore) put(key string, p *Page) error {
 		if used > SlowRequest {
 			logger.Infof("slow request: PUT %v (%v, %.3fs)", key, err, used.Seconds())
 		}
-		objectDataBytes.WithLabelValues("PUT").Add(float64(len(p.Data)))
-		objectReqsHistogram.WithLabelValues("PUT").Observe(used.Seconds())
+		store.objectDataBytes.WithLabelValues("PUT").Add(float64(len(p.Data)))
+		store.objectReqsHistogram.WithLabelValues("PUT").Observe(used.Seconds())
 		if err != nil {
-			objectReqErrors.Add(1)
+			store.objectReqErrors.Add(1)
 		}
 		return err
 	}, store.conf.PutTimeout)
@@ -503,8 +437,10 @@ func (c *wChunk) upload(indx int) {
 						if err = c.store.upload(key, block, nil); err == nil {
 							c.store.bcache.uploaded(key, blen)
 							if os.Remove(stagingPath) == nil {
-								stageBlocks.Sub(1)
-								stageBlockBytes.Sub(float64(blen))
+								if m, ok := c.store.bcache.(*cacheManager); ok {
+									m.stageBlocks.Sub(1)
+									m.stageBlockBytes.Sub(float64(blen))
+								}
 							}
 						} else { // add to delay list and wait for later scanning
 							c.store.addDelayedStaging(key, stagingPath, time.Now().Add(time.Second*30), false)
@@ -620,6 +556,15 @@ type cachedStore struct {
 	seekable      bool
 	upLimit       *ratelimit.Bucket
 	downLimit     *ratelimit.Bucket
+
+	cacheHits           prometheus.Counter
+	cacheMiss           prometheus.Counter
+	cacheHitBytes       prometheus.Counter
+	cacheMissBytes      prometheus.Counter
+	cacheReadHist       prometheus.Histogram
+	objectReqsHistogram *prometheus.HistogramVec
+	objectReqErrors     prometheus.Counter
+	objectDataBytes     *prometheus.CounterVec
 }
 
 func (store *cachedStore) load(key string, page *Page, cache bool, forceCache bool) (err error) {
@@ -644,7 +589,7 @@ func (store *cachedStore) load(key string, page *Page, cache bool, forceCache bo
 		time.Sleep(time.Second * time.Duration(tried*tried))
 		if tried > 0 {
 			logger.Warnf("GET %s: %s; retrying", key, err)
-			objectReqErrors.Add(1)
+			store.objectReqErrors.Add(1)
 			start = time.Now()
 		}
 		in, err = store.storage.Get(key, 0, -1)
@@ -674,10 +619,10 @@ func (store *cachedStore) load(key string, page *Page, cache bool, forceCache bo
 	if store.downLimit != nil && compressed {
 		store.downLimit.Wait(int64(n))
 	}
-	objectDataBytes.WithLabelValues("GET").Add(float64(n))
-	objectReqsHistogram.WithLabelValues("GET").Observe(used.Seconds())
+	store.objectDataBytes.WithLabelValues("GET").Add(float64(n))
+	store.objectReqsHistogram.WithLabelValues("GET").Observe(used.Seconds())
 	if err != nil {
-		objectReqErrors.Add(1)
+		store.objectReqErrors.Add(1)
 		return fmt.Errorf("get %s: %s", key, err)
 	}
 	if compressed {
@@ -694,7 +639,7 @@ func (store *cachedStore) load(key string, page *Page, cache bool, forceCache bo
 }
 
 // NewCachedStore create a cached store.
-func NewCachedStore(storage object.ObjectStorage, config Config, registerer prometheus.Registerer) ChunkStore {
+func NewCachedStore(storage object.ObjectStorage, config Config, reg prometheus.Registerer) ChunkStore {
 	compressor := compress.NewCompressor(config.Compress)
 	if compressor == nil {
 		logger.Fatalf("unknown compress algorithm: %s", config.Compress)
@@ -726,7 +671,7 @@ func NewCachedStore(storage object.ObjectStorage, config Config, registerer prom
 	if config.DownloadLimit > 0 {
 		store.downLimit = ratelimit.NewBucketWithRate(float64(config.DownloadLimit)*0.85, config.DownloadLimit)
 	}
-	store.bcache = newCacheManager(&config, func(key, fpath string, force bool) bool {
+	store.bcache = newCacheManager(&config, reg, func(key, fpath string, force bool) bool {
 		if force {
 			return store.addDelayedStaging(key, fpath, time.Time{}, true)
 		} else if fi, err := os.Stat(fpath); err == nil {
@@ -747,7 +692,7 @@ func NewCachedStore(storage object.ObjectStorage, config Config, registerer prom
 		defer p.Release()
 		_ = store.load(key, p, true, true)
 	})
-	initMetrics(store, registerer)
+
 	if store.conf.CacheDir != "memory" && store.conf.Writeback {
 		for i := 0; i < store.conf.MaxUpload; i++ {
 			go store.uploader()
@@ -766,24 +711,59 @@ func NewCachedStore(storage object.ObjectStorage, config Config, registerer prom
 			}
 		}()
 	}
+
+	store.cacheHits = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "blockcache_hits",
+		Help: "read from cached block",
+	})
+	store.cacheMiss = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "blockcache_miss",
+		Help: "missed read from cached block",
+	})
+	store.cacheHitBytes = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "blockcache_hit_bytes",
+		Help: "read bytes from cached block",
+	})
+	store.cacheMissBytes = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "blockcache_miss_bytes",
+		Help: "missed bytes from cached block",
+	})
+	store.cacheReadHist = prometheus.NewHistogram(prometheus.HistogramOpts{
+		Name:    "blockcache_read_hist_seconds",
+		Help:    "read cached block latency distribution",
+		Buckets: prometheus.ExponentialBuckets(0.00001, 2, 20),
+	})
+	store.objectReqsHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
+		Name:    "object_request_durations_histogram_seconds",
+		Help:    "Object requests latency distributions.",
+		Buckets: prometheus.ExponentialBuckets(0.01, 1.5, 25),
+	}, []string{"method"})
+	store.objectReqErrors = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "object_request_errors",
+		Help: "failed requests to object store",
+	})
+	store.objectDataBytes = prometheus.NewCounterVec(prometheus.CounterOpts{
+		Name: "object_request_data_bytes",
+		Help: "Object requests size in bytes.",
+	}, []string{"method"})
+	store.initMetrics(reg)
+
 	return store
 }
 
-func initMetrics(store *cachedStore, registerer prometheus.Registerer) {
-	if registerer == nil {
+func (store *cachedStore) initMetrics(reg prometheus.Registerer) {
+	if reg == nil {
 		return
 	}
-	_ = registerer.Register(cacheHits)
-	_ = registerer.Register(cacheHitBytes)
-	_ = registerer.Register(cacheMiss)
-	_ = registerer.Register(cacheMissBytes)
-	_ = registerer.Register(cacheWrites)
-	_ = registerer.Register(cacheWriteBytes)
-	_ = registerer.Register(cacheDrops)
-	_ = registerer.Register(cacheEvicts)
-	_ = registerer.Register(cacheReadHist)
-	_ = registerer.Register(cacheWriteHist)
-	_ = registerer.Register(prometheus.NewGaugeFunc(
+	reg.MustRegister(store.cacheHits)
+	reg.MustRegister(store.cacheHitBytes)
+	reg.MustRegister(store.cacheMiss)
+	reg.MustRegister(store.cacheMissBytes)
+	reg.MustRegister(store.cacheReadHist)
+	reg.MustRegister(store.objectReqsHistogram)
+	reg.MustRegister(store.objectReqErrors)
+	reg.MustRegister(store.objectDataBytes)
+	reg.MustRegister(prometheus.NewGaugeFunc(
 		prometheus.GaugeOpts{
 			Name: "blockcache_blocks",
 			Help: "number of cached blocks",
@@ -792,7 +772,7 @@ func initMetrics(store *cachedStore, registerer prometheus.Registerer) {
 			cnt, _ := store.bcache.stats()
 			return float64(cnt)
 		}))
-	_ = registerer.Register(prometheus.NewGaugeFunc(
+	reg.MustRegister(prometheus.NewGaugeFunc(
 		prometheus.GaugeOpts{
 			Name: "blockcache_bytes",
 			Help: "number of cached bytes",
@@ -801,11 +781,6 @@ func initMetrics(store *cachedStore, registerer prometheus.Registerer) {
 			_, used := store.bcache.stats()
 			return float64(used)
 		}))
-	_ = registerer.Register(objectReqsHistogram)
-	_ = registerer.Register(objectReqErrors)
-	_ = registerer.Register(objectDataBytes)
-	_ = registerer.Register(stageBlocks)
-	_ = registerer.Register(stageBlockBytes)
 }
 
 func (store *cachedStore) shouldCache(size int) bool {
@@ -857,8 +832,10 @@ func (store *cachedStore) uploadStagingFile(key string, stagingPath string) {
 		store.bcache.uploaded(key, blen)
 		store.removePending(key)
 		if os.Remove(stagingPath) == nil {
-			stageBlocks.Sub(1)
-			stageBlockBytes.Sub(float64(blen))
+			if m, ok := store.bcache.(*cacheManager); ok {
+				m.stageBlocks.Sub(1)
+				m.stageBlockBytes.Sub(float64(blen))
+			}
 		}
 	}
 }
diff --git a/pkg/chunk/disk_cache.go b/pkg/chunk/disk_cache.go
index b366bf65..4b82a8c4 100644
--- a/pkg/chunk/disk_cache.go
+++ b/pkg/chunk/disk_cache.go
@@ -31,6 +31,7 @@ import (
 	"time"
 
 	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/prometheus/client_golang/prometheus"
 )
 
 var (
@@ -57,6 +58,7 @@ type cacheStore struct {
 	freeRatio float32
 	pending   chan pendingFile
 	pages     map[string]*Page
+	m         *cacheManager
 
 	used     int64
 	keys     map[string]cacheItem
@@ -65,7 +67,7 @@ type cacheStore struct {
 	uploader func(key, path string, force bool) bool
 }
 
-func newCacheStore(dir string, cacheSize int64, pendingPages int, config *Config, uploader func(key, path string, force bool) bool) *cacheStore {
+func newCacheStore(m *cacheManager, dir string, cacheSize int64, pendingPages int, config *Config, uploader func(key, path string, force bool) bool) *cacheStore {
 	if config.CacheMode == 0 {
 		config.CacheMode = 0600 // only owner can read/write cache
 	}
@@ -73,6 +75,7 @@ func newCacheStore(dir string, cacheSize int64, pendingPages int, config *Config
 		config.FreeSpace = 0.1 // 10%
 	}
 	c := &cacheStore{
+		m:         m,
 		dir:       dir,
 		mode:      config.CacheMode,
 		capacity:  cacheSize,
@@ -153,7 +156,7 @@ func (cache *cacheStore) cache(key string, p *Page, force bool) {
 		} else {
 			// does not have enough bandwidth to write it into disk, discard it
 			logger.Debugf("Caching queue is full (%s), drop %s (%d bytes)", cache.dir, key, len(p.Data))
-			cacheDrops.Add(1)
+			cache.m.cacheDrops.Add(1)
 			delete(cache.pages, key)
 			atomic.AddInt64(&cache.totalPages, -int64(cap(p.Data)))
 			p.Release()
@@ -168,10 +171,10 @@ func (cache *cacheStore) curFreeRatio() (float32, float32) {
 
 func (cache *cacheStore) flushPage(path string, data []byte) (err error) {
 	start := time.Now()
-	cacheWrites.Add(1)
-	cacheWriteBytes.Add(float64(len(data)))
+	cache.m.cacheWrites.Add(1)
+	cache.m.cacheWriteBytes.Add(float64(len(data)))
 	defer func() {
-		cacheWriteHist.Observe(time.Since(start).Seconds())
+		cache.m.cacheWriteHist.Observe(time.Since(start).Seconds())
 	}()
 	cache.createDir(filepath.Dir(path))
 	tmp := path + ".tmp"
@@ -235,8 +238,8 @@ func (cache *cacheStore) remove(key string) {
 		if fi, err := os.Stat(stagingPath); err == nil {
 			size := fi.Size()
 			if err = os.Remove(stagingPath); err == nil {
-				stageBlocks.Sub(1)
-				stageBlockBytes.Sub(float64(size))
+				cache.m.stageBlocks.Sub(1)
+				cache.m.stageBlockBytes.Sub(float64(size))
 			}
 		}
 	}
@@ -322,8 +325,8 @@ func (cache *cacheStore) stage(key string, data []byte, keepCache bool) (string,
 	}
 	err := cache.flushPage(stagingPath, data)
 	if err == nil {
-		stageBlocks.Add(1)
-		stageBlockBytes.Add(float64(len(data)))
+		cache.m.stageBlocks.Add(1)
+		cache.m.stageBlockBytes.Add(float64(len(data)))
 		if cache.capacity > 0 && keepCache {
 			path := cache.cachePath(key)
 			cache.createDir(filepath.Dir(path))
@@ -391,7 +394,7 @@ func (cache *cacheStore) cleanup() {
 			cache.used -= int64(lastValue.size + 4096)
 			todel = append(todel, lastKey)
 			logger.Debugf("remove %s from cache, age: %d", lastKey, now-lastValue.atime)
-			cacheEvicts.Add(1)
+			cache.m.cacheEvicts.Add(1)
 			cnt = 0
 			if len(cache.keys) < num && cache.used < goal {
 				break
@@ -528,8 +531,8 @@ func (cache *cacheStore) scanStaging() {
 				}
 			} else {
 				logger.Debugf("Found staging block: %s", path)
-				stageBlocks.Add(1)
-				stageBlockBytes.Add(float64(fi.Size()))
+				cache.m.stageBlocks.Add(1)
+				cache.m.stageBlockBytes.Add(float64(fi.Size()))
 				key := path[len(stagingPrefix)+1:]
 				if runtime.GOOS == "windows" {
 					key = strings.ReplaceAll(key, "\\", "/")
@@ -547,6 +550,14 @@ func (cache *cacheStore) scanStaging() {
 
 type cacheManager struct {
 	stores []*cacheStore
+
+	cacheDrops      prometheus.Counter
+	cacheWrites     prometheus.Counter
+	cacheEvicts     prometheus.Counter
+	cacheWriteBytes prometheus.Counter
+	cacheWriteHist  prometheus.Histogram
+	stageBlocks     prometheus.Gauge
+	stageBlockBytes prometheus.Gauge
 }
 
 func keyHash(s string) uint32 {
@@ -605,9 +616,9 @@ type CacheManager interface {
 	usedMemory() int64
 }
 
-func newCacheManager(config *Config, uploader func(key, path string, force bool) bool) CacheManager {
+func newCacheManager(config *Config, reg prometheus.Registerer, uploader func(key, path string, force bool) bool) CacheManager {
 	if config.CacheDir == "memory" || config.CacheSize == 0 {
-		return newMemStore(config)
+		return newMemStore(config, reg)
 	}
 	var dirs []string
 	for _, d := range utils.SplitDir(config.CacheDir) {
@@ -624,18 +635,58 @@ func newCacheManager(config *Config, uploader func(key, path string, force bool)
 	}
 	if len(dirs) == 0 {
 		logger.Warnf("No cache dir existed")
-		return newMemStore(config)
+		return newMemStore(config, reg)
 	}
 	sort.Strings(dirs)
 	dirCacheSize := config.CacheSize << 20
 	dirCacheSize /= int64(len(dirs))
 	m := &cacheManager{
 		stores: make([]*cacheStore, len(dirs)),
+
+		cacheDrops: prometheus.NewCounter(prometheus.CounterOpts{
+			Name: "blockcache_drops",
+			Help: "dropped block",
+		}),
+		cacheWrites: prometheus.NewCounter(prometheus.CounterOpts{
+			Name: "blockcache_writes",
+			Help: "written cached block",
+		}),
+		cacheEvicts: prometheus.NewCounter(prometheus.CounterOpts{
+			Name: "blockcache_evicts",
+			Help: "evicted cache blocks",
+		}),
+		cacheWriteBytes: prometheus.NewCounter(prometheus.CounterOpts{
+			Name: "blockcache_write_bytes",
+			Help: "write bytes of cached block",
+		}),
+		cacheWriteHist: prometheus.NewHistogram(prometheus.HistogramOpts{
+			Name:    "blockcache_write_hist_seconds",
+			Help:    "write cached block latency distribution",
+			Buckets: prometheus.ExponentialBuckets(0.00001, 2, 20),
+		}),
+		stageBlocks: prometheus.NewGauge(prometheus.GaugeOpts{
+			Name: "staging_blocks",
+			Help: "Number of blocks in the staging path.",
+		}),
+		stageBlockBytes: prometheus.NewGauge(prometheus.GaugeOpts{
+			Name: "staging_block_bytes",
+			Help: "Total bytes of blocks in the staging path.",
+		}),
+	}
+	if reg != nil {
+		reg.MustRegister(m.cacheWrites)
+		reg.MustRegister(m.cacheWriteBytes)
+		reg.MustRegister(m.cacheDrops)
+		reg.MustRegister(m.cacheEvicts)
+		reg.MustRegister(m.cacheWriteHist)
+		reg.MustRegister(m.stageBlocks)
+		reg.MustRegister(m.stageBlockBytes)
 	}
+
 	// 20% of buffer could be used for pending pages
 	pendingPages := config.BufferSize * 2 / 10 / config.BlockSize / len(dirs)
 	for i, d := range dirs {
-		m.stores[i] = newCacheStore(strings.TrimSpace(d)+string(filepath.Separator), dirCacheSize, pendingPages, config, uploader)
+		m.stores[i] = newCacheStore(m, strings.TrimSpace(d)+string(filepath.Separator), dirCacheSize, pendingPages, config, uploader)
 	}
 	return m
 }
diff --git a/pkg/chunk/mem_cache.go b/pkg/chunk/mem_cache.go
index 8dc586c0..27437610 100644
--- a/pkg/chunk/mem_cache.go
+++ b/pkg/chunk/mem_cache.go
@@ -21,6 +21,8 @@ import (
 	"runtime"
 	"sync"
 	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
 )
 
 type memItem struct {
@@ -33,12 +35,34 @@ type memcache struct {
 	capacity int64
 	used     int64
 	pages    map[string]memItem
+
+	cacheWrites     prometheus.Counter
+	cacheEvicts     prometheus.Counter
+	cacheWriteBytes prometheus.Counter
 }
 
-func newMemStore(config *Config) *memcache {
+func newMemStore(config *Config, reg prometheus.Registerer) *memcache {
 	c := &memcache{
 		capacity: config.CacheSize << 20,
 		pages:    make(map[string]memItem),
+
+		cacheWrites: prometheus.NewCounter(prometheus.CounterOpts{
+			Name: "blockcache_writes",
+			Help: "written cached block",
+		}),
+		cacheEvicts: prometheus.NewCounter(prometheus.CounterOpts{
+			Name: "blockcache_evicts",
+			Help: "evicted cache blocks",
+		}),
+		cacheWriteBytes: prometheus.NewCounter(prometheus.CounterOpts{
+			Name: "blockcache_write_bytes",
+			Help: "write bytes of cached block",
+		}),
+	}
+	if reg != nil {
+		reg.MustRegister(c.cacheWrites)
+		reg.MustRegister(c.cacheWriteBytes)
+		reg.MustRegister(c.cacheEvicts)
 	}
 	runtime.SetFinalizer(c, func(c *memcache) {
 		for _, p := range c.pages {
@@ -71,8 +95,8 @@ func (c *memcache) cache(key string, p *Page, force bool) {
 		return
 	}
 	size := int64(cap(p.Data))
-	cacheWrites.Add(1)
-	cacheWriteBytes.Add(float64(size))
+	c.cacheWrites.Add(1)
+	c.cacheWriteBytes.Add(float64(size))
 	p.Acquire()
 	c.pages[key] = memItem{time.Now(), p}
 	c.used += size
@@ -122,7 +146,7 @@ func (c *memcache) cleanup() {
 		cnt++
 		if cnt > 1 {
 			logger.Debugf("remove %s from cache, age: %d", lastKey, now.Sub(lastValue.atime))
-			cacheEvicts.Add(1)
+			c.cacheEvicts.Add(1)
 			c.delete(lastKey, lastValue.page)
 			cnt = 0
 			if c.used < c.capacity {
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index 85da5c61..1472fde2 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -35,6 +35,7 @@ import (
 	"github.com/juicedata/juicefs/pkg/meta"
 	"github.com/juicedata/juicefs/pkg/utils"
 	"github.com/juicedata/juicefs/pkg/vfs"
+	"github.com/prometheus/client_golang/prometheus"
 )
 
 var logger = utils.GetLogger("juicefs")
@@ -137,6 +138,10 @@ type FileSystem struct {
 	attrs   map[Ino]*attrCache
 
 	logBuffer chan string
+
+	readSizeHistogram     prometheus.Histogram
+	writtenSizeHistogram  prometheus.Histogram
+	opsDurationsHistogram prometheus.Histogram
 }
 
 type File struct {
@@ -163,7 +168,24 @@ func NewFileSystem(conf *vfs.Config, m meta.Meta, d chunk.ChunkStore) (*FileSyst
 		writer:  vfs.NewDataWriter(conf, m, d, reader),
 		entries: make(map[meta.Ino]map[string]*entryCache),
 		attrs:   make(map[meta.Ino]*attrCache),
+
+		readSizeHistogram: prometheus.NewHistogram(prometheus.HistogramOpts{
+			Name:    "sdk_read_size_bytes",
+			Help:    "size of read distributions.",
+			Buckets: prometheus.LinearBuckets(4096, 4096, 32),
+		}),
+		writtenSizeHistogram: prometheus.NewHistogram(prometheus.HistogramOpts{
+			Name:    "sdk_written_size_bytes",
+			Help:    "size of write distributions.",
+			Buckets: prometheus.LinearBuckets(4096, 4096, 32),
+		}),
+		opsDurationsHistogram: prometheus.NewHistogram(prometheus.HistogramOpts{
+			Name:    "sdk_ops_durations_histogram_seconds",
+			Help:    "Operations latency distributions.",
+			Buckets: prometheus.ExponentialBuckets(0.0001, 1.5, 30),
+		}),
 	}
+
 	go fs.cleanupCache()
 	if conf.AccessLog != "" {
 		f, err := os.OpenFile(conf.AccessLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
@@ -178,6 +200,14 @@ func NewFileSystem(conf *vfs.Config, m meta.Meta, d chunk.ChunkStore) (*FileSyst
 	return fs, nil
 }
 
+func (fs *FileSystem) InitMetrics(reg prometheus.Registerer) {
+	if reg != nil {
+		reg.MustRegister(fs.readSizeHistogram)
+		reg.MustRegister(fs.writtenSizeHistogram)
+		reg.MustRegister(fs.opsDurationsHistogram)
+	}
+}
+
 func (fs *FileSystem) cleanupCache() {
 	for {
 		fs.cacheM.Lock()
@@ -233,7 +263,7 @@ func (fs *FileSystem) invalidateAttr(ino Ino) {
 
 func (fs *FileSystem) log(ctx LogContext, format string, args ...interface{}) {
 	used := ctx.Duration()
-	opsDurationsHistogram.Observe(used.Seconds())
+	fs.opsDurationsHistogram.Observe(used.Seconds())
 	if fs.logBuffer == nil {
 		return
 	}
@@ -967,7 +997,7 @@ func (f *File) pread(ctx meta.Context, b []byte, offset int64) (n int, err error
 	if got == 0 {
 		return 0, io.EOF
 	}
-	readSizeHistogram.Observe(float64(got))
+	f.fs.readSizeHistogram.Observe(float64(got))
 	return got, nil
 }
 
@@ -1005,7 +1035,7 @@ func (f *File) pwrite(ctx meta.Context, b []byte, offset int64) (n int, err sysc
 	if offset+int64(len(b)) > int64(f.info.attr.Length) {
 		f.info.attr.Length = uint64(offset + int64(len(b)))
 	}
-	writtenSizeHistogram.Observe(float64(len(b)))
+	f.fs.writtenSizeHistogram.Observe(float64(len(b)))
 	return len(b), 0
 }
 
diff --git a/pkg/fs/metrics.go b/pkg/fs/metrics.go
index bc175f72..04b5d33c 100644
--- a/pkg/fs/metrics.go
+++ b/pkg/fs/metrics.go
@@ -15,23 +15,3 @@
  */
 
 package fs
-
-import "github.com/prometheus/client_golang/prometheus"
-
-var (
-	readSizeHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
-		Name:    "sdk_read_size_bytes",
-		Help:    "size of read distributions.",
-		Buckets: prometheus.LinearBuckets(4096, 4096, 32),
-	})
-	writtenSizeHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
-		Name:    "sdk_written_size_bytes",
-		Help:    "size of write distributions.",
-		Buckets: prometheus.LinearBuckets(4096, 4096, 32),
-	})
-	opsDurationsHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
-		Name:    "sdk_ops_durations_histogram_seconds",
-		Help:    "Operations latency distributions.",
-		Buckets: prometheus.ExponentialBuckets(0.0001, 1.5, 30),
-	})
-)
diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go
index 2e1f3040..a3ae2084 100644
--- a/pkg/gateway/gateway.go
+++ b/pkg/gateway/gateway.go
@@ -34,7 +34,6 @@ import (
 	"github.com/minio/minio-go/pkg/s3utils"
 	minio "github.com/minio/minio/cmd"
 
-	"github.com/juicedata/juicefs/pkg/chunk"
 	"github.com/juicedata/juicefs/pkg/fs"
 	"github.com/juicedata/juicefs/pkg/meta"
 	"github.com/juicedata/juicefs/pkg/utils"
@@ -55,11 +54,7 @@ type Config struct {
 	Mode        uint16
 }
 
-func NewJFSGateway(conf *vfs.Config, m meta.Meta, store chunk.ChunkStore, gConf *Config) (minio.ObjectLayer, error) {
-	jfs, err := fs.NewFileSystem(conf, m, store)
-	if err != nil {
-		return nil, fmt.Errorf("Initialize failed: %s", err)
-	}
+func NewJFSGateway(jfs *fs.FileSystem, conf *vfs.Config, gConf *Config) (minio.ObjectLayer, error) {
 	mctx = meta.NewContext(uint32(os.Getpid()), uint32(os.Getuid()), []uint32{uint32(os.Getgid())})
 	jfsObj := &jfsObjects{fs: jfs, conf: conf, listPool: minio.NewTreeWalkPool(time.Minute * 30), gConf: gConf}
 	go jfsObj.cleanup()
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index ece5ac69..5a57f98c 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -29,6 +29,7 @@ import (
 
 	"github.com/juicedata/juicefs/pkg/utils"
 	"github.com/juicedata/juicefs/pkg/version"
+	"github.com/prometheus/client_golang/prometheus"
 )
 
 const (
@@ -102,6 +103,12 @@ type baseMeta struct {
 	freeInodes freeID
 	freeChunks freeID
 
+	usedSpaceG  prometheus.Gauge
+	usedInodesG prometheus.Gauge
+	txDist      prometheus.Histogram
+	txRestart   prometheus.Counter
+	opDist      prometheus.Histogram
+
 	en engine
 }
 
@@ -124,7 +131,57 @@ func newBaseMeta(addr string, conf *Config) *baseMeta {
 		msgCallbacks: &msgCallbacks{
 			callbacks: make(map[uint32]MsgCallback),
 		},
+
+		usedSpaceG: prometheus.NewGauge(prometheus.GaugeOpts{
+			Name: "used_space",
+			Help: "Total used space in bytes.",
+		}),
+		usedInodesG: prometheus.NewGauge(prometheus.GaugeOpts{
+			Name: "used_inodes",
+			Help: "Total number of inodes.",
+		}),
+		txDist: prometheus.NewHistogram(prometheus.HistogramOpts{
+			Name:    "transaction_durations_histogram_seconds",
+			Help:    "Transactions latency distributions.",
+			Buckets: prometheus.ExponentialBuckets(0.0001, 1.5, 30),
+		}),
+		txRestart: prometheus.NewCounter(prometheus.CounterOpts{
+			Name: "transaction_restart",
+			Help: "The number of times a transaction is restarted.",
+		}),
+		opDist: prometheus.NewHistogram(prometheus.HistogramOpts{
+			Name:    "meta_ops_durations_histogram_seconds",
+			Help:    "Operation latency distributions.",
+			Buckets: prometheus.ExponentialBuckets(0.0001, 1.5, 30),
+		}),
+	}
+}
+
+func (m *baseMeta) InitMetrics(reg prometheus.Registerer) {
+	if reg == nil {
+		return
 	}
+	reg.MustRegister(m.usedSpaceG)
+	reg.MustRegister(m.usedInodesG)
+	reg.MustRegister(m.txDist)
+	reg.MustRegister(m.txRestart)
+	reg.MustRegister(m.opDist)
+
+	go func() {
+		for {
+			var totalSpace, availSpace, iused, iavail uint64
+			err := m.StatFS(Background, &totalSpace, &availSpace, &iused, &iavail)
+			if err == 0 {
+				m.usedSpaceG.Set(float64(totalSpace - availSpace))
+				m.usedInodesG.Set(float64(iused))
+			}
+			utils.SleepWithJitter(time.Second * 10)
+		}
+	}()
+}
+
+func (m *baseMeta) timeit(start time.Time) {
+	m.opDist.Observe(time.Since(start).Seconds())
 }
 
 func (m *baseMeta) getBase() *baseMeta {
@@ -351,7 +408,7 @@ func (m *baseMeta) cleanupSlices() {
 }
 
 func (m *baseMeta) StatFS(ctx Context, totalspace, availspace, iused, iavail *uint64) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	var used, inodes int64
 	var err error
 	err = utils.WithTimeout(func() error {
@@ -420,7 +477,7 @@ func (m *baseMeta) Lookup(ctx Context, parent Ino, name string, inode *Ino, attr
 	if inode == nil || attr == nil {
 		return syscall.EINVAL // bad request
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	parent = m.checkRoot(parent)
 	if name == ".." {
 		if parent == m.root {
@@ -584,7 +641,7 @@ func (m *baseMeta) GetAttr(ctx Context, inode Ino, attr *Attr) syscall.Errno {
 	if m.conf.OpenCache > 0 && m.of.Check(inode, attr) {
 		return 0
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	var err syscall.Errno
 	if inode == 1 {
 		e := utils.WithTimeout(func() error {
@@ -641,7 +698,7 @@ func (m *baseMeta) Mknod(ctx Context, parent Ino, name string, _type uint8, mode
 		return syscall.ENOENT
 	}
 
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	if m.checkQuota(4<<10, 1) {
 		return syscall.ENOSPC
 	}
@@ -684,7 +741,7 @@ func (m *baseMeta) Link(ctx Context, inode, parent Ino, name string, attr *Attr)
 		return syscall.ENOENT
 	}
 
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	parent = m.checkRoot(parent)
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFE) }()
 	return m.en.doLink(ctx, inode, parent, name, attr)
@@ -695,7 +752,7 @@ func (m *baseMeta) ReadLink(ctx Context, inode Ino, path *[]byte) syscall.Errno
 		*path = target.([]byte)
 		return 0
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	target, err := m.en.doReadlink(ctx, inode)
 	if err != nil {
 		return errno(err)
@@ -716,7 +773,7 @@ func (m *baseMeta) Unlink(ctx Context, parent Ino, name string) syscall.Errno {
 		return syscall.EROFS
 	}
 
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	return m.en.doUnlink(ctx, m.checkRoot(parent), name)
 }
 
@@ -734,7 +791,7 @@ func (m *baseMeta) Rmdir(ctx Context, parent Ino, name string) syscall.Errno {
 		return syscall.EROFS
 	}
 
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	return m.en.doRmdir(ctx, m.checkRoot(parent), name)
 }
 
@@ -759,7 +816,7 @@ func (m *baseMeta) Rename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		return syscall.EINVAL
 	}
 
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	return m.en.doRename(ctx, m.checkRoot(parentSrc), nameSrc, m.checkRoot(parentDst), nameDst, flags, inode, attr)
 }
 
@@ -820,7 +877,7 @@ func (m *baseMeta) Readdir(ctx Context, inode Ino, plus uint8, entries *[]*Entry
 	if err := m.GetAttr(ctx, inode, &attr); err != 0 {
 		return err
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	if inode == m.root {
 		attr.Parent = m.root
 	}
@@ -847,7 +904,7 @@ func (m *baseMeta) SetXattr(ctx Context, inode Ino, name string, value []byte, f
 		return syscall.EINVAL
 	}
 
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	return m.en.doSetXattr(ctx, m.checkRoot(inode), name, value, flags)
 }
 
@@ -859,7 +916,7 @@ func (m *baseMeta) RemoveXattr(ctx Context, inode Ino, name string) syscall.Errn
 		return syscall.EINVAL
 	}
 
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	return m.en.doRemoveXattr(ctx, m.checkRoot(inode), name)
 }
 
diff --git a/pkg/meta/config.go b/pkg/meta/config.go
index 003e895c..aae04d76 100644
--- a/pkg/meta/config.go
+++ b/pkg/meta/config.go
@@ -50,6 +50,7 @@ type Format struct {
 	Bucket           string
 	AccessKey        string `json:",omitempty"`
 	SecretKey        string `json:",omitempty"`
+	SessionToken     string `json:",omitempty"`
 	BlockSize        int
 	Compression      string `json:",omitempty"`
 	Shards           int    `json:",omitempty"`
@@ -98,6 +99,9 @@ func (f *Format) RemoveSecret() {
 	if f.SecretKey != "" {
 		f.SecretKey = "removed"
 	}
+	if f.SessionToken != "" {
+		f.SessionToken = "removed"
+	}
 	if f.EncryptKey != "" {
 		f.EncryptKey = "removed"
 	}
@@ -137,7 +141,7 @@ func (f *Format) CheckVersion() error {
 }
 
 func (f *Format) Encrypt() error {
-	if f.KeyEncrypted || f.SecretKey == "" && f.EncryptKey == "" {
+	if f.KeyEncrypted || f.SecretKey == "" && f.EncryptKey == "" && f.SessionToken == "" {
 		return nil
 	}
 	key := md5.Sum([]byte(f.UUID))
@@ -149,38 +153,32 @@ func (f *Format) Encrypt() error {
 	if err != nil {
 		return fmt.Errorf("new GCM: %s", err)
 	}
-	nonce := make([]byte, 12)
-	encrypt := func(k string) string {
-		ciphertext := aesgcm.Seal(nil, nonce, []byte(k), nil)
+	encrypt := func(k *string) {
+		if *k == "" {
+			return
+		}
+		nonce := make([]byte, 12)
+		if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
+			logger.Fatalf("generate nonce for secret key: %s", err)
+		}
+		ciphertext := aesgcm.Seal(nil, nonce, []byte(*k), nil)
 		buf := make([]byte, 12+len(ciphertext))
 		copy(buf, nonce)
 		copy(buf[12:], ciphertext)
-		return base64.StdEncoding.EncodeToString(buf)
+		*k = base64.StdEncoding.EncodeToString(buf)
 	}
 
-	if f.SecretKey != "" {
-		if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
-			return fmt.Errorf("generate nonce for secret key: %s", err)
-		}
-		f.SecretKey = encrypt(f.SecretKey)
-	}
-	if f.EncryptKey != "" {
-		if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
-			return fmt.Errorf("generate nonce for encrypt key: %s", err)
-		}
-		f.EncryptKey = encrypt(f.EncryptKey)
-	}
+	encrypt(&f.SecretKey)
+	encrypt(&f.SessionToken)
+	encrypt(&f.EncryptKey)
 	f.KeyEncrypted = true
 	return nil
 }
 
 func (f *Format) Decrypt() error {
-	if !f.KeyEncrypted || f.SecretKey == "" && f.EncryptKey == "" {
+	if !f.KeyEncrypted {
 		return nil
 	}
-	if f.SecretKey == "removed" {
-		return fmt.Errorf("secret key was removed; please correct it with `config` command")
-	}
 	key := md5.Sum([]byte(f.UUID))
 	block, err := aes.NewCipher(key[:])
 	if err != nil {
@@ -190,29 +188,30 @@ func (f *Format) Decrypt() error {
 	if err != nil {
 		return fmt.Errorf("new GCM: %s", err)
 	}
-	decrypt := func(k *string) error {
-		buf, err := base64.StdEncoding.DecodeString(*k)
-		if err != nil {
-			return fmt.Errorf("decode key: %s", err)
+	decrypt := func(k *string) {
+		if *k == "" {
+			return
 		}
-		plaintext, err := aesgcm.Open(nil, buf[:12], buf[12:], nil)
-		if err != nil {
-			return fmt.Errorf("open cipher: %s", err)
+		if *k == "removed" {
+			err = fmt.Errorf("secret was removed; please correct it with `config` command")
+			return
 		}
-		*k = string(plaintext)
-		return nil
-	}
-
-	if f.SecretKey != "" {
-		if err = decrypt(&f.SecretKey); err != nil {
-			return err
+		buf, e := base64.StdEncoding.DecodeString(*k)
+		if e != nil {
+			err = fmt.Errorf("decode key: %s", e)
+			return
 		}
-	}
-	if f.EncryptKey != "" {
-		if err = decrypt(&f.EncryptKey); err != nil {
-			return err
+		plaintext, e := aesgcm.Open(nil, buf[:12], buf[12:], nil)
+		if e != nil {
+			err = fmt.Errorf("open cipher: %s", e)
+			return
 		}
+		*k = string(plaintext)
 	}
+
+	decrypt(&f.EncryptKey)
+	decrypt(&f.SecretKey)
+	decrypt(&f.SessionToken)
 	f.KeyEncrypted = false
-	return nil
+	return err
 }
diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go
index ca42d873..84349171 100644
--- a/pkg/meta/interface.go
+++ b/pkg/meta/interface.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/juicedata/juicefs/pkg/utils"
 	"github.com/juicedata/juicefs/pkg/version"
+	"github.com/prometheus/client_golang/prometheus"
 )
 
 const (
@@ -349,6 +350,7 @@ type Meta interface {
 
 	// getBase return the base engine.
 	getBase() *baseMeta
+	InitMetrics(registerer prometheus.Registerer)
 }
 
 type Creator func(driver, addr string, conf *Config) (Meta, error)
@@ -415,10 +417,6 @@ func newSessionInfo() *SessionInfo {
 	return &SessionInfo{Version: version.Version(), HostName: host, ProcessID: os.Getpid()}
 }
 
-func timeit(start time.Time) {
-	opDist.Observe(time.Since(start).Seconds())
-}
-
 // Get all paths of an inode
 func GetPaths(m Meta, ctx Context, inode Ino) []string {
 	getDirPath := func(ino Ino) (string, error) {
diff --git a/pkg/meta/metrics.go b/pkg/meta/metrics.go
deleted file mode 100644
index bcc79c20..00000000
--- a/pkg/meta/metrics.go
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * JuiceFS, Copyright 2021 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 "github.com/prometheus/client_golang/prometheus"
-
-var (
-	txDist = prometheus.NewHistogram(prometheus.HistogramOpts{
-		Name:    "transaction_durations_histogram_seconds",
-		Help:    "Transactions latency distributions.",
-		Buckets: prometheus.ExponentialBuckets(0.0001, 1.5, 30),
-	})
-	txRestart = prometheus.NewCounter(prometheus.CounterOpts{
-		Name: "transaction_restart",
-		Help: "The number of times a transaction is restarted.",
-	})
-	opDist = prometheus.NewHistogram(prometheus.HistogramOpts{
-		Name:    "meta_ops_durations_histogram_seconds",
-		Help:    "Operation latency distributions.",
-		Buckets: prometheus.ExponentialBuckets(0.0001, 1.5, 30),
-	})
-)
-
-func InitMetrics(registerer prometheus.Registerer) {
-	if registerer == nil {
-		return
-	}
-	registerer.MustRegister(txDist)
-	registerer.MustRegister(txRestart)
-	registerer.MustRegister(opDist)
-}
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 50075f0d..e780fac4 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -672,7 +672,7 @@ func (m *redisMeta) Resolve(ctx Context, parent Ino, path string, inode *Ino, at
 	if len(m.shaResolve) == 0 || m.conf.CaseInsensi || m.prefix != "" {
 		return syscall.ENOTSUP
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	parent = m.checkRoot(parent)
 	args := []string{parent.String(), path,
 		strconv.FormatUint(uint64(ctx.Uid()), 10),
@@ -763,7 +763,7 @@ func (m *redisMeta) txn(ctx Context, txf func(tx *redis.Tx) error, keys ...strin
 	_, _ = khash.Write([]byte(keys[0]))
 	h := uint(khash.Sum32())
 	start := time.Now()
-	defer func() { txDist.Observe(time.Since(start).Seconds()) }()
+	defer func() { m.txDist.Observe(time.Since(start).Seconds()) }()
 	m.txLock(h)
 	defer m.txUnlock(h)
 	// TODO: enable retry for some of idempodent transactions
@@ -778,7 +778,7 @@ func (m *redisMeta) txn(ctx Context, txf func(tx *redis.Tx) error, keys ...strin
 			err = nil
 		}
 		if err != nil && m.shouldRetry(err, retryOnFailture) {
-			txRestart.Add(1)
+			m.txRestart.Add(1)
 			logger.Debugf("Transaction failed, restart it (tried %d): %s", i+1, err)
 			lastErr = err
 			time.Sleep(time.Millisecond * time.Duration(rand.Int()%((i+1)*(i+1))))
@@ -793,7 +793,7 @@ func (m *redisMeta) txn(ctx Context, txf func(tx *redis.Tx) error, keys ...strin
 }
 
 func (m *redisMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, attr *Attr) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -907,7 +907,7 @@ func (m *redisMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, si
 	if size == 0 {
 		return syscall.EINVAL
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -976,7 +976,7 @@ func (m *redisMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, si
 }
 
 func (m *redisMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint8, attr *Attr) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFE) }()
 	return errno(m.txn(ctx, func(tx *redis.Tx) error {
@@ -1942,7 +1942,7 @@ func (m *redisMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) s
 		*chunks = cs
 		return 0
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	vals, err := m.rdb.LRange(ctx, m.chunkKey(inode, indx), 0, 1000000).Result()
 	if err != nil {
 		return errno(err)
@@ -1957,7 +1957,7 @@ func (m *redisMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) s
 }
 
 func (m *redisMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Slice) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -2016,7 +2016,7 @@ func (m *redisMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice
 }
 
 func (m *redisMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, offOut uint64, size uint64, flags uint32, copied *uint64) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(fout)
 	if f != nil {
 		f.Lock()
@@ -2778,7 +2778,7 @@ func (m *redisMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool,
 }
 
 func (m *redisMeta) GetXattr(ctx Context, inode Ino, name string, vbuff *[]byte) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	var err error
 	*vbuff, err = m.rdb.HGet(ctx, m.xattrKey(inode), name).Bytes()
@@ -2789,7 +2789,7 @@ func (m *redisMeta) GetXattr(ctx Context, inode Ino, name string, vbuff *[]byte)
 }
 
 func (m *redisMeta) ListXattr(ctx Context, inode Ino, names *[]byte) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	vals, err := m.rdb.HKeys(ctx, m.xattrKey(inode)).Result()
 	if err != nil {
@@ -3191,6 +3191,10 @@ func (m *redisMeta) DumpMeta(w io.Writer, root Ino) (err error) {
 		dm.Setting.SecretKey = "removed"
 		logger.Warnf("Secret key is removed for the sake of safety")
 	}
+	if dm.Setting.SessionToken != "" {
+		dm.Setting.SessionToken = "removed"
+		logger.Warnf("Session token is removed for the sake of safety")
+	}
 	bw, err := dm.writeJsonWithOutTree(w)
 	if err != nil {
 		return err
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index 88516d22..733259fc 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -158,6 +158,8 @@ type dbMeta struct {
 	*baseMeta
 	db   *xorm.Engine
 	snap *dbSnap
+
+	noReadOnlyTxn bool
 }
 
 type dbSnap struct {
@@ -621,7 +623,7 @@ func (m *dbMeta) txn(f func(s *xorm.Session) error, inodes ...Ino) error {
 		return syscall.EROFS
 	}
 	start := time.Now()
-	defer func() { txDist.Observe(time.Since(start).Seconds()) }()
+	defer func() { m.txDist.Observe(time.Since(start).Seconds()) }()
 	if len(inodes) > 0 {
 		if m.db.DriverName() == "sqlite3" {
 			// sqlite only allow one writer at a time
@@ -639,7 +641,7 @@ func (m *dbMeta) txn(f func(s *xorm.Session) error, inodes ...Ino) error {
 			err = nil
 		}
 		if err != nil && m.shouldRetry(err) {
-			txRestart.Add(1)
+			m.txRestart.Add(1)
 			logger.Debugf("Transaction failed, restart it (tried %d): %s", i+1, err)
 			lastErr = err
 			time.Sleep(time.Millisecond * time.Duration(i*i))
@@ -655,28 +657,37 @@ func (m *dbMeta) txn(f func(s *xorm.Session) error, inodes ...Ino) error {
 
 func (m *dbMeta) roTxn(f func(s *xorm.Session) error) error {
 	start := time.Now()
-	defer func() { txDist.Observe(time.Since(start).Seconds()) }()
+	defer func() { m.txDist.Observe(time.Since(start).Seconds()) }()
 	s := m.db.NewSession()
 	defer s.Close()
-	var opt = sql.TxOptions{
-		Isolation: sql.LevelRepeatableRead,
-		ReadOnly:  true,
+	var opt sql.TxOptions
+	if !m.noReadOnlyTxn {
+		opt.ReadOnly = true
+		opt.Isolation = sql.LevelRepeatableRead
 	}
+
 	var lastErr error
 	for i := 0; i < 50; i++ {
-		if err := s.BeginTx(&opt); err != nil {
+		err := s.BeginTx(&opt)
+		if err != nil && opt.ReadOnly && (strings.Contains(err.Error(), "READ") || strings.Contains(err.Error(), "driver does not support read-only transactions")) {
+			logger.Warnf("the database does not support read-only transaction")
+			m.noReadOnlyTxn = true
+			opt = sql.TxOptions{} // use default level
+			err = s.BeginTx(&opt)
+		}
+		if err != nil {
 			logger.Debugf("Start transaction failed, try again (tried %d): %s", i+1, err)
 			lastErr = err
 			time.Sleep(time.Millisecond * time.Duration(i*i))
 			continue
 		}
-		err := f(s)
+		err = f(s)
 		if eno, ok := err.(syscall.Errno); ok && eno == 0 {
 			err = nil
 		}
 		_ = s.Rollback()
 		if err != nil && m.shouldRetry(err) {
-			txRestart.Add(1)
+			m.txRestart.Add(1)
 			logger.Debugf("Read transaction failed, restart it (tried %d): %s", i+1, err)
 			lastErr = err
 			time.Sleep(time.Millisecond * time.Duration(i*i))
@@ -796,7 +807,7 @@ func clearSUGIDSQL(ctx Context, cur *node, set *Attr) {
 }
 
 func (m *dbMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint8, attr *Attr) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFE) }()
 	return errno(m.txn(func(s *xorm.Session) error {
@@ -883,7 +894,7 @@ func (m *dbMeta) appendSlice(s *xorm.Session, inode Ino, indx uint32, buf []byte
 }
 
 func (m *dbMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, attr *Attr) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -973,7 +984,7 @@ func (m *dbMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 	if size == 0 {
 		return syscall.EINVAL
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -1917,7 +1928,7 @@ func (m *dbMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) sysc
 		*chunks = cs
 		return 0
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	var c = chunk{Inode: inode, Indx: indx}
 	err := m.roTxn(func(s *xorm.Session) error {
 		_, err := s.MustCols("indx").Get(&c)
@@ -1939,7 +1950,7 @@ func (m *dbMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) sysc
 }
 
 func (m *dbMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Slice) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -2006,7 +2017,7 @@ func (m *dbMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 }
 
 func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, offOut uint64, size uint64, flags uint32, copied *uint64) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(fout)
 	if f != nil {
 		f.Lock()
@@ -2525,7 +2536,7 @@ func (m *dbMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, sh
 }
 
 func (m *dbMeta) GetXattr(ctx Context, inode Ino, name string, vbuff *[]byte) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	return errno(m.roTxn(func(s *xorm.Session) error {
 		var x = xattr{Inode: inode, Name: name}
@@ -2542,7 +2553,7 @@ func (m *dbMeta) GetXattr(ctx Context, inode Ino, name string, vbuff *[]byte) sy
 }
 
 func (m *dbMeta) ListXattr(ctx Context, inode Ino, names *[]byte) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	return errno(m.roTxn(func(s *xorm.Session) error {
 		var xs []xattr
@@ -2927,6 +2938,10 @@ func (m *dbMeta) DumpMeta(w io.Writer, root Ino) (err error) {
 			dm.Setting.SecretKey = "removed"
 			logger.Warnf("Secret key is removed for the sake of safety")
 		}
+		if dm.Setting.SessionToken != "" {
+			dm.Setting.SessionToken = "removed"
+			logger.Warnf("Session token is removed for the sake of safety")
+		}
 		bw, err := dm.writeJsonWithOutTree(w)
 		if err != nil {
 			return err
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index 51aa907a..dc7b638c 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -630,7 +630,7 @@ func (m *kvMeta) txn(f func(tx kvTxn) error, inodes ...Ino) error {
 		return syscall.EROFS
 	}
 	start := time.Now()
-	defer func() { txDist.Observe(time.Since(start).Seconds()) }()
+	defer func() { m.txDist.Observe(time.Since(start).Seconds()) }()
 	if len(inodes) > 0 {
 		m.txLock(uint(inodes[0]))
 		defer m.txUnlock(uint(inodes[0]))
@@ -642,7 +642,7 @@ func (m *kvMeta) txn(f func(tx kvTxn) error, inodes ...Ino) error {
 			err = nil
 		}
 		if err != nil && m.shouldRetry(err) {
-			txRestart.Add(1)
+			m.txRestart.Add(1)
 			logger.Debugf("Transaction failed, restart it (tried %d): %s", i+1, err)
 			lastErr = err
 			time.Sleep(time.Millisecond * time.Duration(rand.Int()%((i+1)*(i+1))))
@@ -736,7 +736,7 @@ func (m *kvMeta) doGetAttr(ctx Context, inode Ino, attr *Attr) syscall.Errno {
 }
 
 func (m *kvMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint8, attr *Attr) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFE) }()
 	return errno(m.txn(func(tx kvTxn) error {
@@ -807,7 +807,7 @@ func (m *kvMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint
 }
 
 func (m *kvMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, attr *Attr) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -888,7 +888,7 @@ func (m *kvMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 	if size == 0 {
 		return syscall.EINVAL
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -1640,7 +1640,7 @@ func (m *kvMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) sysc
 		*chunks = cs
 		return 0
 	}
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	val, err := m.get(m.chunkKey(inode, indx))
 	if err != nil {
 		return errno(err)
@@ -1658,7 +1658,7 @@ func (m *kvMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) sysc
 }
 
 func (m *kvMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Slice) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	f := m.of.find(inode)
 	if f != nil {
 		f.Lock()
@@ -1705,7 +1705,7 @@ func (m *kvMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 }
 
 func (m *kvMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, offOut uint64, size uint64, flags uint32, copied *uint64) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	var newSpace int64
 	f := m.of.find(fout)
 	if f != nil {
@@ -2173,7 +2173,7 @@ func (m *kvMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, sh
 }
 
 func (m *kvMeta) GetXattr(ctx Context, inode Ino, name string, vbuff *[]byte) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	buf, err := m.get(m.xattrKey(inode, name))
 	if err != nil {
@@ -2187,7 +2187,7 @@ func (m *kvMeta) GetXattr(ctx Context, inode Ino, name string, vbuff *[]byte) sy
 }
 
 func (m *kvMeta) ListXattr(ctx Context, inode Ino, names *[]byte) syscall.Errno {
-	defer timeit(time.Now())
+	defer m.timeit(time.Now())
 	inode = m.checkRoot(inode)
 	keys, err := m.scanKeys(m.xattrKey(inode, ""))
 	if err != nil {
@@ -2526,6 +2526,10 @@ func (m *kvMeta) DumpMeta(w io.Writer, root Ino) (err error) {
 		dm.Setting.SecretKey = "removed"
 		logger.Warnf("Secret key is removed for the sake of safety")
 	}
+	if dm.Setting.SessionToken != "" {
+		dm.Setting.SessionToken = "removed"
+		logger.Warnf("Session token is removed for the sake of safety")
+	}
 	bw, err := dm.writeJsonWithOutTree(w)
 	if err != nil {
 		return err
diff --git a/pkg/metric/metrics.go b/pkg/metric/metrics.go
index 1e6009da..fb785232 100644
--- a/pkg/metric/metrics.go
+++ b/pkg/metric/metrics.go
@@ -55,14 +55,6 @@ var (
 	}, func() float64 {
 		return time.Since(start).Seconds()
 	})
-	usedSpace = prometheus.NewGauge(prometheus.GaugeOpts{
-		Name: "used_space",
-		Help: "Total used space in bytes.",
-	})
-	usedInodes = prometheus.NewGauge(prometheus.GaugeOpts{
-		Name: "used_inodes",
-		Help: "Total number of inodes.",
-	})
 )
 
 func UpdateMetrics(m meta.Meta, registerer prometheus.Registerer) {
@@ -72,18 +64,6 @@ func UpdateMetrics(m meta.Meta, registerer prometheus.Registerer) {
 	registerer.MustRegister(cpu)
 	registerer.MustRegister(memory)
 	registerer.MustRegister(uptime)
-	registerer.MustRegister(usedSpace)
-	registerer.MustRegister(usedInodes)
-
-	for {
-		var totalSpace, availSpace, iused, iavail uint64
-		err := m.StatFS(meta.Background, &totalSpace, &availSpace, &iused, &iavail)
-		if err == 0 {
-			usedSpace.Set(float64(totalSpace - availSpace))
-			usedInodes.Set(float64(iused))
-		}
-		utils.SleepWithJitter(time.Second * 10)
-	}
 }
 
 func RegisterToConsul(consulAddr, metricsAddr, mountPoint string) {
diff --git a/pkg/object/azure.go b/pkg/object/azure.go
index 774df1f1..d5f47486 100644
--- a/pkg/object/azure.go
+++ b/pkg/object/azure.go
@@ -104,12 +104,18 @@ func (b *wasb) List(prefix, marker string, limit int64) ([]Object, error) {
 
 	limit32 := int32(limit)
 	pager := b.container.ListBlobsFlat(&azblob.ContainerListBlobFlatSegmentOptions{Prefix: &prefix, Marker: &marker, Maxresults: &(limit32)})
+	if pager.Err() != nil {
+		return nil, pager.Err()
+	}
 	if pager.NextPage(ctx) {
 		b.marker = *pager.PageResponse().NextMarker
 	} else {
 		b.marker = ""
 	}
-	n := len(pager.PageResponse().Segment.BlobItems)
+	var n int
+	if pager.PageResponse().Segment != nil {
+		n = len(pager.PageResponse().Segment.BlobItems)
+	}
 	objs := make([]Object, n)
 	for i := 0; i < n; i++ {
 		blob := pager.PageResponse().Segment.BlobItems[i]
@@ -150,7 +156,7 @@ func autoWasbEndpoint(containerName, accountName, scheme string, credential *azb
 	return endpoint, nil
 }
 
-func newWabs(endpoint, accountName, accountKey string) (ObjectStorage, error) {
+func newWabs(endpoint, accountName, accountKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -159,51 +165,31 @@ func newWabs(endpoint, accountName, accountKey string) (ObjectStorage, error) {
 		return nil, fmt.Errorf("Invalid endpoint: %v, error: %v", endpoint, err)
 	}
 	hostParts := strings.SplitN(uri.Host, ".", 2)
-
-	scheme := ""
-	domain := ""
+	containerName := hostParts[0]
 	// Connection string support: DefaultEndpointsProtocol=[http|https];AccountName=***;AccountKey=***;EndpointSuffix=[core.windows.net|core.chinacloudapi.cn]
 	if connString := os.Getenv("AZURE_STORAGE_CONNECTION_STRING"); connString != "" {
-		items := strings.Split(connString, ";")
-		for _, item := range items {
-			if item = strings.TrimSpace(item); item == "" {
-				continue
-			}
-			parts := strings.SplitN(item, "=", 2)
-			if len(parts) != 2 {
-				return nil, fmt.Errorf("Invalid connection string item: %s", item)
-			}
-			// Arguments from command line take precedence
-			if parts[0] == "DefaultEndpointsProtocol" && scheme == "" {
-				scheme = parts[1]
-			} else if parts[0] == "AccountName" && accountName == "" {
-				accountName = parts[1]
-			} else if parts[0] == "AccountKey" && accountKey == "" {
-				accountKey = parts[1]
-			} else if parts[0] == "EndpointSuffix" && domain == "" {
-				domain = parts[1]
-			}
+		var client azblob.ContainerClient
+		if client, err = azblob.NewContainerClientFromConnectionString(connString, containerName, nil); err != nil {
+			return nil, err
 		}
+		return &wasb{container: &client, cName: containerName}, nil
 	}
+
 	credential, err := azblob.NewSharedKeyCredential(accountName, accountKey)
 	if err != nil {
 		return nil, err
 	}
-
-	if scheme == "" {
-		scheme = "https"
-	}
-	containerName := hostParts[0]
+	var domain string
 	if len(hostParts) > 1 {
-		// Arguments from command line take precedence
 		domain = hostParts[1]
-	} else if domain == "" {
-		if domain, err = autoWasbEndpoint(containerName, accountName, scheme, credential); err != nil {
-			return nil, fmt.Errorf("Unable to get endpoint of container %s: %s", containerName, err)
+		if !strings.HasPrefix(hostParts[1], "blob") {
+			domain = fmt.Sprintf("blob.%s", hostParts[1])
 		}
+	} else if domain, err = autoWasbEndpoint(containerName, accountName, uri.Scheme, credential); err != nil {
+		return nil, fmt.Errorf("Unable to get endpoint of container %s: %s", containerName, err)
 	}
 
-	client, err := azblob.NewContainerClientWithSharedKey(fmt.Sprintf("%s://%s.%s/%s", scheme, accountName, domain, containerName), credential, nil)
+	client, err := azblob.NewContainerClientWithSharedKey(fmt.Sprintf("%s://%s.%s/%s", uri.Scheme, accountName, domain, containerName), credential, nil)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/object/b2.go b/pkg/object/b2.go
index 954f1538..63bf95be 100644
--- a/pkg/object/b2.go
+++ b/pkg/object/b2.go
@@ -144,7 +144,7 @@ func (c *b2client) List(prefix, marker string, limit int64) ([]Object, error) {
 
 // TODO: support multipart upload using S3 client
 
-func newB2(endpoint, keyID, applicationKey string) (ObjectStorage, error) {
+func newB2(endpoint, keyID, applicationKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/bos.go b/pkg/object/bos.go
index 65f24ee4..299c52c3 100644
--- a/pkg/object/bos.go
+++ b/pkg/object/bos.go
@@ -199,7 +199,7 @@ func autoBOSEndpoint(bucketName, accessKey, secretKey string) (string, error) {
 	}
 }
 
-func newBOS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newBOS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/cos.go b/pkg/object/cos.go
index 8a01022d..2557286b 100644
--- a/pkg/object/cos.go
+++ b/pkg/object/cos.go
@@ -192,11 +192,12 @@ func (c *COS) ListUploads(marker string) ([]*PendingPart, string, error) {
 	return parts, result.NextKeyMarker, nil
 }
 
-func autoCOSEndpoint(bucketName, accessKey, secretKey string) (string, error) {
+func autoCOSEndpoint(bucketName, accessKey, secretKey, token string) (string, error) {
 	client := cos.NewClient(nil, &http.Client{
 		Transport: &cos.AuthorizationTransport{
-			SecretID:  accessKey,
-			SecretKey: secretKey,
+			SecretID:     accessKey,
+			SecretKey:    secretKey,
+			SessionToken: token,
 		},
 	})
 	client.UserAgent = UserAgent
@@ -215,7 +216,7 @@ func autoCOSEndpoint(bucketName, accessKey, secretKey string) (string, error) {
 	return "", fmt.Errorf("bucket %q doesnot exist", bucketName)
 }
 
-func newCOS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newCOS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -231,7 +232,7 @@ func newCOS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 	}
 
 	if len(hostParts) == 1 {
-		if endpoint, err = autoCOSEndpoint(hostParts[0], accessKey, secretKey); err != nil {
+		if endpoint, err = autoCOSEndpoint(hostParts[0], accessKey, secretKey, token); err != nil {
 			return nil, fmt.Errorf("Unable to get endpoint of bucket %s: %s", hostParts[0], err)
 		}
 		if uri, err = url.ParseRequestURI(endpoint); err != nil {
@@ -243,9 +244,10 @@ func newCOS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 	b := &cos.BaseURL{BucketURL: uri}
 	client := cos.NewClient(b, &http.Client{
 		Transport: &cos.AuthorizationTransport{
-			SecretID:  accessKey,
-			SecretKey: secretKey,
-			Transport: httpClient.Transport,
+			SecretID:     accessKey,
+			SecretKey:    secretKey,
+			SessionToken: token,
+			Transport:    httpClient.Transport,
 		},
 	})
 	client.UserAgent = UserAgent
diff --git a/pkg/object/eos.go b/pkg/object/eos.go
index c6cc2cf9..dc08afee 100644
--- a/pkg/object/eos.go
+++ b/pkg/object/eos.go
@@ -39,7 +39,7 @@ func (s *eos) String() string {
 	return fmt.Sprintf("eos://%s/", s.s3client.bucket)
 }
 
-func newEos(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newEos(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -59,6 +59,9 @@ func newEos(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 	if secretKey == "" {
 		secretKey = os.Getenv("EOS_SECRET_KEY")
 	}
+	if token == "" {
+		token = os.Getenv("EOS_TOKEN")
+	}
 
 	awsConfig := &aws.Config{
 		Endpoint:         &endpoint,
@@ -66,7 +69,7 @@ func newEos(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		DisableSSL:       aws.Bool(!ssl),
 		S3ForcePathStyle: aws.Bool(false),
 		HTTPClient:       httpClient,
-		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, token),
 	}
 
 	ses, err := session.NewSession(awsConfig)
diff --git a/pkg/object/etcd.go b/pkg/object/etcd.go
index dbabf99e..914a668a 100644
--- a/pkg/object/etcd.go
+++ b/pkg/object/etcd.go
@@ -155,7 +155,7 @@ func buildTlsConfig(u *url.URL) (*tls.Config, error) {
 	return nil, nil
 }
 
-func newEtcd(addr, user, passwd string) (ObjectStorage, error) {
+func newEtcd(addr, user, passwd, token string) (ObjectStorage, error) {
 	if !strings.Contains(addr, "://") {
 		addr = "http://" + addr
 	}
diff --git a/pkg/object/file.go b/pkg/object/file.go
index c5bcce0f..c1272070 100644
--- a/pkg/object/file.go
+++ b/pkg/object/file.go
@@ -406,7 +406,7 @@ func (d *filestore) Chown(path string, owner, group string) error {
 	return os.Chown(p, uid, gid)
 }
 
-func newDisk(root, accesskey, secretkey string) (ObjectStorage, error) {
+func newDisk(root, accesskey, secretkey, token string) (ObjectStorage, error) {
 	// For Windows, the path looks like /C:/a/b/c/
 	if runtime.GOOS == "windows" && strings.HasPrefix(root, "/") {
 		root = root[1:]
diff --git a/pkg/object/gs.go b/pkg/object/gs.go
index d559b4d9..e15997f9 100644
--- a/pkg/object/gs.go
+++ b/pkg/object/gs.go
@@ -157,7 +157,7 @@ func (g *gs) List(prefix, marker string, limit int64) ([]Object, error) {
 	return objs, nil
 }
 
-func newGS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newGS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("gs://%s", endpoint)
 	}
diff --git a/pkg/object/hdfs.go b/pkg/object/hdfs.go
index d9481885..5fa22838 100644
--- a/pkg/object/hdfs.go
+++ b/pkg/object/hdfs.go
@@ -315,7 +315,7 @@ func (h *hdfsclient) Chown(key string, owner, group string) error {
 	return h.c.Chown(h.path(key), owner, group)
 }
 
-func newHDFS(addr, username, sk string) (ObjectStorage, error) {
+func newHDFS(addr, username, sk, token string) (ObjectStorage, error) {
 	conf, err := hadoopconf.LoadFromEnvironment()
 	if err != nil {
 		return nil, fmt.Errorf("Problem loading configuration: %s", err)
diff --git a/pkg/object/ibmcos.go b/pkg/object/ibmcos.go
index 48b6ebf0..25cf499a 100644
--- a/pkg/object/ibmcos.go
+++ b/pkg/object/ibmcos.go
@@ -233,7 +233,7 @@ func (s *ibmcos) ListUploads(marker string) ([]*PendingPart, string, error) {
 	return parts, nextMarker, nil
 }
 
-func newIBMCOS(endpoint, apiKey, serviceInstanceID string) (ObjectStorage, error) {
+func newIBMCOS(endpoint, apiKey, serviceInstanceID, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/jss.go b/pkg/object/jss.go
index bb36bc3f..73c1e5b6 100644
--- a/pkg/object/jss.go
+++ b/pkg/object/jss.go
@@ -49,7 +49,7 @@ func (j *jss) Copy(dst, src string) error {
 	return err
 }
 
-func newJSS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newJSS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -66,7 +66,7 @@ func newJSS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		DisableSSL:       aws.Bool(!ssl),
 		S3ForcePathStyle: aws.Bool(true),
 		HTTPClient:       httpClient,
-		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, token),
 	}
 
 	ses, err := session.NewSession(awsConfig)
diff --git a/pkg/object/ks3.go b/pkg/object/ks3.go
index d9cd11eb..d5378291 100644
--- a/pkg/object/ks3.go
+++ b/pkg/object/ks3.go
@@ -251,7 +251,7 @@ var ks3Regions = map[string]string{
 	"sgp":          "SINGAPORE",
 }
 
-func newKS3(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newKS3(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -281,7 +281,7 @@ func newKS3(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		DisableSSL:       !ssl,
 		HTTPClient:       httpClient,
 		S3ForcePathStyle: true,
-		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, token),
 	}
 
 	return &ks3{bucket, s3.New(awsConfig), nil}, nil
diff --git a/pkg/object/mem.go b/pkg/object/mem.go
index 79d73b0c..eddd04c0 100644
--- a/pkg/object/mem.go
+++ b/pkg/object/mem.go
@@ -165,7 +165,7 @@ func (m *memStore) ListAll(prefix, marker string) (<-chan Object, error) {
 	return nil, notSupported
 }
 
-func newMem(endpoint, accesskey, secretkey string) (ObjectStorage, error) {
+func newMem(endpoint, accesskey, secretkey, token string) (ObjectStorage, error) {
 	store := &memStore{name: endpoint}
 	store.objects = make(map[string]*mobj)
 	return store, nil
diff --git a/pkg/object/minio.go b/pkg/object/minio.go
index 45fd5ed5..fa74c07a 100644
--- a/pkg/object/minio.go
+++ b/pkg/object/minio.go
@@ -39,7 +39,7 @@ func (m *minio) String() string {
 	return fmt.Sprintf("minio://%s/%s/", *m.s3client.ses.Config.Endpoint, m.s3client.bucket)
 }
 
-func newMinio(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newMinio(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("http://%s", endpoint)
 	}
@@ -62,7 +62,7 @@ func newMinio(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		secretKey = os.Getenv("MINIO_SECRET_KEY")
 	}
 	if accessKey != "" {
-		awsConfig.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, "")
+		awsConfig.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, token)
 	}
 
 	ses, err := session.NewSession(awsConfig)
diff --git a/pkg/object/nos.go b/pkg/object/nos.go
index cb4b30b6..20f0c16c 100644
--- a/pkg/object/nos.go
+++ b/pkg/object/nos.go
@@ -154,7 +154,7 @@ func (s *nos) List(prefix, marker string, limit int64) ([]Object, error) {
 	return objs, nil
 }
 
-func newNOS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newNOS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/object_storage.go b/pkg/object/object_storage.go
index dd3b270e..137351a9 100644
--- a/pkg/object/object_storage.go
+++ b/pkg/object/object_storage.go
@@ -140,7 +140,7 @@ func (s DefaultObjectStorage) ListAll(prefix, marker string) (<-chan Object, err
 	return nil, notSupported
 }
 
-type Creator func(bucket, accessKey, secretKey string) (ObjectStorage, error)
+type Creator func(bucket, accessKey, secretKey, token string) (ObjectStorage, error)
 
 var storages = make(map[string]Creator)
 
@@ -148,11 +148,11 @@ func Register(name string, register Creator) {
 	storages[name] = register
 }
 
-func CreateStorage(name, endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func CreateStorage(name, endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	f, ok := storages[name]
 	if ok {
 		logger.Debugf("Creating %s storage at endpoint %s", name, endpoint)
-		return f(endpoint, accessKey, secretKey)
+		return f(endpoint, accessKey, secretKey, token)
 	}
 	return nil, fmt.Errorf("invalid storage: %s", name)
 }
diff --git a/pkg/object/obs.go b/pkg/object/obs.go
index 17097690..d341a60d 100644
--- a/pkg/object/obs.go
+++ b/pkg/object/obs.go
@@ -253,14 +253,14 @@ func (s *obsClient) ListUploads(marker string) ([]*PendingPart, string, error) {
 	return parts, nextMarker, nil
 }
 
-func autoOBSEndpoint(bucketName, accessKey, secretKey string) (string, error) {
+func autoOBSEndpoint(bucketName, accessKey, secretKey, token string) (string, error) {
 	region := obsDefaultRegion
 	if r := os.Getenv("HWCLOUD_DEFAULT_REGION"); r != "" {
 		region = r
 	}
 	endpoint := fmt.Sprintf("https://obs.%s.myhuaweicloud.com", region)
 
-	obsCli, err := obs.New(accessKey, secretKey, endpoint)
+	obsCli, err := obs.New(accessKey, secretKey, endpoint, obs.WithSecurityToken(token))
 	if err != nil {
 		return "", err
 	}
@@ -279,7 +279,7 @@ func autoOBSEndpoint(bucketName, accessKey, secretKey string) (string, error) {
 	return "", fmt.Errorf("bucket %q does not exist", bucketName)
 }
 
-func newOBS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newOBS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -300,7 +300,7 @@ func newOBS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 
 	var region string
 	if len(hostParts) == 1 {
-		if endpoint, err = autoOBSEndpoint(bucketName, accessKey, secretKey); err != nil {
+		if endpoint, err = autoOBSEndpoint(bucketName, accessKey, secretKey, token); err != nil {
 			return nil, fmt.Errorf("cannot get location of bucket %s: %q", bucketName, err)
 		}
 		if !strings.HasPrefix(endpoint, "http") {
@@ -325,7 +325,8 @@ func newOBS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 
 	// Empty proxy url string has no effect
 	// there is a bug in the retry of PUT (did not call Seek(0,0) before retry), so disable the retry here
-	c, err := obs.New(accessKey, secretKey, endpoint, obs.WithProxyUrl(urlString), obs.WithMaxRetryCount(0))
+	c, err := obs.New(accessKey, secretKey, endpoint, obs.WithSecurityToken(token),
+		obs.WithProxyUrl(urlString), obs.WithMaxRetryCount(0))
 	if err != nil {
 		return nil, fmt.Errorf("fail to initialize OBS: %q", err)
 	}
diff --git a/pkg/object/oos.go b/pkg/object/oos.go
index eb22fbb0..7887823f 100644
--- a/pkg/object/oos.go
+++ b/pkg/object/oos.go
@@ -57,7 +57,7 @@ func (s *oos) List(prefix, marker string, limit int64) ([]Object, error) {
 	return objs, err
 }
 
-func newOOS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newOOS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -78,7 +78,7 @@ func newOOS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		DisableSSL:       aws.Bool(!ssl),
 		S3ForcePathStyle: aws.Bool(!forcePathStyle),
 		HTTPClient:       httpClient,
-		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, token),
 	}
 
 	ses, err := session.NewSession(awsConfig)
diff --git a/pkg/object/oss.go b/pkg/object/oss.go
index 15aff26a..fcce105e 100644
--- a/pkg/object/oss.go
+++ b/pkg/object/oss.go
@@ -325,7 +325,7 @@ func autoOSSEndpoint(bucketName, accessKey, secretKey, securityToken string) (st
 	return fmt.Sprintf("https://%s.aliyuncs.com", bucketLocation), nil
 }
 
-func newOSS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newOSS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -341,12 +341,12 @@ func newOSS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		domain = uri.Scheme + "://" + hostParts[1]
 	}
 
-	securityToken := ""
+	var refresh bool
 	if accessKey == "" {
 		// try environment variable
 		accessKey = os.Getenv("ALICLOUD_ACCESS_KEY_ID")
 		secretKey = os.Getenv("ALICLOUD_ACCESS_KEY_SECRET")
-		securityToken = os.Getenv("SECURITY_TOKEN")
+		token = os.Getenv("SECURITY_TOKEN")
 
 		if accessKey == "" {
 			if cred, err := fetchStsToken(); err != nil {
@@ -354,24 +354,20 @@ func newOSS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 			} else {
 				accessKey = cred.AccessKeyId
 				secretKey = cred.AccessKeySecret
-				securityToken = cred.SecurityToken
+				token = cred.SecurityToken
+				refresh = true
 			}
 		}
 	}
 
 	if domain == "" {
-		if domain, err = autoOSSEndpoint(bucketName, accessKey, secretKey, securityToken); err != nil {
+		if domain, err = autoOSSEndpoint(bucketName, accessKey, secretKey, token); err != nil {
 			return nil, fmt.Errorf("Unable to get endpoint of bucket %s: %s", bucketName, err)
 		}
 		logger.Debugf("Use endpoint %q", domain)
 	}
 
-	var client *oss.Client
-	if securityToken == "" {
-		client, err = oss.New(domain, accessKey, secretKey)
-	} else {
-		client, err = oss.New(domain, accessKey, secretKey, oss.SecurityToken(securityToken))
-	}
+	client, err := oss.New(domain, accessKey, secretKey, oss.SecurityToken(token))
 	if err != nil {
 		return nil, fmt.Errorf("Cannot create OSS client with endpoint %s: %s", endpoint, err)
 	}
@@ -391,7 +387,7 @@ func newOSS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 	}
 
 	o := &ossClient{client: client, bucket: bucket}
-	if securityToken != "" {
+	if token != "" && refresh {
 		go func() {
 			for {
 				next := o.refreshToken()
diff --git a/pkg/object/qingstor.go b/pkg/object/qingstor.go
index 1da0cb2b..56f2491a 100644
--- a/pkg/object/qingstor.go
+++ b/pkg/object/qingstor.go
@@ -256,7 +256,7 @@ func (q *qingstor) ListUploads(marker string) ([]*PendingPart, string, error) {
 	return parts, nextMarker, nil
 }
 
-func newQingStor(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newQingStor(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/qiniu.go b/pkg/object/qiniu.go
index f7799476..b1076979 100644
--- a/pkg/object/qiniu.go
+++ b/pkg/object/qiniu.go
@@ -166,7 +166,7 @@ func (q *qiniu) List(prefix, marker string, limit int64) ([]Object, error) {
 	return objs, nil
 }
 
-func newQiniu(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newQiniu(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -189,7 +189,7 @@ func newQiniu(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		region = endpoint[:strings.LastIndex(endpoint, "-")]
 	}
 	awsConfig := &aws.Config{
-		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, token),
 		Endpoint:         &endpoint,
 		Region:           &region,
 		DisableSSL:       aws.Bool(uri.Scheme == "http"),
diff --git a/pkg/object/redis.go b/pkg/object/redis.go
index 7aed71b4..7eff5e98 100644
--- a/pkg/object/redis.go
+++ b/pkg/object/redis.go
@@ -160,7 +160,7 @@ func (t *redisStore) Head(key string) (Object, error) {
 	}, err
 }
 
-func newRedis(url, user, passwd string) (ObjectStorage, error) {
+func newRedis(url, user, passwd, token string) (ObjectStorage, error) {
 	opt, err := redis.ParseURL(url)
 	if err != nil {
 		return nil, fmt.Errorf("parse %s: %s", url, err)
diff --git a/pkg/object/s3.go b/pkg/object/s3.go
index 95ef8297..e618232a 100644
--- a/pkg/object/s3.go
+++ b/pkg/object/s3.go
@@ -341,7 +341,7 @@ func parseRegion(endpoint string) string {
 	return region
 }
 
-func newS3(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newS3(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		if len(strings.Split(endpoint, ".")) > 1 && !strings.HasSuffix(endpoint, ".amazonaws.com") {
 			endpoint = fmt.Sprintf("http://%s", endpoint)
@@ -436,7 +436,7 @@ func newS3(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 	if accessKey == "anonymous" {
 		awsConfig.Credentials = credentials.AnonymousCredentials
 	} else if accessKey != "" {
-		awsConfig.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, "")
+		awsConfig.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, token)
 	}
 	if ep != "" {
 		awsConfig.Endpoint = aws.String(ep)
diff --git a/pkg/object/scs.go b/pkg/object/scs.go
index bfa01d71..351e4fc4 100644
--- a/pkg/object/scs.go
+++ b/pkg/object/scs.go
@@ -167,7 +167,7 @@ func (s *scsClient) ListUploads(marker string) ([]*PendingPart, string, error) {
 	return nil, "", notSupported
 }
 
-func newSCS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newSCS(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/scw.go b/pkg/object/scw.go
index 33493245..51ff8b74 100644
--- a/pkg/object/scw.go
+++ b/pkg/object/scw.go
@@ -39,7 +39,7 @@ func (s *scw) String() string {
 	return fmt.Sprintf("scw://%s/", s.s3client.bucket)
 }
 
-func newScw(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newScw(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -66,7 +66,7 @@ func newScw(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		DisableSSL:       aws.Bool(!ssl),
 		S3ForcePathStyle: aws.Bool(false),
 		HTTPClient:       httpClient,
-		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, token),
 	}
 
 	ses, err := session.NewSession(awsConfig)
diff --git a/pkg/object/sftp.go b/pkg/object/sftp.go
index 8ae4eed6..c9f4f027 100644
--- a/pkg/object/sftp.go
+++ b/pkg/object/sftp.go
@@ -431,7 +431,7 @@ func SshInteractive(user, instruction string, questions []string, echos []bool)
 	return answers, nil
 }
 
-func newSftp(endpoint, username, pass string) (ObjectStorage, error) {
+func newSftp(endpoint, username, pass, token string) (ObjectStorage, error) {
 	idx := strings.LastIndex(endpoint, ":")
 	host, port, err := net.SplitHostPort(endpoint[:idx])
 	if err != nil && strings.Contains(err.Error(), "missing port") {
diff --git a/pkg/object/sharding.go b/pkg/object/sharding.go
index 0ba70393..8fea94e1 100644
--- a/pkg/object/sharding.go
+++ b/pkg/object/sharding.go
@@ -190,7 +190,7 @@ func (s *sharded) CompleteUpload(key string, uploadID string, parts []*Part) err
 	return s.pick(key).CompleteUpload(key, uploadID, parts)
 }
 
-func NewSharded(name, endpoint, ak, sk string, shards int) (ObjectStorage, error) {
+func NewSharded(name, endpoint, ak, sk, token string, shards int) (ObjectStorage, error) {
 	stores := make([]ObjectStorage, shards)
 	var err error
 	for i := range stores {
@@ -198,7 +198,7 @@ func NewSharded(name, endpoint, ak, sk string, shards int) (ObjectStorage, error
 		if strings.HasSuffix(ep, "%!(EXTRA int=0)") {
 			return nil, fmt.Errorf("can not generate different endpoint using %s", endpoint)
 		}
-		stores[i], err = CreateStorage(name, ep, ak, sk)
+		stores[i], err = CreateStorage(name, ep, ak, sk, token)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/object/space.go b/pkg/object/space.go
index 21f1420c..6db6c4ba 100644
--- a/pkg/object/space.go
+++ b/pkg/object/space.go
@@ -38,7 +38,7 @@ func (s *space) String() string {
 	return fmt.Sprintf("space://%s/", s.s3client.bucket)
 }
 
-func newSpace(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newSpace(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -55,7 +55,7 @@ func newSpace(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		DisableSSL:       aws.Bool(!ssl),
 		S3ForcePathStyle: aws.Bool(false),
 		HTTPClient:       httpClient,
-		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, token),
 	}
 
 	ses, err := session.NewSession(awsConfig)
diff --git a/pkg/object/speedy.go b/pkg/object/speedy.go
index 41ca3fd3..fc0c16fa 100644
--- a/pkg/object/speedy.go
+++ b/pkg/object/speedy.go
@@ -102,7 +102,7 @@ func (s *speedy) List(prefix, marker string, limit int64) ([]Object, error) {
 	return objs, nil
 }
 
-func newSpeedy(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newSpeedy(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/sql.go b/pkg/object/sql.go
index f1228cc7..3d8250b1 100644
--- a/pkg/object/sql.go
+++ b/pkg/object/sql.go
@@ -184,21 +184,21 @@ func newSQLStore(driver, addr, user, password string) (ObjectStorage, error) {
 }
 
 func init() {
-	Register("sqlite3", func(addr, user, pass string) (ObjectStorage, error) {
+	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)
 	})
-	Register("mysql", func(addr, user, pass string) (ObjectStorage, error) {
+	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)
 	})
-	Register("postgres", func(addr, user, pass string) (ObjectStorage, error) {
+	Register("postgres", func(addr, user, pass, token string) (ObjectStorage, error) {
 		return newSQLStore("postgres", addr, user, pass)
 	})
 }
diff --git a/pkg/object/swift.go b/pkg/object/swift.go
index 37fdba57..e4cfe20c 100644
--- a/pkg/object/swift.go
+++ b/pkg/object/swift.go
@@ -103,7 +103,7 @@ func (s *swiftOSS) Head(key string) (Object, error) {
 	}, err
 }
 
-func newSwiftOSS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newSwiftOSS(endpoint, username, apiKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("http://%s", endpoint)
 	}
@@ -126,9 +126,10 @@ func newSwiftOSS(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 	authURL := uri.Scheme + "://" + host + "/auth/v1.0"
 
 	conn := swift.Connection{
-		UserName: accessKey,
-		ApiKey:   secretKey,
-		AuthUrl:  authURL,
+		UserName:  username,
+		ApiKey:    apiKey,
+		AuthToken: token,
+		AuthUrl:   authURL,
 	}
 	err = conn.Authenticate()
 	if err != nil {
diff --git a/pkg/object/tikv.go b/pkg/object/tikv.go
index c303ce5e..32c6a181 100644
--- a/pkg/object/tikv.go
+++ b/pkg/object/tikv.go
@@ -110,7 +110,7 @@ func (t *tikv) List(prefix, marker string, limit int64) ([]Object, error) {
 	return objs, nil
 }
 
-func newTiKV(endpoint, accesskey, secretkey string) (ObjectStorage, error) {
+func newTiKV(endpoint, accesskey, secretkey, token string) (ObjectStorage, error) {
 	var plvl string // TiKV (PingCap) uses uber-zap logging, make it less verbose
 	switch logger.Level {
 	case logrus.TraceLevel:
diff --git a/pkg/object/ufile.go b/pkg/object/ufile.go
index 48897fa8..e5cdd707 100644
--- a/pkg/object/ufile.go
+++ b/pkg/object/ufile.go
@@ -321,7 +321,7 @@ func (u *ufile) ListUploads(marker string) ([]*PendingPart, string, error) {
 	return parts, out.NextMarker, nil
 }
 
-func newUFile(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newUFile(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/upyun.go b/pkg/object/upyun.go
index 76419a27..ac912ffc 100644
--- a/pkg/object/upyun.go
+++ b/pkg/object/upyun.go
@@ -129,7 +129,7 @@ func (u *up) List(prefix, marker string, limit int64) ([]Object, error) {
 	return nil, u.err
 }
 
-func newUpyun(endpoint, user, passwd string) (ObjectStorage, error) {
+func newUpyun(endpoint, user, passwd, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
diff --git a/pkg/object/wasabi.go b/pkg/object/wasabi.go
index 352fb513..a9306b76 100644
--- a/pkg/object/wasabi.go
+++ b/pkg/object/wasabi.go
@@ -38,7 +38,7 @@ func (s *wasabi) String() string {
 	return fmt.Sprintf("wasabi://%s/", s.s3client.bucket)
 }
 
-func newWasabi(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
+func newWasabi(endpoint, accessKey, secretKey, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("https://%s", endpoint)
 	}
@@ -58,7 +58,7 @@ func newWasabi(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 		DisableSSL:       aws.Bool(!ssl),
 		S3ForcePathStyle: aws.Bool(false),
 		HTTPClient:       httpClient,
-		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, token),
 	}
 
 	ses, err := session.NewSession(awsConfig)
diff --git a/pkg/object/webdav.go b/pkg/object/webdav.go
index 8e02a375..7d3adacf 100644
--- a/pkg/object/webdav.go
+++ b/pkg/object/webdav.go
@@ -186,7 +186,7 @@ func (w *webdav) ListAll(prefix, marker string) (<-chan Object, error) {
 	return listed, nil
 }
 
-func newWebDAV(endpoint, user, passwd string) (ObjectStorage, error) {
+func newWebDAV(endpoint, user, passwd, token string) (ObjectStorage, error) {
 	if !strings.Contains(endpoint, "://") {
 		endpoint = fmt.Sprintf("http://%s", endpoint)
 	}
diff --git a/pkg/vfs/internal.go b/pkg/vfs/internal.go
index eede654d..d0cad65f 100644
--- a/pkg/vfs/internal.go
+++ b/pkg/vfs/internal.go
@@ -22,6 +22,7 @@ import (
 	"os"
 	"strconv"
 	"strings"
+	"sync"
 	"sync/atomic"
 	"syscall"
 	"time"
@@ -41,6 +42,31 @@ const (
 	trashInode      = meta.TrashInode
 )
 
+var controlMutex sync.Mutex
+var controlHandlers = make(map[uint32]uint64)
+
+func (v *VFS) getControlHandle(pid uint32) uint64 {
+	controlMutex.Lock()
+	defer controlMutex.Unlock()
+	fh := controlHandlers[pid]
+	if fh == 0 {
+		h := v.newHandle(controlInode)
+		fh = h.fh
+		controlHandlers[pid] = fh
+	}
+	return fh
+}
+
+func (v *VFS) releaseControlHandle(pid uint32) {
+	controlMutex.Lock()
+	defer controlMutex.Unlock()
+	fh := controlHandlers[pid]
+	if fh != 0 {
+		v.releaseHandle(controlInode, fh)
+		delete(controlHandlers, pid)
+	}
+}
+
 type internalNode struct {
 	inode Ino
 	name  string
diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go
index e4628666..a5cbe8aa 100644
--- a/pkg/vfs/vfs.go
+++ b/pkg/vfs/vfs.go
@@ -367,6 +367,13 @@ func (v *VFS) Create(ctx Context, parent Ino, name string, mode uint16, cumask u
 }
 
 func (v *VFS) Open(ctx Context, ino Ino, flags uint32) (entry *meta.Entry, fh uint64, err syscall.Errno) {
+	defer func() {
+		if entry != nil {
+			logit(ctx, "open (%d): %s [fh:%d]", ino, strerr(err), fh)
+		} else {
+			logit(ctx, "open (%d): %s", ino, strerr(err))
+		}
+	}()
 	var attr = &Attr{}
 	if IsSpecialNode(ino) {
 		if ino != controlInode && (flags&O_ACCMODE) != syscall.O_RDONLY {
@@ -392,13 +399,7 @@ func (v *VFS) Open(ctx Context, ino Ino, flags uint32) (entry *meta.Entry, fh ui
 		}
 		return
 	}
-	defer func() {
-		if entry != nil {
-			logit(ctx, "open (%d): %s [fh:%d]", ino, strerr(err), fh)
-		} else {
-			logit(ctx, "open (%d): %s", ino, strerr(err))
-		}
-	}()
+
 	err = v.Meta.Open(ctx, ino, flags, attr)
 	if err == 0 {
 		v.UpdateLength(ino, attr)
@@ -444,6 +445,8 @@ func (v *VFS) ReleaseHandler(ino Ino, fh uint64) {
 }
 
 func (v *VFS) Release(ctx Context, ino Ino, fh uint64) {
+	var err syscall.Errno
+	defer func() { logit(ctx, "release (%d): %s", ino, strerr(err)) }()
 	if IsSpecialNode(ino) {
 		if ino == logInode {
 			closeAccessLog(fh)
@@ -451,8 +454,6 @@ func (v *VFS) Release(ctx Context, ino Ino, fh uint64) {
 		v.releaseHandle(ino, fh)
 		return
 	}
-	var err syscall.Errno
-	defer func() { logit(ctx, "release (%d): %s", ino, strerr(err)) }()
 	if fh > 0 {
 		f := v.findHandle(ino, fh)
 		if f != nil {
@@ -486,6 +487,9 @@ func (v *VFS) Read(ctx Context, ino Ino, buf []byte, off uint64, fh uint64) (n i
 		if ino == logInode {
 			n = readAccessLog(fh, buf)
 		} else {
+			if ino == controlInode && runtime.GOOS == "darwin" {
+				fh = v.getControlHandle(ctx.Pid())
+			}
 			h := v.findHandle(ino, fh)
 			if h == nil {
 				err = syscall.EBADF
@@ -509,6 +513,7 @@ func (v *VFS) Read(ctx Context, ino Ino, buf []byte, off uint64, fh uint64) (n i
 				h.off += 1 << 20
 				h.data = h.data[1<<20:]
 			}
+			logit(ctx, "read (%d,%d,%d,%d): %s (%d)", ino, size, off, fh, strerr(err), n)
 		}
 		return
 	}
@@ -550,7 +555,10 @@ func (v *VFS) Read(ctx Context, ino Ino, buf []byte, off uint64, fh uint64) (n i
 
 func (v *VFS) Write(ctx Context, ino Ino, buf []byte, off, fh uint64) (err syscall.Errno) {
 	size := uint64(len(buf))
-	defer func() { logit(ctx, "write (%d,%d,%d): %s", ino, size, off, strerr(err)) }()
+	if ino == controlInode && runtime.GOOS == "darwin" {
+		fh = v.getControlHandle(ctx.Pid())
+	}
+	defer func() { logit(ctx, "write (%d,%d,%d,%d): %s", ino, size, off, fh, strerr(err)) }()
 	h := v.findHandle(ino, fh)
 	if h == nil {
 		err = syscall.EBADF
@@ -710,7 +718,11 @@ func (v *VFS) CopyFileRange(ctx Context, nodeIn Ino, fhIn, offIn uint64, nodeOut
 }
 
 func (v *VFS) Flush(ctx Context, ino Ino, fh uint64, lockOwner uint64) (err syscall.Errno) {
-	defer func() { logit(ctx, "flush (%d): %s", ino, strerr(err)) }()
+	if ino == controlInode && runtime.GOOS == "darwin" {
+		fh = v.getControlHandle(ctx.Pid())
+		defer v.releaseControlHandle(ctx.Pid())
+	}
+	defer func() { logit(ctx, "flush (%d,%d): %s", ino, fh, strerr(err)) }()
 	h := v.findHandle(ino, fh)
 	if h == nil {
 		err = syscall.EBADF
diff --git a/sdk/java/libjfs/main.go b/sdk/java/libjfs/main.go
index befd9618..6c86e904 100644
--- a/sdk/java/libjfs/main.go
+++ b/sdk/java/libjfs/main.go
@@ -415,7 +415,7 @@ func jfs_init(cname, jsonConf, user, group, superuser, supergroup *C.char) uintp
 			if jConf.PushGateway != "" {
 				push2Gateway(jConf.PushGateway, jConf.PushAuth, interval, registry, commonLabels)
 			}
-			meta.InitMetrics(registerer)
+			m.InitMetrics(registerer)
 			vfs.InitMetrics(registerer)
 			go metric.UpdateMetrics(m, registerer)
 		}
@@ -510,6 +510,7 @@ func jfs_init(cname, jsonConf, user, group, superuser, supergroup *C.char) uintp
 			logger.Errorf("Initialize failed: %s", err)
 			return nil
 		}
+		jfs.InitMetrics(registerer)
 		return jfs
 	})
 }


Test patch:
diff --git a/.github/workflows/aitests.yml b/.github/workflows/aitests.yml
index d4fd9f14..b1093cd6 100644
--- a/.github/workflows/aitests.yml
+++ b/.github/workflows/aitests.yml
@@ -1,19 +1,19 @@
 name: "aitests"
 
 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/**'
+#  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 18 * * *'
+    - cron:  '30 20 * * *'
   workflow_dispatch:
 
 
@@ -76,19 +76,20 @@ jobs:
           cd /jfs/
           git clone https://github.com/sanwan/SVHNClassifier
           cd SVHNClassifier
-          git checkout 3429204
+          git checkout 1a8c6f1
           mkdir data
           cd data
           wget -O train.tar.gz -nv http://ufldl.stanford.edu/housenumbers/train.tar.gz
           wget -O test.tar.gz -nv http://ufldl.stanford.edu/housenumbers/test.tar.gz
-          wget -O extra.tar.gz -nv http://ufldl.stanford.edu/housenumbers/extra.tar.gz
           ls -lah
           time tar -zxf train.tar.gz
           time tar -zxf test.tar.gz
-          time tar -zxf extra.tar.gz
+          mkdir extra
+          mv test/1.png extra/
+          cp test/digitStruct.mat extra/
+          cp test/see_bboxes.m extra/
           rm train.tar.gz
           rm test.tar.gz
-          rm extra.tar.gz
           ls -lah
           du -sh
           cd ..
diff --git a/.github/workflows/aitests_limitcache.yml b/.github/workflows/aitests_limitcache.yml
index 891c51b9..760aed27 100644
--- a/.github/workflows/aitests_limitcache.yml
+++ b/.github/workflows/aitests_limitcache.yml
@@ -1,19 +1,19 @@
 name: "aitests_limitcache"
 
 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/**'
+#  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 18 * * *'
+    - cron:  '30 20 * * *'
   workflow_dispatch:
 
 
@@ -77,19 +77,18 @@ jobs:
           cd /jfs/
           git clone https://github.com/sanwan/SVHNClassifier
           cd SVHNClassifier
-          git checkout 3429204
+          git checkout 1a8c6f1
           mkdir data
           cd data
           wget -O train.tar.gz -nv http://ufldl.stanford.edu/housenumbers/train.tar.gz
           wget -O test.tar.gz -nv http://ufldl.stanford.edu/housenumbers/test.tar.gz
-          wget -O extra.tar.gz -nv http://ufldl.stanford.edu/housenumbers/extra.tar.gz
           ls -lah
           time tar -zxf train.tar.gz
           time tar -zxf test.tar.gz
-          time tar -zxf extra.tar.gz
-          rm train.tar.gz
-          rm test.tar.gz
-          rm extra.tar.gz
+          mkdir extra
+          mv test/1.png extra/
+          cp test/digitStruct.mat extra/
+          cp test/see_bboxes.m extra/
           ls -lah
           du -sh
           cd ..
diff --git a/.github/workflows/elastictest.yml b/.github/workflows/elastictest.yml
index 338c165f..c4e7b160 100644
--- a/.github/workflows/elastictest.yml
+++ b/.github/workflows/elastictest.yml
@@ -1,17 +1,17 @@
 name: "elastictest"
 
 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/**'
+#  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 * * *'
 
diff --git a/.github/workflows/mysqltest_bigtable.yml b/.github/workflows/mysqltest_bigtable.yml
index 3a0bacb1..0a06b572 100644
--- a/.github/workflows/mysqltest_bigtable.yml
+++ b/.github/workflows/mysqltest_bigtable.yml
@@ -1,17 +1,17 @@
 name: "mysqltest"
 
 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/**'
+#  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:
diff --git a/.github/workflows/mysqltest_smalltable.yml b/.github/workflows/mysqltest_smalltable.yml
index dd915d2a..4988d621 100644
--- a/.github/workflows/mysqltest_smalltable.yml
+++ b/.github/workflows/mysqltest_smalltable.yml
@@ -1,18 +1,19 @@
 name: "mysqltest"
 
 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/**'
-
+#  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:
diff --git a/.github/workflows/mysqltest_smalltablelocal.yml b/.github/workflows/mysqltest_smalltablelocal.yml
index 61144fa3..22804065 100644
--- a/.github/workflows/mysqltest_smalltablelocal.yml
+++ b/.github/workflows/mysqltest_smalltablelocal.yml
@@ -1,17 +1,17 @@
 name: "mysqltestlocal"
 
 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/**'
+#  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:
diff --git a/.github/workflows/mysqltest_smalltablelocalchangedir.yml b/.github/workflows/mysqltest_smalltablelocalchangedir.yml
index ace6b815..882bd999 100644
--- a/.github/workflows/mysqltest_smalltablelocalchangedir.yml
+++ b/.github/workflows/mysqltest_smalltablelocalchangedir.yml
@@ -1,17 +1,17 @@
 name: "mysqltestchangedir"
 
 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/**'
+#  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:
diff --git a/.github/workflows/pjdfstest.yml b/.github/workflows/pjdfstest.yml
index 73fbb77d..c372bc87 100644
--- a/.github/workflows/pjdfstest.yml
+++ b/.github/workflows/pjdfstest.yml
@@ -2,13 +2,17 @@ name: "pjdfstest"
 
 on:
   push:
-    branches: [ main ]
+    branches:
+      - 'main'
+      - 'release-**'
     paths-ignore:
       - 'docs/**'
       - '**.md'
   pull_request:
     #The branches below must be a subset of the branches above
-    branches: [ main ]
+    branches:
+      - 'main'
+      - 'release-**'
     paths-ignore:
       - 'docs/**'
       - '**.md'
diff --git a/cmd/config_test.go b/cmd/config_test.go
index 51a04edd..31c363cd 100644
--- a/cmd/config_test.go
+++ b/cmd/config_test.go
@@ -65,7 +65,7 @@ func TestConfig(t *testing.T) {
 	if err = Main([]string{"", "config", testMeta, "--capacity", "10", "--inodes", "1000000"}); err != nil {
 		t.Fatalf("config: %s", err)
 	}
-	if err = Main([]string{"", "config", testMeta, "--bucket", "/tmp/newBucket", "--access-key", "testAK", "--secret-key", "testSK"}); err != nil {
+	if err = Main([]string{"", "config", testMeta, "--bucket", "/tmp/newBucket", "--access-key", "testAK", "--secret-key", "testSK", "--session-token", "token"}); err != nil {
 		t.Fatalf("config: %s", err)
 	}
 	if data, err = getStdout([]string{"", "config", testMeta}); err != nil {
@@ -75,7 +75,7 @@ func TestConfig(t *testing.T) {
 		t.Fatalf("json unmarshal: %s", err)
 	}
 	if format.Capacity != 10<<30 || format.Inodes != 1000000 ||
-		format.Bucket != "/tmp/newBucket/" || format.AccessKey != "testAK" || format.SecretKey != "removed" {
+		format.Bucket != "/tmp/newBucket/" || format.AccessKey != "testAK" || format.SecretKey != "removed" || format.SessionToken != "removed" {
 		t.Fatalf("unexpect format: %+v", format)
 	}
 }
diff --git a/cmd/mdtest.go b/cmd/mdtest.go
index b75e7448..7b25f40b 100644
--- a/cmd/mdtest.go
+++ b/cmd/mdtest.go
@@ -16,7 +16,6 @@ import (
 	"github.com/juicedata/juicefs/pkg/fs"
 	"github.com/juicedata/juicefs/pkg/meta"
 	"github.com/juicedata/juicefs/pkg/metric"
-	"github.com/juicedata/juicefs/pkg/vfs"
 	"github.com/urfave/cli/v2"
 )
 
@@ -181,7 +180,7 @@ $ juicefs mdtest redis://localhost /test1`,
 	}
 }
 
-func initForMdtest(c *cli.Context, mp string, metaUrl string) (meta.Meta, chunk.ChunkStore, *vfs.Config) {
+func initForMdtest(c *cli.Context, mp string, metaUrl string) *fs.FileSystem {
 	metaConf := getMetaConf(c, mp, c.Bool("read-only"))
 	m := meta.NewClient(metaUrl, metaConf)
 	format, err := m.Load(true)
@@ -220,7 +219,12 @@ func initForMdtest(c *cli.Context, mp string, metaUrl string) (meta.Meta, chunk.
 	if c.IsSet("consul") {
 		metric.RegisterToConsul(c.String("consul"), metricsAddr, conf.Meta.MountPoint)
 	}
-	return m, store, conf
+	jfs, err := fs.NewFileSystem(conf, m, store)
+	if err != nil {
+		logger.Fatalf("initialize failed: %s", err)
+	}
+	jfs.InitMetrics(registerer)
+	return jfs
 }
 
 func mdtest(c *cli.Context) error {
@@ -228,11 +232,7 @@ func mdtest(c *cli.Context) error {
 	metaUrl := c.Args().Get(0)
 	rootDir := c.Args().Get(1)
 	removePassword(metaUrl)
-	m, store, conf := initForMdtest(c, "mdtest", metaUrl)
-	jfs, err := fs.NewFileSystem(conf, m, store)
-	if err != nil {
-		logger.Fatalf("initialize failed: %s", err)
-	}
+	jfs := initForMdtest(c, "mdtest", metaUrl)
 	runTest(jfs, rootDir, c.Int("threads"), c.Int("dirs"), c.Int("depth"), c.Int("files"), c.Int("write"))
-	return m.CloseSession()
+	return jfs.Meta().CloseSession()
 }
diff --git a/cmd/sync_test.go b/cmd/sync_test.go
index b809c1d5..dbd0496b 100644
--- a/cmd/sync_test.go
+++ b/cmd/sync_test.go
@@ -33,7 +33,7 @@ func TestSync(t *testing.T) {
 	minioDir := "synctest"
 	localDir := "/tmp/synctest"
 	defer os.RemoveAll(localDir)
-	storage, err := object.CreateStorage("minio", os.Getenv("MINIO_TEST_BUCKET"), os.Getenv("MINIO_ACCESS_KEY"), os.Getenv("MINIO_SECRET_KEY"))
+	storage, err := object.CreateStorage("minio", os.Getenv("MINIO_TEST_BUCKET"), os.Getenv("MINIO_ACCESS_KEY"), os.Getenv("MINIO_SECRET_KEY"), "")
 	if err != nil {
 		t.Fatalf("create storage failed: %v", err)
 	}
diff --git a/pkg/chunk/cached_store_test.go b/pkg/chunk/cached_store_test.go
index dd3181b0..70c1e8b8 100644
--- a/pkg/chunk/cached_store_test.go
+++ b/pkg/chunk/cached_store_test.go
@@ -104,7 +104,7 @@ var defaultConf = Config{
 }
 
 func TestStoreDefault(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	_ = os.RemoveAll(defaultConf.CacheDir)
 	store := NewCachedStore(mem, defaultConf, nil)
 	testStore(t, store)
@@ -117,7 +117,7 @@ func TestStoreDefault(t *testing.T) {
 }
 
 func TestStoreMemCache(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.CacheDir = "memory"
 	store := NewCachedStore(mem, conf, nil)
@@ -130,7 +130,7 @@ func TestStoreMemCache(t *testing.T) {
 	}
 }
 func TestStoreCompressed(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.Compress = "lz4"
 	conf.AutoCreate = false
@@ -139,7 +139,7 @@ func TestStoreCompressed(t *testing.T) {
 }
 
 func TestStoreLimited(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.UploadLimit = 1 << 20
 	conf.DownloadLimit = 1 << 20
@@ -148,7 +148,7 @@ func TestStoreLimited(t *testing.T) {
 }
 
 func TestStoreFull(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.FreeSpace = 0.9999
 	store := NewCachedStore(mem, conf, nil)
@@ -156,7 +156,7 @@ func TestStoreFull(t *testing.T) {
 }
 
 func TestStoreSmallBuffer(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.BufferSize = 1 << 20
 	store := NewCachedStore(mem, conf, nil)
@@ -164,7 +164,7 @@ func TestStoreSmallBuffer(t *testing.T) {
 }
 
 func TestStoreAsync(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.Writeback = true
 	p := filepath.Join(conf.CacheDir, stagingDir, "chunks/0/0/123_0_4")
@@ -186,7 +186,7 @@ func TestStoreAsync(t *testing.T) {
 }
 
 func TestStoreDelayed(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.Writeback = true
 	conf.UploadDelay = time.Millisecond * 200
@@ -204,7 +204,7 @@ func TestStoreDelayed(t *testing.T) {
 }
 
 func TestStoreMultiBuckets(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.HashPrefix = true
 	store := NewCachedStore(mem, conf, nil)
@@ -212,7 +212,7 @@ func TestStoreMultiBuckets(t *testing.T) {
 }
 
 func TestFillCache(t *testing.T) {
-	mem, _ := object.CreateStorage("mem", "", "", "")
+	mem, _ := object.CreateStorage("mem", "", "", "", "")
 	conf := defaultConf
 	conf.CacheSize = 10
 	_ = os.RemoveAll(conf.CacheDir)
@@ -246,7 +246,7 @@ func TestFillCache(t *testing.T) {
 }
 
 func BenchmarkCachedRead(b *testing.B) {
-	blob, _ := object.CreateStorage("mem", "", "", "")
+	blob, _ := object.CreateStorage("mem", "", "", "", "")
 	config := defaultConf
 	config.BlockSize = 4 << 20
 	store := NewCachedStore(blob, config, nil)
@@ -269,7 +269,7 @@ func BenchmarkCachedRead(b *testing.B) {
 }
 
 func BenchmarkUncachedRead(b *testing.B) {
-	blob, _ := object.CreateStorage("mem", "", "", "")
+	blob, _ := object.CreateStorage("mem", "", "", "", "")
 	config := defaultConf
 	config.BlockSize = 4 << 20
 	config.CacheSize = 0
diff --git a/pkg/chunk/disk_cache_test.go b/pkg/chunk/disk_cache_test.go
index c810fc76..22d3c91d 100644
--- a/pkg/chunk/disk_cache_test.go
+++ b/pkg/chunk/disk_cache_test.go
@@ -24,7 +24,7 @@ import (
 )
 
 func TestNewCacheStore(t *testing.T) {
-	s := newCacheStore(defaultConf.CacheDir, 1<<30, 1, &defaultConf, nil)
+	s := newCacheStore(nil, defaultConf.CacheDir, 1<<30, 1, &defaultConf, nil)
 	if s == nil {
 		t.Fatalf("Create new cache store failed")
 	}
@@ -53,7 +53,7 @@ func TestExpand(t *testing.T) {
 
 func BenchmarkLoadCached(b *testing.B) {
 	dir := b.TempDir()
-	s := newCacheStore(filepath.Join(dir, "diskCache"), 1<<30, 1, &defaultConf, nil)
+	s := newCacheStore(nil, filepath.Join(dir, "diskCache"), 1<<30, 1, &defaultConf, nil)
 	p := NewPage(make([]byte, 1024))
 	key := "/chunks/1_1024"
 	s.cache(key, p, false)
@@ -70,8 +70,8 @@ func BenchmarkLoadCached(b *testing.B) {
 
 func BenchmarkLoadUncached(b *testing.B) {
 	dir := b.TempDir()
-	s := newCacheStore(filepath.Join(dir, "diskCache"), 1<<30, 1, &defaultConf, nil)
-	key := "/chunks/222_1024"
+	s := newCacheStore(nil, filepath.Join(dir, "diskCache"), 1<<30, 1, &defaultConf, nil)
+	key := "chunks/222_1024"
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 		if f, e := s.load(key); e != nil {
diff --git a/pkg/fs/fs_test.go b/pkg/fs/fs_test.go
index 73f4c1ca..2d3725d8 100644
--- a/pkg/fs/fs_test.go
+++ b/pkg/fs/fs_test.go
@@ -288,7 +288,7 @@ func createTestFS(t *testing.T) *FileSystem {
 		AttrTimeout:     time.Millisecond * 100,
 		AccessLog:       "/tmp/juicefs.access.log",
 	}
-	objStore, _ := object.CreateStorage("mem", "", "", "")
+	objStore, _ := object.CreateStorage("mem", "", "", "", "")
 	store := chunk.NewCachedStore(objStore, *conf.Chunk, nil)
 	jfs, err := NewFileSystem(&conf, m, store)
 	if err != nil {
diff --git a/pkg/fuse/fuse_test.go b/pkg/fuse/fuse_test.go
index 75e3460c..b0b79333 100644
--- a/pkg/fuse/fuse_test.go
+++ b/pkg/fuse/fuse_test.go
@@ -84,7 +84,7 @@ func mount(url, mp string) {
 		CacheDir:   "memory",
 	}
 
-	blob, err := object.CreateStorage(strings.ToLower(format.Storage), format.Bucket, format.AccessKey, format.SecretKey)
+	blob, err := object.CreateStorage(strings.ToLower(format.Storage), format.Bucket, format.AccessKey, format.SecretKey, format.SessionToken)
 	if err != nil {
 		log.Fatalf("object storage: %s", err)
 	}
diff --git a/pkg/meta/config_test.go b/pkg/meta/config_test.go
index 61678163..fe6724d7 100644
--- a/pkg/meta/config_test.go
+++ b/pkg/meta/config_test.go
@@ -16,29 +16,39 @@
 
 package meta
 
-import "testing"
+import (
+	"strings"
+	"testing"
+)
 
 func TestRemoveSecret(t *testing.T) {
-	format := Format{Name: "test", SecretKey: "testSecret", EncryptKey: "testEncrypt"}
+	format := Format{Name: "test", SecretKey: "testSecret", EncryptKey: "testEncrypt", SessionToken: "token"}
+	if err := format.Encrypt(); err != nil {
+		t.Fatal(err)
+	}
 
 	format.RemoveSecret()
-	if format.SecretKey != "removed" || format.EncryptKey != "removed" {
+	if format.SecretKey != "removed" || format.EncryptKey != "removed" || format.SessionToken != "removed" {
 		t.Fatalf("invalid format: %+v", format)
 	}
+
+	if err := format.Decrypt(); err != nil && !strings.Contains(err.Error(), "secret was removed") {
+		t.Fatal(err)
+	}
 }
 
 func TestEncrypt(t *testing.T) {
-	format := Format{Name: "test", SecretKey: "testSecret", EncryptKey: "testEncrypt"}
+	format := Format{Name: "test", SecretKey: "testSecret", SessionToken: "token", EncryptKey: "testEncrypt"}
 	if err := format.Encrypt(); err != nil {
 		t.Fatalf("Format encrypt: %s", err)
 	}
-	if format.SecretKey == "testSecret" || format.EncryptKey == "testEncrypt" {
+	if format.SecretKey == "testSecret" || format.SessionToken == "token" || format.EncryptKey == "testEncrypt" {
 		t.Fatalf("invalid format: %+v", format)
 	}
 	if err := format.Decrypt(); err != nil {
 		t.Fatalf("Format decrypt: %s", err)
 	}
-	if format.SecretKey != "testSecret" || format.EncryptKey != "testEncrypt" {
+	if format.SecretKey != "testSecret" || format.SessionToken != "token" || format.EncryptKey != "testEncrypt" {
 		t.Fatalf("invalid format: %+v", format)
 	}
 }
diff --git a/pkg/meta/metrics_test.go b/pkg/meta/metrics_test.go
deleted file mode 100644
index 743687c2..00000000
--- a/pkg/meta/metrics_test.go
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * JuiceFS, Copyright 2021 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 (
-	"testing"
-
-	"github.com/prometheus/client_golang/prometheus"
-)
-
-func TestInitMetrics(t *testing.T) {
-	registry := prometheus.NewRegistry()
-	InitMetrics(registry)
-	for _, collector := range []prometheus.Collector{txDist, txRestart, opDist} {
-		if _, ok := registry.Register(collector).(prometheus.AlreadyRegisteredError); !ok {
-			t.Fatalf("TestInitMetrics Failed")
-		}
-	}
-}
diff --git a/pkg/object/encrypt_test.go b/pkg/object/encrypt_test.go
index 02795cf3..81c53c66 100644
--- a/pkg/object/encrypt_test.go
+++ b/pkg/object/encrypt_test.go
@@ -137,7 +137,7 @@ func TestAESGCM(t *testing.T) {
 }
 
 func TestEncryptedStore(t *testing.T) {
-	s, _ := CreateStorage("mem", "", "", "")
+	s, _ := CreateStorage("mem", "", "", "", "")
 	kc := NewRSAEncryptor(testkey)
 	dc := NewAESEncryptor(kc)
 	es := NewEncrypted(s, dc)
diff --git a/pkg/object/filesystem_test.go b/pkg/object/filesystem_test.go
index af343b85..35d33287 100644
--- a/pkg/object/filesystem_test.go
+++ b/pkg/object/filesystem_test.go
@@ -44,7 +44,7 @@ func testKeysEqual(objs []Object, expectedKeys []string) error {
 }
 
 func TestDisk2(t *testing.T) {
-	s, _ := newDisk("/tmp/abc/", "", "")
+	s, _ := newDisk("/tmp/abc/", "", "", "")
 	testFileSystem(t, s)
 }
 
@@ -52,7 +52,7 @@ func TestSftp2(t *testing.T) {
 	if os.Getenv("SFTP_HOST") == "" {
 		t.SkipNow()
 	}
-	sftp, _ := newSftp(os.Getenv("SFTP_HOST"), os.Getenv("SFTP_USER"), os.Getenv("SFTP_PASS"))
+	sftp, _ := newSftp(os.Getenv("SFTP_HOST"), os.Getenv("SFTP_USER"), os.Getenv("SFTP_PASS"), "")
 	testFileSystem(t, sftp)
 }
 
@@ -60,7 +60,7 @@ func TestHDFS2(t *testing.T) {
 	if os.Getenv("HDFS_ADDR") == "" {
 		t.Skip()
 	}
-	dfs, _ := newHDFS(os.Getenv("HDFS_ADDR"), "", "")
+	dfs, _ := newHDFS(os.Getenv("HDFS_ADDR"), "", "", "")
 	testFileSystem(t, dfs)
 }
 
diff --git a/pkg/object/object_storage_test.go b/pkg/object/object_storage_test.go
index 33c102a3..54903ce9 100644
--- a/pkg/object/object_storage_test.go
+++ b/pkg/object/object_storage_test.go
@@ -263,12 +263,12 @@ func testStorage(t *testing.T, s ObjectStorage) {
 }
 
 func TestMem(t *testing.T) {
-	m, _ := newMem("", "", "")
+	m, _ := newMem("", "", "", "")
 	testStorage(t, m)
 }
 
 func TestDisk(t *testing.T) {
-	s, _ := newDisk("/tmp/abc/", "", "")
+	s, _ := newDisk("/tmp/abc/", "", "", "")
 	testStorage(t, s)
 }
 
@@ -277,7 +277,7 @@ func TestQingStor(t *testing.T) {
 		t.SkipNow()
 	}
 	s, _ := newQingStor(os.Getenv("QY_ENDPOINT"),
-		os.Getenv("QY_ACCESS_KEY"), os.Getenv("QY_SECRET_KEY"))
+		os.Getenv("QY_ACCESS_KEY"), os.Getenv("QY_SECRET_KEY"), "")
 	testStorage(t, s)
 
 	//private cloud
@@ -285,7 +285,7 @@ func TestQingStor(t *testing.T) {
 		t.SkipNow()
 	}
 	s2, _ := newQingStor("http://test.jn1.is.shanhe.com",
-		os.Getenv("PRIVATE_QY_ACCESS_KEY"), os.Getenv("PRIVATE_QY_SECRET_KEY"))
+		os.Getenv("PRIVATE_QY_ACCESS_KEY"), os.Getenv("PRIVATE_QY_SECRET_KEY"), "")
 	testStorage(t, s2)
 }
 
@@ -294,7 +294,9 @@ func TestS3(t *testing.T) {
 		t.SkipNow()
 	}
 	s, _ := newS3(os.Getenv("AWS_ENDPOINT"),
-		os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY"))
+		os.Getenv("AWS_ACCESS_KEY_ID"),
+		os.Getenv("AWS_SECRET_ACCESS_KEY"),
+		os.Getenv("AWS_SESSION_TOKEN"))
 	testStorage(t, s)
 }
 
@@ -303,7 +305,8 @@ func TestOSS(t *testing.T) {
 		t.SkipNow()
 	}
 	s, _ := newOSS(os.Getenv("ALICLOUD_ENDPOINT"),
-		os.Getenv("ALICLOUD_ACCESS_KEY_ID"), os.Getenv("ALICLOUD_ACCESS_KEY_SECRET"))
+		os.Getenv("ALICLOUD_ACCESS_KEY_ID"),
+		os.Getenv("ALICLOUD_ACCESS_KEY_SECRET"), "")
 	testStorage(t, s)
 }
 
@@ -312,7 +315,7 @@ func TestUFile(t *testing.T) {
 		t.SkipNow()
 	}
 	ufile, _ := newUFile(os.Getenv("UCLOUD_ENDPOINT"),
-		os.Getenv("UCLOUD_PUBLIC_KEY"), os.Getenv("UCLOUD_PRIVATE_KEY"))
+		os.Getenv("UCLOUD_PUBLIC_KEY"), os.Getenv("UCLOUD_PRIVATE_KEY"), "")
 	testStorage(t, ufile)
 }
 
@@ -320,7 +323,7 @@ func TestGS(t *testing.T) {
 	if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") == "" {
 		t.SkipNow()
 	}
-	gs, _ := newGS(os.Getenv("GOOGLE_ENDPOINT"), "", "")
+	gs, _ := newGS(os.Getenv("GOOGLE_ENDPOINT"), "", "", "")
 	testStorage(t, gs)
 }
 
@@ -329,7 +332,7 @@ func TestQiniu(t *testing.T) {
 		t.SkipNow()
 	}
 	qiniu, _ := newQiniu(os.Getenv("QINIU_ENDPOINT"),
-		os.Getenv("QINIU_ACCESS_KEY"), os.Getenv("QINIU_SECRET_KEY"))
+		os.Getenv("QINIU_ACCESS_KEY"), os.Getenv("QINIU_SECRET_KEY"), "")
 	testStorage(t, qiniu)
 	//qiniu, _ = newQiniu("https://test.cn-north-1-s3.qiniu.com",
 	//	os.Getenv("QINIU_ACCESS_KEY"), os.Getenv("QINIU_SECRET_KEY"))
@@ -341,7 +344,7 @@ func TestKS3(t *testing.T) {
 		t.SkipNow()
 	}
 	ks3, _ := newKS3(os.Getenv("KS3_ENDPOINT"),
-		os.Getenv("KS3_ACCESS_KEY"), os.Getenv("KS3_SECRET_KEY"))
+		os.Getenv("KS3_ACCESS_KEY"), os.Getenv("KS3_SECRET_KEY"), "")
 	testStorage(t, ks3)
 }
 
@@ -351,7 +354,7 @@ func TestCOS(t *testing.T) {
 	}
 	cos, _ := newCOS(
 		os.Getenv("COS_ENDPOINT"),
-		os.Getenv("COS_SECRETID"), os.Getenv("COS_SECRETKEY"))
+		os.Getenv("COS_SECRETID"), os.Getenv("COS_SECRETKEY"), "")
 	testStorage(t, cos)
 }
 
@@ -359,8 +362,9 @@ func TestAzure(t *testing.T) {
 	if os.Getenv("AZURE_STORAGE_ACCOUNT") == "" {
 		t.SkipNow()
 	}
+	//https://containersName.core.windows.net
 	abs, _ := newWabs(os.Getenv("AZURE_ENDPOINT"),
-		os.Getenv("AZURE_STORAGE_ACCOUNT"), os.Getenv("AZURE_STORAGE_KEY"))
+		os.Getenv("AZURE_STORAGE_ACCOUNT"), os.Getenv("AZURE_STORAGE_KEY"), "")
 	testStorage(t, abs)
 }
 
@@ -369,7 +373,7 @@ func TestNOS(t *testing.T) {
 		t.SkipNow()
 	}
 	nos, _ := newNOS(os.Getenv("NOS_ENDPOINT"),
-		os.Getenv("NOS_ACCESS_KEY"), os.Getenv("NOS_SECRET_KEY"))
+		os.Getenv("NOS_ACCESS_KEY"), os.Getenv("NOS_SECRET_KEY"), "")
 	testStorage(t, nos)
 }
 
@@ -378,7 +382,7 @@ func TestJSS(t *testing.T) {
 		t.SkipNow()
 	}
 	jss, _ := newJSS(os.Getenv("JSS_ENDPOINT"),
-		os.Getenv("JSS_ACCESS_KEY"), os.Getenv("JSS_SECRET_KEY"))
+		os.Getenv("JSS_ACCESS_KEY"), os.Getenv("JSS_SECRET_KEY"), "")
 	testStorage(t, jss)
 }
 
@@ -387,7 +391,7 @@ func TestSpeedy(t *testing.T) {
 		t.SkipNow()
 	}
 	cos, _ := newSpeedy(os.Getenv("SPEEDY_ENDPOINT"),
-		os.Getenv("SPEEDY_ACCESS_KEY"), os.Getenv("SPEEDY_SECRET_KEY"))
+		os.Getenv("SPEEDY_ACCESS_KEY"), os.Getenv("SPEEDY_SECRET_KEY"), "")
 	testStorage(t, cos)
 }
 
@@ -395,7 +399,7 @@ func TestB2(t *testing.T) {
 	if os.Getenv("B2_ACCOUNT_ID") == "" {
 		t.SkipNow()
 	}
-	b, err := newB2(os.Getenv("B2_ENDPOINT"), os.Getenv("B2_ACCOUNT_ID"), os.Getenv("B2_APP_KEY"))
+	b, err := newB2(os.Getenv("B2_ENDPOINT"), os.Getenv("B2_ACCOUNT_ID"), os.Getenv("B2_APP_KEY"), "")
 	if err != nil {
 		t.Fatalf("create B2: %s", err)
 	}
@@ -406,7 +410,7 @@ func TestSpace(t *testing.T) {
 	if os.Getenv("SPACE_ACCESS_KEY") == "" {
 		t.SkipNow()
 	}
-	b, _ := newSpace(os.Getenv("SPACE_ENDPOINT"), os.Getenv("SPACE_ACCESS_KEY"), os.Getenv("SPACE_SECRET_KEY"))
+	b, _ := newSpace(os.Getenv("SPACE_ENDPOINT"), os.Getenv("SPACE_ACCESS_KEY"), os.Getenv("SPACE_SECRET_KEY"), "")
 	testStorage(t, b)
 }
 
@@ -415,7 +419,7 @@ func TestBOS(t *testing.T) {
 		t.SkipNow()
 	}
 	b, _ := newBOS(os.Getenv("BDCLOUD_ENDPOINT"),
-		os.Getenv("BDCLOUD_ACCESS_KEY"), os.Getenv("BDCLOUD_SECRET_KEY"))
+		os.Getenv("BDCLOUD_ACCESS_KEY"), os.Getenv("BDCLOUD_SECRET_KEY"), "")
 	testStorage(t, b)
 }
 
@@ -423,7 +427,7 @@ func TestSftp(t *testing.T) {
 	if os.Getenv("SFTP_HOST") == "" {
 		t.SkipNow()
 	}
-	b, _ := newSftp(os.Getenv("SFTP_HOST"), os.Getenv("SFTP_USER"), os.Getenv("SFTP_PASS"))
+	b, _ := newSftp(os.Getenv("SFTP_HOST"), os.Getenv("SFTP_USER"), os.Getenv("SFTP_PASS"), "")
 	testStorage(t, b)
 }
 
@@ -432,7 +436,7 @@ func TestOBS(t *testing.T) {
 		t.SkipNow()
 	}
 	b, _ := newOBS(os.Getenv("HWCLOUD_ENDPOINT"),
-		os.Getenv("HWCLOUD_ACCESS_KEY"), os.Getenv("HWCLOUD_SECRET_KEY"))
+		os.Getenv("HWCLOUD_ACCESS_KEY"), os.Getenv("HWCLOUD_SECRET_KEY"), "")
 	testStorage(t, b)
 }
 
@@ -440,7 +444,7 @@ func TestHDFS(t *testing.T) {
 	if os.Getenv("HDFS_ADDR") == "" {
 		t.SkipNow()
 	}
-	dfs, _ := newHDFS(os.Getenv("HDFS_ADDR"), "", "")
+	dfs, _ := newHDFS(os.Getenv("HDFS_ADDR"), "", "", "")
 	testStorage(t, dfs)
 }
 
@@ -449,7 +453,7 @@ func TestOOS(t *testing.T) {
 		t.SkipNow()
 	}
 	b, _ := newOOS(os.Getenv("OOS_ENDPOINT"),
-		os.Getenv("OOS_ACCESS_KEY"), os.Getenv("OOS_SECRET_KEY"))
+		os.Getenv("OOS_ACCESS_KEY"), os.Getenv("OOS_SECRET_KEY"), "")
 	testStorage(t, b)
 }
 
@@ -457,7 +461,7 @@ func TestScw(t *testing.T) {
 	if os.Getenv("SCW_ACCESS_KEY") == "" {
 		t.SkipNow()
 	}
-	b, _ := newScw(os.Getenv("SCW_ENDPOINT"), os.Getenv("SCW_ACCESS_KEY"), os.Getenv("SCW_SECRET_KEY"))
+	b, _ := newScw(os.Getenv("SCW_ENDPOINT"), os.Getenv("SCW_ACCESS_KEY"), os.Getenv("SCW_SECRET_KEY"), "")
 	testStorage(t, b)
 }
 
@@ -465,7 +469,7 @@ func TestMinIO(t *testing.T) {
 	if os.Getenv("MINIO_TEST_BUCKET") == "" {
 		t.SkipNow()
 	}
-	b, _ := newMinio(os.Getenv("MINIO_TEST_BUCKET"), os.Getenv("MINIO_ACCESS_KEY"), os.Getenv("MINIO_SECRET_KEY"))
+	b, _ := newMinio(os.Getenv("MINIO_TEST_BUCKET"), os.Getenv("MINIO_ACCESS_KEY"), os.Getenv("MINIO_SECRET_KEY"), "")
 	testStorage(t, b)
 }
 
@@ -478,7 +482,7 @@ func TestTiKV(t *testing.T) {
 	if os.Getenv("TIKV_ADDR") == "" {
 		t.SkipNow()
 	}
-	s, err := newTiKV(os.Getenv("TIKV_ADDR"), "", "")
+	s, err := newTiKV(os.Getenv("TIKV_ADDR"), "", "", "")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -493,7 +497,7 @@ func TestRedis(t *testing.T) {
 	rdb := redis.NewClient(opt)
 	_ = rdb.FlushDB(context.Background())
 
-	s, err := newRedis(os.Getenv("REDIS_ADDR"), "", "")
+	s, err := newRedis(os.Getenv("REDIS_ADDR"), "", "", "")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -504,7 +508,7 @@ func TestSwift(t *testing.T) {
 	if os.Getenv("SWIFT_ADDR") == "" {
 		t.SkipNow()
 	}
-	s, err := newSwiftOSS(os.Getenv("SWIFT_ADDR"), "", "")
+	s, err := newSwiftOSS(os.Getenv("SWIFT_ADDR"), "", "", "")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -515,12 +519,12 @@ func TestWebDAV(t *testing.T) {
 	if os.Getenv("WEBDAV_TEST_BUCKET") == "" {
 		t.SkipNow()
 	}
-	s, _ := newWebDAV(os.Getenv("WEBDAV_TEST_BUCKET"), "", "")
+	s, _ := newWebDAV(os.Getenv("WEBDAV_TEST_BUCKET"), "", "", "")
 	testStorage(t, s)
 }
 
 func TestEncrypted(t *testing.T) {
-	s, _ := CreateStorage("mem", "", "", "")
+	s, _ := CreateStorage("mem", "", "", "", "")
 	privkey, _ := rsa.GenerateKey(rand.Reader, 2048)
 	kc := NewRSAEncryptor(privkey)
 	dc := NewAESEncryptor(kc)
@@ -532,7 +536,7 @@ func TestMarsharl(t *testing.T) {
 	if os.Getenv("HDFS_ADDR") == "" {
 		t.SkipNow()
 	}
-	s, _ := newHDFS(os.Getenv("HDFS_ADDR"), "", "")
+	s, _ := newHDFS(os.Getenv("HDFS_ADDR"), "", "", "")
 	if err := s.Put("hello", bytes.NewReader([]byte("world"))); err != nil {
 		t.Fatalf("PUT failed: %s", err)
 	}
@@ -561,7 +565,7 @@ func TestMarsharl(t *testing.T) {
 }
 
 func TestSharding(t *testing.T) {
-	s, _ := NewSharded("mem", "%d", "", "", 10)
+	s, _ := NewSharded("mem", "%d", "", "", "", 10)
 	testStorage(t, s)
 }
 
@@ -597,7 +601,7 @@ func TestMySQL(t *testing.T) {
 }
 
 func TestNameString(t *testing.T) {
-	s, _ := newMem("test", "", "")
+	s, _ := newMem("test", "", "", "")
 	s = WithPrefix(s, "a/")
 	s = WithPrefix(s, "b/")
 	if s.String() != "mem://test/a/b/" {
@@ -609,7 +613,7 @@ func TestEtcd(t *testing.T) {
 	if os.Getenv("ETCD_ADDR") == "" {
 		t.SkipNow()
 	}
-	s, _ := newEtcd(os.Getenv("ETCD_ADDR"), "", "")
+	s, _ := newEtcd(os.Getenv("ETCD_ADDR"), "", "", "")
 	testStorage(t, s)
 }
 
@@ -625,7 +629,7 @@ func TestEOS(t *testing.T) {
 	if os.Getenv("EOS_ENDPOINT") == "" {
 		t.SkipNow()
 	}
-	s, _ := newEos(os.Getenv("EOS_ENDPOINT"), os.Getenv("EOS_ACCESS_KEY"), os.Getenv("EOS_SECRET_KEY"))
+	s, _ := newEos(os.Getenv("EOS_ENDPOINT"), os.Getenv("EOS_ACCESS_KEY"), os.Getenv("EOS_SECRET_KEY"), "")
 	testStorage(t, s)
 }
 
@@ -633,7 +637,7 @@ func TestWASABI(t *testing.T) {
 	if os.Getenv("WASABI_ENDPOINT") == "" {
 		t.SkipNow()
 	}
-	s, _ := newWasabi(os.Getenv("WASABI_ENDPOINT"), os.Getenv("WASABI_ACCESS_KEY"), os.Getenv("WASABI_SECRET_KEY"))
+	s, _ := newWasabi(os.Getenv("WASABI_ENDPOINT"), os.Getenv("WASABI_ACCESS_KEY"), os.Getenv("WASABI_SECRET_KEY"), "")
 	testStorage(t, s)
 }
 
@@ -641,7 +645,7 @@ func TestSCS(t *testing.T) {
 	if os.Getenv("SCS_ENDPOINT") == "" {
 		t.SkipNow()
 	}
-	s, _ := newSCS(os.Getenv("SCS_ENDPOINT"), os.Getenv("SCS_ACCESS_KEY"), os.Getenv("SCS_SECRET_KEY"))
+	s, _ := newSCS(os.Getenv("SCS_ENDPOINT"), os.Getenv("SCS_ACCESS_KEY"), os.Getenv("SCS_SECRET_KEY"), "")
 	testStorage(t, s)
 }
 
@@ -649,7 +653,7 @@ func TestIBMCOS(t *testing.T) {
 	if os.Getenv("IBMCOS_ENDPOINT") == "" {
 		t.SkipNow()
 	}
-	s, _ := newIBMCOS(os.Getenv("IBMCOS_ENDPOINT"), os.Getenv("IBMCOS_ACCESS_KEY"), os.Getenv("IBMCOS_SECRET_KEY"))
+	s, _ := newIBMCOS(os.Getenv("IBMCOS_ENDPOINT"), os.Getenv("IBMCOS_ACCESS_KEY"), os.Getenv("IBMCOS_SECRET_KEY"), "")
 	testStorage(t, s)
 }
 
diff --git a/pkg/sync/sync_test.go b/pkg/sync/sync_test.go
index 717d3f89..7606783f 100644
--- a/pkg/sync/sync_test.go
+++ b/pkg/sync/sync_test.go
@@ -38,7 +38,7 @@ func collectAll(c <-chan object.Object) []string {
 
 // nolint:errcheck
 func TestIterator(t *testing.T) {
-	m, _ := object.CreateStorage("mem", "", "", "")
+	m, _ := object.CreateStorage("mem", "", "", "", "")
 	m.Put("a", bytes.NewReader([]byte("a")))
 	m.Put("b", bytes.NewReader([]byte("a")))
 	m.Put("aa", bytes.NewReader([]byte("a")))
@@ -54,7 +54,7 @@ func TestIterator(t *testing.T) {
 	}
 
 	// Single object
-	s, _ := object.CreateStorage("mem", "", "", "")
+	s, _ := object.CreateStorage("mem", "", "", "", "")
 	s.Put("a", bytes.NewReader([]byte("a")))
 	ch, _ = ListAll(s, "", "")
 	keys = collectAll(ch)
@@ -67,7 +67,7 @@ func TestIeratorSingleEmptyKey(t *testing.T) {
 	// utils.SetLogLevel(logrus.DebugLevel)
 
 	// Construct mem storage
-	s, _ := object.CreateStorage("mem", "", "", "")
+	s, _ := object.CreateStorage("mem", "", "", "", "")
 	err := s.Put("abc", bytes.NewReader([]byte("abc")))
 	if err != nil {
 		t.Fatalf("Put error: %q", err)
@@ -109,14 +109,14 @@ func TestSync(t *testing.T) {
 		Quiet:     true,
 	}
 	os.Args = []string{"--include", "a[1-9]", "--exclude", "a*", "--exclude", "c*"}
-	a, _ := object.CreateStorage("file", "/tmp/a/", "", "")
+	a, _ := object.CreateStorage("file", "/tmp/a/", "", "", "")
 	a.Put("a1", bytes.NewReader([]byte("a1")))
 	a.Put("a2", bytes.NewReader([]byte("a2")))
 	a.Put("abc", bytes.NewReader([]byte("abc")))
 	a.Put("c1", bytes.NewReader([]byte("c1")))
 	a.Put("c2", bytes.NewReader([]byte("c2")))
 
-	b, _ := object.CreateStorage("file", "/tmp/b/", "", "")
+	b, _ := object.CreateStorage("file", "/tmp/b/", "", "", "")
 	b.Put("a1", bytes.NewReader([]byte("a1")))
 	b.Put("ba", bytes.NewReader([]byte("a1")))
 
@@ -193,8 +193,8 @@ func TestSyncIncludeAndExclude(t *testing.T) {
 		Quiet:     true,
 		Exclude:   []string{"1"},
 	}
-	a, _ := object.CreateStorage("file", "/tmp/a/", "", "")
-	b, _ := object.CreateStorage("file", "/tmp/b/", "", "")
+	a, _ := object.CreateStorage("file", "/tmp/a/", "", "", "")
+	b, _ := object.CreateStorage("file", "/tmp/b/", "", "", "")
 
 	simple := []string{"a1/z1/z2", "a2", "ab1", "ab2", "b1", "b2", "c1", "c2"}
 	testCases := []struct {
@@ -310,14 +310,14 @@ func TestSyncLink(t *testing.T) {
 		_ = os.RemoveAll("/tmp/b")
 	}()
 
-	a, _ := object.CreateStorage("file", "/tmp/a/", "", "")
+	a, _ := object.CreateStorage("file", "/tmp/a/", "", "", "")
 	a.Put("a1", bytes.NewReader([]byte("test")))
 	as := a.(object.SupportSymlink)
 	as.Symlink("/tmp/a/a1", "l1")
 	as.Symlink("./../a1", "d1/l2")
 	as.Symlink("./../notExist", "l3")
 
-	b, _ := object.CreateStorage("file", "/tmp/b/", "", "")
+	b, _ := object.CreateStorage("file", "/tmp/b/", "", "", "")
 	bs := b.(object.SupportSymlink)
 	bs.Symlink("/tmp/b/a1", "l1")
 
@@ -369,13 +369,13 @@ func TestSyncLinkWithOutFollow(t *testing.T) {
 		_ = os.RemoveAll("/tmp/b")
 	}()
 
-	a, _ := object.CreateStorage("file", "/tmp/a/", "", "")
+	a, _ := object.CreateStorage("file", "/tmp/a/", "", "", "")
 	a.Put("a1", bytes.NewReader([]byte("test")))
 	as := a.(object.SupportSymlink)
 	as.Symlink("/tmp/a/a1", "l1")
 	as.Symlink("./../notExist", "l3")
 
-	b, _ := object.CreateStorage("file", "/tmp/b/", "", "")
+	b, _ := object.CreateStorage("file", "/tmp/b/", "", "", "")
 
 	if err := Sync(a, b, &Config{
 		Threads:     50,
@@ -409,8 +409,8 @@ func TestSingleLink(t *testing.T) {
 		_ = os.RemoveAll("/tmp/b")
 	}()
 	_ = os.Symlink("/tmp/aa", "/tmp/a")
-	a, _ := object.CreateStorage("file", "/tmp/a", "", "")
-	b, _ := object.CreateStorage("file", "/tmp/b", "", "")
+	a, _ := object.CreateStorage("file", "/tmp/a", "", "", "")
+	b, _ := object.CreateStorage("file", "/tmp/b", "", "", "")
 	if err := Sync(a, b, &Config{
 		Threads:     50,
 		Update:      true,
@@ -439,9 +439,9 @@ func TestLimits(t *testing.T) {
 		_ = os.RemoveAll("/tmp/b/")
 		_ = os.RemoveAll("/tmp/c/")
 	}()
-	a, _ := object.CreateStorage("file", "/tmp/a/", "", "")
-	b, _ := object.CreateStorage("file", "/tmp/b/", "", "")
-	c, _ := object.CreateStorage("file", "/tmp/c/", "", "")
+	a, _ := object.CreateStorage("file", "/tmp/a/", "", "", "")
+	b, _ := object.CreateStorage("file", "/tmp/b/", "", "", "")
+	c, _ := object.CreateStorage("file", "/tmp/c/", "", "", "")
 	put := func(storage object.ObjectStorage, keys []string) {
 		for _, key := range keys {
 			if key != "" {
diff --git a/pkg/vfs/compact_test.go b/pkg/vfs/compact_test.go
index b60945ee..2d6190f5 100644
--- a/pkg/vfs/compact_test.go
+++ b/pkg/vfs/compact_test.go
@@ -35,7 +35,7 @@ func TestCompact(t *testing.T) {
 		CacheSize:  10,
 		CacheDir:   "memory",
 	}
-	blob, _ := object.CreateStorage("mem", "", "", "")
+	blob, _ := object.CreateStorage("mem", "", "", "", "")
 	store := chunk.NewCachedStore(blob, cconf, nil)
 
 	// prepare the slices
diff --git a/pkg/vfs/vfs_test.go b/pkg/vfs/vfs_test.go
index 9c3729bf..ad398713 100644
--- a/pkg/vfs/vfs_test.go
+++ b/pkg/vfs/vfs_test.go
@@ -69,7 +69,7 @@ func createTestVFS() (*VFS, object.ObjectStorage) {
 			CacheDir:   "memory",
 		},
 	}
-	blob, _ := object.CreateStorage("mem", "", "", "")
+	blob, _ := object.CreateStorage("mem", "", "", "", "")
 	registry := prometheus.NewRegistry() // replace default so only JuiceFS metrics are exposed
 	registerer := prometheus.WrapRegistererWithPrefix("juicefs_",
 		prometheus.WrapRegistererWith(prometheus.Labels{"mp": mp, "vol_name": format.Name}, registry))
@@ -685,7 +685,7 @@ func TestInternalFile(t *testing.T) {
 	}
 	if n, e = v.Read(ctx, fe.Inode, buf, 0, fh); e != 0 {
 		t.Fatalf("read .accesslog: %s", e)
-	} else if !strings.Contains(string(buf[:n]), "#\n") {
+	} else if !strings.Contains(string(buf[:n]), "open (9223372032559808513)") {
 		t.Fatalf("invalid access log: %q", string(buf[:n]))
 	}
 	_ = v.Flush(ctx, fe.Inode, fh, 0)