Repository: juicedata/juicefs
Pull request #3227 (2023-02-10 05:14:00)

meta: move the control of max-deletes from chunk to meta


Patch:
diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml
index d088bbfb..417d84bd 100644
--- a/.github/actions/build/action.yml
+++ b/.github/actions/build/action.yml
@@ -10,7 +10,7 @@ runs:
   steps:
     - uses: actions/setup-go@v3
       with:
-        go-version: '1.18.x'
+        go-version: 'oldstable'
         cache: true
 
     - name: Change go version for root user
diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml
index b75322aa..6dc08a36 100644
--- a/.github/workflows/chaos.yml
+++ b/.github/workflows/chaos.yml
@@ -34,7 +34,7 @@ jobs:
 
       - uses: actions/setup-go@v3
         with:
-          go-version: '1.18.x'
+          go-version: 'oldstable'
           cache: true
 
       - name: Build 
diff --git a/.github/workflows/fio_benchmark.yml b/.github/workflows/fio_benchmark.yml
index 57f180cd..3cc55233 100644
--- a/.github/workflows/fio_benchmark.yml
+++ b/.github/workflows/fio_benchmark.yml
@@ -35,7 +35,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.18.x'
+          go-version: 'oldstable'
           cache: true
             
       - name: Build linux target
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cb8673ee..8cc191e0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,7 +12,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.18.x'
+          go-version: 'oldstable'
 
       - name: Set up Java
         uses: actions/setup-java@v3
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 926865e5..15b3d705 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -25,7 +25,7 @@ jobs:
     steps:
       - uses: actions/setup-go@v3
         with:
-          go-version: '1.18.x'
+          go-version: 'oldstable'
       - uses: actions/checkout@v3
       - name: golangci-lint
         uses: golangci/golangci-lint-action@v3
@@ -40,7 +40,7 @@ jobs:
     - name: Set up Go
       uses: actions/setup-go@v3
       with:
-        go-version: '1.18.x'
+        go-version: 'oldstable'
         cache: true
       id: go
       
diff --git a/cmd/format.go b/cmd/format.go
index 065ca1fc..0bfe1f62 100644
--- a/cmd/format.go
+++ b/cmd/format.go
@@ -157,7 +157,7 @@ Details: https://juicefs.com/docs/community/quick_start_guide`,
 			},
 			&cli.BoolFlag{
 				Name:  "hash-prefix",
-				Usage: "give each object a hashed prefix",
+				Usage: "add a hash prefix to name of objects",
 			},
 			&cli.BoolFlag{
 				Name:  "force",
diff --git a/cmd/gc.go b/cmd/gc.go
index 6f073c2a..9dcb672a 100644
--- a/cmd/gc.go
+++ b/cmd/gc.go
@@ -73,17 +73,13 @@ $ juicefs gc redis://localhost --delete`,
 	}
 }
 
-type dSlice struct {
-	id     uint64
-	length uint32
-}
-
 func gc(ctx *cli.Context) error {
 	setup(ctx, 1)
 	removePassword(ctx.Args().Get(0))
 	m := meta.NewClient(ctx.Args().Get(0), &meta.Config{
-		Retries: 10,
-		Strict:  true,
+		Retries:    10,
+		Strict:     true,
+		MaxDeletes: ctx.Int("threads"),
 	})
 	format, err := m.Load(true)
 	if err != nil {
@@ -96,7 +92,6 @@ func gc(ctx *cli.Context) error {
 		GetTimeout: time.Second * 60,
 		PutTimeout: time.Second * 60,
 		MaxUpload:  20,
-		MaxDeletes: ctx.Int("threads"),
 		BufferSize: 300 << 20,
 		CacheDir:   "memory",
 	}
@@ -120,14 +115,14 @@ func gc(ctx *cli.Context) error {
 
 	var wg sync.WaitGroup
 	var delSpin *utils.Bar
-	var sliceChan chan *dSlice // pending delete slices
+	var sliceChan chan meta.Slice // pending delete slices
 
 	if delete || compact {
 		delSpin = progress.AddCountSpinner("Deleted pending")
-		sliceChan = make(chan *dSlice, 10240)
+		sliceChan = make(chan meta.Slice, 10240)
 		m.OnMsg(meta.DeleteSlice, func(args ...interface{}) error {
 			delSpin.Increment()
-			sliceChan <- &dSlice{args[0].(uint64), args[1].(uint32)}
+			sliceChan <- meta.Slice{Id: args[0].(uint64), Size: args[1].(uint32)}
 			return nil
 		})
 		for i := 0; i < threads; i++ {
@@ -135,8 +130,8 @@ func gc(ctx *cli.Context) error {
 			go func() {
 				defer wg.Done()
 				for s := range sliceChan {
-					if err := store.Remove(s.id, int(s.length)); err != nil {
-						logger.Warnf("remove %d_%d: %s", s.id, s.length, err)
+					if err := store.Remove(s.Id, int(s.Size)); err != nil {
+						logger.Warnf("remove %d_%d: %s", s.Id, s.Size, err)
 					}
 				}
 			}()
diff --git a/cmd/mount.go b/cmd/mount.go
index d3a0d78b..d0c27273 100644
--- a/cmd/mount.go
+++ b/cmd/mount.go
@@ -350,6 +350,7 @@ func getMetaConf(c *cli.Context, mp string, readOnly bool) *meta.Config {
 	cfg := &meta.Config{
 		Retries:    c.Int("io-retries"),
 		Strict:     true,
+		MaxDeletes: c.Int("max-deletes"),
 		ReadOnly:   readOnly,
 		NoBGJob:    c.Bool("no-bgjob"),
 		OpenCache:  time.Duration(c.Float64("open-cache") * 1e9),
@@ -357,6 +358,9 @@ func getMetaConf(c *cli.Context, mp string, readOnly bool) *meta.Config {
 		MountPoint: mp,
 		Subdir:     c.String("subdir"),
 	}
+	if cfg.MaxDeletes == 0 {
+		logger.Warnf("Deleting object will be disabled since max-deletes is 0")
+	}
 	if cfg.Heartbeat < time.Second {
 		logger.Warnf("heartbeat should not be less than 1 second")
 		cfg.Heartbeat = time.Second
@@ -382,7 +386,6 @@ func getChunkConf(c *cli.Context, format *meta.Format) *chunk.Config {
 		GetTimeout:    time.Second * time.Duration(c.Int("get-timeout")),
 		PutTimeout:    time.Second * time.Duration(c.Int("put-timeout")),
 		MaxUpload:     c.Int("max-uploads"),
-		MaxDeletes:    c.Int("max-deletes"),
 		MaxRetries:    c.Int("io-retries"),
 		Writeback:     c.Bool("writeback"),
 		Prefetch:      c.Int("prefetch"),
diff --git a/cmd/mount_unix.go b/cmd/mount_unix.go
index 3ba78ccc..c5cc0a3c 100644
--- a/cmd/mount_unix.go
+++ b/cmd/mount_unix.go
@@ -21,7 +21,6 @@ package cmd
 
 import (
 	"bytes"
-	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
@@ -154,7 +153,7 @@ func mount_flags() []cli.Flag {
 
 func disableUpdatedb() {
 	path := "/etc/updatedb.conf"
-	data, err := ioutil.ReadFile(path)
+	data, err := os.ReadFile(path)
 	if err != nil {
 		return
 	}
@@ -172,7 +171,7 @@ func disableUpdatedb() {
 		nd = append(nd, fstype...)
 		nd = append(nd, ' ')
 		nd = append(nd, data[p2:]...)
-		err = ioutil.WriteFile(path, nd, 0644)
+		err = os.WriteFile(path, nd, 0644)
 		if err != nil {
 			logger.Warnf("update %s: %s", path, err)
 		} else {
diff --git a/cmd/objbench.go b/cmd/objbench.go
index 9eead9b5..a7feb737 100644
--- a/cmd/objbench.go
+++ b/cmd/objbench.go
@@ -21,7 +21,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"math"
 	"math/rand"
 	"os"
@@ -624,7 +623,7 @@ func functionalTesting(blob object.ObjectStorage, result *[][]string, colorful b
 		if err != nil {
 			return "", err
 		}
-		data, err := ioutil.ReadAll(r)
+		data, err := io.ReadAll(r)
 		if err != nil {
 			return "", err
 		}
diff --git a/cmd/object.go b/cmd/object.go
index e96cb34b..b7edbed2 100644
--- a/cmd/object.go
+++ b/cmd/object.go
@@ -399,7 +399,6 @@ func getDefaultChunkConf(format *meta.Format) *chunk.Config {
 		GetTimeout: time.Minute,
 		PutTimeout: time.Minute,
 		MaxUpload:  50,
-		MaxDeletes: 10,
 		MaxRetries: 10,
 		BufferSize: 300 << 20,
 	}
@@ -413,10 +412,11 @@ func newJFS(endpoint, accessKey, secretKey, token string) (object.ObjectStorage,
 		metaUrl = endpoint
 	}
 	metaConf := &meta.Config{
-		Retries:   10,
-		Strict:    true,
-		NoBGJob:   true,
-		Heartbeat: time.Second * 10,
+		Retries:    10,
+		Strict:     true,
+		MaxDeletes: 10,
+		NoBGJob:    true,
+		Heartbeat:  time.Second * 10,
 	}
 	metaCli := meta.NewClient(metaUrl, metaConf)
 	format, err := metaCli.Load(true)
diff --git a/cmd/stats.go b/cmd/stats.go
index 7c9eb7ca..38d9161f 100644
--- a/cmd/stats.go
+++ b/cmd/stats.go
@@ -18,7 +18,7 @@ package cmd
 
 import (
 	"fmt"
-	"io/ioutil"
+	"io"
 	"os"
 	"path"
 	"strconv"
@@ -346,7 +346,7 @@ func readStats(path string) map[string]float64 {
 		return nil
 	}
 	defer f.Close()
-	d, err := ioutil.ReadAll(f)
+	d, err := io.ReadAll(f)
 	if err != nil {
 		logger.Warnf("read %s: %s", path, err)
 		return nil
diff --git a/docs/en/administration/destroy.md b/docs/en/administration/destroy.md
index e9a6c899..7e954d7f 100644
--- a/docs/en/administration/destroy.md
+++ b/docs/en/administration/destroy.md
@@ -61,7 +61,7 @@ $ juicefs destroy redis://127.0.0.1:6379/1 eabb96d5-7228-461e-9240-fddbf2b576d8
 data storage: file://jfs/
   used bytes: 18620416
  used inodes: 23
-WARNING: The target volume will be destoried permanently, including:
+WARNING: The target volume will be destroyed permanently, including:
 WARNING: 1. objects in the data storage
 WARNING: 2. entries in the metadata engine
 Proceed anyway? [y/N]: y
diff --git a/docs/en/administration/metadata/postgresql_best_practices.md b/docs/en/administration/metadata/postgresql_best_practices.md
index 43bc8e84..d04a7994 100644
--- a/docs/en/administration/metadata/postgresql_best_practices.md
+++ b/docs/en/administration/metadata/postgresql_best_practices.md
@@ -52,5 +52,5 @@ Connection pooler is a middleware that works between client and database and reu
 The official PostgreSQL document [High Availability, Load Balancing, and Replication](https://www.postgresql.org/docs/current/different-replication-solutions.html) compares several common databases in terms of high availability solutions. Please choose the appropriate ones according to your needs.
 
 :::note
-JuiceFS uses [transactions](https://www.postgresql.org/docs/current/tutorial-transactions.html) to ensure atomicity of metadata operations. Since PostgreSQL does not yet support Muti-Shard (Distributed) transactions, do not use a multi-server distributed architecture for the JuiceFS metadata.
+JuiceFS uses [transactions](https://www.postgresql.org/docs/current/tutorial-transactions.html) to ensure atomicity of metadata operations. Since PostgreSQL does not yet support Multi-Shard (Distributed) transactions, do not use a multi-server distributed architecture for the JuiceFS metadata.
 :::
diff --git a/docs/en/benchmark/metadata_engines_benchmark.md b/docs/en/benchmark/metadata_engines_benchmark.md
index 4b1ba608..4abe4110 100644
--- a/docs/en/benchmark/metadata_engines_benchmark.md
+++ b/docs/en/benchmark/metadata_engines_benchmark.md
@@ -38,7 +38,7 @@ Amazon S3
 
 - Amazon c5d.xlarge: 4 vCPUs, 8 GiB Memory, Up to 10 Gigabit Network, 100 GB SSD (local storage for metadata engines)
 - Ubuntu 20.04.1 LTS
-- SSD is formated as ext4 and mounted on `/data`
+- SSD is formatted as ext4 and mounted on `/data`
 
 ### Metadata Engines
 
diff --git a/docs/en/benchmark/operations_profiling.md b/docs/en/benchmark/operations_profiling.md
index 3342fc70..9b11c3f6 100644
--- a/docs/en/benchmark/operations_profiling.md
+++ b/docs/en/benchmark/operations_profiling.md
@@ -33,7 +33,7 @@ Running the `profile` command on an existing log file enables the **replay mode*
 juicefs profile LOGFILE
 ```
 
-When debugging or analyzing perfomance issues, it is usually more practical to record access log first and then replay it (multiple times). For example:
+When debugging or analyzing performance issues, it is usually more practical to record access log first and then replay it (multiple times). For example:
 
 ```bash
 cat /jfs/.accesslog > /tmp/jfs-oplog
diff --git a/docs/en/benchmark/performance_evaluation_guide.md b/docs/en/benchmark/performance_evaluation_guide.md
index 7a7f0177..03f5ad42 100644
--- a/docs/en/benchmark/performance_evaluation_guide.md
+++ b/docs/en/benchmark/performance_evaluation_guide.md
@@ -187,7 +187,7 @@ Based on the bench performance test flows as described above, a total of (1 + 10
 - 808 flush requests: flush is automatically invoked whenever a file is closed
 - 33168 write/read requests: each large file takes 1024 1 MiB IOs on write, while the maximum size of a request at the FUSE level is 128 KiB by default. It means that each application IO is split into 8 FUSE requests, so there are `(1024 * 8 + 100) * 4 = 33168` requests. The read IOs work in a similar way, and so does its counting.
 
-All these values correspond exactly to the results of `profile`. In addition, the test result shows that the average latency for the `write` operations is extreamly low (45 μs). This is because JuiceFS `write` writes to a memory buffer first by default and then calls flush to upload data to the object storage when the file is closed, as expected.
+All these values correspond exactly to the results of `profile`. In addition, the test result shows that the average latency for the `write` operations is extremely low (45 μs). This is because JuiceFS `write` writes to a memory buffer first by default and then calls flush to upload data to the object storage when the file is closed, as expected.
 
 ## Other Test Tool Configuration Examples
 
diff --git a/docs/en/deployment/hadoop_java_sdk.md b/docs/en/deployment/hadoop_java_sdk.md
index 7e6f906a..0a382f9f 100644
--- a/docs/en/deployment/hadoop_java_sdk.md
+++ b/docs/en/deployment/hadoop_java_sdk.md
@@ -7,7 +7,7 @@ slug: /hadoop_java_sdk
 import Tabs from '@theme/Tabs';
 import TabItem from '@theme/TabItem';
 
-JuiceFS provides [Hadoop-compatible FileSystem](https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/filesystem/introduction.html) by Hadoop Java SDK. Various applications in the Hadoop ecosystem can smoothly use JuiceFS to store data without changing the code.
+JuiceFS provides [Hadoop-compatible File System](https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/filesystem/introduction.html) by Hadoop Java SDK. Various applications in the Hadoop ecosystem can smoothly use JuiceFS to store data without changing the code.
 
 ## Requirements
 
@@ -140,8 +140,8 @@ Please refer to the following table to set the relevant parameters of the JuiceF
 
 | Configuration                    | Default Value                | Description                                                                                                                                                                                                                                                                                  |
 |----------------------------------|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `fs.jfs.impl`                    | `io.juicefs.JuiceFileSystem` | Specify the storage implementation to be used. By default, `jfs://` scheme is used. If you want to use different scheme (e.g. `cfs://`), just modify it to `fs.cfs.impl`. No matter what sheme you use, it is always access the data in JuiceFS.                                             |
-| `fs.AbstractFileSystem.jfs.impl` | `io.juicefs.JuiceFS`         | Specify the storage implementation to be used. By default, `jfs://` scheme is used. If you want to use different scheme (e.g. `cfs://`), just modify it to `fs.AbstractFileSystem.cfs.impl`. No matter what sheme you use, it is always access the data in JuiceFS.                          |
+| `fs.jfs.impl`                    | `io.juicefs.JuiceFileSystem` | Specify the storage implementation to be used. By default, `jfs://` scheme is used. If you want to use different scheme (e.g. `cfs://`), just modify it to `fs.cfs.impl`. No matter what scheme you use, it is always access the data in JuiceFS.                                             |
+| `fs.AbstractFileSystem.jfs.impl` | `io.juicefs.JuiceFS`         | Specify the storage implementation to be used. By default, `jfs://` scheme is used. If you want to use different scheme (e.g. `cfs://`), just modify it to `fs.AbstractFileSystem.cfs.impl`. No matter what scheme you use, it is always access the data in JuiceFS.                          |
 | `juicefs.meta`                   |                              | Specify the metadata engine address of the pre-created JuiceFS file system. You can configure multiple file systems for the client at the same time through the format of `juicefs.{vol_name}.meta`. Refer to ["Multiple file systems configuration"](#multiple-file-systems-configuration). |
 
 #### Cache Configurations
@@ -421,6 +421,7 @@ CREATE TABLE IF NOT EXISTS person
 
 2. Use the following sample code to verify:
 
+<!-- autocorrect: false -->
    ```java
    package demo;
 
@@ -444,6 +445,7 @@ CREATE TABLE IF NOT EXISTS person
        }
    }
    ```
+<!-- autocorrect: true -->
 
 ## Monitoring metrics collection
 
diff --git a/docs/en/deployment/s3_gateway.md b/docs/en/deployment/s3_gateway.md
index 9f715719..a2338f98 100644
--- a/docs/en/deployment/s3_gateway.md
+++ b/docs/en/deployment/s3_gateway.md
@@ -53,7 +53,7 @@ In this way, the S3 gateway will accept all network requests by default. S3 clie
 - A third-party client on the same LAN as the host where the S3 gateway is located can access it using `http://192.168.1.8:9000` (assuming the intranet IP address of the S3 gateway-enabled host is 192.168.1.8).
 - The S3 gateway can be accessed over the Internet using `http://110.220.110.220:9000` (assuming that the public IP address of the S3 gateway-enabled host is 110.220.110.220).
 
-## Configure S3 Gaterway as Daemon Service
+## Configure S3 Gateway as Daemon Service
 
 The S3 gateway can be configured as a `daemon service` with `systemd`.
 
@@ -79,7 +79,7 @@ WantedBy=multi-user.target
 EOF
 ```
 
-To enable the service at starup
+To enable the service at startup
 
 ```shell
 sudo systemctl daemon-reload
diff --git a/docs/en/development/data_structures.md b/docs/en/development/data_structures.md
index 81ad0ae5..dd7ddec9 100644
--- a/docs/en/development/data_structures.md
+++ b/docs/en/development/data_structures.md
@@ -793,7 +793,7 @@ Slice{pos: 10M, id: 10, size: 30M, off:   0, len:  6M}
 Slice{pos: 16M, id: 12, size: 10M, off:   0, len: 10M}
 Slice{pos: 26M, id: 11, size: 16M, off:  6M, len: 10M}
 Slice{pos: 36M, id: 10, size: 30M, off: 26M, len:  4M}
-Slice{pos: 40M, id:  0, size: 24M, off:   0, len: 24M} // can be ommited
+Slice{pos: 40M, id:  0, size: 24M, off:   0, len: 24M} // can be omitted
 ```
 
 ### 4.3 Data objects
diff --git a/docs/en/faq.md b/docs/en/faq.md
index 45314948..e6d5f930 100644
--- a/docs/en/faq.md
+++ b/docs/en/faq.md
@@ -63,7 +63,7 @@ No. However, you can set up multiple buckets associated with the same object sto
 
 ### How is the performance of JuiceFS?
 
-JuiceFS is a distributed file system, the latency of metedata is determined by 1 (reading) or 2 (writing) round trip(s) between client and metadata service (usually 1-3 ms). The latency of first byte is determined by the performance of underlying object storage (usually 20-100 ms). Throughput of sequential read/write could be 50MB/s - 2800MiB/s (see [fio benchmark](benchmark/fio.md) for more information), depends on network bandwidth and how the data could be compressed.
+JuiceFS is a distributed file system, the latency of metadata is determined by 1 (reading) or 2 (writing) round trip(s) between client and metadata service (usually 1-3 ms). The latency of first byte is determined by the performance of underlying object storage (usually 20-100 ms). Throughput of sequential read/write could be 50MB/s - 2800MiB/s (see [fio benchmark](benchmark/fio.md) for more information), depends on network bandwidth and how the data could be compressed.
 
 JuiceFS is built with multiple layers of caching (invalidated automatically), once the caching is warmed up, the latency and throughput of JuiceFS could be close to local filesystem (having the overhead of FUSE).
 
@@ -111,7 +111,7 @@ See ["Write Cache in Client"](guide/cache_management.md#writeback) for more info
 
 In addition to mounting, the following methods are also supported:
 
-- Kuberenetes CSI Driver: Use JuiceFS as the storage layer of Kubernetes cluster through the Kubernetes CSI Driver. For details, please refer to ["Use JuiceFS on Kubernetes"](deployment/how_to_use_on_kubernetes.md).
+- Kubernetes CSI Driver: Use JuiceFS as the storage layer of Kubernetes cluster through the Kubernetes CSI Driver. For details, please refer to ["Use JuiceFS on Kubernetes"](deployment/how_to_use_on_kubernetes.md).
 - Hadoop Java SDK: It is convenient to use a Java client compatible with the HDFS interface to access JuiceFS in the Hadoop ecosystem. For details, please refer to ["Use JuiceFS on Hadoop Ecosystem"](deployment/hadoop_java_sdk.md).
 - S3 Gateway: Access JuiceFS through the S3 protocol. For details, please refer to ["Deploy JuiceFS S3 Gateway"](deployment/s3_gateway.md).
 - Docker Volume Plugin: A convenient way to use JuiceFS in Docker, please refer to ["Use JuiceFS on Docker"](deployment/juicefs_on_docker.md).
diff --git a/docs/en/guide/cache_management.md b/docs/en/guide/cache_management.md
index 91fc7a22..245703af 100644
--- a/docs/en/guide/cache_management.md
+++ b/docs/en/guide/cache_management.md
@@ -39,7 +39,7 @@ There are three kinds of metadata which can be cached in kernel: **attribute**,
 --dir-entry-cache value  dir entry cache TTL in seconds (default: 1)
 ```
 
-Attribute, entry and direntries are cached for 1 second by default, this speeds up lookup and getattr operations. When clients on multiple nodes are sharing the same file system, the metadata cached in kernel will only expire by TTL. So in edge cases, it's possible that metadata modifications (e.g., `chown`) on node A cannot be seen immediately on node B. Nevertheless, all nodes will eventually be able to see the changes made by A after cache expiration.
+Attribute, entry and directories are cached for 1 second by default, this speeds up lookup and getattr operations. When clients on multiple nodes are sharing the same file system, the metadata cached in kernel will only expire by TTL. So in edge cases, it's possible that metadata modifications (e.g., `chown`) on node A cannot be seen immediately on node B. Nevertheless, all nodes will eventually be able to see the changes made by A after cache expiration.
 
 ### In-memory client metadata cache
 
diff --git a/docs/en/guide/how_to_set_up_metadata_engine.md b/docs/en/guide/how_to_set_up_metadata_engine.md
index f2d78242..72809782 100644
--- a/docs/en/guide/how_to_set_up_metadata_engine.md
+++ b/docs/en/guide/how_to_set_up_metadata_engine.md
@@ -121,7 +121,7 @@ If you maintain the Redis database on your own, it is recommended to read [Redis
 [KeyDB](https://keydb.dev) is an open source fork of Redis, developed to stay aligned with the Redis community. KeyDB implements multi-threading support, better memory utilization, and greater throughput on top of Redis, and also supports [Active Replication](https://github.com/JohnSully/KeyDB/wiki/Active-Replication), i.e., the Active Active feature.
 
 :::note
-Same as Redis, the Active Replication is asychronous, which may cause consistency issues. So use with caution!
+Same as Redis, the Active Replication is asynchronous, which may cause consistency issues. So use with caution!
 :::
 
 When being used as metadata storage engine for Juice, KeyDB is used exactly in the same way as Redis. So please refer to the [Redis](#redis) section for usage.
diff --git a/docs/en/guide/sync.md b/docs/en/guide/sync.md
index 9021d0d3..6bf31e24 100644
--- a/docs/en/guide/sync.md
+++ b/docs/en/guide/sync.md
@@ -89,7 +89,7 @@ sudo juicefs mount -d redis://10.10.0.8:6379/1 /mnt/jfs
 juicefs sync s3://ABCDEFG:HIJKLMN@aaa.s3.us-west-1.amazonaws.com/movies/ /mnt/jfs/movies/
 ```
 
-For JuiceFS 1.1+, we can sycn with JuiceFS without mounting it:
+For JuiceFS 1.1+, we can sync with JuiceFS without mounting it:
 
 ```shell
 myfs=redis://10.10.0.8:6379/1 juicefs sync s3://ABCDEFG:HIJKLMN@aaa.s3.us-west-1.amazonaws.com/movies/ jfs://myfs/movies/
diff --git a/docs/en/introduction/README.md b/docs/en/introduction/README.md
index 23ae4c98..4ddb2bb6 100644
--- a/docs/en/introduction/README.md
+++ b/docs/en/introduction/README.md
@@ -9,7 +9,7 @@ pagination_next: introduction/architecture
 
 JuiceFS separates "data" and "metadata" storage, files are split into chunks and stored in [object storage](../guide/how_to_set_up_object_storage.md#supported-object-storage) like Amazon S3, and the corresponding metadata can be stored in various [databases](../guide/how_to_set_up_metadata_engine.md) such as Redis, MySQL, TiKV, SQLite, etc., based on the scenarios and requirements.
 
-JuiceFS provides rich APIs for various forms of data management, analysis, archiving, and backup. It can seamlessly interface with big data, machine learning, artificial intelligence and other application platforms without modifying code, and provide massive, elastic and high-performance storage at low cost. With JuiceFS, you do not need to worry about availability, disaster recovery, monitoring and expansion, and thus maintainence work can be greatly reduced, perfect for DevOps.
+JuiceFS provides rich APIs for various forms of data management, analysis, archiving, and backup. It can seamlessly interface with big data, machine learning, artificial intelligence and other application platforms without modifying code, and provide massive, elastic and high-performance storage at low cost. With JuiceFS, you do not need to worry about availability, disaster recovery, monitoring and expansion, and thus maintenance work can be greatly reduced, perfect for DevOps.
 
 ## Features
 
diff --git a/docs/en/introduction/comparison/juicefs_vs_s3ql.md b/docs/en/introduction/comparison/juicefs_vs_s3ql.md
index 102a8257..7d9d2419 100644
--- a/docs/en/introduction/comparison/juicefs_vs_s3ql.md
+++ b/docs/en/introduction/comparison/juicefs_vs_s3ql.md
@@ -21,7 +21,7 @@ S3QL was no longer maintained since November 9, 2022, see [S3QL Repository](http
 
 - S3QL only supports SQLite. But JuiceFS supports more databases, such as Redis, TiKV, MySQL, PostgreSQL, and SQLite.
 - S3QL has no distributed capability and **does not** support multi-host shared mounting. JuiceFS is a typical distributed file system. When using a network-based database, it supports multi-host distributed mount read and write.
-- S3QL commits a data block to S3 when it has not been accessed for more than a few seconds. After a file closed or even fsynced, it is only guranteed to stay in system memory, which may result in data loss if node fails. JuiceFS ensures high data durability, uploading all blocks synchronously when a file is closed.
+- S3QL commits a data block to S3 when it has not been accessed for more than a few seconds. After a file closed or even fsynced, it is only guaranteed to stay in system memory, which may result in data loss if node fails. JuiceFS ensures high data durability, uploading all blocks synchronously when a file is closed.
 - S3QL provides data deduplication. Only one copy of the same data is stored, which can reduce the storage usage, but it will also increase the performance overhead of the system. JuiceFS pays more attention to performance, and it is too expensive to perform deduplication on large-scale data, so this function is temporarily not provided.
 
 |                           | **S3QL**              | **JuiceFS**                   |
@@ -119,4 +119,4 @@ JuiceFS only sets the object storage API access key when creating a file system,
 
 **S3QL** adopts the storage structure of object storage + SQLite. Storing data in blocks can not only improve the read and write efficiency of the file but also reduce the resource overhead when the file is modified. The advanced features such as snapshots, data deduplication, and data retention, as well as the default data compression and data encryption make S3QL very suitable for individuals to store files in cloud storage at a lower cost and with higher security.
 
-**JuiceFS** supports object storage, HDFS, WebDAV, and local disks as data storage engines, and supports popular databases such as Redis, TiKV, MySQL, MariaDB, PostgreSQL, and SQLite as metadata storage engines. It provides a standard POSIX file system interface through FUSE and a Java API, which can directly replace HDFS to provide storage for Hadoop. At the same time, it also provides [Kubernetes CSI Driver](https://github.com/juicedata/juicefs-csi-driver), which can be used as the storage layer of Kubernetes for data persistent storage. JucieFS is a file system designed for enterprise-level distributed data storage scenarios. It is widely used in various scenarios such as big data analysis, machine learning, container shared storage, data sharing, and backup.
+**JuiceFS** supports object storage, HDFS, WebDAV, and local disks as data storage engines, and supports popular databases such as Redis, TiKV, MySQL, MariaDB, PostgreSQL, and SQLite as metadata storage engines. It provides a standard POSIX file system interface through FUSE and a Java API, which can directly replace HDFS to provide storage for Hadoop. At the same time, it also provides [Kubernetes CSI Driver](https://github.com/juicedata/juicefs-csi-driver), which can be used as the storage layer of Kubernetes for data persistent storage. JuiceFS is a file system designed for enterprise-level distributed data storage scenarios. It is widely used in various scenarios such as big data analysis, machine learning, container shared storage, data sharing, and backup.
diff --git a/docs/en/reference/command_reference.md b/docs/en/reference/command_reference.md
index b43f67cd..16cf9c48 100644
--- a/docs/en/reference/command_reference.md
+++ b/docs/en/reference/command_reference.md
@@ -172,6 +172,9 @@ A path to RSA private key (PEM)
 `--trash-days value`<br />
 number of days after which removed files will be permanently deleted (default: 1)
 
+`--hash-prefix`<br />
+add a hash prefix to name of objects (default: false)
+
 `--force`<br />
 overwrite existing format (default: false)
 
@@ -682,7 +685,7 @@ limit the number of objects that will be processed (default: -1)
 manager address
 
 `--worker value`<br />
-hosts (seperated by comma) to launch worker
+hosts (separated by comma) to launch worker
 
 `--bwlimit value`<br />
 limit bandwidth in Mbps (0 means unlimited) (default: 0)
diff --git a/docs/en/security/encrypt.md b/docs/en/security/encrypt.md
index 112d14e5..0c2efbe8 100644
--- a/docs/en/security/encrypt.md
+++ b/docs/en/security/encrypt.md
@@ -3,7 +3,7 @@ sidebar_position: 1
 ---
 # Data Encryption
 
-JuiceFS provides data encrytion from two aspects:
+JuiceFS provides data encryption from two aspects:
 
 1. Data Encryption In Transit
 2. Data Encryption At Rest
@@ -28,7 +28,7 @@ In the case where server and object storage run on the same VPC network, explici
 
 ### Connect to database via TLS/SSL
 
-For [all the supported metadata engines](../guide/how_to_set_up_metadata_engine.md), as long as the database supports encryption and has been configured with encryption such as TLS/SSL, JuiceFS can connect to the database through its encrytped channel. For instance, a Redis database configured with TLS can use `rediss://` for connecting.
+For [all the supported metadata engines](../guide/how_to_set_up_metadata_engine.md), as long as the database supports encryption and has been configured with encryption such as TLS/SSL, JuiceFS can connect to the database through its encrypted channel. For instance, a Redis database configured with TLS can use `rediss://` for connecting.
 
 ```shell {3}
 juicefs format --storage s3 \
@@ -89,7 +89,7 @@ There are following steps to enable Data Encryption At Rest:
 
 #### Step 1: create a RSA private key
 
-The RSA private key, usually manually generated by OpenSSL, plays a crtical role in Data Encryption At Rest. The following command generates a 2048-bit RSA private key with a file name of `my-priv-key.pem` in the currect directory using aes256 algorithm.
+The RSA private key, usually manually generated by OpenSSL, plays a critical role in Data Encryption At Rest. The following command generates a 2048-bit RSA private key with a file name of `my-priv-key.pem` in the current directory using aes256 algorithm.
 
 ```shell
 openssl genrsa -out my-priv-key.pem -aes256 2048
diff --git a/docs/en/tutorials/juicefs_on_colab.md b/docs/en/tutorials/juicefs_on_colab.md
index 89a6de9d..1ef38193 100644
--- a/docs/en/tutorials/juicefs_on_colab.md
+++ b/docs/en/tutorials/juicefs_on_colab.md
@@ -43,7 +43,7 @@ a reference and with other reference docs provided by `juicefs` to achieve a sim
 You might also want the Colab instance is in the same zone or close to the region where CloudSQL and GCS are deployed,
 to make JuiceFS reaching the best performance. The tutorial should work for a random hosted Colab instance but you might
 notice a slow performance due to the latency between the Colab instance and the CloudSQL/GCS regions. To start Colab
-instaces in a specific region,
+instances in a specific region,
 see [instructions for starting a GCE VM on Colab via GCP Marketplace](https://research.google.com/colaboratory/marketplace.html)
 .
 
@@ -55,7 +55,7 @@ So in order to follow this guide you will need to have these resources ready:
   `juicefs-learning:europe-west1:juicefs-sql-example-1` as the metadata service
 * A GCS bucket created as the object storage service. In this demo we use
   `gs://juicefs-bucket-example-1` as the bucket to store file chunks.
-* An IAM ServiceAccount or an authrized user account that has write access to the posgtes server and GCS buckets
+* An IAM ServiceAccount or an authorized user account that has write access to the Postgres server and GCS buckets
 
 ## Detailed Steps
 
diff --git a/docs/en/tutorials/juicefs_on_k3s.md b/docs/en/tutorials/juicefs_on_k3s.md
index b39f3fbe..14891141 100644
--- a/docs/en/tutorials/juicefs_on_k3s.md
+++ b/docs/en/tutorials/juicefs_on_k3s.md
@@ -130,7 +130,7 @@ Next, deploy an Nginx Pod using a persistent storage declared by the JuiceFS sto
 
 ### Deployment
 
-Create a configuration file, for example: `depolyment.yaml`
+Create a configuration file, for example: `deployment.yaml`
 
 ```yaml
 apiVersion: v1
@@ -175,10 +175,10 @@ spec:
             claimName: web-pvc
 ```
 
-Depoly it:
+Deploy it:
 
 ```
-sudo kubectl apply -f depolyment.yaml
+sudo kubectl apply -f deployment.yaml
 ```
 
 ### Service
@@ -198,7 +198,7 @@ spec:
       port: 80
 ```
 
-Depoly it:
+Deploy it:
 
 ```shell
 sudo kubectl apply -f service.yaml
@@ -228,7 +228,7 @@ spec:
                   number: 80
 ```
 
-Depoly it:
+Deploy it:
 
 ```shell
 sudo kubectl apply -f ingress.yaml
diff --git a/docs/zh_cn/administration/destroy.md b/docs/zh_cn/administration/destroy.md
index 2dfece46..a192db39 100644
--- a/docs/zh_cn/administration/destroy.md
+++ b/docs/zh_cn/administration/destroy.md
@@ -61,7 +61,7 @@ $ juicefs destroy redis://127.0.0.1:6379/1 eabb96d5-7228-461e-9240-fddbf2b576d8
 data storage: file://jfs/
   used bytes: 18620416
  used inodes: 23
-WARNING: The target volume will be destoried permanently, including:
+WARNING: The target volume will be destroyed permanently, including:
 WARNING: 1. objects in the data storage
 WARNING: 2. entries in the metadata engine
 Proceed anyway? [y/N]: y
diff --git a/docs/zh_cn/administration/metadata/postgresql_best_practices.md b/docs/zh_cn/administration/metadata/postgresql_best_practices.md
index 267aac35..16c69629 100644
--- a/docs/zh_cn/administration/metadata/postgresql_best_practices.md
+++ b/docs/zh_cn/administration/metadata/postgresql_best_practices.md
@@ -48,5 +48,5 @@ juicefs mount -d "postgres://user:$PG_PASSWD@192.168.1.6:5432/juicefs" /mnt/jfs
 PostgreSQL 官方文档 [High Availability, Load Balancing, and Replication](https://www.postgresql.org/docs/current/different-replication-solutions.html) 对比了几种常用的数据库高可用方案，请根据实际业务需要选择恰当的高可用方案。
 
 :::note 注意
-JuiceFS 使用[事务](https://www.postgresql.org/docs/current/tutorial-transactions.html)保证元数据操作的原子性。由于 PostgreSQL 尚不支持 Muti-Shard (Distributed) 分布式事务，因此请勿将多服务器分布式架构用于 JuiceFS 元数据存储。
+JuiceFS 使用[事务](https://www.postgresql.org/docs/current/tutorial-transactions.html)保证元数据操作的原子性。由于 PostgreSQL 尚不支持 Multi-Shard (Distributed) 分布式事务，因此请勿将多服务器分布式架构用于 JuiceFS 元数据存储。
 :::
diff --git a/docs/zh_cn/deployment/hadoop_java_sdk.md b/docs/zh_cn/deployment/hadoop_java_sdk.md
index 5884340a..f84f44b7 100644
--- a/docs/zh_cn/deployment/hadoop_java_sdk.md
+++ b/docs/zh_cn/deployment/hadoop_java_sdk.md
@@ -532,6 +532,7 @@ CREATE TABLE IF NOT EXISTS person
 
 2. 使用以下示例代码验证：
 
+<!-- autocorrect: false -->
    ```java
    package demo;
 
@@ -555,6 +556,7 @@ CREATE TABLE IF NOT EXISTS person
        }
    }
    ```
+<!-- autocorrect: true -->
 
 ## 监控指标收集
 
diff --git a/docs/zh_cn/introduction/comparison/juicefs_vs_cephfs.md b/docs/zh_cn/introduction/comparison/juicefs_vs_cephfs.md
index 9bbc87e8..824bccaf 100644
--- a/docs/zh_cn/introduction/comparison/juicefs_vs_cephfs.md
+++ b/docs/zh_cn/introduction/comparison/juicefs_vs_cephfs.md
@@ -27,7 +27,7 @@ description: Ceph 是一套提供对象存储、块存储和文件存储的统
 
 #### JuiceFS
 
-JuiceFS 主要实现一个 libjfs 库和 FUSE 客户端程序、Java SDK 等，支持对接多种元数据引擎和对象存储，适合在公有云、私有云或混合云环境下部署；
+JuiceFS 主要实现一个 libjfs 库和 FUSE 客户端程序、Java SDK 等，支持对接多种元数据引擎和对象存储，适合在公有云、私有云或混合云环境下部署。
 
 - 元数据：支持多种已有的[数据库实现](../../guide/how_to_set_up_metadata_engine.md)，包括：
   - Redis 及各种兼容 Redis 协议的变种（需要支持事务）；
@@ -58,7 +58,7 @@ JuiceFS 主要实现一个 libjfs 库和 FUSE 客户端程序、Java SDK 等，
 
 #### 注 1：文件分块
 
-虽然两者都做了大文件的分块，但在实现原理上有本质区别。CephFS 会将文件按 [`object_size`](https://docs.ceph.com/en/latest/cephfs/file-layouts/#reading-layouts-with-getfattr)（默认为 4MiB）拆分，每个分块对应一个 RADOS object。而 JuiceFS 则将文件先按 64MiB Chunk 拆分，每个 Chunk 在写入时根据实际情况进一步拆分成一个或多个逻辑 Slice，每个 Slice 在写入对象存储时再拆分成默认 4MiB 的 Block，Block 与对象存储中 object 一一对应。在处理覆盖写时，CephFS 需要直接修改对应的 objects，流程较为复杂；尤其是冗余策略为 EC 或者开启数据压缩时，往往需要先读取部分 object 内容，在内存中修改后再写入，这个流程会带来很大的性能开销。而 JuiceFS 在覆盖写时将更新数据作为新 objects 写入并修改元数据即可，性能大幅提升。过程中出现的冗余数据会异步完成垃圾回收。
+虽然两者都做了大文件的分块，但在实现原理上有本质区别。CephFS 会将文件按 [`object_size`](https://docs.ceph.com/en/latest/cephfs/file-layouts/#reading-layouts-with-getfattr)（默认为 4MiB）拆分，每个分块对应一个 RADOS object。而 JuiceFS 则将文件先按 64MiB Chunk 拆分，每个 Chunk 在写入时根据实际情况进一步拆分成一个或多个逻辑 Slice，每个 Slice 在写入对象存储时再拆分成默认 4MiB 的 Block，Block 与对象存储中 object 一一对应。在处理覆盖写时，CephFS 需要直接修改对应的 objects，流程较为复杂；尤其是冗余策略为 EC 或者开启数据压缩时，往往需要先读取部分 object 内容，在内存中修改后再写入，这个流程会带来很大的性能开销。而 JuiceFS 在覆盖写时将更新数据作为新 objects 写入并修改元数据即可，性能大幅提升；此外，过程中出现的冗余数据会异步完成垃圾回收。
 
 #### 注 2：数据压缩
 
diff --git a/docs/zh_cn/reference/command_reference.md b/docs/zh_cn/reference/command_reference.md
index 431245d7..5280366a 100644
--- a/docs/zh_cn/reference/command_reference.md
+++ b/docs/zh_cn/reference/command_reference.md
@@ -172,6 +172,9 @@ RSA 私钥的路径 (PEM)
 `--trash-days value`<br />
 文件被自动清理前在回收站内保留的天数 (默认：1)
 
+`--hash-prefix`<br />
+给每个对象添加 hash 前缀 (默认：false)
+
 `--force`<br />
 强制覆盖当前的格式化配置 (默认：false)
 
diff --git a/docs/zh_cn/tutorials/digitalocean.md b/docs/zh_cn/tutorials/digitalocean.md
index 7986bfbe..08e920e3 100644
--- a/docs/zh_cn/tutorials/digitalocean.md
+++ b/docs/zh_cn/tutorials/digitalocean.md
@@ -36,7 +36,7 @@ JuiceFS 使用对象存储来存储所有的数据，在 DigitalOcean 上使用
 
 ### 3. 数据库
 
-与一般的文件系统不同，JuiceFS 将数据所对应的所有元数据都存储在独立的数据库，存储的数据规模越大性能越出色。目前，JuiceFS 支持 Redis、TiKV、MySQL/MariaDB、PostgreSQL、SQLite 等常见数据库，同时也在持续开发对其他数据库的支持。如果你需要的数据库暂未支持，请提交 [Issuse](https://github.com/juicedata/juicefs/issues) 反馈。
+与一般的文件系统不同，JuiceFS 将数据所对应的所有元数据都存储在独立的数据库，存储的数据规模越大性能越出色。目前，JuiceFS 支持 Redis、TiKV、MySQL/MariaDB、PostgreSQL、SQLite 等常见数据库，同时也在持续开发对其他数据库的支持。如果你需要的数据库暂未支持，请提交 [Issue](https://github.com/juicedata/juicefs/issues) 反馈。
 
 在性能、规模和可靠性等方面，每种数据库都有各自的优缺点，你应该根据实际的场景需要进行选择。
 
@@ -52,7 +52,7 @@ JuiceFS 使用对象存储来存储所有的数据，在 DigitalOcean 上使用
 
 > **注意**：为了确保元数据的安全和完整，回收策略请不要选择 `allkeys-lru` 和 `allkey-random`。
 
-Redis 的访问地址可以从控制台的 `Connection Details` 中找到，如果所有计算资源都在 DigitalOcea，则建议优先使用 VPC 私有网络进行连接，这样能最大程度的提升安全性。
+Redis 的访问地址可以从控制台的 `Connection Details` 中找到，如果所有计算资源都在 DigitalOcean，则建议优先使用 VPC 私有网络进行连接，这样能最大程度的提升安全性。
 
 ![](../images/digitalocean-redis-url.png)
 
diff --git a/go.mod b/go.mod
index bb4bbaf2..1ed9d90b 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,6 @@ require (
 	github.com/colinmarc/hdfs/v2 v2.3.0
 	github.com/dgraph-io/badger/v3 v3.2103.2
 	github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
-	github.com/go-redis/redis/v8 v8.11.5
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/goccy/go-json v0.10.0
 	github.com/gofrs/flock v0.8.1
@@ -51,6 +50,7 @@ require (
 	github.com/pyroscope-io/client v0.2.1
 	github.com/qingstor/qingstor-sdk-go/v4 v4.4.0
 	github.com/qiniu/go-sdk/v7 v7.11.1
+	github.com/redis/go-redis/v9 v9.0.2
 	github.com/sirupsen/logrus v1.9.0
 	github.com/smartystreets/goconvey v1.7.2
 	github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
@@ -95,7 +95,7 @@ require (
 	github.com/benbjohnson/clock v1.1.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash v1.1.0 // indirect
-	github.com/cespare/xxhash/v2 v2.1.2 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/cheggaaa/pb v1.0.29 // indirect
 	github.com/clbanning/mxj v1.8.4 // indirect
 	github.com/coredns/coredns v1.6.6 // indirect
@@ -114,7 +114,7 @@ require (
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/djherbis/atime v1.0.0 // indirect
 	github.com/dswarbrick/smart v0.0.0-20190505152634-909a45200d6d // indirect
-	github.com/dustin/go-humanize v1.0.0 // indirect
+	github.com/dustin/go-humanize v1.0.0
 	github.com/elazarl/go-bindata-assetfs v1.0.0 // indirect
 	github.com/fatih/color v1.10.0 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
@@ -185,6 +185,7 @@ require (
 	github.com/montanaflynn/stats v0.5.0 // indirect
 	github.com/mozillazg/go-httpheader v0.2.1 // indirect
 	github.com/ncw/directio v1.0.5 // indirect
+	github.com/nxadm/tail v1.4.8 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 // indirect
 	github.com/philhofer/fwd v1.1.1 // indirect
@@ -206,6 +207,7 @@ require (
 	github.com/smartystreets/assertions v1.2.0 // indirect
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
 	github.com/stathat/consistent v1.0.0 // indirect
+	github.com/stretchr/objx v0.5.0 // indirect
 	github.com/syndtr/goleveldb v1.0.0 // indirect
 	github.com/tidwall/gjson v1.9.3 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index 23bd5f5c..2de53e68 100644
--- a/go.sum
+++ b/go.sum
@@ -178,6 +178,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/billziss-gh/cgofuse v1.5.0 h1:kH516I/s+Ab4diL/Y/ayFeUjjA8ey+JK12xDfBf4HEs=
 github.com/billziss-gh/cgofuse v1.5.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM=
+github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ=
+github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8=
 github.com/caddyserver/caddy v1.0.4/go.mod h1:uruyfVsyMcDb3IOzSKsi1x0wOjy1my/PxOSTcD+24jM=
 github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
 github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -188,8 +190,8 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
-github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
 github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo=
 github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
@@ -307,6 +309,7 @@ github.com/frankban/quicktest v1.10.2 h1:19ARM85nVi4xH7xPXuc5eM/udya5ieh7b/Sv+d8
 github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-acme/lego/v3 v3.1.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
 github.com/go-acme/lego/v3 v3.2.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
@@ -338,8 +341,6 @@ github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
 github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
-github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
-github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -753,6 +754,7 @@ github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/Hzq
 github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ=
 github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
@@ -851,6 +853,8 @@ github.com/qiniu/go-sdk/v7 v7.11.1/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFs
 github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
 github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE=
+github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@@ -919,8 +923,9 @@ github.com/stathat/consistent v1.0.0 h1:ZFJ1QTRn8npNBKW065raSZ8xfOqhpb8vLOkfp4Cc
 github.com/stathat/consistent v1.0.0/go.mod h1:uajTPbgSygZBJ+V+0mY7meZ8i0XAcZs7AQ6V121XSxw=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -928,8 +933,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 h1:b2nJXyPCa9HY7giGM+kYcnQ71m14JnGdQabMPmyt++8=
 github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
@@ -1224,6 +1229,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/pkg/chunk/cached_store.go b/pkg/chunk/cached_store.go
index 85734c7a..1e510c66 100644
--- a/pkg/chunk/cached_store.go
+++ b/pkg/chunk/cached_store.go
@@ -235,17 +235,12 @@ func (s *rSlice) Remove() error {
 		s.store.bcache.remove(key)
 	}
 
-	if s.store.conf.MaxDeletes == 0 {
-		return errors.New("skip deleting objects because MaxDeletes is 0")
-	}
 	var err error
-	s.store.currentDelete <- struct{}{}
 	for i := 0; i <= lastIndx; i++ {
 		if e := s.delete(i); e != nil {
 			err = e
 		}
 	}
-	<-s.store.currentDelete
 	return err
 }
 
@@ -535,7 +530,6 @@ type Config struct {
 	AutoCreate        bool
 	Compress          string
 	MaxUpload         int
-	MaxDeletes        int
 	MaxRetries        int
 	UploadLimit       int64 // bytes per second
 	DownloadLimit     int64 // bytes per second
@@ -592,7 +586,6 @@ type cachedStore struct {
 	conf          Config
 	group         *Controller
 	currentUpload chan bool
-	currentDelete chan struct{}
 	pendingCh     chan *pendingItem
 	pendingKeys   map[string]*pendingItem
 	pendingMutex  sync.Mutex
@@ -701,7 +694,6 @@ func NewCachedStore(storage object.ObjectStorage, config Config, reg prometheus.
 		storage:       storage,
 		conf:          config,
 		currentUpload: make(chan bool, config.MaxUpload),
-		currentDelete: make(chan struct{}, config.MaxDeletes),
 		compressor:    compressor,
 		seekable:      compressor.CompressBound(0) == 0,
 		pendingCh:     make(chan *pendingItem, 100*config.MaxUpload),
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index 57ff54ce..4c2e4c2b 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -125,6 +125,7 @@ type baseMeta struct {
 	removedFiles map[Ino]bool
 	compacting   map[uint64]bool
 	maxDeleting  chan struct{}
+	dslices      chan Slice // slices to delete
 	symlinks     *sync.Map
 	msgCallbacks *msgCallbacks
 	reloadCb     []func(*Format)
@@ -161,6 +162,7 @@ func newBaseMeta(addr string, conf *Config) *baseMeta {
 		removedFiles: make(map[Ino]bool),
 		compacting:   make(map[uint64]bool),
 		maxDeleting:  make(chan struct{}, 100),
+		dslices:      make(chan Slice, conf.MaxDeletes*10240),
 		symlinks:     &sync.Map{},
 		fsStat:       new(fsStat),
 		msgCallbacks: &msgCallbacks{
@@ -314,6 +316,9 @@ func (m *baseMeta) NewSession() error {
 	}
 	logger.Infof("Create session %d OK with version: %s", m.sid, version.Version())
 
+	for i := 0; i < m.conf.MaxDeletes; i++ {
+		go m.deleteSlices()
+	}
 	if !m.conf.NoBGJob {
 		go m.cleanupDeletedFiles()
 		go m.cleanupSlices()
@@ -1316,17 +1321,24 @@ func (m *baseMeta) tryDeleteFileData(inode Ino, length uint64, force bool) {
 	}()
 }
 
+func (m *baseMeta) deleteSlices() {
+	var err error
+	for s := range m.dslices {
+		if err = m.newMsg(DeleteSlice, s.Id, s.Size); err != nil {
+			logger.Warnf("Delete data blocks of slice %d (%d bytes): %s", s.Id, s.Size, err)
+			continue
+		}
+		if err = m.en.doDeleteSlice(s.Id, s.Size); err != nil {
+			logger.Errorf("Delete meta entry of slice %d (%d bytes): %s", s.Id, s.Size, err)
+		}
+	}
+}
+
 func (m *baseMeta) deleteSlice(id uint64, size uint32) {
-	if id == 0 {
+	if id == 0 || m.conf.MaxDeletes == 0 {
 		return
 	}
-	if err := m.newMsg(DeleteSlice, id, size); err == nil {
-		if err = m.en.doDeleteSlice(id, size); err != nil {
-			logger.Errorf("delete slice %d: %s", id, err)
-		}
-	} else if !strings.Contains(err.Error(), "skip deleting") {
-		logger.Warnf("delete slice %d (%d bytes): %s", id, size, err)
-	}
+	m.dslices <- Slice{Id: id, Size: size}
 }
 
 func (m *baseMeta) toTrash(parent Ino) bool {
diff --git a/pkg/meta/config.go b/pkg/meta/config.go
index d8aae170..5c4f7721 100644
--- a/pkg/meta/config.go
+++ b/pkg/meta/config.go
@@ -34,6 +34,7 @@ import (
 type Config struct {
 	Strict      bool // update ctime
 	Retries     int
+	MaxDeletes  int
 	CaseInsensi bool
 	ReadOnly    bool
 	NoBGJob     bool // disable background jobs like clean-up, backup, etc.
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 70e4b66d..8652ad7f 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -43,8 +43,8 @@ import (
 
 	"github.com/pkg/errors"
 
-	"github.com/go-redis/redis/v8"
 	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/redis/go-redis/v9"
 )
 
 /*
@@ -158,7 +158,7 @@ func newRedisMeta(driver, addr string, conf *Config) (Meta, error) {
 		fopt.WriteTimeout = opt.WriteTimeout
 		if conf.ReadOnly {
 			// NOTE: RouteByLatency and RouteRandomly are not supported since they require cluster client
-			fopt.SlaveOnly = routeRead == "replica"
+			fopt.ReplicaOnly = routeRead == "replica"
 		}
 		rdb = redis.NewFailoverClient(&fopt)
 	} else {
@@ -290,7 +290,7 @@ func (m *redisMeta) doLoad() ([]byte, error) {
 }
 
 func (m *redisMeta) doNewSession(sinfo []byte) error {
-	err := m.rdb.ZAdd(Background, m.allSessions(), &redis.Z{
+	err := m.rdb.ZAdd(Background, m.allSessions(), redis.Z{
 		Score:  float64(m.expireTime()),
 		Member: strconv.FormatUint(m.sid, 10)}).Err()
 	if err != nil {
@@ -1304,7 +1304,7 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 						pipe.Set(ctx, m.inodeKey(inode), m.marshal(&attr), 0)
 						pipe.SAdd(ctx, m.sustained(m.sid), strconv.Itoa(int(inode)))
 					} else {
-						pipe.ZAdd(ctx, m.delfiles(), &redis.Z{Score: float64(now.Unix()), Member: m.toDelete(inode, attr.Length)})
+						pipe.ZAdd(ctx, m.delfiles(), redis.Z{Score: float64(now.Unix()), Member: m.toDelete(inode, attr.Length)})
 						pipe.Del(ctx, m.inodeKey(inode))
 						newSpace, newInode = -align4K(attr.Length), -1
 						pipe.IncrBy(ctx, m.usedSpaceKey(), newSpace)
@@ -1635,7 +1635,7 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 								pipe.Set(ctx, m.inodeKey(dino), m.marshal(&tattr), 0)
 								pipe.SAdd(ctx, m.sustained(m.sid), strconv.Itoa(int(dino)))
 							} else {
-								pipe.ZAdd(ctx, m.delfiles(), &redis.Z{Score: float64(now.Unix()), Member: m.toDelete(dino, tattr.Length)})
+								pipe.ZAdd(ctx, m.delfiles(), redis.Z{Score: float64(now.Unix()), Member: m.toDelete(dino, tattr.Length)})
 								pipe.Del(ctx, m.inodeKey(dino))
 								newSpace, newInode = -align4K(tattr.Length), -1
 								pipe.IncrBy(ctx, m.usedSpaceKey(), newSpace)
@@ -1949,7 +1949,7 @@ func (m *redisMeta) doRefreshSession() error {
 	if err != nil {
 		return err
 	}
-	return m.rdb.ZAdd(ctx, m.allSessions(), &redis.Z{
+	return m.rdb.ZAdd(ctx, m.allSessions(), redis.Z{
 		Score:  float64(m.expireTime()),
 		Member: ssid}).Err()
 }
@@ -1967,7 +1967,7 @@ func (m *redisMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	m.parseAttr(a, &attr)
 	var newSpace int64
 	_, err = m.rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
-		pipe.ZAdd(ctx, m.delfiles(), &redis.Z{Score: float64(time.Now().Unix()), Member: m.toDelete(inode, attr.Length)})
+		pipe.ZAdd(ctx, m.delfiles(), redis.Z{Score: float64(time.Now().Unix()), Member: m.toDelete(inode, attr.Length)})
 		pipe.Del(ctx, m.inodeKey(inode))
 		newSpace = -align4K(attr.Length)
 		pipe.IncrBy(ctx, m.usedSpaceKey(), newSpace)
@@ -3101,8 +3101,8 @@ func (m *redisMeta) checkServerConfig() {
 			logger.Errorf("try to reconfigure maxmemory-policy to 'noeviction' failed: %s", err)
 		} else if result, err := m.rdb.ConfigGet(Background, "maxmemory-policy").Result(); err != nil {
 			logger.Warnf("get config maxmemory-policy failed: %s", err)
-		} else if len(result) == 2 && result[1] != "noeviction" {
-			logger.Warnf("reconfigured maxmemory-policy to 'noeviction', but it's still %s", result[1])
+		} else if len(result) == 1 && result["maxmemory-policy"] != "noeviction" {
+			logger.Warnf("reconfigured maxmemory-policy to 'noeviction', but it's still %s", result["maxmemory-policy"])
 		} else {
 			logger.Infof("set maxmemory-policy to 'noeviction' successfully")
 		}
@@ -3129,10 +3129,10 @@ func (m *redisMeta) dumpEntries(es ...*DumpedEntry) error {
 	return m.txn(ctx, func(tx *redis.Tx) error {
 		p := tx.Pipeline()
 		var ar = make([]*redis.StringCmd, len(es))
-		var xr = make([]*redis.StringStringMapCmd, len(es))
+		var xr = make([]*redis.MapStringStringCmd, len(es))
 		var sr = make([]*redis.StringCmd, len(es))
 		var cr = make([]*redis.StringSliceCmd, len(es))
-		var dr = make([]*redis.StringStringMapCmd, len(es))
+		var dr = make([]*redis.MapStringStringCmd, len(es))
 		for i, e := range es {
 			inode := e.Attr.Inode
 			ar[i] = p.Get(ctx, m.inodeKey(inode))
@@ -3612,14 +3612,14 @@ func (m *redisMeta) LoadMeta(r io.Reader) (err error) {
 		if l > 100 {
 			l = 100
 		}
-		zs := make([]*redis.Z, 0, l)
+		zs := make([]redis.Z, 0, l)
 		for _, d := range dm.DelFiles {
 			if len(zs) >= 100 {
 				p.ZAdd(ctx, m.delfiles(), zs...)
 				tryExec()
 				zs = zs[:0]
 			}
-			zs = append(zs, &redis.Z{
+			zs = append(zs, redis.Z{
 				Score:  float64(d.Expire),
 				Member: m.toDelete(d.Inode, d.Length),
 			})
diff --git a/pkg/meta/redis_lock.go b/pkg/meta/redis_lock.go
index 2d828404..075c8529 100644
--- a/pkg/meta/redis_lock.go
+++ b/pkg/meta/redis_lock.go
@@ -26,7 +26,7 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/go-redis/redis/v8"
+	"github.com/redis/go-redis/v9"
 )
 
 func (r *redisMeta) Flock(ctx Context, inode Ino, owner uint64, ltype uint32, block bool) syscall.Errno {
diff --git a/pkg/meta/tkv_mem.go b/pkg/meta/tkv_mem.go
index 81b7fb20..b6bfd785 100644
--- a/pkg/meta/tkv_mem.go
+++ b/pkg/meta/tkv_mem.go
@@ -20,7 +20,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"os"
 	"strings"
 	"sync"
 
@@ -36,7 +36,7 @@ const settingPath = "/tmp/juicefs.memkv.setting.json"
 
 func newMockClient(addr string) (tkvClient, error) {
 	client := &memKV{items: btree.New(2), temp: &kvItem{}}
-	if d, err := ioutil.ReadFile(settingPath); err == nil {
+	if d, err := os.ReadFile(settingPath); err == nil {
 		var buffer map[string][]byte
 		if err = json.Unmarshal(d, &buffer); err == nil {
 			for k, v := range buffer {
@@ -224,7 +224,7 @@ func (c *memKV) txn(f func(*kvTxn) error) error {
 	}
 	if _, ok := tx.buffer["setting"]; ok {
 		d, _ := json.Marshal(tx.buffer)
-		if err := ioutil.WriteFile(settingPath, d, 0644); err != nil {
+		if err := os.WriteFile(settingPath, d, 0644); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/meta/utils.go b/pkg/meta/utils.go
index 0d6c0b54..c486dfe0 100644
--- a/pkg/meta/utils.go
+++ b/pkg/meta/utils.go
@@ -29,8 +29,8 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/go-redis/redis/v8"
 	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/redis/go-redis/v9"
 )
 
 const (
diff --git a/pkg/object/encrypt.go b/pkg/object/encrypt.go
index 82903c6c..bbff660b 100644
--- a/pkg/object/encrypt.go
+++ b/pkg/object/encrypt.go
@@ -28,7 +28,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
+	"os"
 	"strings"
 
 	"github.com/youmark/pkcs8"
@@ -107,7 +107,7 @@ func ParseRsaPrivateKeyFromPem(enc []byte, passphrase []byte) (*rsa.PrivateKey,
 }
 
 func ParseRsaPrivateKeyFromPath(path, passphrase string) (*rsa.PrivateKey, error) {
-	b, err := ioutil.ReadFile(path)
+	b, err := os.ReadFile(path)
 	if err != nil {
 		return nil, err
 	}
@@ -235,7 +235,7 @@ func (e *encrypted) Get(key string, off, limit int64) (io.ReadCloser, error) {
 		return nil, err
 	}
 	defer r.Close()
-	ciphertext, err := ioutil.ReadAll(r)
+	ciphertext, err := io.ReadAll(r)
 	if err != nil {
 		return nil, err
 	}
@@ -251,11 +251,11 @@ func (e *encrypted) Get(key string, off, limit int64) (io.ReadCloser, error) {
 		limit = l - off
 	}
 	data := plain[off : off+limit]
-	return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+	return io.NopCloser(bytes.NewBuffer(data)), nil
 }
 
 func (e *encrypted) Put(key string, in io.Reader) error {
-	plain, err := ioutil.ReadAll(in)
+	plain, err := io.ReadAll(in)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/object/etcd.go b/pkg/object/etcd.go
index 2fa22a2e..8573b3bd 100644
--- a/pkg/object/etcd.go
+++ b/pkg/object/etcd.go
@@ -25,7 +25,6 @@ import (
 	"crypto/tls"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/url"
 	"os"
@@ -61,14 +60,14 @@ func (c *etcdClient) Get(key string, off, limit int64) (io.ReadCloser, error) {
 			if limit > 0 && limit < int64(len(data)) {
 				data = data[:limit]
 			}
-			return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+			return io.NopCloser(bytes.NewBuffer(data)), nil
 		}
 	}
 	return nil, os.ErrNotExist
 }
 
 func (c *etcdClient) Put(key string, in io.Reader) error {
-	d, err := ioutil.ReadAll(in)
+	d, err := io.ReadAll(in)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/object/file.go b/pkg/object/file.go
index 4d93e8ef..beaa5565 100644
--- a/pkg/object/file.go
+++ b/pkg/object/file.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"io"
 	"io/fs"
-	"io/ioutil"
 	"math/rand"
 	"os"
 	"path"
@@ -117,7 +116,7 @@ func (d *filestore) Get(key string, off, limit int64) (io.ReadCloser, error) {
 	}
 	if finfo.IsDir() {
 		_ = f.Close()
-		return ioutil.NopCloser(bytes.NewBuffer([]byte{})), nil
+		return io.NopCloser(bytes.NewBuffer([]byte{})), nil
 	}
 
 	if off > 0 {
@@ -132,7 +131,7 @@ func (d *filestore) Get(key string, off, limit int64) (io.ReadCloser, error) {
 		if n, err := f.Read(buf); err != nil {
 			return nil, err
 		} else {
-			return ioutil.NopCloser(bytes.NewBuffer(buf[:n])), nil
+			return io.NopCloser(bytes.NewBuffer(buf[:n])), nil
 		}
 	}
 	return f, nil
diff --git a/pkg/object/hdfs.go b/pkg/object/hdfs.go
index 1447cd65..76ef0b01 100644
--- a/pkg/object/hdfs.go
+++ b/pkg/object/hdfs.go
@@ -23,15 +23,14 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"math/rand"
 	"os"
 	"os/user"
 	"path/filepath"
 	"sort"
+	"strconv"
 	"strings"
 	"time"
-	"strconv"
 
 	"github.com/colinmarc/hdfs/v2"
 	"github.com/colinmarc/hdfs/v2/hadoopconf"
@@ -42,8 +41,8 @@ var supergroup = "supergroup"
 
 type hdfsclient struct {
 	DefaultObjectStorage
-	addr string
-	c    *hdfs.Client
+	addr           string
+	c              *hdfs.Client
 	dfsReplication int
 }
 
@@ -107,7 +106,7 @@ func (h *hdfsclient) Get(key string, off, limit int64) (io.ReadCloser, error) {
 
 	finfo := f.Stat()
 	if finfo.IsDir() {
-		return ioutil.NopCloser(bytes.NewBuffer([]byte{})), nil
+		return io.NopCloser(bytes.NewBuffer([]byte{})), nil
 	}
 
 	if off > 0 {
diff --git a/pkg/object/ibmcos.go b/pkg/object/ibmcos.go
index 5374fbe2..ef7d32bd 100644
--- a/pkg/object/ibmcos.go
+++ b/pkg/object/ibmcos.go
@@ -23,7 +23,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -79,7 +78,7 @@ func (s *ibmcos) Put(key string, in io.Reader) error {
 	if b, ok := in.(io.ReadSeeker); ok {
 		body = b
 	} else {
-		data, err := ioutil.ReadAll(in)
+		data, err := io.ReadAll(in)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/object/ks3.go b/pkg/object/ks3.go
index 10203e8a..9b96c176 100644
--- a/pkg/object/ks3.go
+++ b/pkg/object/ks3.go
@@ -23,7 +23,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -102,7 +101,7 @@ func (s *ks3) Put(key string, in io.Reader) error {
 	if b, ok := in.(io.ReadSeeker); ok {
 		body = b
 	} else {
-		data, err := ioutil.ReadAll(in)
+		data, err := io.ReadAll(in)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/object/mem.go b/pkg/object/mem.go
index 17eee54a..758bd349 100644
--- a/pkg/object/mem.go
+++ b/pkg/object/mem.go
@@ -21,7 +21,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"sort"
 	"strings"
@@ -92,7 +91,7 @@ func (m *memStore) Get(key string, off, limit int64) (io.ReadCloser, error) {
 	if limit > 0 && limit < int64(len(data)) {
 		data = data[:limit]
 	}
-	return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+	return io.NopCloser(bytes.NewBuffer(data)), nil
 }
 
 func (m *memStore) Put(key string, in io.Reader) error {
@@ -106,7 +105,7 @@ func (m *memStore) Put(key string, in io.Reader) error {
 	if ok {
 		logger.Debugf("overwrite %s", key)
 	}
-	data, err := ioutil.ReadAll(in)
+	data, err := io.ReadAll(in)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/object/nos.go b/pkg/object/nos.go
index 0b640458..aaf38e6a 100644
--- a/pkg/object/nos.go
+++ b/pkg/object/nos.go
@@ -23,7 +23,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -98,7 +97,7 @@ func (s *nos) Put(key string, in io.Reader) error {
 	case io.ReadSeeker:
 		body = in.(io.ReadSeeker)
 	default:
-		data, err := ioutil.ReadAll(in)
+		data, err := io.ReadAll(in)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/object/obs.go b/pkg/object/obs.go
index 84df2ec7..653fdbe3 100644
--- a/pkg/object/obs.go
+++ b/pkg/object/obs.go
@@ -25,7 +25,6 @@ import (
 	"encoding/base64"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -118,7 +117,7 @@ func (s *obsClient) Put(key string, in io.Reader) error {
 		sum = h.Sum(nil)
 		body = b
 	} else {
-		data, err := ioutil.ReadAll(in)
+		data, err := io.ReadAll(in)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/object/oss.go b/pkg/object/oss.go
index 64e959f6..6526fe34 100644
--- a/pkg/object/oss.go
+++ b/pkg/object/oss.go
@@ -24,7 +24,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 	"net/url"
@@ -229,7 +228,7 @@ func fetch(url string) ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	return ioutil.ReadAll(resp.Body)
+	return io.ReadAll(resp.Body)
 }
 
 func fetchStsToken() (*stsCred, error) {
diff --git a/pkg/object/qingstor.go b/pkg/object/qingstor.go
index a651ff62..f6b110f2 100644
--- a/pkg/object/qingstor.go
+++ b/pkg/object/qingstor.go
@@ -23,7 +23,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -111,7 +110,7 @@ func findLen(in io.Reader) (io.Reader, int64, error) {
 			return nil, 0, err
 		}
 	default:
-		d, err := ioutil.ReadAll(in)
+		d, err := io.ReadAll(in)
 		if err != nil {
 			return nil, 0, err
 		}
diff --git a/pkg/object/redis.go b/pkg/object/redis.go
index 700740ee..f61df749 100644
--- a/pkg/object/redis.go
+++ b/pkg/object/redis.go
@@ -24,7 +24,6 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/url"
 	"os"
@@ -33,7 +32,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/go-redis/redis/v8"
+	"github.com/redis/go-redis/v9"
 )
 
 // redisStore stores data chunks into Redis.
@@ -65,11 +64,11 @@ func (r *redisStore) Get(key string, off, limit int64) (io.ReadCloser, error) {
 	if limit > 0 && limit < int64(len(data)) {
 		data = data[:limit]
 	}
-	return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+	return io.NopCloser(bytes.NewBuffer(data)), nil
 }
 
 func (r *redisStore) Put(key string, in io.Reader) error {
-	data, err := ioutil.ReadAll(in)
+	data, err := io.ReadAll(in)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/object/restful.go b/pkg/object/restful.go
index 3c8bb029..4a8feb69 100644
--- a/pkg/object/restful.go
+++ b/pkg/object/restful.go
@@ -24,7 +24,6 @@ import (
 	"encoding/base64"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"math/rand"
 	"net"
 	"net/http"
@@ -90,7 +89,7 @@ func GetHttpClient() *http.Client {
 
 func cleanup(response *http.Response) {
 	if response != nil && response.Body != nil {
-		_, _ = ioutil.ReadAll(response.Body)
+		_, _ = io.ReadAll(response.Body)
 		_ = response.Body.Close()
 	}
 }
@@ -150,7 +149,7 @@ func (s *RestfulStorage) request(method, key string, body io.Reader, headers map
 }
 
 func parseError(resp *http.Response) error {
-	data, err := ioutil.ReadAll(resp.Body)
+	data, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return fmt.Errorf("request failed: %s", err)
 	}
@@ -220,7 +219,7 @@ func (s *RestfulStorage) Copy(dst, src string) error {
 		return err
 	}
 	defer in.Close()
-	d, err := ioutil.ReadAll(in)
+	d, err := io.ReadAll(in)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/object/s3.go b/pkg/object/s3.go
index 72e32a18..cf0326fa 100644
--- a/pkg/object/s3.go
+++ b/pkg/object/s3.go
@@ -23,7 +23,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -128,7 +127,7 @@ func (s *s3client) Put(key string, in io.Reader) error {
 	if b, ok := in.(io.ReadSeeker); ok {
 		body = b
 	} else {
-		data, err := ioutil.ReadAll(in)
+		data, err := io.ReadAll(in)
 		if err != nil {
 			return err
 		}
@@ -449,6 +448,12 @@ func newS3(endpoint, accessKey, secretKey, token string) (ObjectStorage, error)
 		DisableSSL: aws.Bool(!ssl),
 		HTTPClient: httpClient,
 	}
+
+	disable100Continue := strings.EqualFold(uri.Query().Get("disable-100-continue"), "true")
+	if disable100Continue {
+		awsConfig.S3Disable100Continue = aws.Bool(true)
+	}
+
 	if accessKey == "anonymous" {
 		awsConfig.Credentials = credentials.AnonymousCredentials
 	} else if accessKey != "" {
diff --git a/pkg/object/sftp.go b/pkg/object/sftp.go
index 468a4039..b694cf90 100644
--- a/pkg/object/sftp.go
+++ b/pkg/object/sftp.go
@@ -10,7 +10,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/url"
 	"os"
@@ -194,7 +193,7 @@ func (f *sftpStore) Get(key string, off, limit int64) (io.ReadCloser, error) {
 		return nil, err
 	}
 	if finfo.IsDir() {
-		return ioutil.NopCloser(bytes.NewBuffer([]byte{})), nil
+		return io.NopCloser(bytes.NewBuffer([]byte{})), nil
 	}
 
 	if off > 0 {
@@ -208,7 +207,7 @@ func (f *sftpStore) Get(key string, off, limit int64) (io.ReadCloser, error) {
 		if n, err := ff.Read(buf); n == 0 && err != nil {
 			return nil, err
 		} else {
-			return ioutil.NopCloser(bytes.NewBuffer(buf[:n])), nil
+			return io.NopCloser(bytes.NewBuffer(buf[:n])), nil
 		}
 	}
 	return ff, err
@@ -476,7 +475,7 @@ func newSftp(endpoint, username, pass, token string) (ObjectStorage, error) {
 
 	var signers []ssh.Signer
 	if privateKeyPath := os.Getenv("SSH_PRIVATE_KEY_PATH"); privateKeyPath != "" {
-		key, err := ioutil.ReadFile(privateKeyPath)
+		key, err := os.ReadFile(privateKeyPath)
 		if err != nil {
 			return nil, fmt.Errorf("unable to read private key, error: %v", err)
 		}
@@ -489,9 +488,9 @@ func newSftp(endpoint, username, pass, token string) (ObjectStorage, error) {
 		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))
+			key, err := os.ReadFile(filepath.Join(home, "id_"+a))
 			if err != nil {
-				key, err = ioutil.ReadFile(filepath.Join(home, "id_"+a+"-cert"))
+				key, err = os.ReadFile(filepath.Join(home, "id_"+a+"-cert"))
 			}
 			if err == nil {
 				signer, err := ssh.ParsePrivateKey(key)
diff --git a/pkg/object/sql.go b/pkg/object/sql.go
index 34eb07fc..4407f0d5 100644
--- a/pkg/object/sql.go
+++ b/pkg/object/sql.go
@@ -25,7 +25,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/url"
 	"os"
 	"strings"
@@ -79,11 +78,11 @@ func (s *sqlStore) Get(key string, off, limit int64) (io.ReadCloser, error) {
 	if limit > 0 && limit < int64(len(data)) {
 		data = data[:limit]
 	}
-	return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+	return io.NopCloser(bytes.NewBuffer(data)), nil
 }
 
 func (s *sqlStore) Put(key string, in io.Reader) error {
-	d, err := ioutil.ReadAll(in)
+	d, err := io.ReadAll(in)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/object/tikv.go b/pkg/object/tikv.go
index ea2f2dfc..74f08845 100644
--- a/pkg/object/tikv.go
+++ b/pkg/object/tikv.go
@@ -24,7 +24,6 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/url"
 	"os"
 	"strings"
@@ -61,11 +60,11 @@ func (t *tikv) Get(key string, off, limit int64) (io.ReadCloser, error) {
 	if limit > 0 && limit < int64(len(data)) {
 		data = data[:limit]
 	}
-	return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+	return io.NopCloser(bytes.NewBuffer(data)), nil
 }
 
 func (t *tikv) Put(key string, in io.Reader) error {
-	d, err := ioutil.ReadAll(in)
+	d, err := io.ReadAll(in)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/object/ufile.go b/pkg/object/ufile.go
index 3d7712c1..0ae401ea 100644
--- a/pkg/object/ufile.go
+++ b/pkg/object/ufile.go
@@ -29,7 +29,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -121,7 +120,7 @@ func (u *ufile) parseResp(resp *http.Response, out interface{}) error {
 	defer resp.Body.Close()
 	var data []byte
 	if resp.ContentLength <= 0 || resp.ContentLength > (1<<31) {
-		d, err := ioutil.ReadAll(resp.Body)
+		d, err := io.ReadAll(resp.Body)
 		if err != nil {
 			return err
 		}
@@ -149,7 +148,7 @@ func copyObj(store ObjectStorage, dst, src string) error {
 		return err
 	}
 	defer in.Close()
-	d, err := ioutil.ReadAll(in)
+	d, err := io.ReadAll(in)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/object/upyun.go b/pkg/object/upyun.go
index 4cd2a006..582be87f 100644
--- a/pkg/object/upyun.go
+++ b/pkg/object/upyun.go
@@ -23,7 +23,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/url"
 	"os"
 	"strings"
@@ -75,7 +74,7 @@ func (u *up) Get(key string, off, limit int64) (io.ReadCloser, error) {
 	if limit > 0 && limit < int64(len(data)) {
 		data = data[:limit]
 	}
-	return ioutil.NopCloser(bytes.NewBuffer(data)), nil
+	return io.NopCloser(bytes.NewBuffer(data)), nil
 }
 
 func (u *up) Put(key string, in io.Reader) error {
diff --git a/pkg/sync/cluster.go b/pkg/sync/cluster.go
index b3f4dd92..cd55db58 100644
--- a/pkg/sync/cluster.go
+++ b/pkg/sync/cluster.go
@@ -22,7 +22,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net"
 	"net/http"
 	"os"
@@ -70,7 +70,7 @@ func httpRequest(url string, body []byte) (ans []byte, err error) {
 		return nil, err
 	}
 	defer resp.Body.Close()
-	return ioutil.ReadAll(resp.Body)
+	return io.ReadAll(resp.Body)
 }
 
 func sendStats(addr string) {
@@ -169,7 +169,7 @@ func startManager(tasks <-chan object.Object) (string, error) {
 			http.Error(w, "POST required", http.StatusBadRequest)
 			return
 		}
-		d, err := ioutil.ReadAll(req.Body)
+		d, err := io.ReadAll(req.Body)
 		if err != nil {
 			logger.Errorf("read: %s", err)
 			return
diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go
index 3bdc849c..0fcfc6c9 100644
--- a/pkg/sync/sync.go
+++ b/pkg/sync/sync.go
@@ -21,7 +21,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"path"
 	"runtime"
@@ -337,7 +336,7 @@ func doCopySingle(src, dst object.ObjectStorage, key string, size int64) error {
 		} else {
 			var f *os.File
 			// download the object into disk
-			if f, err = ioutil.TempFile("", "rep"); err != nil {
+			if f, err = os.CreateTemp("", "rep"); err != nil {
 				logger.Warnf("create temp file: %s", err)
 				goto SINGLE
 			}
diff --git a/pkg/usage/usage.go b/pkg/usage/usage.go
index 019a4993..bd0dce4c 100644
--- a/pkg/usage/usage.go
+++ b/pkg/usage/usage.go
@@ -20,7 +20,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"math/rand"
 	"net/http"
 	"time"
@@ -60,7 +60,7 @@ func sendUsage(u usage) error {
 	if resp.StatusCode != 200 {
 		return fmt.Errorf("got %s", resp.Status)
 	}
-	_, err = ioutil.ReadAll(resp.Body)
+	_, err = io.ReadAll(resp.Body)
 	return err
 }
 
diff --git a/pkg/utils/memusage.go b/pkg/utils/memusage.go
index 4926ea87..b86d5bb8 100644
--- a/pkg/utils/memusage.go
+++ b/pkg/utils/memusage.go
@@ -21,13 +21,13 @@ package utils
 
 import (
 	"bytes"
-	"io/ioutil"
+	"os"
 	"strconv"
 	"syscall"
 )
 
 func MemoryUsage() (virt, rss uint64) {
-	stat, err := ioutil.ReadFile("/proc/self/stat")
+	stat, err := os.ReadFile("/proc/self/stat")
 	if err == nil {
 		stats := bytes.Split(stat, []byte(" "))
 		if len(stats) >= 24 {
diff --git a/sdk/java/libjfs/main.go b/sdk/java/libjfs/main.go
index a9cb7c3c..687ff390 100644
--- a/sdk/java/libjfs/main.go
+++ b/sdk/java/libjfs/main.go
@@ -382,12 +382,13 @@ func jfs_init(cname, jsonConf, user, group, superuser, supergroup *C.char) uintp
 		}
 
 		metaConf := &meta.Config{
-			Retries:   jConf.IORetries,
-			Strict:    true,
-			ReadOnly:  jConf.ReadOnly,
-			NoBGJob:   jConf.NoBGJob,
-			OpenCache: time.Duration(jConf.OpenCache * 1e9),
-			Heartbeat: time.Second * time.Duration(jConf.Heartbeat),
+			Retries:    jConf.IORetries,
+			Strict:     true,
+			MaxDeletes: jConf.MaxDeletes,
+			ReadOnly:   jConf.ReadOnly,
+			NoBGJob:    jConf.NoBGJob,
+			OpenCache:  time.Duration(jConf.OpenCache * 1e9),
+			Heartbeat:  time.Second * time.Duration(jConf.Heartbeat),
 		}
 		m := meta.NewClient(jConf.MetaURL, metaConf)
 		format, err := m.Load(true)
@@ -453,7 +454,6 @@ func jfs_init(cname, jsonConf, user, group, superuser, supergroup *C.char) uintp
 			CacheChecksum:     jConf.CacheChecksum,
 			CacheScanInterval: time.Second * time.Duration(jConf.CacheScanInterval),
 			MaxUpload:         jConf.MaxUploads,
-			MaxDeletes:        jConf.MaxDeletes,
 			MaxRetries:        jConf.IORetries,
 			UploadLimit:       int64(jConf.UploadLimit) * 1e6 / 8,
 			DownloadLimit:     int64(jConf.DownloadLimit) * 1e6 / 8,


Test patch:
diff --git a/.github/workflows/mutate-test.yml b/.github/workflows/mutate-test.yml
index e521efc9..7c673bcf 100644
--- a/.github/workflows/mutate-test.yml
+++ b/.github/workflows/mutate-test.yml
@@ -36,7 +36,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.18.x'
+          go-version: 'oldstable'
 
       - name: install go-mutesting
         run: |
@@ -198,7 +198,7 @@ jobs:
           test_file=$(echo ${{matrix.test_file}} | awk -F'-' '{print $1}')
           job_index=$(echo ${{matrix.test_file}} | awk -F'-' '{print $2}')
           job_total=$(echo ${{matrix.test_file}} | awk -F'-' '{print $3}')
-          echo "test file is :$test_file, job_index is $job_index, job_total is $job_total"
+          echo "test file is: $test_file, job_index is $job_index, job_total is $job_total"
           if [ -z "$test_file" ]; then 
             echo "test file is empty, will not run mutate test"
             exit 0
@@ -208,8 +208,8 @@ jobs:
           package_path=$(dirname $test_file)
           echo "package path is :" $package_path
 
-          test_cases=$(TEST_FILE_NAME=$test_file python3 .github/scripts/mutate/parse_test_cases.py)
-          if [ $? -ne 0 ]; then
+          test_cases=$(TEST_FILE_NAME=$test_file python3 .github/scripts/mutate/parse_test_cases.py) || result=$?
+          if [ $result -ne 0 ]; then
             echo "no test cases in test file, will not run mutate test"
             exit 0
           fi
diff --git a/.github/workflows/wintest.yml b/.github/workflows/wintest.yml
index b7006868..4f1d8332 100644
--- a/.github/workflows/wintest.yml
+++ b/.github/workflows/wintest.yml
@@ -25,7 +25,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v3
         with:
-          go-version: '1.18.x'
+          go-version: 'oldstable'
 
       - name: Checkout
         uses: actions/checkout@v3
diff --git a/cmd/dump_test.go b/cmd/dump_test.go
index e3690558..777c3d23 100644
--- a/cmd/dump_test.go
+++ b/cmd/dump_test.go
@@ -21,7 +21,7 @@ import (
 	"os"
 	"testing"
 
-	"github.com/go-redis/redis/v8"
+	"github.com/redis/go-redis/v9"
 )
 
 func TestDumpAndLoad(t *testing.T) {
diff --git a/cmd/integration_test.go b/cmd/integration_test.go
index dcccfa98..09a86f04 100644
--- a/cmd/integration_test.go
+++ b/cmd/integration_test.go
@@ -23,7 +23,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/go-redis/redis/v8"
+	"github.com/redis/go-redis/v9"
 )
 
 const gatewayMeta = "redis://127.0.0.1:6379/14"
diff --git a/cmd/mount_test.go b/cmd/mount_test.go
index 2ac03b3b..11c1c8cb 100644
--- a/cmd/mount_test.go
+++ b/cmd/mount_test.go
@@ -30,10 +30,10 @@ import (
 	"time"
 
 	"github.com/agiledragon/gomonkey/v2"
-	"github.com/go-redis/redis/v8"
 	"github.com/juicedata/juicefs/pkg/meta"
 	"github.com/juicedata/juicefs/pkg/utils"
 	"github.com/juicedata/juicefs/pkg/vfs"
+	"github.com/redis/go-redis/v9"
 	. "github.com/smartystreets/goconvey/convey"
 	"github.com/urfave/cli/v2"
 )
diff --git a/cmd/object_test.go b/cmd/object_test.go
index 7f08a10c..a0baed55 100644
--- a/cmd/object_test.go
+++ b/cmd/object_test.go
@@ -151,11 +151,10 @@ func TestJFS(t *testing.T) {
 	}
 	_ = m.Init(format, true)
 	var conf = vfs.Config{
-		Meta: &meta.Config{},
+		Meta: &meta.Config{MaxDeletes: 1},
 		Chunk: &chunk.Config{
 			BlockSize:  format.BlockSize << 10,
 			MaxUpload:  1,
-			MaxDeletes: 1,
 			BufferSize: 100 << 20,
 		},
 		DirEntryTimeout: time.Millisecond * 100,
diff --git a/cmd/sync_test.go b/cmd/sync_test.go
index 6d0916c6..27fddc50 100644
--- a/cmd/sync_test.go
+++ b/cmd/sync_test.go
@@ -19,7 +19,6 @@ package cmd
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"testing"
 
@@ -57,7 +56,7 @@ func TestSync(t *testing.T) {
 	}
 
 	for _, instance := range testInstances {
-		c, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", localDir, instance.path))
+		c, err := os.ReadFile(fmt.Sprintf("%s/%s", localDir, instance.path))
 		if err != nil || string(c) != instance.content {
 			t.Fatalf("sync failed: %v", err)
 		}
diff --git a/pkg/chunk/cached_store_test.go b/pkg/chunk/cached_store_test.go
index 82f0b08f..86690f5b 100644
--- a/pkg/chunk/cached_store_test.go
+++ b/pkg/chunk/cached_store_test.go
@@ -98,7 +98,6 @@ var defaultConf = Config{
 	CacheChecksum:     CsNone,
 	CacheScanInterval: time.Second * 300,
 	MaxUpload:         1,
-	MaxDeletes:        1,
 	MaxRetries:        10,
 	PutTimeout:        time.Second,
 	GetTimeout:        time.Second * 2,
diff --git a/pkg/fs/fs_test.go b/pkg/fs/fs_test.go
index 8ab181a9..116da6ab 100644
--- a/pkg/fs/fs_test.go
+++ b/pkg/fs/fs_test.go
@@ -280,11 +280,10 @@ func createTestFS(t *testing.T) *FileSystem {
 	}
 	_ = m.Init(format, true)
 	var conf = vfs.Config{
-		Meta: &meta.Config{},
+		Meta: &meta.Config{MaxDeletes: 2},
 		Chunk: &chunk.Config{
 			BlockSize:  format.BlockSize << 10,
 			MaxUpload:  1,
-			MaxDeletes: 1,
 			BufferSize: 100 << 20,
 		},
 		DirEntryTimeout: time.Millisecond * 100,
diff --git a/pkg/fuse/fuse_test.go b/pkg/fuse/fuse_test.go
index 92be5a46..4fd7954c 100644
--- a/pkg/fuse/fuse_test.go
+++ b/pkg/fuse/fuse_test.go
@@ -24,7 +24,6 @@ import (
 	"bytes"
 	"errors"
 	"io"
-	"io/ioutil"
 	"log"
 	"os"
 	"os/exec"
@@ -197,7 +196,7 @@ func StatFS(t *testing.T, mp string) {
 
 func Xattrs(t *testing.T, mp string) {
 	path := filepath.Join(mp, "myfile")
-	ioutil.WriteFile(path, []byte(""), 0644)
+	os.WriteFile(path, []byte(""), 0644)
 
 	const prefix = "user."
 	var value = []byte("test-attr-value")
@@ -224,7 +223,7 @@ func Xattrs(t *testing.T, mp string) {
 
 func Flock(t *testing.T, mp string) {
 	path := filepath.Join(mp, "go-lock.lock")
-	ioutil.WriteFile(path, []byte(""), 0644)
+	os.WriteFile(path, []byte(""), 0644)
 
 	fileLock := flock.New(path)
 	locked, err := fileLock.TryLock()
diff --git a/pkg/meta/base_test.go b/pkg/meta/base_test.go
index c540727a..d342fd7c 100644
--- a/pkg/meta/base_test.go
+++ b/pkg/meta/base_test.go
@@ -31,13 +31,13 @@ import (
 	"testing"
 	"time"
 
-	"github.com/go-redis/redis/v8"
 	"github.com/juicedata/juicefs/pkg/utils"
+	"github.com/redis/go-redis/v9"
 	"xorm.io/xorm"
 )
 
 func TestRedisClient(t *testing.T) {
-	var conf = Config{}
+	var conf = Config{MaxDeletes: 2}
 	m, err := newRedisMeta("redis", "127.0.0.1:6379/10", &conf)
 	if err != nil || m.Name() != "redis" {
 		t.Fatalf("create meta: %s", err)
@@ -48,7 +48,7 @@ func TestRedisClient(t *testing.T) {
 func TestKeyDB(t *testing.T) {
 	// 127.0.0.1:6378 enable flash, 127.0.0.1:6377 disable flash
 	for _, addr := range []string{"127.0.0.1:6378/10", "127.0.0.1:6377/10"} {
-		m, err := newRedisMeta("redis", addr, &Config{})
+		m, err := newRedisMeta("redis", addr, &Config{MaxDeletes: 2})
 		if err != nil || m.Name() != "redis" {
 			t.Fatalf("create meta: %s", err)
 		}
@@ -88,7 +88,7 @@ func TestKeyDB(t *testing.T) {
 }
 
 func TestRedisCluster(t *testing.T) {
-	var conf = Config{}
+	var conf = Config{MaxDeletes: 2}
 	m, err := newRedisMeta("redis", "127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003/2", &conf)
 	if err != nil {
 		t.Fatalf("create meta: %s", err)
@@ -1032,10 +1032,10 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 		t.Fatalf("list all slices: %s", st)
 	}
 
-	l.Lock()
-	deletes := len(deleted)
-	l.Unlock()
 	if trash {
+		l.Lock()
+		deletes := len(deleted)
+		l.Unlock()
 		if deletes > 10 {
 			t.Fatalf("deleted slices %d is greater than 10", deletes)
 		}
@@ -1043,10 +1043,11 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 			t.Fatalf("list delayed slices %d is less than 200", len(sliceMap[1]))
 		}
 		m.(engine).doCleanupDelayedSlices(time.Now().Unix() + 1)
-		l.Lock()
-		deletes = len(deleted)
-		l.Unlock()
 	}
+	time.Sleep(time.Second * 3) // wait for all slices deleted
+	l.Lock()
+	deletes := len(deleted)
+	l.Unlock()
 	if deletes < 200 {
 		t.Fatalf("deleted slices %d is less than 200", deletes)
 	}
diff --git a/pkg/meta/benchmarks_test.go b/pkg/meta/benchmarks_test.go
index 66f1f699..19d9ed96 100644
--- a/pkg/meta/benchmarks_test.go
+++ b/pkg/meta/benchmarks_test.go
@@ -631,16 +631,16 @@ func benchmarkAll(b *testing.B, m Meta) {
 }
 
 func BenchmarkRedis(b *testing.B) {
-	m := NewClient(redisAddr, &Config{})
+	m := NewClient(redisAddr, &Config{MaxDeletes: 2})
 	benchmarkAll(b, m)
 }
 
 func BenchmarkSQL(b *testing.B) {
-	m := NewClient(sqlAddr, &Config{})
+	m := NewClient(sqlAddr, &Config{MaxDeletes: 2})
 	benchmarkAll(b, m)
 }
 
 func BenchmarkTKV(b *testing.B) {
-	m := NewClient(tkvAddr, &Config{})
+	m := NewClient(tkvAddr, &Config{MaxDeletes: 2})
 	benchmarkAll(b, m)
 }
diff --git a/pkg/meta/load_dump_test.go b/pkg/meta/load_dump_test.go
index 55978949..80a7a57f 100644
--- a/pkg/meta/load_dump_test.go
+++ b/pkg/meta/load_dump_test.go
@@ -18,7 +18,7 @@ package meta
 
 import (
 	"bytes"
-	"io/ioutil"
+	"io"
 	"os"
 	"os/exec"
 	"path"
@@ -71,7 +71,7 @@ func TestEscape(t *testing.T) {
 
 func Utf8ToGbk(s []byte) ([]byte, error) {
 	reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewEncoder())
-	d, e := ioutil.ReadAll(reader)
+	d, e := io.ReadAll(reader)
 	if e != nil {
 		return nil, e
 	}
@@ -80,7 +80,7 @@ func Utf8ToGbk(s []byte) ([]byte, error) {
 
 func GbkToUtf8(s []byte) ([]byte, error) {
 	reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder())
-	d, e := ioutil.ReadAll(reader)
+	d, e := io.ReadAll(reader)
 	if e != nil {
 		return nil, e
 	}
diff --git a/pkg/meta/sql_test.go b/pkg/meta/sql_test.go
index 039c3e9c..4769d7c4 100644
--- a/pkg/meta/sql_test.go
+++ b/pkg/meta/sql_test.go
@@ -24,7 +24,7 @@ import (
 )
 
 func TestSQLiteClient(t *testing.T) {
-	m, err := newSQLMeta("sqlite3", path.Join(t.TempDir(), "jfs-unit-test.db"), &Config{})
+	m, err := newSQLMeta("sqlite3", path.Join(t.TempDir(), "jfs-unit-test.db"), &Config{MaxDeletes: 2})
 	if err != nil || m.Name() != "sqlite3" {
 		t.Fatalf("create meta: %s", err)
 	}
@@ -32,7 +32,7 @@ func TestSQLiteClient(t *testing.T) {
 }
 
 func TestMySQLClient(t *testing.T) {
-	m, err := newSQLMeta("mysql", "root:@/dev", &Config{})
+	m, err := newSQLMeta("mysql", "root:@/dev", &Config{MaxDeletes: 2})
 	if err != nil || m.Name() != "mysql" {
 		t.Fatalf("create meta: %s", err)
 	}
@@ -40,7 +40,7 @@ func TestMySQLClient(t *testing.T) {
 }
 
 func TestPostgreSQLClient(t *testing.T) {
-	m, err := newSQLMeta("postgres", "localhost:5432/test?sslmode=disable", &Config{})
+	m, err := newSQLMeta("postgres", "localhost:5432/test?sslmode=disable", &Config{MaxDeletes: 2})
 	if err != nil || m.Name() != "postgres" {
 		t.Fatalf("create meta: %s", err)
 	}
@@ -48,7 +48,7 @@ func TestPostgreSQLClient(t *testing.T) {
 }
 
 func TestPostgreSQLClientWithSearchPath(t *testing.T) {
-	_, err := newSQLMeta("postgres", "localhost:5432/test?sslmode=disable&search_path=juicefs,public", &Config{})
+	_, err := newSQLMeta("postgres", "localhost:5432/test?sslmode=disable&search_path=juicefs,public", &Config{MaxDeletes: 2})
 	if !strings.Contains(err.Error(), "currently, only one schema is supported in search_path") {
 		t.Fatalf("TestPostgreSQLClientWithSearchPath error: %s", err)
 	}
diff --git a/pkg/meta/tkv_fdb_test.go b/pkg/meta/tkv_fdb_test.go
index dbd32fc5..1dab810f 100644
--- a/pkg/meta/tkv_fdb_test.go
+++ b/pkg/meta/tkv_fdb_test.go
@@ -25,7 +25,7 @@ import (
 )
 
 func TestFdbClient(t *testing.T) {
-	m, err := newKVMeta("fdb", "/etc/foundationdb/fdb.cluster?prefix=test2", &Config{})
+	m, err := newKVMeta("fdb", "/etc/foundationdb/fdb.cluster?prefix=test2", &Config{MaxDeletes: 2})
 	if err != nil {
 		t.Fatalf("create meta: %s", err)
 	}
diff --git a/pkg/meta/tkv_test.go b/pkg/meta/tkv_test.go
index ebf09a0f..611db0cd 100644
--- a/pkg/meta/tkv_test.go
+++ b/pkg/meta/tkv_test.go
@@ -28,7 +28,7 @@ import (
 
 func TestMemKVClient(t *testing.T) {
 	_ = os.Remove(settingPath)
-	m, err := newKVMeta("memkv", "jfs-unit-test", &Config{})
+	m, err := newKVMeta("memkv", "jfs-unit-test", &Config{MaxDeletes: 2})
 	if err != nil || m.Name() != "memkv" {
 		t.Fatalf("create meta: %s", err)
 	}
@@ -36,7 +36,7 @@ func TestMemKVClient(t *testing.T) {
 }
 
 func TestTiKVClient(t *testing.T) {
-	m, err := newKVMeta("tikv", "127.0.0.1:2379/jfs-unit-test", &Config{})
+	m, err := newKVMeta("tikv", "127.0.0.1:2379/jfs-unit-test", &Config{MaxDeletes: 2})
 	if err != nil || m.Name() != "tikv" {
 		t.Fatalf("create meta: %s", err)
 	}
@@ -44,7 +44,7 @@ func TestTiKVClient(t *testing.T) {
 }
 
 func TestBadgerClient(t *testing.T) {
-	m, err := newKVMeta("badger", "badger", &Config{})
+	m, err := newKVMeta("badger", "badger", &Config{MaxDeletes: 2})
 	if err != nil || m.Name() != "badger" {
 		t.Fatalf("create meta: %s", err)
 	}
@@ -52,7 +52,7 @@ func TestBadgerClient(t *testing.T) {
 }
 
 func TestEtcdClient(t *testing.T) {
-	m, err := newKVMeta("etcd", "localhost:2379", &Config{})
+	m, err := newKVMeta("etcd", "localhost:2379", &Config{MaxDeletes: 2})
 	if err != nil {
 		t.Fatalf("create meta: %s", err)
 	}
diff --git a/pkg/object/encrypt_test.go b/pkg/object/encrypt_test.go
index da58f71e..18e35b31 100644
--- a/pkg/object/encrypt_test.go
+++ b/pkg/object/encrypt_test.go
@@ -22,7 +22,8 @@ import (
 	"crypto/rsa"
 	"crypto/x509"
 	"encoding/pem"
-	"io/ioutil"
+	"io"
+	"os"
 	"path/filepath"
 	"testing"
 )
@@ -153,7 +154,7 @@ func genrsa(path string, password string) error {
 			return err
 		}
 	}
-	if err := ioutil.WriteFile(path, pem.EncodeToMemory(block), 0755); err != nil {
+	if err := os.WriteFile(path, pem.EncodeToMemory(block), 0755); err != nil {
 		return err
 	}
 	return nil
@@ -213,13 +214,13 @@ func TestEncryptedStore(t *testing.T) {
 		t.Errorf("Get a: %s", err)
 		t.Fail()
 	}
-	d, _ := ioutil.ReadAll(r)
+	d, _ := io.ReadAll(r)
 	if string(d) != "el" {
 		t.Fail()
 	}
 
 	r, _ = es.Get("a", 0, -1)
-	d, _ = ioutil.ReadAll(r)
+	d, _ = io.ReadAll(r)
 	if string(d) != "hello" {
 		t.Fail()
 	}
diff --git a/pkg/object/object_storage_test.go b/pkg/object/object_storage_test.go
index de47b163..60ab17e9 100644
--- a/pkg/object/object_storage_test.go
+++ b/pkg/object/object_storage_test.go
@@ -24,7 +24,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"math"
 	"os"
 	"reflect"
@@ -34,7 +34,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/go-redis/redis/v8"
+	"github.com/redis/go-redis/v9"
 )
 
 func get(s ObjectStorage, k string, off, limit int64) (string, error) {
@@ -42,7 +42,7 @@ func get(s ObjectStorage, k string, off, limit int64) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	data, err := ioutil.ReadAll(r)
+	data, err := io.ReadAll(r)
 	if err != nil {
 		return "", err
 	}
@@ -246,7 +246,7 @@ func testStorage(t *testing.T, s ObjectStorage) {
 		}
 	}
 
-	f, _ := ioutil.TempFile("", "test")
+	f, _ := os.CreateTemp("", "test")
 	f.Write([]byte("this is a file"))
 	f.Seek(0, 0)
 	os.Remove(f.Name())
@@ -307,7 +307,7 @@ func testStorage(t *testing.T, s ObjectStorage) {
 		}
 		if in, err := s.Get(k, 0, -1); err != nil {
 			t.Fatalf("large not exists")
-		} else if d, err := ioutil.ReadAll(in); err != nil {
+		} else if d, err := io.ReadAll(in); err != nil {
 			t.Fatalf("fail to read large file")
 		} else if len(d) != partSize+part2Size {
 			t.Fatalf("size of large file: %d != %d", len(d), partSize+part2Size)
diff --git a/pkg/sync/sync_test.go b/pkg/sync/sync_test.go
index e64d301d..fe230a32 100644
--- a/pkg/sync/sync_test.go
+++ b/pkg/sync/sync_test.go
@@ -18,7 +18,7 @@ package sync
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"math"
 	"os"
 	"reflect"
@@ -341,7 +341,7 @@ func TestSyncLink(t *testing.T) {
 	if err != nil {
 		t.Fatalf("get content failed: %s", err)
 	}
-	if c, err := ioutil.ReadAll(content); err != nil || string(c) != "test" {
+	if c, err := io.ReadAll(content); err != nil || string(c) != "test" {
 		t.Fatalf("read content failed: err %s content %s", err, string(c))
 	}
 
@@ -353,7 +353,7 @@ func TestSyncLink(t *testing.T) {
 	if err != nil {
 		t.Fatalf("content failed: %s", err)
 	}
-	if c, err := ioutil.ReadAll(content); err != nil || string(c) != "test" {
+	if c, err := io.ReadAll(content); err != nil || string(c) != "test" {
 		t.Fatalf("read content failed: err %s content %s", err, string(c))
 	}
 
@@ -391,7 +391,7 @@ func TestSyncLinkWithOutFollow(t *testing.T) {
 	if err != nil {
 		t.Fatalf("get content error: %s", err)
 	}
-	if c, err := ioutil.ReadAll(content); err != nil || string(c) != "test" {
+	if c, err := io.ReadAll(content); err != nil || string(c) != "test" {
 		t.Fatalf("read content error: %s", err)
 	}
 
diff --git a/pkg/usage/usage_test.go b/pkg/usage/usage_test.go
index 254fa813..19a9dcb0 100644
--- a/pkg/usage/usage_test.go
+++ b/pkg/usage/usage_test.go
@@ -19,7 +19,7 @@ package usage
 import (
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net"
 	"net/http"
 	"testing"
@@ -47,7 +47,7 @@ func TestUsageReport(t *testing.T) {
 	var u usage
 	done := make(chan bool)
 	mux.HandleFunc("/report-usage", func(rw http.ResponseWriter, r *http.Request) {
-		d, _ := ioutil.ReadAll(r.Body)
+		d, _ := io.ReadAll(r.Body)
 		_ = json.Unmarshal(d, &u)
 		_, _ = rw.Write([]byte("OK"))
 		done <- true
diff --git a/pkg/utils/logger_test.go b/pkg/utils/logger_test.go
index 6217a635..d5abc736 100644
--- a/pkg/utils/logger_test.go
+++ b/pkg/utils/logger_test.go
@@ -17,7 +17,6 @@
 package utils
 
 import (
-	"io/ioutil"
 	"os"
 	"strings"
 	"testing"
@@ -48,7 +47,7 @@ func TestLogger(t *testing.T) {
 	logger.Warnf("warn level")
 	logger.Error("error level")
 
-	d, _ := ioutil.ReadFile(f.Name())
+	d, _ := os.ReadFile(f.Name())
 	s := string(d)
 	if strings.Contains(s, "info level") || strings.Contains(s, "debug level") {
 		t.Fatalf("info/debug should not be logged: %s", s)
diff --git a/pkg/vfs/compact_test.go b/pkg/vfs/compact_test.go
index ca5e96f6..93a63c5d 100644
--- a/pkg/vfs/compact_test.go
+++ b/pkg/vfs/compact_test.go
@@ -30,7 +30,6 @@ func TestCompact(t *testing.T) {
 		BlockSize:  256 * 1024,
 		Compress:   "lz4",
 		MaxUpload:  2,
-		MaxDeletes: 1,
 		BufferSize: 30 << 20,
 		CacheSize:  10,
 		CacheDir:   "memory",
diff --git a/pkg/vfs/vfs_test.go b/pkg/vfs/vfs_test.go
index ecb00d08..6ba18574 100644
--- a/pkg/vfs/vfs_test.go
+++ b/pkg/vfs/vfs_test.go
@@ -42,6 +42,7 @@ func createTestVFS() (*VFS, object.ObjectStorage) {
 	metaConf := &meta.Config{
 		Retries:    10,
 		Strict:     true,
+		MaxDeletes: 2,
 		MountPoint: mp,
 	}
 	m := meta.NewClient("memkv://", metaConf)
@@ -64,7 +65,6 @@ func createTestVFS() (*VFS, object.ObjectStorage) {
 			BlockSize:  format.BlockSize * 1024,
 			Compress:   format.Compression,
 			MaxUpload:  2,
-			MaxDeletes: 1,
 			BufferSize: 30 << 20,
 			CacheSize:  10,
 			CacheDir:   "memory",