Repository: juicedata/juicefs
Pull request #2010 (2022-05-12 14:40:46)

test config ak/sk online


Patch:
diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
new file mode 100644
index 00000000..a2ca50b5
--- /dev/null
+++ b/.github/workflows/config.yml
@@ -0,0 +1,93 @@
+name: "config-test"
+
+on:
+  push:
+    branches: 
+      - main
+      - release-**
+    paths-ignore:
+      - 'docs/**'
+      - '**.md'
+  pull_request:
+    #The branches below must be a subset of the branches above
+    branches: 
+      - main
+      - release-**
+    paths-ignore:
+      - 'docs/**'
+      - '**.md'
+  workflow_dispatch:
+  
+jobs:
+  config-test:
+    runs-on: ubuntu-latest
+    services:
+      redis:
+        # Docker Hub image
+        image: redis
+        # Set health checks to wait until redis has started
+        options: >-
+          --health-cmd "redis-cli ping"
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+        ports:
+          # Maps port 6379 on service container to the host
+          - 6379:6379
+    steps:
+      - name: 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: 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
+          chmod +x mc 
+          
+      - name: Test
+        run: |
+          meta_url=redis://127.0.0.1/1
+          mount_point=/tmp/sync-test/
+          ./mc config host add minio http://127.0.0.1:9000 minioadmin minioadmin
+          ./mc admin user add minio juicedata juicedata
+          ./mc admin policy set minio consoleAdmin user=juicedata
+          ./juicefs format --storage minio --bucket http://localhost:9000/jfs-test --access-key juicedata --secret-key juicedata $meta_url myjfs
+          ./juicefs mount $meta_url $mount_point -d --io-retries 1
+
+          ./mc admin user remove minio juicedata
+          ./mc admin user add minio juicedata1 juicedata1
+          ./mc admin policy set minio consoleAdmin user=juicedata1
+          ./juicefs config $meta_url --access-key juicedata1 --secret-key juicedata1
+          sleep 60
+          echo abc > abc.txt
+          cp abc.txt $mount_point/abc.txt && echo "write success"
+          cat $mount_point/abc.txt | grep abc && echo "read success"
+
+      - 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/encoding_mysql.yml b/.github/workflows/encoding_mysql.yml
index 74993d0c..9b4da4bf 100644
--- a/.github/workflows/encoding_mysql.yml
+++ b/.github/workflows/encoding_mysql.yml
@@ -63,13 +63,13 @@ jobs:
           if [ -d ${{ steps.vars.outputs.BUCKET}}/${{ steps.vars.outputs.NAME}} ]; then
             rm ${{ steps.vars.outputs.BUCKET}}/${{ steps.vars.outputs.NAME}} -rf
           fi
-
-          filename="file_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          dirname="dir_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
+          
+          filename="file_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          dirname="dir_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
           attr_key=user.k
-          attr_value_set="attr_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          data_write="data_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          soft_link="softlink_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
+          attr_value_set="attr_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          data_write="data_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          soft_link="softlink_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
 
           ./juicefs format --trash-days 0 --bucket ${{ steps.vars.outputs.BUCKET}} ${{steps.vars.outputs.META_URL1}} ${{steps.vars.outputs.NAME}}
           ./juicefs mount --no-usage-report --enable-xattr ${{steps.vars.outputs.META_URL1}}  ${{steps.vars.outputs.MOUNT_POINT1}} -d 
@@ -78,7 +78,9 @@ jobs:
           setfattr -n $attr_key -v  $attr_value_set `printf "${{steps.vars.outputs.MOUNT_POINT1}}/$filename"`
           ln -s `printf "${{steps.vars.outputs.MOUNT_POINT1}}/$filename"` `printf "${{steps.vars.outputs.MOUNT_POINT1}}/$soft_link"`
           ./juicefs dump ${{steps.vars.outputs.META_URL1}}  backup.dump
-
+          cat backup.dump | grep file_果汁
+          cat backup.dump | grep dir_果汁
+          cat backup.dump | grep softlink_果汁
           ./juicefs load ${{steps.vars.outputs.META_URL2}}  backup.dump
           ./juicefs mount --enable-xattr ${{steps.vars.outputs.META_URL2}} ${{steps.vars.outputs.MOUNT_POINT2}} -d
 
@@ -124,14 +126,14 @@ jobs:
           if [ -d ${{ steps.vars.outputs.BUCKET}}/${{ steps.vars.outputs.NAME}} ]; then
             rm ${{ steps.vars.outputs.BUCKET}}/${{ steps.vars.outputs.NAME}} -rf
           fi
-
-          filename="file_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          filename2="file2_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          dirname="dir_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
+          
+          filename="file_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          filename2="file2_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          dirname="dir_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
           attr_key=user.k
-          attr_value_set="attr_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          data_write="data_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          soft_link="softlink_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
+          attr_value_set="attr_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          data_write="data_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          soft_link="softlink_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
 
           ./juicefs_old format --trash-days 0 --bucket ${{ steps.vars.outputs.BUCKET}} ${{steps.vars.outputs.META_URL1}} ${{steps.vars.outputs.NAME}}
           ./juicefs_old mount --no-usage-report --enable-xattr ${{steps.vars.outputs.META_URL1}}  ${{steps.vars.outputs.MOUNT_POINT1}} -d 
@@ -227,13 +229,13 @@ jobs:
           if [ -d ${{ steps.vars.outputs.BUCKET}}/${{ steps.vars.outputs.NAME}} ]; then
             rm ${{ steps.vars.outputs.BUCKET}}/${{ steps.vars.outputs.NAME}} -rf
           fi
-
-          filename="file_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          dirname="dir_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
+          #果汁（utf8）果汁(gbk)
+          filename="file_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          dirname="dir_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
           attr_key=user.k
-          attr_value_set="attr_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          data_write="data_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
-          soft_link="softlink_\xb9\xfb\xd6\xad\xca\xfd\xbe\xdd\\\"%%\\\\"
+          attr_value_set="attr_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          data_write="data_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
+          soft_link="softlink_\xe6\x9e\x9c\xe6\xb1\x81\xb9\xfb\xd6\xad\\\"%%\\\\"
 
           ./juicefs_old format --trash-days 0 --bucket ${{ steps.vars.outputs.BUCKET}} ${{steps.vars.outputs.META_URL1}} ${{steps.vars.outputs.NAME}}
           ./juicefs_old mount --no-usage-report --enable-xattr ${{steps.vars.outputs.META_URL1}}  ${{steps.vars.outputs.MOUNT_POINT1}} -d 
@@ -251,6 +253,9 @@ jobs:
           # upgrade table schema
           ./juicefs mount --enable-xattr ${{steps.vars.outputs.META_URL1}}  ${{steps.vars.outputs.MOUNT_POINT1}} -d
           ./juicefs dump ${{steps.vars.outputs.META_URL1}}  backup.dump
+          cat backup.dump | grep file_果汁
+          cat backup.dump | grep dir_果汁
+          cat backup.dump | grep softlink_果汁
           ./juicefs load ${{steps.vars.outputs.META_URL2}}  backup.dump
           ./juicefs mount --no-usage-report --enable-xattr ${{steps.vars.outputs.META_URL2}} ${{steps.vars.outputs.MOUNT_POINT2}} -d
 
diff --git a/.github/workflows/fio_benchmark.yml b/.github/workflows/fio_benchmark.yml
index 1287c76b..36c1c719 100644
--- a/.github/workflows/fio_benchmark.yml
+++ b/.github/workflows/fio_benchmark.yml
@@ -52,7 +52,10 @@ jobs:
         run: |
           export PYROSCOPE_AUTH_TOKEN="${{ secrets.PYROSCOPE_AUTH_TOKEN }}"  
           size=${{ github.event.inputs.size }}
+          test -z "$size" && size='1G' 
           nrfiles=${{ github.event.inputs.nrfiles }}
+          test -z "$nrfiles" && nrfiles=10000
+
           disk_warning_value=90
           decrease_ratio=0.8
           fio_jobs=(
diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml
new file mode 100644
index 00000000..034cd94e
--- /dev/null
+++ b/.github/workflows/gateway.yml
@@ -0,0 +1,155 @@
+name: "gateway-test"
+
+on:
+  push:
+    branches: 
+      - main
+      - release-**
+    paths-ignore:
+      - 'docs/**'
+      - '**.md'
+  pull_request:
+    #The branches below must be a subset of the branches above
+    branches: 
+      - main
+      - release-**
+    paths-ignore:
+      - 'docs/**'
+      - '**.md'
+  workflow_dispatch:
+
+jobs:
+  upload-small-files:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+
+      - name: Build linux target
+        run: |
+          make juicefs
+
+      - name: Test copy small files with gateway
+        run: | 
+          wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
+          chmod +x mc 
+          sudo apt install md5deep
+          sudo /etc/init.d/mysql start
+
+          meta_url="mysql://root:root@(127.0.0.1)/test_gateway"
+          db_name=$(basename $meta_url)
+          volume=myjfs
+          mp=/tmp/myjfs
+          mysql -uroot -proot -e "drop database if exists $db_name; create database $db_name;" 
+          export MINIO_ROOT_USER=minioadmin
+          export MINIO_ROOT_PASSWORD=minioadmin
+          ./juicefs format $meta_url $volume
+          ./juicefs gateway $meta_url localhost:9000 &
+
+          dd if=/dev/urandom of=file iflag=fullblock,count_bytes bs=4k count='400K' > /dev/null
+          mkdir data
+          for i in {1..1000}; do
+            cp file data/file$i
+          done
+          md5deep -lr data/ > sum.md5
+
+          ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:9000/$volume/data/ --no-https -p 50
+          #./mc alias set minio http://localhost:9000 minioadmin minioadmin --api S3v4
+          #./mc mb minio/$volume
+          #./mc cp --recursive data/  minio/$volume/data
+
+          ./juicefs mount -d $meta_url $mp
+          count=$(md5deep -lrX sum.md5 || wc -l)
+          [ $count = 0 ] && echo "check md5 succeed" || echo "check md5 failed"
+
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
+
+  upload-big-files:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+
+      - name: Build linux target
+        run: |
+          make juicefs
+
+      - name: Test copy big files with gateway
+        run: | 
+          wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
+          chmod +x mc 
+          sudo apt install md5deep
+          sudo /etc/init.d/mysql start
+
+          meta_url="mysql://root:root@(127.0.0.1)/test_gateway"
+          db_name=$(basename $meta_url)
+          volume=myjfs
+          mp=/tmp/myjfs
+          mysql -uroot -proot -e "drop database if exists $db_name; create database $db_name;" 
+          export MINIO_ROOT_USER=minioadmin
+          export MINIO_ROOT_PASSWORD=minioadmin
+          ./juicefs format $meta_url $volume
+          ./juicefs gateway $meta_url localhost:9000 &
+
+          dd if=/dev/urandom of=file iflag=fullblock,count_bytes bs=4k count='1G' > /dev/null
+          mkdir data
+          for i in {1..5}; do
+            cp file data/file_$i
+          done          
+          md5deep -lr data/ > sum.md5
+
+          ./juicefs sync --dirs data/  s3://minioadmin:minioadmin@localhost:9000/$volume/data/ --no-https -p 50
+          #./mc alias set minio http://localhost:9000 minioadmin minioadmin --api S3v4
+          #./mc mb minio/$volume
+          #./mc cp --recursive data/  minio/$volume/data
+          
+          ./juicefs mount -d $meta_url $mp
+          count=$(md5deep -lrX sum.md5 || wc -l)
+          [ $count = 0 ] && echo "check md5 succeed" || echo "check md5 failed"
+
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
+
+  sync-with-multithread:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+
+      - name: Build linux target
+        run: |
+          make juicefs
+
+      - name: Test sync to gateway with multithread
+        run: | 
+          #test for PR: https://github.com/juicedata/juicefs/pull/2024
+          wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
+          chmod +x mc 
+          sudo apt install md5deep
+          sudo /etc/init.d/mysql start
+
+          meta_url="mysql://root:root@(127.0.0.1)/test_gateway"
+          volume=myjfs
+          mp=/tmp/myjfs
+          db_name=$(basename $meta_url)
+          mysql -uroot -proot -e "drop database if exists $db_name; create database $db_name;" 
+          export MINIO_ROOT_USER=minioadmin
+          export MINIO_ROOT_PASSWORD=minioadmin
+          ./juicefs format $meta_url $volume
+          ./juicefs gateway $meta_url localhost:9000 &
+          ./juicefs sync --dirs ./  s3://minioadmin:minioadmin@localhost:9000/$volume/source_dir/ --no-https -p 20
+          ./juicefs mount -d $meta_url $mp
+          rm -rf $mp/source_dir
+
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/list.yml b/.github/workflows/list.yml
new file mode 100644
index 00000000..b4f3d1ac
--- /dev/null
+++ b/.github/workflows/list.yml
@@ -0,0 +1,206 @@
+name: "list-test"
+
+on:
+  push:
+    branches:
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+  pull_request:
+    #The branches below must be a subset of the branches above
+    branches:
+      - 'release-**'
+    paths-ignore:
+      - 'docs/**'
+  schedule:
+    - cron:  '0 3 * * *'
+  workflow_dispatch:
+
+jobs:
+  pg-list:
+    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_7M_EMPTY_FILE.json.gz* >META_7M_EMPTY_FILE.json.gz
+          gzip -dk  META_7M_EMPTY_FILE.json.gz
+          ls -lh
+          mv META_7M_EMPTY_FILE.json ../
+          cd ../
+          ls -lh
+      - name: Load
+        uses: ./.github/actions/load
+        with:
+          meta_url: "postgres://postgres:postgres@127.0.0.1:5432/sync_test?sslmode=disable" 
+          load_file: "META_7M_EMPTY_FILE.json"
+      - name: List
+        run: |
+          cd /tmp/juicefs-sync-test/createfile/
+          ls -lh |wc -l >/tmp/count.log
+          cat /tmp/count.log | grep 7000001
+          if [ $? != 0 ];then echo "file num is error";exit 1; fi
+      - name: Send Slack Notification
+        if: ${{ failure() }}
+        uses: juicedata/slack-notify-action@main
+        with:
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
+
+
+  mysql-list:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+      - name: Init
+        run: |
+          sudo chmod 777 /var
+          sudo /etc/init.d/mysql start
+          git clone https://github.com/sanwan/actionfile.git
+          cd actionfile
+          cat META_7M_EMPTY_FILE.json.gz* >META_7M_EMPTY_FILE.json.gz
+          gzip -dk  META_7M_EMPTY_FILE.json.gz
+          ls -lh
+          mv META_7M_EMPTY_FILE.json ../
+          cd ../
+          ls -lh
+      - name: Load
+        uses: ./.github/actions/load
+        with:
+          meta_url: "mysql://root:root@\\(127.0.0.1\\)/sync_test" 
+          load_file: "META_7M_EMPTY_FILE.json"
+      - name: List
+        run: |
+          cd /tmp/juicefs-sync-test/createfile/
+          ls -lh |wc -l >/tmp/count.log
+          cat /tmp/count.log | grep 7000001
+          if [ $? != 0 ];then echo "file num is error";exit 1; fi
+      - name: Send Slack Notification
+        if: ${{ failure() }}
+        uses: juicedata/slack-notify-action@main
+        with:
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
+
+  redis-list:
+    runs-on: ubuntu-latest
+    services:
+      redis:
+        # Docker Hub image
+        image: redis
+        # Set health checks to wait until redis has started
+        options: >-
+          --health-cmd "redis-cli ping"
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+        ports:
+          # Maps port 6379 on service container to the host
+          - 6379:6379
+    steps:
+      - uses: actions/checkout@v3
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+      - name: Init
+        run: |
+          sudo chmod 777 /var
+          git clone https://github.com/sanwan/actionfile.git
+          cd actionfile
+          cat META_7M_EMPTY_FILE.json.gz* >META_7M_EMPTY_FILE.json.gz
+          gzip -dk  META_7M_EMPTY_FILE.json.gz
+          ls -lh
+          mv META_7M_EMPTY_FILE.json ../
+          cd ../
+          ls -lh
+      - name: Load
+        uses: ./.github/actions/load
+        with:
+          meta_url: redis://127.0.0.1:6379/1 
+          load_file: "META_7M_EMPTY_FILE.json"
+      - name: List
+        run: |
+          cd /tmp/juicefs-sync-test/createfile/
+          ls -lh |wc -l >/tmp/count.log
+          cat /tmp/count.log | grep 7000001
+          if [ $? != 0 ];then echo "file num is error";exit 1; fi
+      - name: Send Slack Notification
+        if: ${{ failure() }}
+        uses: juicedata/slack-notify-action@main
+        with:
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
+
+
+  sqlite3-list:
+    runs-on: ubuntu-latest   
+    steps:
+      - uses: actions/checkout@v3
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 1
+      - name: Init
+        run: |
+          sudo chmod 777 /var
+          git clone https://github.com/sanwan/actionfile.git
+          cd actionfile
+          cat META_7M_EMPTY_FILE.json.gz* >META_7M_EMPTY_FILE.json.gz
+          gzip -dk  META_7M_EMPTY_FILE.json.gz
+          ls -lh
+          mv META_7M_EMPTY_FILE.json ../
+          cd ../
+          ls -lh
+      - name: Load
+        uses: ./.github/actions/load
+        with:
+          meta_url: sqlite3://sync-test.db
+          load_file: "META_7M_EMPTY_FILE.json"
+      - name: List
+        run: |
+          cd /tmp/juicefs-sync-test/createfile/
+          ls -lh |wc -l >/tmp/count.log
+          cat /tmp/count.log | grep 7000001
+          if [ $? != 0 ];then echo "file num is error";exit 1; fi
+      - name: Send Slack Notification
+        if: ${{ failure() }}
+        uses: juicedata/slack-notify-action@main
+        with:
+          channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
+          slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/.github/workflows/version_compatible.yml b/.github/workflows/version_compatible.yml
index 31a79346..7359a5f3 100644
--- a/.github/workflows/version_compatible.yml
+++ b/.github/workflows/version_compatible.yml
@@ -47,7 +47,9 @@ jobs:
                      -e "MINIO_SECRET_KEY=minioadmin" \
                      -v /tmp/data:/data \
                      -v /tmp/config:/root/.minio \
-                     minio/minio server /data       
+                     minio/minio server /data
+          wget -q https://dl.minio.io/client/mc/release/linux-amd64/mc
+          chmod +x mc
 
       - name: Set Variable
         id: vars
@@ -65,6 +67,7 @@ jobs:
 
       - name: Version Compatible Test
         run: |
+          ./mc config host add minio http://127.0.0.1:9000 minioadmin minioadmin
           tests=(
               "format:jfs_new, format:jfs_old, destroy:jfs_new"
               "format:jfs_new, format:jfs_old, destroy:jfs_old"
@@ -107,7 +110,10 @@ jobs:
                       echo 'jfs_bin is ' $jfs_bin, "cmd is "  $jfs_cmd
                       case "$jfs_cmd" in
                         "format") 
-                            sudo ./$jfs_bin format --trash-days 0 --storage minio --bucket  ${{ steps.vars.outputs.MINIO_URL }} --access-key minioadmin --secret-key minioadmin  ${{ steps.vars.outputs.META_URL }} juicefs-version-compatible
+                            ./mc admin user remove minio juicedata || true
+                            ./mc admin user add minio juicedata juicedata
+                            ./mc admin policy set minio consoleAdmin user=juicedata  
+                            sudo ./$jfs_bin format --trash-days 0 --storage minio --bucket  ${{ steps.vars.outputs.MINIO_URL }} --access-key juicedata --secret-key juicedata  ${{ steps.vars.outputs.META_URL }} juicefs-version-compatible
                             echo "format finished" 
                             ;;
                         "mount") 
@@ -160,10 +166,13 @@ jobs:
                             ;;
                         "config")
                             encrypt=$(echo $command | awk -F: '{print $1}'| xargs | awk -F_ '{print $2}')
+                            ./mc admin user remove minio juicedata || true
+                            ./mc admin user add minio juicedata juicedata1
+                            ./mc admin policy set minio consoleAdmin user=juicedata  
                             if [ -n "$encrypt" ]; then
-                              sudo ./$jfs_bin config ${{steps.vars.outputs.META_URL}} --secret-key minioadmin --encrypt-secret
+                              sudo ./$jfs_bin config ${{steps.vars.outputs.META_URL}} --secret-key juicedata1 --encrypt-secret
                             else
-                              sudo ./$jfs_bin config ${{steps.vars.outputs.META_URL}} --secret-key minioadmin
+                              sudo ./$jfs_bin config ${{steps.vars.outputs.META_URL}} --secret-key juicedata1
                             fi
                             echo "config finished"
                             ;;
diff --git a/cmd/main.go b/cmd/main.go
index 2802dd33..6c7c5307 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -77,7 +77,6 @@ func Main(args []string) error {
 	}
 
 	return app.Run(reorderOptions(app, args))
-
 }
 
 func handleSysMountArgs(args []string) []string {
@@ -219,7 +218,7 @@ func reorderOptions(app *cli.App, args []string) []string {
 // Check number of positional arguments, set logger level and setup agent if needed
 func setup(c *cli.Context, n int) {
 	if c.NArg() < n {
-		logger.Errorf("This command requires at least %d arguments", n)
+		fmt.Printf("ERROR: This command requires at least %d arguments\n", n)
 		fmt.Printf("USAGE:\n   juicefs %s [command options] %s\n", c.Command.Name, c.Command.ArgsUsage)
 		os.Exit(1)
 	}
diff --git a/cmd/mount.go b/cmd/mount.go
index c954d97c..3c506ce4 100644
--- a/cmd/mount.go
+++ b/cmd/mount.go
@@ -91,10 +91,12 @@ func installHandler(mp string) {
 	signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)
 	go func() {
 		for {
-			<-signalChan
+			sig := <-signalChan
+			logger.Infof("Received signal %s, exiting...", sig.String())
 			go func() { _ = doUmount(mp, true) }()
 			go func() {
 				time.Sleep(time.Second * 3)
+				logger.Warnf("Umount not finished after 3 seconds, force exit")
 				os.Exit(1)
 			}()
 		}
diff --git a/cmd/status.go b/cmd/status.go
index 0baec2b4..e3ff4bc3 100644
--- a/cmd/status.go
+++ b/cmd/status.go
@@ -73,7 +73,7 @@ func status(ctx *cli.Context) error {
 	format.RemoveSecret()
 
 	if sid := ctx.Uint64("session"); sid != 0 {
-		s, err := m.GetSession(sid)
+		s, err := m.GetSession(sid, true)
 		if err != nil {
 			logger.Fatalf("get session: %s", err)
 		}
diff --git a/cmd/sync.go b/cmd/sync.go
index f0864139..64ee9206 100644
--- a/cmd/sync.go
+++ b/cmd/sync.go
@@ -22,17 +22,14 @@ import (
 	"net/http"
 	_ "net/http/pprof"
 	"net/url"
-	"os"
 	"path/filepath"
 	"regexp"
 	"runtime"
 	"strings"
-	"syscall"
 
 	"github.com/juicedata/juicefs/pkg/object"
 	"github.com/juicedata/juicefs/pkg/sync"
 	"github.com/urfave/cli/v2"
-	"golang.org/x/term"
 )
 
 func cmdSync() *cli.Command {
@@ -154,7 +151,7 @@ Supported storage systems: https://juicefs.com/docs/community/how_to_setup_objec
 			},
 			&cli.StringSliceFlag{
 				Name:  "worker",
-				Usage: "hosts (seperated by comma) to launch worker",
+				Usage: "hosts (separated by comma) to launch worker",
 			},
 			&cli.IntFlag{
 				Name:  "bwlimit",
@@ -234,13 +231,6 @@ func createSyncStorage(uri string, conf *sync.Config) (object.ObjectStorage, err
 				parts := strings.Split(user, ":")
 				user = parts[0]
 				pass = parts[1]
-			} else if os.Getenv("SSH_PRIVATE_KEY_PATH") == "" && os.Getenv("SSH_AUTH_SOCK") == "" {
-				fmt.Printf("%s's password: ", uri)
-				bytePassword, err := term.ReadPassword(int(syscall.Stdin))
-				if err != nil {
-					logger.Fatalf("Read password: %s", err.Error())
-				}
-				pass = string(bytePassword)
 			}
 			return object.CreateStorage("sftp", uri, user, pass)
 		}
diff --git a/cmd/warmup.go b/cmd/warmup.go
index 1fa20191..6abb7eb1 100644
--- a/cmd/warmup.go
+++ b/cmd/warmup.go
@@ -103,7 +103,7 @@ func sendCommand(cf *os.File, batch []string, count int, threads uint, backgroun
 		logger.Fatalf("Write message: %s", err)
 	}
 	if background {
-		logger.Infof("Warm-up cache for %d paths in backgroud", count)
+		logger.Infof("Warm-up cache for %d paths in background", count)
 		return
 	}
 	var errs = make([]byte, 1)
diff --git a/docs/en/getting-started/for_distributed.md b/docs/en/getting-started/for_distributed.md
index 380cc6b1..021f2c81 100644
--- a/docs/en/getting-started/for_distributed.md
+++ b/docs/en/getting-started/for_distributed.md
@@ -103,7 +103,7 @@ Once a file system is created, the relevant information including name, object s
 Since the "data" and "metadata" of this file system are stored in cloud services, the file system can be mounted on any computer with a JuiceFS client installed for shared reads and writes at the same time. For example:
 
 ```shell
-juicefs mount redis://tom:mypassword@myjfs-sh-abc.apse1.cache.amazonaws.com:6379/1 mnt
+juicefs mount redis://tom:mypassword@myjfs-sh-abc.apse1.cache.amazonaws.com:6379/1 ~/jfs
 ```
 
 #### Strong data consistency guarantee
@@ -123,7 +123,8 @@ juicefs mount
     --background \
     --cache-dir /mycache \
     --cache-size 512000 \
-    redis://tom:mypassword@myjfs-sh-abc.apse1.cache.amazonaws.com:6379/1 mnt
+    redis://tom:mypassword@myjfs-sh-abc.apse1.cache.amazonaws.com:6379/1 \
+    ~/jfs
 ```
 
 :::note
@@ -152,10 +153,10 @@ By default, CentOS 6 does not mount the network file system during boot, you nee
 
 ### 6. Unmounting the file system
 
-You can unmount the JuiceFS file system (assuming the mount point path is `mnt`) by the command `juicefs umount`.
+You can unmount the JuiceFS file system (assuming the mount point path is `~/jfs`) by the command `juicefs umount`.
 
 ```shell
-juicefs umount mnt
+juicefs umount ~/jfs
 ```
 
 #### Unmounting failure
@@ -163,7 +164,7 @@ juicefs umount mnt
 If the command fails to unmount the file system after execution, it will prompt `Device or resource busy`.
 
 ```shell
-2021-05-09 22:42:55.757097 I | fusermount: failed to unmount mnt: Device or resource busy
+2021-05-09 22:42:55.757097 I | fusermount: failed to unmount ~/jfs: Device or resource busy
 exit status 1
 ```
 
@@ -176,5 +177,5 @@ The following command may result in file corruption and loss, so be careful to u
 You can add the option `--force` or `-f` to force the file system unmounted if you are clear about the consequence of the operation.
 
 ```shell
-juicefs umount --force mnt
+juicefs umount --force ~/jfs
 ```
diff --git a/docs/en/getting-started/for_local.md b/docs/en/getting-started/for_local.md
index f2dff261..071dbc7d 100644
--- a/docs/en/getting-started/for_local.md
+++ b/docs/en/getting-started/for_local.md
@@ -26,7 +26,7 @@ juicefs format [command options] META-URL NAME
 
 As you can see, 3 types of information are required to format a file system.
 
-1. **[command options]**: used to set up the storage medium for the file system; **local disk** will be used by default, and default path is `"$HOME/.juicefs/local"` or `"/var/jfs"`.
+1. **[command options]**: used to set up the storage medium for the file system; **local disk** will be used by default, and default path is `"$HOME/.juicefs/local"`, `"/var/jfs"` or `"C:/jfs/local"`.
 2. **META-URL**: used to set up the metadata engine, usually a URL or the file path of a database.
 3. **NAME**: the name of the file system.
 
@@ -84,28 +84,28 @@ The mount point (MOUNTPOINT) on Windows systems should use a disk letter that is
 As SQLite is a single-file database, please pay attention to the path of the database file when mounting it. JuiceFS supports both relative and absolute paths.
 :::
 
-The following command mounts the `myjfs` file system to the `mnt` folder in the current directory.
+The following command mounts the `myjfs` file system to the `~/jfs` folder:
 
 ```shell
-juicefs mount sqlite3://myjfs.db mnt
+juicefs mount sqlite3://myjfs.db ~/jfs
 ```
 
 ![](../images/sqlite-mount-local.png)
 
 The client mounts the file system in the foreground by default. As you can see in the above image, the program keeps running in the current terminal. To unmount the file system, press <kbd>Ctrl</kbd> + <kbd>C</kbd> or close the terminal window.
 
-In order to keep the file system mounted in the background, you can specify the `-d` or `--background` option when mounting, i.e. to mount the file system in the daemon.
+In order to keep the file system mounted in the background, you can specify the `-d` or `--background` option when mounting, i.e. to mount the file system in the daemon:
 
 ```shell
-juicefs mount sqlite3://myjfs.db mnt -d
+juicefs mount sqlite3://myjfs.db ~/jfs -d
 ```
 
-Next, any files stored in the mount point `mnt` will be split into specific blocks according to [How JuiceFS Stores Files](../introduction/architecture.md#how-juicefs-stores-files), and stored in `$HOME/.juicefs/local/myjfs` directory; the corresponding metadata will be stored in the `myjfs.db` database.
+Next, any files stored in the mount point `~/jfs` will be split into specific blocks according to [How JuiceFS Stores Files](../introduction/architecture.md#how-juicefs-stores-files), and stored in `$HOME/.juicefs/local/myjfs` directory; the corresponding metadata will be stored in the `myjfs.db` database.
 
-In the end, the mount point `mnt` can be unmounted by executing the following command.
+In the end, the mount point `~/jfs` can be unmounted by executing the following command.
 
 ```shell
-juicefs umount mnt
+juicefs umount ~/jfs
 ```
 
 ## Go Further
@@ -120,8 +120,8 @@ JuiceFS supports almost all object storage services, see [JuiceFS supported stor
 
 In general, only 2 steps are required to create an object storage:
 
-1. Create a `Bucket` and get the Endpoint address.
-2. Create the `Access Key ID` and `Access Key Secret`, i.e., the access keys for the Object Storage API.
+1. Create a **Bucket** and get the Endpoint address.
+2. Create the **Access Key ID** and **Access Key Secret**, i.e., the access keys for the Object Storage API.
 
 Taking AWS S3 as an example, a resource that has been created would be like below.
 
@@ -151,7 +151,7 @@ juicefs format --storage s3 \
 
 The command above creates a file system using the same database name and file system name with the object storage options provided.
 
-- `--storage`: Used to set the storage type, e.g. oss, s3, etc.
+- `--storage`: Used to set the storage type, e.g. `oss`, `s3`, etc.
 - `--bucket`: Used to set the Endpoint address of the object storage.
 - `--access-key`: Used to set the Object Storage Access Key ID.
 - `--secret-key`: Used to set the Object Storage Access Key Secret.
@@ -163,7 +163,7 @@ Please replace the information in the above command with your own object storage
 You can mount the file system once it is created.
 
 ```shell
-juicefs mount sqlite3://myjfs.db mnt
+juicefs mount sqlite3://myjfs.db ~/jfs
 ```
 
 As you can see, the mount command is exactly the same as using the local storage because JuiceFS has already written the metadata of the object storage to the `myjfs.db` database, so there is no need to provide it again when mounting.
diff --git a/docs/en/reference/how_to_setup_object_storage.md b/docs/en/reference/how_to_setup_object_storage.md
index 2e57ebeb..7a84cb03 100644
--- a/docs/en/reference/how_to_setup_object_storage.md
+++ b/docs/en/reference/how_to_setup_object_storage.md
@@ -56,7 +56,7 @@ Some cloud computing platforms also distinguish between internal and public netw
 
 JuiceFS also provides flexible support for this object storage service that distinguishes between internal and public addresses. For scenarios where the same file system is shared, the object storage is accessed through internal Endpoint on the servers that meet the criteria, and other computers are accessed through public Endpoint, which can be used as follows:
 
-- **When creating a file system**: It is recommended to use internal Endpoint address for `--bucket` 
+- **When creating a file system**: It is recommended to use internal Endpoint address for `--bucket`
 - **When mounting a file system**: For clients that do not satisfy the internal line, you can specify a public Endpoint address to `--bucket`.
 
 Creating a file system using an internal Endpoint ensures better performance and lower latency, and for clients that cannot be accessed through an internal address, you can specify a public Endpoint to mount with the option `--bucket`.
@@ -215,6 +215,20 @@ As you can see, there is no need to include authentication information in the co
 
 ## Azure Blob Storage
 
+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:
+
+```bash
+juicefs format \
+    --storage wasb \
+    --bucket https://<container>.<endpoint> \
+    --access-key <storage-account-name> \
+    --secret-key <storage-account-access-key> \
+    ... \
+    myjfs
+```
+
 In addition to providing authorization information through the options `--access-key` and `--secret-key`, you could also create a [connection string](https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string) and set the environment variable `AZURE_STORAGE_CONNECTION_STRING`. For example:
 
 ```bash
diff --git a/docs/zh_cn/getting-started/for_distributed.md b/docs/zh_cn/getting-started/for_distributed.md
index 3e2fc879..e2e71518 100644
--- a/docs/zh_cn/getting-started/for_distributed.md
+++ b/docs/zh_cn/getting-started/for_distributed.md
@@ -42,7 +42,7 @@ JuiceFS 目前支持的基于网络的数据库有：
 
 ### 1. 安装客户端
 
-在所有需要挂载文件系统的计算机上安装 JuiceFS 客户端，详情参照[安装 & 升级](installation.md)。
+在所有需要挂载文件系统的计算机上安装 JuiceFS 客户端，详情参照[「安装 & 升级」](installation.md)。
 
 ### 2. 准备对象存储
 
@@ -103,7 +103,7 @@ juicefs format \
 由于这个文件系统的「数据」和「元数据」都存储在基于网络的云服务中，因此在任何安装了 JuiceFS 客户端的计算机上都可以同时挂载该文件系统进行共享读写。例如：
 
 ```shell
-juicefs mount redis://tom:mypassword@myjfs-sh-abc.redis.rds.aliyuncs.com:6379/1 mnt
+juicefs mount redis://tom:mypassword@myjfs-sh-abc.redis.rds.aliyuncs.com:6379/1 ~/jfs
 ```
 
 #### 数据强一致性保证
@@ -123,7 +123,8 @@ juicefs mount
     --background \
     --cache-dir /mycache \
     --cache-size 512000 \
-    redis://tom:mypassword@myjfs-sh-abc.redis.rds.aliyuncs.com:6379/1 mnt
+    redis://tom:mypassword@myjfs-sh-abc.redis.rds.aliyuncs.com:6379/1 \
+    ~/jfs
 ```
 
 :::note 注意
@@ -152,10 +153,10 @@ redis://tom:mypassword@myjfs-sh-abc.redis.rds.aliyuncs.com:6379/1    /mnt/myjfs
 
 ### 6. 卸载文件系统
 
-你可以通过 `juicefs umount` 命令卸载 JuiceFS 文件系统（假设挂载点路径是 `mnt`）：
+你可以通过 `juicefs umount` 命令卸载 JuiceFS 文件系统（假设挂载点路径是 `~/jfs`）：
 
 ```shell
-juicefs umount mnt
+juicefs umount ~/jfs
 ```
 
 #### 卸载失败
@@ -163,7 +164,7 @@ juicefs umount mnt
 如果执行命令后，文件系统卸载失败，提示 `Device or resource busy`：
 
 ```shell
-2021-05-09 22:42:55.757097 I | fusermount: failed to unmount mnt: Device or resource busy
+2021-05-09 22:42:55.757097 I | fusermount: failed to unmount ~/jfs: Device or resource busy
 exit status 1
 ```
 
@@ -176,5 +177,5 @@ exit status 1
 当然，在你能够确保数据安全的前提下，也可以在卸载命令中添加 `--force` 或 `-f` 参数，强制卸载文件系统：
 
 ```shell
-juicefs umount --force mnt
+juicefs umount --force ~/jfs
 ```
diff --git a/docs/zh_cn/getting-started/for_local.md b/docs/zh_cn/getting-started/for_local.md
index 2feedbdf..bfc23011 100644
--- a/docs/zh_cn/getting-started/for_local.md
+++ b/docs/zh_cn/getting-started/for_local.md
@@ -26,7 +26,7 @@ juicefs format [command options] META-URL NAME
 
 可见，格式化文件系统需要提供 3 种信息：
 
-1. **[command options]**：设定文件系统的存储介质，留空则**默认使用本地磁盘**作为存储介质，路径为 `"$HOME/.juicefs/local"` 或 `"/var/jfs"`；
+1. **[command options]**：设定文件系统的存储介质，留空则**默认使用本地磁盘**作为存储介质，路径为 `"$HOME/.juicefs/local"`，`"/var/jfs"` 或 `"C:/jfs/local"`；
 2. **META-URL**：用来设置元数据存储，即数据库相关的信息，通常是数据库的 URL 或文件路径；
 3. **NAME**：是文件系统的名称。
 
@@ -84,28 +84,28 @@ Windows 系统的挂载点（MOUNTPOINT）应该使用尚未占用的盘符，
 由于 SQLite 是单文件数据库，挂载时要注意数据库文件的的路径，JuiceFS 同时支持相对路径和绝对路径。
 :::
 
-以下命令将 `myjfs` 文件系统挂载到当前目录下的 `mnt` 文件夹：
+以下命令将 `myjfs` 文件系统挂载到 `~/jfs` 文件夹：
 
 ```shell
-juicefs mount sqlite3://myjfs.db mnt
+juicefs mount sqlite3://myjfs.db ~/jfs
 ```
 
 ![](../images/sqlite-mount-local.png)
 
 默认情况下，客户端会在前台挂载文件系统。就像你在上图中看到的那样，程序会一直运行在当前终端进程中，使用 <kbd>Ctrl</kbd> + <kbd>C</kbd> 组合键或关闭终端窗口，文件系统会被卸载。
 
-为了让文件系统可以在后台保持挂载，你可以在挂载时指定 `-d` 或 `--background` 选项，即让客户端在守护进程中挂载文件系统。
+为了让文件系统可以在后台保持挂载，你可以在挂载时指定 `-d` 或 `--background` 选项，即让客户端在守护进程中挂载文件系统：
 
 ```shell
-juicefs mount sqlite3://myjfs.db mnt -d
+juicefs mount sqlite3://myjfs.db ~/jfs -d
 ```
 
-接下来，任何存入挂载点 `mnt` 的文件，都会按照 [JuiceFS 的文件存储格式](../introduction/architecture.md#如何存储文件)被拆分成特定的「数据块」并存入 `$HOME/.juicefs/local/myjfs` 目录中，相对应的「元数据」会全部存储在 `myjfs.db` 数据库中。
+接下来，任何存入挂载点 `~/jfs` 的文件，都会按照 [JuiceFS 的文件存储格式](../introduction/architecture.md#如何存储文件)被拆分成特定的「数据块」并存入 `$HOME/.juicefs/local/myjfs` 目录中，相对应的「元数据」会全部存储在 `myjfs.db` 数据库中。
 
-最后执行以下命令可以将挂载点 `mnt` 卸载：
+最后执行以下命令可以将挂载点 `~/jfs` 卸载：
 
 ```shell
-juicefs umount mnt
+juicefs umount ~/jfs
 ```
 
 ## 更进一步
@@ -120,8 +120,8 @@ JuiceFS 支持几乎所有的对象存储服务，查看「[JuiceFS 支持的存
 
 一般来说，创建对象存储通常只需要 2 个环节：
 
-1. 创建 `Bucket` 存储桶，拿到 Endpoint 地址；
-2. 创建 `Access Key ID` 和 `Access Key Secret`，即对象存储 API 的访问密钥。
+1. 创建 **Bucket** 存储桶，拿到 Endpoint 地址；
+2. 创建 **Access Key ID** 和 **Access Key Secret**，即对象存储 API 的访问密钥。
 
 以阿里云 OSS 为例，创建好的资源大概像下面这样：
 
@@ -130,7 +130,7 @@ JuiceFS 支持几乎所有的对象存储服务，查看「[JuiceFS 支持的存
 - **Access Key Secret**：`ZYXwvutsrqpoNMLkJiHgfeDCBA`
 
 :::note 注意
-创建对象存储时的过程各个平台会略有差别，建议查看云平台的帮助手册操作。另外，有些平台可能会针对内外网提供不同的 Endpoint 地址，由于本文要从本地访问对象存储，因此请选择使用面向外网访问的地址。
+创建对象存储的过程各个平台会略有差别，建议查看云平台的帮助手册操作。另外，有些平台可能会针对内外网提供不同的 Endpoint 地址，由于本文要从本地访问对象存储，因此请选择使用面向外网访问的地址。
 :::
 
 ### 上手实践
@@ -151,7 +151,7 @@ juicefs format --storage oss \
 
 在上述命令中，数据库和文件系统名称保持不变，增加了对象存储相关的信息：
 
-- `--storage`：设置存储类型，比如 oss、s3 等；
+- `--storage`：设置存储类型，比如 `oss`、`s3` 等；
 - `--bucket`：设置对象存储的 Endpoint 地址；
 - `--access-key`：设置对象存储 API 访问密钥 Access Key ID；
 - `--secret-key`：设置对象存储 API 访问密钥 Access Key Secret。
@@ -163,7 +163,7 @@ juicefs format --storage oss \
 创建完成即可进行挂载：
 
 ```shell
-juicefs mount sqlite3://myjfs.db mnt
+juicefs mount sqlite3://myjfs.db ~/jfs
 ```
 
 可以看到，挂载命令与使用本地存储时完全一样，因为 JuiceFS 已经把对象存储相关的信息写入了 `myjfs.db` 数据库，挂载时无需重复提供。
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 6a8ecf37..9b9ff8a6 100644
--- a/docs/zh_cn/reference/how_to_setup_object_storage.md
+++ b/docs/zh_cn/reference/how_to_setup_object_storage.md
@@ -123,6 +123,10 @@ $ juicefs format \
 | [Apache Ozone](#apache-ozone)               | `s3`         |
 | [Redis](#redis)                             | `redis`      |
 | [TiKV](#tikv)                               | `tikv`       |
+| [etcd](#etcd)                               | `etcd`       |
+| [SQLite](#sqlite)                           | `sqlite3`    |
+| [MySQL](#mysql)                             | `mysql`      |
+| [PostgreSQL](#postgresql)                   | `postgres`   |
 | [本地磁盘](#本地磁盘)                       | `file`       |
 
 ## Amazon S3
@@ -216,7 +220,21 @@ $ juicefs format \
 
 ## Azure Blob 存储
 
-使用 Azure Blob 作为 JuiceFS 的数据存储，除了使用 `--access-key` 和 `--secret-key` 选项之外，你也可以使用 [连接字符串](https://docs.microsoft.com/zh-cn/azure/storage/common/storage-configure-connection-string) 并通过 `AZURE_STORAGE_CONNECTION_STRING` 环境变量进行设定。例如：
+使用 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 中国）。例如：
+
+```bash
+juicefs format \
+    --storage wasb \
+    --bucket https://<container>.<endpoint> \
+    --access-key <storage-account-name> \
+    --secret-key <storage-account-access-key> \
+    ... \
+    myjfs
+```
+
+除了使用 `--access-key` 和 `--secret-key` 选项之外，你也可以使用 [连接字符串](https://docs.microsoft.com/zh-cn/azure/storage/common/storage-configure-connection-string) 并通过 `AZURE_STORAGE_CONNECTION_STRING` 环境变量进行设定。例如：
 
 ```bash
 # Use connection string
@@ -229,7 +247,7 @@ $ juicefs format \
 ```
 
 :::note 注意
-Azure China 用户，`EndpointSuffix` 值为 `core.chinacloudapi.cn`。
+对于 Azure 中国用户，`EndpointSuffix` 的值为 `core.chinacloudapi.cn`。
 :::
 
 ## Backblaze B2
@@ -302,7 +320,7 @@ $ juicefs format \
 
 使用 Scaleway 对象存储作为 JuiceFS 数据存储，请先 [查看文档](https://www.scaleway.com/en/docs/generate-api-keys) 了解如何创建  `Access key` 和 `Secret key`。
 
-`--bucket` 选项的设置格式为 `https://<bucket>.s3.<region>.scw.cloud`，请将其中的  `<region>`  替换成实际的区域代码，例如：荷兰阿姆斯特丹的区域代码是 `nl-ams`。[点此查看](https://www.scaleway.com/en/docs/object-storage-feature/#-Core-Concepts) 所有可用的区域代码。
+`--bucket` 选项的设置格式为 `https://<bucket>.s3.<region>.scw.cloud`，请将其中的 `<region>` 替换成实际的区域代码，例如：荷兰阿姆斯特丹的区域代码是 `nl-ams`。[点此查看](https://www.scaleway.com/en/docs/object-storage-feature/#-Core-Concepts) 所有可用的区域代码。
 
 ```bash
 $ juicefs format \
@@ -316,7 +334,7 @@ $ juicefs format \
 
 使用 DigitalOcean Spaces 作为 JuiceFS 数据存储，请先 [查看文档](https://www.digitalocean.com/community/tutorials/how-to-create-a-digitalocean-space-and-api-key) 了解如何创建  `Access key` 和 `Secret key`。
 
-`--bucket` 选项的设置格式为 `https://<space-name>.<region>.digitaloceanspaces.com`，请将其中的  `<region>`  替换成实际的区域代码，例如：`nyc3`。[点此查看](https://www.digitalocean.com/docs/spaces/#regional-availability) 所有可用的区域代码。
+`--bucket` 选项的设置格式为 `https://<space-name>.<region>.digitaloceanspaces.com`，请将其中的 `<region>` 替换成实际的区域代码，例如：`nyc3`。[点此查看](https://www.digitalocean.com/docs/spaces/#regional-availability) 所有可用的区域代码。
 
 ```bash
 $ juicefs format \
@@ -841,6 +859,109 @@ $ juicefs format \
     myjfs
 ```
 
+## etcd
+
+[etcd](https://etcd.io) 是一个高可用高可靠的小规模键值数据库，既可以用作 JuiceFS 的元数据存储，也可以用于 JuiceFS 的数据存储。
+
+etcd 默认会限制单个请求不能超过 1.5MB，需要将 JuiceFS 的分块大小改成 1MB 甚至更低。
+
+`--bucket` 选项需要填 etcd 的地址，格式类似 `<host1>:<port>,<host2>:<port>,<host3>:<port>`，`--access-key` 和 `--secret-key` 填用户名和密码，当 etcd 没有设置用户认证时可以省略。例如：
+
+```bash
+$ juicefs format \
+    --storage etcd --block-size 1024 \
+    --bucket "<host1>:<port>,<host2>:<port>,<host3>:<port>/prefix" \
+    --access-key myname
+    --secret-key mypass
+    ... \
+    myjfs
+```
+
+### 设置 TLS
+
+如果需要开启 TLS，可以通过在 Bucket-URL 后以添加 query 参数的形式设置 TLS 的配置项，目前支持的配置项：
+
+| 配置项             | 值                    |
+|-------------------|-----------------------|
+| cacert            | CA 根证书              |
+| cert              | 证书文件路径            |
+| key               | 私钥文件路径            |
+| server-name       | 服务器名称              |
+| insecure-skip-verify | 1                  |
+
+例子：
+```bash
+$ juicefs format \
+    --storage etcd \
+    --bucket "<host>:<port>,<host>:<port>,<host>:<port>?cacert=/path/to/ca.pem&cert=/path/to/server.pem&key=/path/to/key.pem&server-name=etcd" \
+    ... \
+    myjfs
+```
+
+注意：证书的路径需要使用绝对路径，并且确保所有需要挂载的机器上能用该路径访问到它们。
+
+## SQLite
+
+[SQLite](https://sqlite.org) 是全球广泛使用的小巧、快速、单文件、可靠、全功能的单文件 SQL 数据库引擎。
+
+使用 SQLite 作为数据存储时只需要指定它的绝对路径即可。
+
+```shell
+$ juicefs format \
+    --storage sqlite3 \
+    --bucket /path/to/sqlite3.db
+    ... \
+    myjfs
+```
+
+:::note 注意
+由于 SQLite 是一款嵌入式数据库，只有数据库所在的主机可以访问它，不能用于多机共享场景。如果格式化时使用的是相对路径，会导致挂载时出问题，请使用绝对路径。
+:::
+
+## MySQL
+
+[MySQL](https://www.mysql.com/) 是受欢迎的开源关系型数据库之一，常被作为 Web 应用程序的首选数据库，既可以作为 JuiceFS 的元数据引擎也可以用来存储文件数据。跟 MySQL 兼容的 [MariaDB](https://mariadb.org)、[TiDB](https://pingcap.com/) 等都可以用来作为数据存储。
+
+使用 MySQL 作为数据存储时，需要提前创建数据库并添加想要权限，通过`--bucket`参数指定访问地址，通过`--access-key`指定用户名，通过 `--secret-key`指定密码，示例如下：
+
+```shell
+$ juicefs format \
+    --storage mysql \
+    --bucket (<host>:3306)/<database-name> \
+    --access-key <username> \
+    --secret-key <password> \
+    ... \
+    myjfs
+```
+
+创建文件系统后，JuiceFS 会在该数据库中创建名为 `jfs_blob` 的表用来存储数据。
+
+:::note 注意
+不要漏掉 `--bucket` 参数里的括号 `()` 。
+:::
+
+## PostgreSQL
+
+[PostgreSQL](https://www.postgresql.org/) 是功能强大的开源关系型数据库，有完善的生态和丰富的应用场景，既可以作为 JuiceFS 的元数据引擎也可以作为数据存储。其他跟 PostgreSQL 协议兼容的数据库（比如 CockroachDB 等) 也可以。
+
+创建文件系统时需要先创建好数据库并添加相应读写权限，使用 `--bucket` 来指定数据的地址，使用 `--access-key` 指定用户名，使用 `--secret-key`指定密码，实例如下：
+
+```shell
+$ juicefs format \
+    --storage postgres \
+    --bucket <host>:<port>/<db>[?parameters] \
+    --access-key <username> \
+    --secret-key <password> \
+    ... \
+    myjfs
+```
+
+创建文件系统后，JuiceFS 会在该数据库中创建名为 `jfs_blob` 的表用来存储数据。
+
+### 故障排除
+
+JuiceFS 客户端默认采用 SSL 加密连接 PostgreSQL，如果连接时报错  `pq: SSL is not enabled on the server` 说明数据库没有启用 SSL。可以根据业务场景为 PostgreSQL 启用 SSL 加密，也可以在元数据 URL 中添加参数 `sslmode=disable` 禁用加密验证。
+
 ## 本地磁盘
 
 在创建 JuiceFS 文件系统时，如果没有指定任何存储类型，会默认使用本地磁盘作为数据存储，root 用户默认存储路径为 `/var/jfs`，普通用户默认存储路径为 `~/.juicefs/local`。
diff --git a/pkg/chunk/cached_store.go b/pkg/chunk/cached_store.go
index c779b544..24726338 100644
--- a/pkg/chunk/cached_store.go
+++ b/pkg/chunk/cached_store.go
@@ -441,8 +441,7 @@ func (store *cachedStore) upload(key string, block *Page, c *wChunk) error {
 	n, err := store.compressor.Compress(buf.Data, block.Data)
 	block.Release()
 	if err != nil {
-		logger.Fatalf("Compress block key %s: %s", key, err)
-		return err
+		return fmt.Errorf("Compress block key %s: %s", key, err)
 	}
 	buf.Data = buf.Data[:n]
 
@@ -488,7 +487,7 @@ func (c *wChunk) upload(indx int) {
 			}
 		}
 		if off != blen {
-			logger.Fatalf("block length does not match: %v != %v", off, blen)
+			panic(fmt.Sprintf("block length does not match: %v != %v", off, blen))
 		}
 		if c.store.conf.Writeback {
 			stagingPath, err := c.store.bcache.stage(key, block.Data, c.store.shouldCache(blen))
@@ -534,7 +533,7 @@ func (c *wChunk) Len() int {
 
 func (c *wChunk) FlushTo(offset int) error {
 	if offset < c.uploaded {
-		logger.Fatalf("Invalid offset: %d < %d", offset, c.uploaded)
+		panic(fmt.Sprintf("Invalid offset: %d < %d", offset, c.uploaded))
 	}
 	for i, block := range c.pages {
 		start := i * c.store.conf.BlockSize
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index fe0f25a1..5031ef1e 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -28,6 +28,7 @@ import (
 	"time"
 
 	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/juicedata/juicefs/pkg/version"
 )
 
 const (
@@ -69,6 +70,8 @@ type engine interface {
 	doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst Ino, nameDst string, flags uint32, inode *Ino, attr *Attr) syscall.Errno
 	doSetXattr(ctx Context, inode Ino, name string, value []byte, flags uint32) syscall.Errno
 	doRemoveXattr(ctx Context, inode Ino, name string) syscall.Errno
+
+	GetSession(sid uint64, detail bool) (*Session, error)
 }
 
 type baseMeta struct {
@@ -82,6 +85,7 @@ type baseMeta struct {
 	of           *openfiles
 	removedFiles map[Ino]bool
 	compacting   map[uint64]bool
+	maxDeleting  chan struct{}
 	symlinks     *sync.Map
 	msgCallbacks *msgCallbacks
 	newSpace     int64
@@ -110,6 +114,7 @@ func newBaseMeta(conf *Config) baseMeta {
 		of:           newOpenFiles(conf.OpenCache),
 		removedFiles: make(map[Ino]bool),
 		compacting:   make(map[uint64]bool),
+		maxDeleting:  make(chan struct{}, 100),
 		symlinks:     &sync.Map{},
 		msgCallbacks: &msgCallbacks{
 			callbacks: make(map[uint32]MsgCallback),
@@ -166,6 +171,7 @@ func (m *baseMeta) Load(checkVersion bool) (*Format, error) {
 func (m *baseMeta) NewSession() error {
 	go m.refreshUsage()
 	if m.conf.ReadOnly {
+		logger.Infof("Create read-only session OK with version: %s", version.Version())
 		return nil
 	}
 
@@ -183,7 +189,7 @@ func (m *baseMeta) NewSession() error {
 	if err = m.en.doNewSession(data); err != nil {
 		return fmt.Errorf("create session: %s", err)
 	}
-	logger.Infof("create session %d OK", m.sid)
+	logger.Infof("Create session %d OK with version: %s", m.sid, version.Version())
 
 	go m.refreshSession()
 	if !m.conf.NoBGJob {
@@ -229,7 +235,12 @@ func (m *baseMeta) CleanStaleSessions() {
 		return
 	}
 	for _, sid := range sids {
-		logger.Infof("clean up stale session %d: %s", sid, m.en.doCleanStaleSession(sid))
+		s, err := m.en.GetSession(sid, false)
+		if err != nil {
+			logger.Warnf("Get session info %d: %s", sid, err)
+			s = &Session{Sid: sid}
+		}
+		logger.Infof("clean up stale session %d %+v: %s", sid, s.SessionInfo, m.en.doCleanStaleSession(sid))
 	}
 }
 
@@ -780,9 +791,7 @@ func (m *baseMeta) Close(ctx Context, inode Ino) syscall.Errno {
 		defer m.Unlock()
 		if m.removedFiles[inode] {
 			delete(m.removedFiles, inode)
-			go func() {
-				_ = m.en.doDeleteSustainedInode(m.sid, inode)
-			}()
+			_ = m.en.doDeleteSustainedInode(m.sid, inode)
 		}
 	}
 	return 0
@@ -843,12 +852,24 @@ func (m *baseMeta) fileDeleted(opened bool, inode Ino, length uint64) {
 		m.removedFiles[inode] = true
 		m.Unlock()
 	} else {
-		go m.en.doDeleteFileData(inode, length)
+		m.tryDeleteFileData(inode, length)
+	}
+}
+
+func (m *baseMeta) tryDeleteFileData(inode Ino, length uint64) {
+	select {
+	case m.maxDeleting <- struct{}{}:
+		go func() {
+			m.en.doDeleteFileData(inode, length)
+			<-m.maxDeleting
+		}()
+	default:
+		// will be cleanup later
 	}
 }
 
 func (m *baseMeta) deleteSlice(chunkid uint64, size uint32) {
-	if err := m.newMsg(DeleteChunk, chunkid, size); err == nil {
+	if err := m.newMsg(DeleteChunk, chunkid, size); err == nil || strings.Contains(err.Error(), "NoSuchKey") || strings.Contains(err.Error(), "not found") {
 		if err = m.en.doDeleteSlice(chunkid, size); err != nil {
 			logger.Errorf("delete slice %d: %s", chunkid, err)
 		}
diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go
index 3627e018..361851ea 100644
--- a/pkg/meta/interface.go
+++ b/pkg/meta/interface.go
@@ -249,7 +249,7 @@ type Meta interface {
 	// CloseSession does cleanup and close the session.
 	CloseSession() error
 	// GetSession retrieves information of session with sid
-	GetSession(sid uint64) (*Session, error)
+	GetSession(sid uint64, detail bool) (*Session, error)
 	// ListSessions returns all client sessions.
 	ListSessions() ([]*Session, error)
 	// CleanStaleSessions cleans up sessions not active for more than 5 minutes
@@ -389,7 +389,7 @@ func NewClient(uri string, conf *Config) Meta {
 	}
 	m, err := f(driver, uri[p+3:], conf)
 	if err != nil {
-		logger.Fatalf("Meta is not available: %s", err)
+		logger.Fatalf("Meta %s is not available: %s", uri, err)
 	}
 	return m
 }
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 46fabf1e..a1eeb6e8 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -228,7 +228,7 @@ func (m *redisMeta) Init(format Format, force bool) error {
 		var old Format
 		err = json.Unmarshal(body, &old)
 		if err != nil {
-			logger.Fatalf("existing format is broken: %s", err)
+			return fmt.Errorf("existing format is broken: %s", err)
 		}
 		if err = format.update(&old, force); err != nil {
 			return err
@@ -237,7 +237,7 @@ func (m *redisMeta) Init(format Format, force bool) error {
 
 	data, err := json.MarshalIndent(format, "", "")
 	if err != nil {
-		logger.Fatalf("json: %s", err)
+		return fmt.Errorf("json: %s", err)
 	}
 	ts := time.Now().Unix()
 	attr := &Attr{
@@ -408,7 +408,7 @@ func (m *redisMeta) getSession(sid string, detail bool) (*Session, error) {
 	return &s, nil
 }
 
-func (m *redisMeta) GetSession(sid uint64) (*Session, error) {
+func (m *redisMeta) GetSession(sid uint64, detail bool) (*Session, error) {
 	var legacy bool
 	key := strconv.FormatUint(sid, 10)
 	score, err := m.rdb.ZScore(Background, m.allSessions(), key).Result()
@@ -422,7 +422,7 @@ func (m *redisMeta) GetSession(sid uint64) (*Session, error) {
 	if err != nil {
 		return nil, err
 	}
-	s, err := m.getSession(key, true)
+	s, err := m.getSession(key, detail)
 	if err != nil {
 		return nil, err
 	}
@@ -1874,7 +1874,7 @@ func (m *redisMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	})
 	if err == nil {
 		m.updateStats(newSpace, -1)
-		go m.doDeleteFileData(inode, attr.Length)
+		m.tryDeleteFileData(inode, attr.Length)
 	}
 	return err
 }
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index 322344f2..2fd41b97 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -218,30 +218,30 @@ func (m *dbMeta) Name() string {
 }
 
 func (m *dbMeta) doDeleteSlice(chunkid uint64, size uint32) error {
-	return m.txn(func(ses *xorm.Session) error {
-		_, err := ses.Exec("delete from jfs_chunk_ref where chunkid=?", chunkid)
+	return m.txn(func(s *xorm.Session) error {
+		_, err := s.Exec("delete from jfs_chunk_ref where chunkid=?", chunkid)
 		return err
 	})
 }
 
 func (m *dbMeta) Init(format Format, force bool) error {
 	if err := m.db.Sync2(new(setting), new(counter)); err != nil {
-		logger.Fatalf("create table setting, counter: %s", err)
+		return fmt.Errorf("create table setting, counter: %s", err)
 	}
 	if err := m.db.Sync2(new(edge)); err != nil && !strings.Contains(err.Error(), "Duplicate entry") {
-		logger.Fatalf("create table edge: %s", err)
+		return fmt.Errorf("create table edge: %s", err)
 	}
 	if err := m.db.Sync2(new(node), new(symlink), new(xattr)); err != nil {
-		logger.Fatalf("create table node, symlink, xattr: %s", err)
+		return fmt.Errorf("create table node, symlink, xattr: %s", err)
 	}
 	if err := m.db.Sync2(new(chunk), new(chunkRef), new(delslices)); err != nil {
-		logger.Fatalf("create table chunk, chunk_ref, delslices: %s", err)
+		return fmt.Errorf("create table chunk, chunk_ref, delslices: %s", err)
 	}
 	if err := m.db.Sync2(new(session2), new(sustained), new(delfile)); err != nil {
-		logger.Fatalf("create table session2, sustaind, delfile: %s", err)
+		return fmt.Errorf("create table session2, sustaind, delfile: %s", err)
 	}
 	if err := m.db.Sync2(new(flock), new(plock)); err != nil {
-		logger.Fatalf("create table flock, plock: %s", err)
+		return fmt.Errorf("create table flock, plock: %s", err)
 	}
 
 	var s = setting{Name: "format"}
@@ -279,7 +279,7 @@ func (m *dbMeta) Init(format Format, force bool) error {
 	}
 	return m.txn(func(s *xorm.Session) error {
 		if format.TrashDays > 0 {
-			ok2, err := s.Get(&node{Inode: TrashInode})
+			ok2, err := s.ForUpdate().Get(&node{Inode: TrashInode})
 			if err != nil {
 				return err
 			}
@@ -420,12 +420,12 @@ func (m *dbMeta) getSession(row interface{}, detail bool) (*Session, error) {
 	return &s, nil
 }
 
-func (m *dbMeta) GetSession(sid uint64) (*Session, error) {
+func (m *dbMeta) GetSession(sid uint64, detail bool) (*Session, error) {
 	row := session2{Sid: sid}
 	if ok, err := m.db.Get(&row); err != nil {
 		return nil, err
 	} else if ok {
-		return m.getSession(&row, true)
+		return m.getSession(&row, detail)
 	}
 	if ok, err := m.db.IsTableExist(&session{}); err != nil {
 		return nil, err
@@ -434,7 +434,7 @@ func (m *dbMeta) GetSession(sid uint64) (*Session, error) {
 		if ok, err := m.db.Get(&row); err != nil {
 			return nil, err
 		} else if ok {
-			return m.getSession(&row, true)
+			return m.getSession(&row, detail)
 		}
 	}
 	return nil, fmt.Errorf("session not found: %d", sid)
@@ -486,7 +486,7 @@ func (m *dbMeta) incrCounter(name string, value int64) (int64, error) {
 	var v int64
 	err := m.txn(func(s *xorm.Session) error {
 		var c = counter{Name: name}
-		ok, err := s.Get(&c)
+		ok, err := s.ForUpdate().Get(&c)
 		if err != nil {
 			return err
 		}
@@ -509,7 +509,7 @@ func (m *dbMeta) setIfSmall(name string, value, diff int64) (bool, error) {
 	err := m.txn(func(s *xorm.Session) error {
 		changed = false
 		c := counter{Name: name}
-		ok, err := s.Get(&c)
+		ok, err := s.ForUpdate().Get(&c)
 		if err != nil {
 			return err
 		}
@@ -582,7 +582,6 @@ func (m *dbMeta) txn(f func(s *xorm.Session) error) error {
 	var err error
 	for i := 0; i < 50; i++ {
 		_, err = m.db.Transaction(func(s *xorm.Session) (interface{}, error) {
-			s.ForUpdate()
 			return nil, f(s)
 		})
 		if m.shouldRetry(err) {
@@ -645,12 +644,12 @@ func (m *dbMeta) flushStats() {
 }
 
 func (m *dbMeta) doLookup(ctx Context, parent Ino, name string, inode *Ino, attr *Attr) syscall.Errno {
-	dbSession := m.db.Table(&edge{})
+	db := m.db.Table(&edge{})
 	if attr != nil {
-		dbSession = dbSession.Join("INNER", &node{}, "jfs_edge.inode=jfs_node.inode")
+		db = db.Join("INNER", &node{}, "jfs_edge.inode=jfs_node.inode")
 	}
 	nn := namedNode{node: node{Parent: parent}, Name: []byte(name)}
-	exist, err := dbSession.Select("*").Get(&nn)
+	exist, err := db.Select("*").Get(&nn)
 	if err != nil {
 		return errno(err)
 	}
@@ -703,7 +702,7 @@ func (m *dbMeta) SetAttr(ctx Context, inode Ino, set uint16, sugidclearmode uint
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFE) }()
 	return errno(m.txn(func(s *xorm.Session) error {
 		var cur = node{Inode: inode}
-		ok, err := s.Get(&cur)
+		ok, err := s.ForUpdate().Get(&cur)
 		if err != nil {
 			return err
 		}
@@ -804,7 +803,7 @@ func (m *dbMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, at
 	var newSpace int64
 	err := m.txn(func(s *xorm.Session) error {
 		var n = node{Inode: inode}
-		ok, err := s.Get(&n)
+		ok, err := s.ForUpdate().Get(&n)
 		if err != nil {
 			return err
 		}
@@ -829,7 +828,7 @@ func (m *dbMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, at
 			right, left = left, right
 		}
 		if right/ChunkSize-left/ChunkSize > 1 {
-			rows, err := s.Where("inode = ? AND indx > ? AND indx < ?", inode, left/ChunkSize, right/ChunkSize).Cols("indx").Rows(&c)
+			rows, err := s.Where("inode = ? AND indx > ? AND indx < ?", inode, left/ChunkSize, right/ChunkSize).Cols("indx").ForUpdate().Rows(&c)
 			if err != nil {
 				return err
 			}
@@ -903,7 +902,7 @@ func (m *dbMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 	var newSpace int64
 	err := m.txn(func(s *xorm.Session) error {
 		var n = node{Inode: inode}
-		ok, err := s.Get(&n)
+		ok, err := s.ForUpdate().Get(&n)
 		if err != nil {
 			return err
 		}
@@ -1006,7 +1005,7 @@ func (m *dbMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 
 	err = m.txn(func(s *xorm.Session) error {
 		var pn = node{Inode: parent}
-		ok, err := s.Get(&pn)
+		ok, err := s.ForUpdate().Get(&pn)
 		if err != nil {
 			return err
 		}
@@ -1017,7 +1016,7 @@ func (m *dbMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 			return syscall.ENOTDIR
 		}
 		var e = edge{Parent: parent, Name: []byte(name)}
-		ok, err = s.Get(&e)
+		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
 			return err
 		}
@@ -1033,7 +1032,7 @@ func (m *dbMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 		if foundIno != 0 {
 			if _type == TypeFile || _type == TypeDirectory {
 				foundNode := node{Inode: foundIno}
-				ok, err = s.Get(&foundNode)
+				ok, err = s.ForUpdate().Get(&foundNode)
 				if err != nil {
 					return err
 				} else if ok {
@@ -1103,7 +1102,7 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 	var opened bool
 	err := m.txn(func(s *xorm.Session) error {
 		var pn = node{Inode: parent}
-		ok, err := s.Get(&pn)
+		ok, err := s.ForUpdate().Get(&pn)
 		if err != nil {
 			return err
 		}
@@ -1114,7 +1113,7 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 			return syscall.ENOTDIR
 		}
 		var e = edge{Parent: parent, Name: []byte(name)}
-		ok, err = s.Get(&e)
+		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
 			return err
 		}
@@ -1134,7 +1133,7 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 		}
 
 		n = node{Inode: e.Inode}
-		ok, err = s.Get(&n)
+		ok, err = s.ForUpdate().Get(&n)
 		if err != nil {
 			return err
 		}
@@ -1235,7 +1234,7 @@ func (m *dbMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 	}
 	err := m.txn(func(s *xorm.Session) error {
 		var pn = node{Inode: parent}
-		ok, err := s.Get(&pn)
+		ok, err := s.ForUpdate().Get(&pn)
 		if err != nil {
 			return err
 		}
@@ -1246,7 +1245,7 @@ func (m *dbMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 			return syscall.ENOTDIR
 		}
 		var e = edge{Parent: parent, Name: []byte(name)}
-		ok, err = s.Get(&e)
+		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
 			return err
 		}
@@ -1264,7 +1263,7 @@ func (m *dbMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 		if e.Type != TypeDirectory {
 			return syscall.ENOTDIR
 		}
-		exist, err := s.Exist(&edge{Parent: e.Inode})
+		exist, err := s.ForUpdate().Exist(&edge{Parent: e.Inode})
 		if err != nil {
 			return err
 		}
@@ -1272,7 +1271,7 @@ func (m *dbMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
 			return syscall.ENOTEMPTY
 		}
 		var n = node{Inode: e.Inode}
-		ok, err = s.Get(&n)
+		ok, err = s.ForUpdate().Get(&n)
 		if err != nil {
 			return err
 		}
@@ -1335,7 +1334,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 	var newSpace, newInode int64
 	err := m.txn(func(s *xorm.Session) error {
 		var se = edge{Parent: parentSrc, Name: []byte(nameSrc)}
-		ok, err := s.Get(&se)
+		ok, err := s.ForUpdate().Get(&se)
 		if err != nil {
 			return err
 		}
@@ -1357,7 +1356,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			return nil
 		}
 		var spn = node{Inode: parentSrc}
-		ok, err = s.Get(&spn)
+		ok, err = s.ForUpdate().Get(&spn)
 		if err != nil {
 			return err
 		}
@@ -1368,7 +1367,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			return syscall.ENOTDIR
 		}
 		var dpn = node{Inode: parentDst}
-		ok, err = s.Get(&dpn)
+		ok, err = s.ForUpdate().Get(&dpn)
 		if err != nil {
 			return err
 		}
@@ -1379,7 +1378,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			return syscall.ENOTDIR
 		}
 		var sn = node{Inode: se.Inode}
-		ok, err = s.Get(&sn)
+		ok, err = s.ForUpdate().Get(&sn)
 		if err != nil {
 			return err
 		}
@@ -1388,7 +1387,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		}
 
 		var de = edge{Parent: parentDst, Name: []byte(nameDst)}
-		ok, err = s.Get(&de)
+		ok, err = s.ForUpdate().Get(&de)
 		if err != nil {
 			return err
 		}
@@ -1409,7 +1408,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				return syscall.EEXIST
 			}
 			dino = de.Inode
-			ok, err := s.Get(&dn)
+			ok, err := s.ForUpdate().Get(&dn)
 			if err != nil {
 				return err
 			}
@@ -1427,7 +1426,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				}
 			} else {
 				if de.Type == TypeDirectory {
-					exist, err := s.Exist(&edge{Parent: de.Inode})
+					exist, err := s.ForUpdate().Exist(&edge{Parent: de.Inode})
 					if err != nil {
 						return err
 					}
@@ -1583,7 +1582,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 func (m *dbMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr) syscall.Errno {
 	return errno(m.txn(func(s *xorm.Session) error {
 		var pn = node{Inode: parent}
-		ok, err := s.Get(&pn)
+		ok, err := s.ForUpdate().Get(&pn)
 		if err != nil {
 			return err
 		}
@@ -1594,7 +1593,7 @@ func (m *dbMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 			return syscall.ENOTDIR
 		}
 		var e = edge{Parent: parent, Name: []byte(name)}
-		ok, err = s.Get(&e)
+		ok, err = s.ForUpdate().Get(&e)
 		if err != nil {
 			return err
 		}
@@ -1603,7 +1602,7 @@ func (m *dbMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 		}
 
 		var n = node{Inode: inode}
-		ok, err = s.Get(&n)
+		ok, err = s.ForUpdate().Get(&n)
 		if err != nil {
 			return err
 		}
@@ -1773,7 +1772,7 @@ func (m *dbMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	var n = node{Inode: inode}
 	var newSpace int64
 	err := m.txn(func(s *xorm.Session) error {
-		ok, err := s.Get(&n)
+		ok, err := s.ForUpdate().Get(&n)
 		if err != nil {
 			return err
 		}
@@ -1793,7 +1792,7 @@ func (m *dbMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	})
 	if err == nil {
 		m.updateStats(newSpace, -1)
-		go m.doDeleteFileData(inode, n.Length)
+		m.tryDeleteFileData(inode, n.Length)
 	}
 	return err
 }
@@ -1838,7 +1837,7 @@ func (m *dbMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 	var needCompact bool
 	err := m.txn(func(s *xorm.Session) error {
 		var n = node{Inode: inode}
-		ok, err := s.Get(&n)
+		ok, err := s.ForUpdate().Get(&n)
 		if err != nil {
 			return err
 		}
@@ -1861,7 +1860,7 @@ func (m *dbMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 		n.Ctime = now
 
 		var ck chunk
-		ok, err = s.Where("Inode = ? and Indx = ?", inode, indx).Get(&ck)
+		ok, err = s.ForUpdate().Where("Inode = ? and Indx = ?", inode, indx).Get(&ck)
 		if err != nil {
 			return err
 		}
@@ -1904,11 +1903,11 @@ func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 	defer func() { m.of.InvalidateChunk(fout, 0xFFFFFFFF) }()
 	err := m.txn(func(s *xorm.Session) error {
 		var nin, nout = node{Inode: fin}, node{Inode: fout}
-		ok, err := s.Get(&nin)
+		ok, err := s.ForUpdate().Get(&nin)
 		if err != nil {
 			return err
 		}
-		ok2, err2 := s.Get(&nout)
+		ok2, err2 := s.ForUpdate().Get(&nout)
 		if err2 != nil {
 			return err2
 		}
@@ -1942,7 +1941,7 @@ func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 		nout.Ctime = now
 
 		var c chunk
-		rows, err := s.Where("inode = ? AND indx >= ? AND indx <= ?", fin, offIn/ChunkSize, (offIn+size)/ChunkSize).Rows(&c)
+		rows, err := s.Where("inode = ? AND indx >= ? AND indx <= ?", fin, offIn/ChunkSize, (offIn+size)/ChunkSize).ForUpdate().Rows(&c)
 		if err != nil {
 			return err
 		}
@@ -2059,8 +2058,8 @@ func (m *dbMeta) doCleanupSlices() {
 func (m *dbMeta) deleteChunk(inode Ino, indx uint32) error {
 	var c chunk
 	var ss []*slice
-	err := m.txn(func(ses *xorm.Session) error {
-		ok, err := ses.Where("inode = ? AND indx = ?", inode, indx).Get(&c)
+	err := m.txn(func(s *xorm.Session) error {
+		ok, err := s.ForUpdate().Where("inode = ? AND indx = ?", inode, indx).Get(&c)
 		if err != nil {
 			return err
 		}
@@ -2068,14 +2067,14 @@ func (m *dbMeta) deleteChunk(inode Ino, indx uint32) error {
 			return nil
 		}
 		ss = readSliceBuf(c.Slices)
-		for _, s := range ss {
-			_, err = ses.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? AND size=?", s.chunkid, s.size)
+		for _, sc := range ss {
+			_, err = s.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? AND size=?", sc.chunkid, sc.size)
 			if err != nil {
 				return err
 			}
 		}
 		c.Slices = nil
-		n, err := ses.Where("inode = ? AND indx = ?", inode, indx).Delete(&c)
+		n, err := s.Where("inode = ? AND indx = ?", inode, indx).Delete(&c)
 		if err == nil && n == 0 {
 			err = fmt.Errorf("chunk %d:%d changed, try restarting transaction", inode, indx)
 		}
@@ -2137,7 +2136,7 @@ func (m *dbMeta) doCleanupDelayedSlices(edge int64, limit int) (int, error) {
 	for _, ds := range result {
 		if err = m.txn(func(ses *xorm.Session) error {
 			ds := delslices{Chunkid: ds.Chunkid}
-			if ok, e := ses.Get(&ds); e != nil {
+			if ok, e := ses.ForUpdate().Get(&ds); e != nil {
 				return e
 			} else if !ok {
 				return nil
@@ -2225,9 +2224,9 @@ func (m *dbMeta) compactChunk(inode Ino, indx uint32, force bool) {
 			buf = append(buf, m.encodeDelayedSlice(s.chunkid, s.size)...)
 		}
 	}
-	err = m.txn(func(ses *xorm.Session) error {
+	err = m.txn(func(s *xorm.Session) error {
 		var c2 = chunk{Inode: inode}
-		_, err := ses.Where("indx=?", indx).Get(&c2)
+		_, err := s.ForUpdate().Where("indx=?", indx).Get(&c2)
 		if err != nil {
 			return err
 		}
@@ -2237,20 +2236,20 @@ func (m *dbMeta) compactChunk(inode Ino, indx uint32, force bool) {
 		}
 
 		c2.Slices = append(append(c2.Slices[:skipped*sliceBytes], marshalSlice(pos, chunkid, size, 0, size)...), c2.Slices[len(c.Slices):]...)
-		if _, err := ses.Where("Inode = ? AND indx = ?", inode, indx).Update(c2); err != nil {
+		if _, err := s.Where("Inode = ? AND indx = ?", inode, indx).Update(c2); err != nil {
 			return err
 		}
 		// create the key to tracking it
-		if err = mustInsert(ses, chunkRef{chunkid, size, 1}); err != nil {
+		if err = mustInsert(s, chunkRef{chunkid, size, 1}); err != nil {
 			return err
 		}
 		if trash {
-			if err = mustInsert(ses, &delslices{chunkid, time.Now().Unix(), buf}); err != nil {
+			if err = mustInsert(s, &delslices{chunkid, time.Now().Unix(), buf}); err != nil {
 				return err
 			}
 		} else {
-			for _, s := range ss {
-				if _, err := ses.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? and size=?", s.chunkid, s.size); err != nil {
+			for _, s_ := range ss {
+				if _, err := s.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? and size=?", s_.chunkid, s_.size); err != nil {
 					return err
 				}
 			}
@@ -2480,9 +2479,9 @@ func (m *dbMeta) doRemoveXattr(ctx Context, inode Ino, name string) syscall.Errn
 
 func (m *dbMeta) dumpEntry(inode Ino, typ uint8) (*DumpedEntry, error) {
 	e := &DumpedEntry{}
-	return e, m.txn(func(s *xorm.Session) error {
+	return e, m.txn(func(db *xorm.Session) error {
 		n := &node{Inode: inode}
-		ok, err := m.db.Get(n)
+		ok, err := db.Get(n)
 		if err != nil {
 			return err
 		}
@@ -2496,7 +2495,7 @@ func (m *dbMeta) dumpEntry(inode Ino, typ uint8) (*DumpedEntry, error) {
 		e.Attr.Inode = inode
 
 		var rows []xattr
-		if err = m.db.Find(&rows, &xattr{Inode: inode}); err != nil {
+		if err = db.Find(&rows, &xattr{Inode: inode}); err != nil {
 			return err
 		}
 		if len(rows) > 0 {
@@ -2511,7 +2510,7 @@ func (m *dbMeta) dumpEntry(inode Ino, typ uint8) (*DumpedEntry, error) {
 		if attr.Typ == TypeFile {
 			for indx := uint32(0); uint64(indx)*ChunkSize < attr.Length; indx++ {
 				c := &chunk{Inode: inode, Indx: indx}
-				if ok, err = m.db.Get(c); err != nil {
+				if ok, err = db.Get(c); err != nil {
 					return err
 				}
 				if !ok {
@@ -2527,7 +2526,7 @@ func (m *dbMeta) dumpEntry(inode Ino, typ uint8) (*DumpedEntry, error) {
 			}
 		} else if attr.Typ == TypeSymlink {
 			l := &symlink{Inode: inode}
-			ok, err = m.db.Get(l)
+			ok, err = db.Get(l)
 			if err != nil {
 				return err
 			}
diff --git a/pkg/meta/sql_lock.go b/pkg/meta/sql_lock.go
index 79113f60..4195f73e 100644
--- a/pkg/meta/sql_lock.go
+++ b/pkg/meta/sql_lock.go
@@ -38,13 +38,13 @@ func (m *dbMeta) Flock(ctx Context, inode Ino, owner_ uint64, ltype uint32, bloc
 	var err syscall.Errno
 	for {
 		err = errno(m.txn(func(s *xorm.Session) error {
-			if exists, err := s.Get(&node{Inode: inode}); err != nil || !exists {
+			if exists, err := s.ForUpdate().Get(&node{Inode: inode}); err != nil || !exists {
 				if err == nil && !exists {
 					err = syscall.ENOENT
 				}
 				return err
 			}
-			rows, err := s.Rows(&flock{Inode: inode})
+			rows, err := s.ForUpdate().Rows(&flock{Inode: inode})
 			if err != nil {
 				return err
 			}
@@ -159,7 +159,7 @@ func (m *dbMeta) Setlk(ctx Context, inode Ino, owner_ uint64, block bool, ltype
 	owner := int64(owner_)
 	for {
 		err = errno(m.txn(func(s *xorm.Session) error {
-			if exists, err := s.Get(&node{Inode: inode}); err != nil || !exists {
+			if exists, err := s.ForUpdate().Get(&node{Inode: inode}); err != nil || !exists {
 				if err == nil && !exists {
 					err = syscall.ENOENT
 				}
@@ -167,7 +167,7 @@ func (m *dbMeta) Setlk(ctx Context, inode Ino, owner_ uint64, block bool, ltype
 			}
 			if ltype == F_UNLCK {
 				var l = plock{Inode: inode, Owner: owner, Sid: m.sid}
-				ok, err := m.db.Get(&l)
+				ok, err := s.ForUpdate().Get(&l)
 				if err != nil {
 					return err
 				}
@@ -186,7 +186,7 @@ func (m *dbMeta) Setlk(ctx Context, inode Ino, owner_ uint64, block bool, ltype
 				}
 				return err
 			}
-			rows, err := s.Rows(&plock{Inode: inode})
+			rows, err := s.ForUpdate().Rows(&plock{Inode: inode})
 			if err != nil {
 				return err
 			}
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index d8bf6b4d..c21e754a 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -120,7 +120,7 @@ func (m *kvMeta) keyLen(args ...interface{}) int {
 		case string:
 			c += len(a)
 		default:
-			logger.Fatalf("invalid type %T, value %v", a, a)
+			panic(fmt.Sprintf("invalid type %T, value %v", a, a))
 		}
 	}
 	return c
@@ -141,7 +141,7 @@ func (m *kvMeta) fmtKey(args ...interface{}) []byte {
 		case string:
 			b.Put([]byte(a))
 		default:
-			logger.Fatalf("invalid type %T, value %v", a, a)
+			panic(fmt.Sprintf("invalid type %T, value %v", a, a))
 		}
 	}
 	return b.Bytes()
@@ -342,7 +342,7 @@ func (m *kvMeta) Init(format Format, force bool) error {
 
 	data, err := json.MarshalIndent(format, "", "")
 	if err != nil {
-		logger.Fatalf("json: %s", err)
+		return fmt.Errorf("json: %s", err)
 	}
 
 	m.fmt = format
@@ -559,7 +559,7 @@ func (m *kvMeta) getSession(sid uint64, detail bool) (*Session, error) {
 	return &s, nil
 }
 
-func (m *kvMeta) GetSession(sid uint64) (*Session, error) {
+func (m *kvMeta) GetSession(sid uint64, detail bool) (*Session, error) {
 	var legacy bool
 	value, err := m.get(m.sessionKey(sid))
 	if err == nil && value == nil {
@@ -572,7 +572,7 @@ func (m *kvMeta) GetSession(sid uint64) (*Session, error) {
 	if value == nil {
 		return nil, fmt.Errorf("session not found: %d", sid)
 	}
-	s, err := m.getSession(sid, true)
+	s, err := m.getSession(sid, detail)
 	if err != nil {
 		return nil, err
 	}
@@ -1575,7 +1575,7 @@ func (m *kvMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	})
 	if err == nil {
 		m.updateStats(newSpace, -1)
-		go m.doDeleteFileData(inode, attr.Length)
+		m.tryDeleteFileData(inode, attr.Length)
 	}
 	return err
 }
diff --git a/pkg/meta/tkv_etcd.go b/pkg/meta/tkv_etcd.go
index 8aa7826d..383961d8 100644
--- a/pkg/meta/tkv_etcd.go
+++ b/pkg/meta/tkv_etcd.go
@@ -24,6 +24,7 @@ import (
 	"context"
 	"crypto/tls"
 	"fmt"
+	"net"
 	"net/url"
 	"strings"
 	"time"
@@ -342,8 +343,15 @@ func newEtcdClient(addr string) (tkvClient, error) {
 		return nil, fmt.Errorf("parse %s: %s", addr, err)
 	}
 	passwd, _ := u.User.Password()
+	hosts := strings.Split(u.Host, ",")
+	for i, h := range hosts {
+		h, _, err := net.SplitHostPort(h)
+		if err != nil {
+			hosts[i] = net.JoinHostPort(h, "2379")
+		}
+	}
 	conf := etcd.Config{
-		Endpoints:        strings.Split(u.Host, ","),
+		Endpoints:        hosts,
 		Username:         u.User.Username(),
 		Password:         passwd,
 		AutoSyncInterval: time.Minute,
diff --git a/pkg/object/azure.go b/pkg/object/azure.go
index ed04098f..00e56595 100644
--- a/pkg/object/azure.go
+++ b/pkg/object/azure.go
@@ -85,7 +85,7 @@ func (b *wasb) Copy(dst, src string) error {
 func (b *wasb) Delete(key string) error {
 	_, err := b.container.NewBlockBlobClient(key).Delete(ctx, &azblob.DeleteBlobOptions{})
 	if err != nil && strings.Contains(err.Error(), string(azblob.StorageErrorCodeBlobNotFound)) {
-		return nil
+		err = nil
 	}
 	return err
 }
diff --git a/pkg/object/bos.go b/pkg/object/bos.go
index bd04edae..4bc624b2 100644
--- a/pkg/object/bos.go
+++ b/pkg/object/bos.go
@@ -101,7 +101,11 @@ func (q *bosclient) Copy(dst, src string) error {
 }
 
 func (q *bosclient) Delete(key string) error {
-	return q.c.DeleteObject(q.bucket, key)
+	err := q.c.DeleteObject(q.bucket, key)
+	if err != nil && strings.Contains(err.Error(), "NoSuchKey") {
+		err = nil
+	}
+	return err
 }
 
 func (q *bosclient) List(prefix, marker string, limit int64) ([]Object, error) {
diff --git a/pkg/object/etcd.go b/pkg/object/etcd.go
new file mode 100644
index 00000000..fbc918af
--- /dev/null
+++ b/pkg/object/etcd.go
@@ -0,0 +1,189 @@
+//go:build !noetcd
+// +build !noetcd
+
+/*
+ * JuiceFS, Copyright 2022 Juicedata, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package object
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/pkg/errors"
+	etcd "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/pkg/transport"
+)
+
+type etcdClient struct {
+	DefaultObjectStorage
+	client *etcd.Client
+	kv     etcd.KV
+	addr   string
+}
+
+func (c *etcdClient) String() string {
+	return fmt.Sprintf("etcd://%s/", c.addr)
+}
+
+func (c *etcdClient) Get(key string, off, limit int64) (io.ReadCloser, error) {
+	resp, err := c.kv.Get(context.TODO(), key, etcd.WithLimit(1))
+	if err != nil {
+		return nil, err
+	}
+	for _, pair := range resp.Kvs {
+		if string(pair.Key) == key {
+			data := pair.Value[off:]
+			if limit > 0 && limit < int64(len(data)) {
+				data = data[:limit]
+			}
+			return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+		}
+	}
+	return nil, errors.New("not found")
+}
+
+func (c *etcdClient) Put(key string, in io.Reader) error {
+	d, err := ioutil.ReadAll(in)
+	if err != nil {
+		return err
+	}
+	_, err = c.kv.Put(context.TODO(), key, string(d))
+	return err
+}
+
+func (c *etcdClient) Head(key string) (Object, error) {
+	resp, err := c.kv.Get(context.TODO(), key, etcd.WithLimit(1))
+	if err != nil {
+		return nil, err
+	}
+	for _, p := range resp.Kvs {
+		if string(p.Key) == key {
+			return &obj{
+				key,
+				int64(len(p.Value)),
+				time.Now(),
+				strings.HasSuffix(key, "/"),
+			}, nil
+		}
+	}
+	return nil, errors.New("not found")
+}
+
+func (c *etcdClient) Delete(key string) error {
+	_, err := c.kv.Delete(context.TODO(), key)
+	return err
+}
+
+func genNextKey(key string) string {
+	next := make([]byte, len(key))
+	copy(next, key)
+	p := len(next) - 1
+	next[p]++
+	for next[p] == 0 {
+		p--
+		next[p]++
+	}
+	return string(next)
+}
+
+func (c *etcdClient) List(prefix, marker string, limit int64) ([]Object, error) {
+	if marker == "" {
+		marker = prefix
+	}
+	var opts = []etcd.OpOption{etcd.WithLimit(limit), etcd.WithSort(etcd.SortByKey, etcd.SortAscend)}
+	if len(prefix) > 0 && prefix[0] != 0xFF {
+		opts = append(opts, etcd.WithRange(genNextKey(prefix)))
+	} else {
+		opts = append(opts, etcd.WithFromKey())
+	}
+	resp, err := c.client.Get(context.Background(), marker, opts...)
+	if err != nil {
+		return nil, fmt.Errorf("get start %v: %s", marker, err)
+	}
+	var objs []Object
+	for _, kv := range resp.Kvs {
+		k := string(kv.Key)
+		if !strings.HasPrefix(k, prefix) {
+			break
+		}
+		objs = append(objs, &obj{
+			k,
+			int64(len(kv.Value)),
+			time.Now(),
+			strings.HasSuffix(k, "/"),
+		})
+	}
+	return objs, nil
+}
+
+func buildTlsConfig(u *url.URL) (*tls.Config, error) {
+	var tsinfo transport.TLSInfo
+	q := u.Query()
+	tsinfo.CAFile = q.Get("cacert")
+	tsinfo.CertFile = q.Get("cert")
+	tsinfo.KeyFile = q.Get("key")
+	tsinfo.ServerName = q.Get("server-name")
+	tsinfo.InsecureSkipVerify = q.Get("insecure-skip-verify") != ""
+	if tsinfo.CAFile != "" || tsinfo.CertFile != "" || tsinfo.KeyFile != "" || tsinfo.ServerName != "" {
+		return tsinfo.ClientConfig()
+	}
+	return nil, nil
+}
+
+func newEtcd(addr, user, passwd string) (ObjectStorage, error) {
+	if !strings.Contains(addr, "://") {
+		addr = "http://" + addr
+	}
+	u, err := url.Parse(addr)
+	if err != nil {
+		return nil, fmt.Errorf("parse %s: %s", addr, err)
+	}
+	hosts := strings.Split(u.Host, ",")
+	for i, h := range hosts {
+		h, _, err := net.SplitHostPort(h)
+		if err != nil {
+			hosts[i] = net.JoinHostPort(h, "2379")
+		}
+	}
+	conf := etcd.Config{
+		Endpoints:        hosts,
+		Username:         user,
+		Password:         passwd,
+		AutoSyncInterval: time.Minute,
+	}
+	conf.TLS, err = buildTlsConfig(u)
+	if err != nil {
+		return nil, fmt.Errorf("build tls config from %s: %s", u.RawQuery, err)
+	}
+	c, err := etcd.New(conf)
+	if err != nil {
+		return nil, err
+	}
+	return &etcdClient{DefaultObjectStorage{}, c, c.KV, u.Host}, nil
+}
+
+func init() {
+	Register("etcd", newEtcd)
+}
diff --git a/pkg/object/minio.go b/pkg/object/minio.go
index 8c627e2f..45fd5ed5 100644
--- a/pkg/object/minio.go
+++ b/pkg/object/minio.go
@@ -72,7 +72,7 @@ func newMinio(endpoint, accessKey, secretKey string) (ObjectStorage, error) {
 	ses.Handlers.Build.PushFront(disableSha256Func)
 
 	if len(uri.Path) < 2 {
-		return nil, fmt.Errorf("no bucket name provided in %s: %s", endpoint, err)
+		return nil, fmt.Errorf("no bucket name provided in %s", endpoint)
 	}
 	bucket := uri.Path[1:]
 	if strings.Contains(bucket, "/") && strings.HasPrefix(bucket, "minio/") {
diff --git a/pkg/object/prefix.go b/pkg/object/prefix.go
index 6a3bcae2..cbc467cd 100644
--- a/pkg/object/prefix.go
+++ b/pkg/object/prefix.go
@@ -129,21 +129,21 @@ func (p *withPrefix) Chmod(path string, mode os.FileMode) error {
 	if fs, ok := p.os.(FileSystem); ok {
 		return fs.Chmod(p.prefix+path, mode)
 	}
-	return nil
+	return notSupported
 }
 
 func (p *withPrefix) Chown(path string, owner, group string) error {
 	if fs, ok := p.os.(FileSystem); ok {
 		return fs.Chown(p.prefix+path, owner, group)
 	}
-	return nil
+	return notSupported
 }
 
 func (p *withPrefix) Chtimes(key string, mtime time.Time) error {
 	if fs, ok := p.os.(FileSystem); ok {
 		return fs.Chtimes(p.prefix+key, mtime)
 	}
-	return nil
+	return notSupported
 }
 
 func (p *withPrefix) CreateMultipartUpload(key string) (*MultipartUpload, error) {
diff --git a/pkg/object/s3.go b/pkg/object/s3.go
index aa4c8cb2..9821cd32 100644
--- a/pkg/object/s3.go
+++ b/pkg/object/s3.go
@@ -158,6 +158,9 @@ func (s *s3client) Delete(key string) error {
 		Key:    &key,
 	}
 	_, err := s.s3.DeleteObject(&param)
+	if err != nil && strings.Contains(err.Error(), "NoSuckKey") {
+		err = nil
+	}
 	return err
 }
 
@@ -176,6 +179,9 @@ func (s *s3client) List(prefix, marker string, limit int64) ([]Object, error) {
 	objs := make([]Object, n)
 	for i := 0; i < n; i++ {
 		o := resp.Contents[i]
+		if !strings.HasPrefix(*o.Key, prefix) || *o.Key < marker {
+			return nil, fmt.Errorf("found invalid key %s from List, prefix: %s, marker: %s", *o.Key, prefix, marker)
+		}
 		objs[i] = &obj{
 			*o.Key,
 			*o.Size,
diff --git a/pkg/object/sftp.go b/pkg/object/sftp.go
index 14070a42..c46324ef 100644
--- a/pkg/object/sftp.go
+++ b/pkg/object/sftp.go
@@ -13,6 +13,7 @@ import (
 	"io/ioutil"
 	"net"
 	"os"
+	"os/user"
 	"path/filepath"
 	"runtime"
 	"sort"
@@ -430,7 +431,7 @@ func SshInteractive(user, instruction string, questions []string, echos []bool)
 	return answers, nil
 }
 
-func newSftp(endpoint, user, pass string) (ObjectStorage, error) {
+func newSftp(endpoint, username, pass string) (ObjectStorage, error) {
 	idx := strings.LastIndex(endpoint, ":")
 	host, port, err := net.SplitHostPort(endpoint[:idx])
 	if err != nil && strings.Contains(err.Error(), "missing port") {
@@ -448,6 +449,12 @@ func newSftp(endpoint, user, pass string) (ObjectStorage, error) {
 		root = root + dirSuffix
 	}
 
+	if username == "" {
+		u, _ := user.Current()
+		if u != nil {
+			username = u.Username
+		}
+	}
 	var auth []ssh.AuthMethod
 	if pass != "" {
 		auth = append(auth, ssh.Password(pass))
@@ -465,6 +472,23 @@ func newSftp(endpoint, user, pass string) (ObjectStorage, error) {
 			return nil, fmt.Errorf("unable to parse private key, error: %v", err)
 		}
 		auth = append(auth, ssh.PublicKeys(signer))
+	} else {
+		home := filepath.Join(os.Getenv("HOME"), ".ssh")
+		var algo = []string{"rsa", "dsa", "ecdsa", "ecdsa_sk", "ed25519", "xmss"}
+		for _, a := range algo {
+			key, err := ioutil.ReadFile(filepath.Join(home, "id_"+a))
+			if err != nil {
+				key, err = ioutil.ReadFile(filepath.Join(home, "id_"+a+"-cert"))
+			}
+			if err == nil {
+				signer, err := ssh.ParsePrivateKey(key)
+				if err == nil {
+					auth = append(auth, ssh.PublicKeys(signer))
+				} else {
+					logger.Debugf("load private key %s: %s", filepath.Join(home, "id_"+a), err)
+				}
+			}
+		}
 	}
 
 	socket := os.Getenv("SSH_AUTH_SOCK")
@@ -479,7 +503,7 @@ func newSftp(endpoint, user, pass string) (ObjectStorage, error) {
 	}
 
 	config := &ssh.ClientConfig{
-		User:            user,
+		User:            username,
 		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
 		Timeout:         time.Second * 3,
 		Auth:            auth,
@@ -492,6 +516,17 @@ func newSftp(endpoint, user, pass string) (ObjectStorage, error) {
 	}
 
 	c, err := f.getSftpConnection()
+	if err != nil && strings.Contains(err.Error(), "unable to authenticate") &&
+		pass == "" && os.Getenv("SSH_PRIVATE_KEY_PATH") == "" {
+		fmt.Printf("%s@%s's password: ", username, host)
+		var password []byte
+		password, err = term.ReadPassword(int(syscall.Stdin))
+		if err != nil {
+			return nil, fmt.Errorf("Read password: %s", err.Error())
+		}
+		f.config.Auth = append(f.config.Auth, ssh.Password(string(password)))
+		c, err = f.getSftpConnection()
+	}
 	if err != nil {
 		logger.Errorf("connect to %s failed: %s", host, err)
 		return nil, err
diff --git a/pkg/object/sharding.go b/pkg/object/sharding.go
index aa20a67c..0ba70393 100644
--- a/pkg/object/sharding.go
+++ b/pkg/object/sharding.go
@@ -94,7 +94,9 @@ func ListAll(store ObjectStorage, prefix, marker string) (<-chan Object, error)
 			for _, obj := range objs {
 				key := obj.Key()
 				if !first && key <= lastkey {
-					logger.Fatalf("The keys are out of order: marker %q, last %q current %q", marker, lastkey, key)
+					logger.Errorf("The keys are out of order: marker %q, last %q current %q", marker, lastkey, key)
+					out <- nil
+					return
 				}
 				lastkey = key
 				// logger.Debugf("found key: %s", key)
diff --git a/pkg/object/sql.go b/pkg/object/sql.go
new file mode 100644
index 00000000..ce058831
--- /dev/null
+++ b/pkg/object/sql.go
@@ -0,0 +1,196 @@
+//go:build !nosqlite || !nomysql || !nopg
+// +build !nosqlite !nomysql !nopg
+
+/*
+ * JuiceFS, Copyright 2022 Juicedata, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package object
+
+import (
+	"bytes"
+	"database/sql"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"strings"
+	"time"
+
+	"github.com/sirupsen/logrus"
+	"xorm.io/xorm"
+	"xorm.io/xorm/log"
+	"xorm.io/xorm/names"
+)
+
+type sqlStore struct {
+	DefaultObjectStorage
+	db   *xorm.Engine
+	addr string
+}
+
+type blob struct {
+	Id       int64     `xorm:"pk bigserial"`
+	Key      string    `xorm:"notnull unique"`
+	Size     int64     `xorm:"notnull"`
+	Modified time.Time `xorm:"notnull updated"`
+	Data     []byte    `xorm:"mediumblob"`
+}
+
+func (s *sqlStore) String() string {
+	return fmt.Sprintf("%s://%s/", s.db.DriverName(), s.addr)
+}
+
+func (s *sqlStore) Get(key string, off, limit int64) (io.ReadCloser, error) {
+	var b = blob{Key: key}
+	// TODO: range
+	ok, err := s.db.Get(&b)
+	if err != nil {
+		return nil, err
+	}
+	if !ok {
+		return nil, errors.New("not found")
+	}
+	data := b.Data[off:]
+	if limit > 0 && limit < int64(len(data)) {
+		data = data[:limit]
+	}
+	return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+}
+
+func (s *sqlStore) Put(key string, in io.Reader) error {
+	d, err := ioutil.ReadAll(in)
+	if err != nil {
+		return err
+	}
+	var n int64
+	b := blob{Key: key, Data: d, Size: int64(len(d)), Modified: time.Now()}
+	if s.db.DriverName() == "postgres" {
+		var r sql.Result
+		r, err = s.db.Exec("INSERT INTO jfs_blob(key, size, data) VALUES(?, ?, ?) "+
+			"ON CONFLICT (key) DO UPDATE SET size=?,data=?", key, b.Size, d, b.Size, d)
+		if err == nil {
+			n, err = r.RowsAffected()
+		}
+	} else {
+		n, err = s.db.Insert(&b)
+		if err != nil || n == 0 {
+			n, err = s.db.Update(&b, &blob{Key: key})
+		}
+	}
+	if err == nil && n == 0 {
+		err = errors.New("not inserted or updated")
+	}
+	return err
+}
+
+func (s *sqlStore) Head(key string) (Object, error) {
+	var b = blob{Key: key}
+	ok, err := s.db.Cols("key", "modified", "size").Get(&b)
+	if err != nil {
+		return nil, err
+	}
+	if !ok {
+		return nil, errors.New("not found")
+	}
+	return &obj{
+		key,
+		b.Size,
+		b.Modified,
+		strings.HasSuffix(key, "/"),
+	}, err
+}
+
+func (s *sqlStore) Delete(key string) error {
+	_, err := s.db.Delete(&blob{Key: key})
+	return err
+}
+
+func (s *sqlStore) List(prefix, marker string, limit int64) ([]Object, error) {
+	if marker == "" {
+		marker = prefix
+	}
+	var b blob
+	rows, err := s.db.Where("key >= ?", marker).Limit(int(limit)).Cols("key", "size", "modified").Rows(&b)
+	if err != nil {
+		return nil, err
+	}
+	var objs []Object
+	for rows.Next() {
+		if rows.Scan(&b) == nil && strings.HasPrefix(b.Key, prefix) {
+			objs = append(objs, &obj{
+				key:   b.Key,
+				size:  b.Size,
+				mtime: b.Modified,
+				isDir: strings.HasSuffix(b.Key, "/"),
+			})
+		} else {
+			break
+		}
+	}
+	return objs, nil
+}
+
+func newSQLStore(driver, addr, user, password string) (ObjectStorage, error) {
+	var err error
+	uri := addr
+	if user != "" {
+		uri = user + ":" + password + "@" + addr
+	}
+	if driver == "posgres" {
+		uri = "posgres://" + uri
+	}
+	engine, err := xorm.NewEngine(driver, uri)
+	if err != nil {
+		return nil, fmt.Errorf("open %s: %s", uri, err)
+	}
+	switch logger.Level { // make xorm less verbose
+	case logrus.TraceLevel:
+		engine.SetLogLevel(log.LOG_DEBUG)
+	case logrus.DebugLevel:
+		engine.SetLogLevel(log.LOG_INFO)
+	case logrus.InfoLevel, logrus.WarnLevel:
+		engine.SetLogLevel(log.LOG_WARNING)
+	case logrus.ErrorLevel:
+		engine.SetLogLevel(log.LOG_ERR)
+	default:
+		engine.SetLogLevel(log.LOG_OFF)
+	}
+	engine.SetTableMapper(names.NewPrefixMapper(engine.GetTableMapper(), "jfs_"))
+	if err := engine.Sync2(new(blob)); err != nil {
+		return nil, fmt.Errorf("create table blob: %s", err)
+	}
+	return &sqlStore{DefaultObjectStorage{}, engine, addr}, nil
+}
+
+func init() {
+	Register("sqlite3", func(addr, user, pass 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) {
+		p := strings.Index(addr, "://")
+		if p > 0 {
+			addr = addr[p+3:]
+		}
+		return newSQLStore("mysql", addr, user, pass)
+	})
+	Register("posgres", func(addr, user, pass string) (ObjectStorage, error) {
+		return newSQLStore("posgres", addr, user, pass)
+	})
+}
diff --git a/pkg/object/swift.go b/pkg/object/swift.go
index 9d58ba71..f4981504 100644
--- a/pkg/object/swift.go
+++ b/pkg/object/swift.go
@@ -20,12 +20,14 @@
 package object
 
 import (
+	"errors"
 	"fmt"
-	"github.com/juicedata/juicefs/pkg/utils"
-	"github.com/ncw/swift"
 	"io"
 	"net/url"
 	"strings"
+
+	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/ncw/swift"
 )
 
 type swiftOSS struct {
@@ -65,7 +67,11 @@ func (s *swiftOSS) Put(key string, in io.Reader) error {
 }
 
 func (s *swiftOSS) Delete(key string) error {
-	return s.conn.ObjectDelete(s.container, key)
+	err := s.conn.ObjectDelete(s.container, key)
+	if err != nil && errors.Is(err, swift.ObjectNotFound) {
+		err = nil
+	}
+	return err
 }
 
 func (s *swiftOSS) List(prefix, marker string, limit int64) ([]Object, error) {
diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go
index 7c37a6bc..b4f2d389 100644
--- a/pkg/sync/sync.go
+++ b/pkg/sync/sync.go
@@ -129,7 +129,9 @@ func ListAll(store object.ObjectStorage, start, end string) (<-chan object.Objec
 			for _, obj := range objs {
 				key := obj.Key()
 				if !first && key <= lastkey {
-					logger.Fatalf("The keys are out of order: marker %q, last %q current %q", marker, lastkey, key)
+					logger.Errorf("The keys are out of order: marker %q, last %q current %q", marker, lastkey, key)
+					out <- nil
+					return
 				}
 				if end != "" && key > end {
 					break END
@@ -612,7 +614,7 @@ func deleteFromDst(tasks chan<- object.Object, dstobj object.Object, config *Con
 	return false
 }
 
-func producer(tasks chan<- object.Object, src, dst object.ObjectStorage, config *Config) {
+func startProducer(tasks chan<- object.Object, src, dst object.ObjectStorage, config *Config) error {
 	start, end := config.Start, config.End
 	logger.Infof("Syncing from %s to %s", src, dst)
 	if start != "" {
@@ -625,12 +627,12 @@ func producer(tasks chan<- object.Object, src, dst object.ObjectStorage, config
 
 	srckeys, err := ListAll(src, start, end)
 	if err != nil {
-		logger.Fatal(err)
+		return fmt.Errorf("list %s: %s", src, err)
 	}
 
 	dstkeys, err := ListAll(dst, start, end)
 	if err != nil {
-		logger.Fatal(err)
+		return fmt.Errorf("list %s: %s", dst, err)
 	}
 	if len(config.Exclude) > 0 {
 		rules := parseIncludeRules(os.Args)
@@ -642,7 +644,11 @@ func producer(tasks chan<- object.Object, src, dst object.ObjectStorage, config
 		srckeys = filter(srckeys, rules)
 		dstkeys = filter(dstkeys, rules)
 	}
+	go produce(tasks, src, dst, srckeys, dstkeys, config)
+	return nil
+}
 
+func produce(tasks chan<- object.Object, src, dst object.ObjectStorage, srckeys, dstkeys <-chan object.Object, config *Config) {
 	defer close(tasks)
 	var dstobj object.Object
 	for obj := range srckeys {
@@ -799,7 +805,7 @@ func matchKey(rules []rule, key string) bool {
 			suffix := suffixForPattern(prefix+s, rule.pattern)
 			ok, err := path.Match(rule.pattern, suffix)
 			if err != nil {
-				logger.Fatalf("match %s with %s: %v", rule.pattern, suffix, err)
+				panic(fmt.Sprintf("match %s with %s: %v", rule.pattern, suffix, err))
 			}
 			if ok {
 				if rule.include {
@@ -856,7 +862,10 @@ func Sync(src, dst object.ObjectStorage, config *Config) error {
 	}
 
 	if config.Manager == "" {
-		go producer(tasks, src, dst, config)
+		err := startProducer(tasks, src, dst, config)
+		if err != nil {
+			return err
+		}
 		if len(config.Workers) > 0 {
 			addr, err := startManager(tasks)
 			if err != nil {
diff --git a/pkg/vfs/backup.go b/pkg/vfs/backup.go
index f542bd29..77021416 100644
--- a/pkg/vfs/backup.go
+++ b/pkg/vfs/backup.go
@@ -104,6 +104,10 @@ func cleanupBackups(blob object.ObjectStorage, now time.Time) {
 	}
 	var objs []string
 	for o := range ch {
+		if o == nil {
+			logger.Warnf("list failed, skip cleanup")
+			return
+		}
 		if !o.IsDir() {
 			objs = append(objs, o.Key())
 		}
diff --git a/pkg/winfsp/winfs.go b/pkg/winfsp/winfs.go
index e4458f98..daac2efa 100644
--- a/pkg/winfsp/winfs.go
+++ b/pkg/winfsp/winfs.go
@@ -564,7 +564,7 @@ func (j *juice) Releasedir(path string, fh uint64) (e int) {
 	return
 }
 
-func Serve(v *vfs.VFS, fuseOpt string, fileCacheTo float64, asRoot bool, delayClose int) error {
+func Serve(v *vfs.VFS, fuseOpt string, fileCacheTo float64, asRoot bool, delayClose int) {
 	var jfs juice
 	conf := v.Conf
 	jfs.conf = conf
@@ -593,5 +593,4 @@ func Serve(v *vfs.VFS, fuseOpt string, fileCacheTo float64, asRoot bool, delayCl
 	host.SetCapReaddirPlus(true)
 	logger.Debugf("mount point: %s, options: %s", conf.Meta.MountPoint, options)
 	_ = host.Mount(conf.Meta.MountPoint, []string{"-o", options})
-	return nil
 }
diff --git a/sdk/java/libjfs/main.go b/sdk/java/libjfs/main.go
index c0b68b60..4f96c445 100644
--- a/sdk/java/libjfs/main.go
+++ b/sdk/java/libjfs/main.go
@@ -436,7 +436,8 @@ func jfs_init(cname, jsonConf, user, group, superuser, supergroup *C.char) uintp
 			return format, err
 		})
 		if err != nil {
-			logger.Fatalf("object storage: %s", err)
+			logger.Errorf("object storage: %s", err)
+			return nil
 		}
 		logger.Infof("Data use %s", blob)
 
diff --git a/sdk/java/pom.xml b/sdk/java/pom.xml
index 2499e19a..a26cdc3e 100644
--- a/sdk/java/pom.xml
+++ b/sdk/java/pom.xml
@@ -37,11 +37,19 @@
 					<relocations>
 						<relocation>
 							<pattern>org.objectweb.asm</pattern>
-							<shadedPattern>juice.org.objectweb.asm</shadedPattern>
+							<shadedPattern>io.juicefs.shaded.org.objectweb.asm</shadedPattern>
 						</relocation>
 						<relocation>
 							<pattern>com.beust</pattern>
-							<shadedPattern>juice.com.beust</shadedPattern>
+							<shadedPattern>io.juicefs.shaded.com.beust</shadedPattern>
+						</relocation>
+						<relocation>
+							<pattern>org.json</pattern>
+							<shadedPattern>io.juicefs.shaded.org.json</shadedPattern>
+						</relocation>
+						<relocation>
+							<pattern>org.javassist</pattern>
+							<shadedPattern>io.juicefs.shaded.org.javassist</shadedPattern>
 						</relocation>
 					</relocations>
 				</configuration>


Test patch:
diff --git a/pkg/meta/redis_test.go b/pkg/meta/redis_test.go
index 05df136a..736a43ca 100644
--- a/pkg/meta/redis_test.go
+++ b/pkg/meta/redis_test.go
@@ -697,11 +697,10 @@ func testLocks(t *testing.T, m Meta) {
 			time.Sleep(time.Millisecond)
 			count--
 			if count > 0 {
-				logger.Fatalf("count should be be zero but got %d", count)
+				t.Fatalf("count should be be zero but got %d", count)
 			}
 			if st := m.Setlk(ctx, inode, uint64(i), false, syscall.F_UNLCK, 0, 0xFFFF, uint32(i)); st != 0 {
-				logger.Fatalf("plock unlock: %s", st)
-				err = st
+				t.Fatalf("plock unlock: %s", st)
 			}
 		}(i)
 	}
@@ -1110,7 +1109,7 @@ func testCloseSession(t *testing.T, m Meta) {
 	case *kvMeta:
 		sid = m.sid
 	}
-	s, err := m.GetSession(sid)
+	s, err := m.GetSession(sid, true)
 	if err != nil {
 		t.Fatalf("get session: %s", err)
 	} else {
@@ -1121,7 +1120,7 @@ func testCloseSession(t *testing.T, m Meta) {
 	if err = m.CloseSession(); err != nil {
 		t.Fatalf("close session: %s", err)
 	}
-	if _, err = m.GetSession(sid); err == nil {
+	if _, err = m.GetSession(sid, true); err == nil {
 		t.Fatalf("get a deleted session: %s", err)
 	}
 	switch m := m.(type) {
diff --git a/pkg/object/object_storage_test.go b/pkg/object/object_storage_test.go
index b67def85..6a5b9f41 100644
--- a/pkg/object/object_storage_test.go
+++ b/pkg/object/object_storage_test.go
@@ -22,6 +22,7 @@ import (
 	"crypto/rand"
 	"crypto/rsa"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"math"
@@ -32,6 +33,7 @@ import (
 	"time"
 
 	"github.com/go-redis/redis/v8"
+	_ "github.com/mattn/go-sqlite3"
 )
 
 func get(s ObjectStorage, k string, off, limit int64) (string, error) {
@@ -48,7 +50,7 @@ func get(s ObjectStorage, k string, off, limit int64) (string, error) {
 
 func listAll(s ObjectStorage, prefix, marker string, limit int64) ([]Object, error) {
 	r, err := s.List(prefix, marker, limit)
-	if err == nil {
+	if !errors.Is(err, notSupported) {
 		return r, nil
 	}
 	ch, err := s.ListAll(prefix, marker)
@@ -561,6 +563,28 @@ func TestSharding(t *testing.T) {
 	testStorage(t, s)
 }
 
+func TestSQLite(t *testing.T) {
+	s, err := newSQLStore("sqlite3", "/tmp/teststore.db", "", "")
+	if err != nil {
+		t.Fatalf("create: %s", err)
+	}
+	testStorage(t, s)
+}
+
+func TestPG(t *testing.T) {
+	s, err := newSQLStore("postgres", "localhost:5432/test?sslmode=disable", "", "")
+	if err == nil {
+		testStorage(t, s)
+	}
+}
+
+func TestMySQL(t *testing.T) {
+	s, err := newSQLStore("mysql", "/dev", "root", "")
+	if err == nil {
+		testStorage(t, s)
+	}
+}
+
 func TestNameString(t *testing.T) {
 	s, _ := newMem("test", "", "")
 	s = WithPrefix(s, "a/")
@@ -569,3 +593,8 @@ func TestNameString(t *testing.T) {
 		t.Fatalf("name with two prefix does not match: %s", s.String())
 	}
 }
+
+func TestEtcd(t *testing.T) {
+	s, _ := newEtcd("127.0.0.1:2379", "", "")
+	testStorage(t, s)
+}