Repository: juicedata/juicefs
Pull request #2393 (2022-07-24 10:50:22)

proofread: alluxio
close: https://github.com/juicedata/docs/issues/100

Hi @AIXjing , could you please review this PR when you have time? Thank you!


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c958590f..775b73c0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,11 +9,11 @@
 
 ## Coding Style
 
-- We're following ["Effective Go"](https://golang.org/doc/effective_go.html) and ["Go Code Review Comments"](https://github.com/golang/go/wiki/CodeReviewComments).
+- We're following ["Effective Go"](https://go.dev/doc/effective_go) and ["Go Code Review Comments"](https://github.com/golang/go/wiki/CodeReviewComments).
 - Use `go fmt` to format your code before committing. You can find information in editor support for Go tools in ["IDEs and Plugins for Go"](https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins).
 - If you see any code which clearly violates the style guide, please fix it and send a pull request.
 - Every new source file must begin with a license header.
-- Install [pre-commit](https://pre-commit.com/) and use it to set up a pre-commit hook for static analysis. Just run `pre-commit install` in the root of the repo.
+- Install [pre-commit](https://pre-commit.com) and use it to set up a pre-commit hook for static analysis. Just run `pre-commit install` in the root of the repo.
 
 ## Sign the CLA
 
@@ -30,11 +30,11 @@ Before you can contribute to JuiceFS, you will need to sign the [Contributor Lic
 
 This is a rough outline of what a contributor's workflow looks like:
 
-- Create a topic branch from where to base the contribution. This is usually `main`.
-- Make commits of logical units.
-- Make sure commit messages are in the proper format.
-- Push changes in a topic branch to a personal fork of the repository.
-- Submit a pull request to [juicedata/juicefs](https://github.com/juicedata/juicefs/compare). The PR should link to one issue which either created by you or others.
-- The PR must receive approval from at least one maintainer before it be merged.
+1. Create a topic branch from where to base the contribution. This is usually `main`.
+1. Make commits of logical units.
+1. Make sure commit messages are in the proper format.
+1. Push changes in a topic branch to a personal fork of the repository.
+1. Submit a pull request to [juicedata/juicefs](https://github.com/juicedata/juicefs/compare). The PR should link to one issue which either created by you or others.
+1. The PR must receive approval from at least one maintainer before it be merged.
 
 Happy hacking!
diff --git a/cmd/fsck.go b/cmd/fsck.go
index 7dcfd4e6..63c8bd8e 100644
--- a/cmd/fsck.go
+++ b/cmd/fsck.go
@@ -127,13 +127,13 @@ func fsck(ctx *cli.Context) error {
 				if i == n {
 					sz = int(s.Size) - int(i)*chunkConf.BlockSize
 				}
-				key := fmt.Sprintf("%d_%d_%d", s.Chunkid, i, sz)
+				key := fmt.Sprintf("%d_%d_%d", s.Id, i, sz)
 				if _, ok := blocks[key]; !ok {
 					var objKey string
 					if format.HashPrefix {
-						objKey = fmt.Sprintf("%02X/%v/%s", s.Chunkid%256, s.Chunkid/1000/1000, key)
+						objKey = fmt.Sprintf("%02X/%v/%s", s.Id%256, s.Id/1000/1000, key)
 					} else {
-						objKey = fmt.Sprintf("%v/%v/%s", s.Chunkid/1000/1000, s.Chunkid/1000, key)
+						objKey = fmt.Sprintf("%v/%v/%s", s.Id/1000/1000, s.Id/1000, key)
 					}
 					if _, err := blob.Head(objKey); err != nil {
 						if _, ok := brokens[inode]; !ok {
diff --git a/cmd/gc.go b/cmd/gc.go
index 39624641..e06424cf 100644
--- a/cmd/gc.go
+++ b/cmd/gc.go
@@ -73,9 +73,9 @@ $ juicefs gc redis://localhost --delete`,
 	}
 }
 
-type dChunk struct {
-	chunkid uint64
-	length  uint32
+type dSlice struct {
+	id     uint64
+	length uint32
 }
 
 func gc(ctx *cli.Context) error {
@@ -113,7 +113,7 @@ func gc(ctx *cli.Context) error {
 	if ctx.Bool("compact") {
 		bar := progress.AddCountBar("Scanned chunks", 0)
 		spin := progress.AddDoubleSpinner("Compacted slices")
-		m.OnMsg(meta.DeleteChunk, func(args ...interface{}) error {
+		m.OnMsg(meta.DeleteSlice, func(args ...interface{}) error {
 			return store.Remove(args[0].(uint64), int(args[1].(uint32)))
 		})
 		m.OnMsg(meta.CompactChunk, func(args ...interface{}) error {
@@ -143,30 +143,30 @@ func gc(ctx *cli.Context) error {
 	// put it above delete count spinner
 	sliceCSpin := progress.AddCountSpinner("Listed slices")
 
-	// Delete pending chunks while listing slices
+	// Delete pending slices while listing all slices
 	delete := ctx.Bool("delete")
 	threads := ctx.Int("threads")
 	if delete && threads <= 0 {
 		logger.Fatal("threads should be greater than 0 to delete objects")
 	}
 	var delSpin *utils.Bar
-	var chunkChan chan *dChunk // pending delete chunks
+	var sliceChan chan *dSlice // pending delete slices
 	var wg sync.WaitGroup
 	if delete {
 		delSpin = progress.AddCountSpinner("Deleted pending")
-		chunkChan = make(chan *dChunk, 10240)
-		m.OnMsg(meta.DeleteChunk, func(args ...interface{}) error {
+		sliceChan = make(chan *dSlice, 10240)
+		m.OnMsg(meta.DeleteSlice, func(args ...interface{}) error {
 			delSpin.Increment()
-			chunkChan <- &dChunk{args[0].(uint64), args[1].(uint32)}
+			sliceChan <- &dSlice{args[0].(uint64), args[1].(uint32)}
 			return nil
 		})
 		for i := 0; i < threads; i++ {
 			wg.Add(1)
 			go func() {
 				defer wg.Done()
-				for c := range chunkChan {
-					if err := store.Remove(c.chunkid, int(c.length)); err != nil {
-						logger.Warnf("remove %d_%d: %s", c.chunkid, c.length, err)
+				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)
 					}
 				}
 			}()
@@ -181,11 +181,11 @@ func gc(ctx *cli.Context) error {
 		logger.Fatalf("list all slices: %s", r)
 	}
 	if delete {
-		close(chunkChan)
+		close(sliceChan)
 		wg.Wait()
 		delSpin.Done()
 		if progress.Quiet {
-			logger.Infof("Deleted %d pending chunks", delSpin.Current())
+			logger.Infof("Deleted %d pending slices", delSpin.Current())
 		}
 	}
 	sliceCSpin.Done()
@@ -201,7 +201,7 @@ func gc(ctx *cli.Context) error {
 	var totalBytes uint64
 	for _, ss := range slices {
 		for _, s := range ss {
-			keys[s.Chunkid] = s.Size
+			keys[s.Id] = s.Size
 			total += int64(int(s.Size-1)/chunkConf.BlockSize) + 1 // s.Size should be > 0
 			totalBytes += uint64(s.Size)
 		}
diff --git a/cmd/info.go b/cmd/info.go
index 6bca9a9d..ce9c1501 100644
--- a/cmd/info.go
+++ b/cmd/info.go
@@ -158,7 +158,7 @@ func printChunks(resp string, raw bool) {
 	result[0] = []string{"chunkIndex", "objectName", "size", "offset", "length"}
 	leftAlign := 1
 	if raw {
-		result[0][1] = "sliceID"
+		result[0][1] = "sliceId"
 		leftAlign = -1
 	}
 	for i := 1; i < len(result); i++ {
diff --git a/cmd/mount.go b/cmd/mount.go
index a3c44b85..967c2e4b 100644
--- a/cmd/mount.go
+++ b/cmd/mount.go
@@ -249,7 +249,7 @@ func getVfsConf(c *cli.Context, metaConf *meta.Config, format *meta.Format, chun
 }
 
 func registerMetaMsg(m meta.Meta, store chunk.ChunkStore, chunkConf *chunk.Config) {
-	m.OnMsg(meta.DeleteChunk, func(args ...interface{}) error {
+	m.OnMsg(meta.DeleteSlice, func(args ...interface{}) error {
 		return store.Remove(args[0].(uint64), int(args[1].(uint32)))
 	})
 	m.OnMsg(meta.CompactChunk, func(args ...interface{}) error {
diff --git a/cmd/objbench.go b/cmd/objbench.go
index 5fd3caf2..90a3ac7f 100644
--- a/cmd/objbench.go
+++ b/cmd/objbench.go
@@ -25,6 +25,7 @@ import (
 	"math"
 	"math/rand"
 	"os"
+	"sort"
 	"strconv"
 	"strings"
 	"sync"
@@ -819,6 +820,31 @@ func functionalTesting(blob object.ObjectStorage, result *[][]string, tty bool)
 				return fmt.Errorf("list should not return anything, but got %d", len(objs))
 			}
 		}
+		keyTotal := 100
+		var sortedKeys []string
+		for i := 0; i < keyTotal; i++ {
+			k := fmt.Sprintf("hashKey%d", i)
+			sortedKeys = append(sortedKeys, k)
+			if err := blob.Put(fmt.Sprintf("hashKey%d", i), bytes.NewReader(br)); err != nil {
+				return fmt.Errorf("put object failed: %s", err.Error())
+			}
+		}
+		sort.Strings(sortedKeys)
+		defer func() {
+			for i := 0; i < keyTotal; i++ {
+				_ = blob.Delete(fmt.Sprintf("hashKey%d", i))
+			}
+		}()
+
+		if objs, err := listAll(blob, "hashKey", "", int64(keyTotal)); err != nil {
+			return fmt.Errorf("list failed: %s", err)
+		} else {
+			for i := 0; i < keyTotal; i++ {
+				if objs[i].Key() != sortedKeys[i] {
+					return fmt.Errorf("the result for list is incorrect")
+				}
+			}
+		}
 		return nil
 	})
 
diff --git a/cmd/profile.go b/cmd/profile.go
index 7d759180..51e9d08e 100644
--- a/cmd/profile.go
+++ b/cmd/profile.go
@@ -401,9 +401,7 @@ func profile(ctx *cli.Context) error {
 	go prof.flusher()
 	var input string
 	for {
-		if _, err = fmt.Scanln(&input); err != nil {
-			logger.Fatalf("Failed to scan input: %s", err)
-		}
+		_, _ = fmt.Scanln(&input)
 		if prof.tty {
 			fmt.Print("\033[1A\033[K") // move cursor back
 		}
diff --git a/docs/en/administration/fault_diagnosis_and_analysis.md b/docs/en/administration/fault_diagnosis_and_analysis.md
index 7b9660dd..54f97094 100644
--- a/docs/en/administration/fault_diagnosis_and_analysis.md
+++ b/docs/en/administration/fault_diagnosis_and_analysis.md
@@ -4,6 +4,9 @@ sidebar_position: 5
 slug: /fault_diagnosis_and_analysis
 ---
 
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
 # Fault Diagnosis and Analysis
 
 ## Client log
@@ -14,29 +17,45 @@ Different JuiceFS clients obtain logs in different ways, which are described bel
 
 ### Mount point
 
-When the JuiceFS file system is mounted with the [`-d` option](../reference/command_reference.md#juicefs-mount) (indicating running in the background), the log will be output to the syslog and `/var/log/juicefs.log` (requires v0.15 and above client, see [`--log` option](../reference/command_reference.md#juicefs-mount)). Depending on the operating system you are using, you can get the logs with different commands:
+When the JuiceFS file system is mounted with the [`-d` option](../reference/command_reference.md#juicefs-mount) (indicating running in the background), the log will be output to the system log and local log file at the same time, depending on the running user when the file system is mounted, the path of the local log file is slightly different. The log file path corresponding to root user is `/var/log/juicefs.log`, and the log file path of non-root user is `$HOME/.juicefs/juicefs.log`. For details, see [`--log` option](../reference/command_reference.md#juicefs-mount).
+
+Depending on the operating system you are using, you can get the logs with different commands:
+
+<Tabs>
+  <TabItem value="local-log-file" label="Local log file">
+
+```bash
+tail -n 100 /var/log/juicefs.log
+```
+
+  </TabItem>
+  <TabItem value="macos-syslog" label="macOS system log">
 
 ```bash
-# macOS
 syslog | grep 'juicefs'
+```
 
-# Debian based system
+  </TabItem>
+  <TabItem value="debian-syslog" label="Debian system log">
+
+```bash
 cat /var/log/syslog | grep 'juicefs'
+```
 
-# CentOS based system
-cat /var/log/messages | grep 'juicefs'
+  </TabItem>
+  <TabItem value="centos-syslog" label="CentOS system log">
 
-# All system (require v0.15+ JuiceFS)
-tail -n 100 /var/log/juicefs.log
+```bash
+cat /var/log/messages | grep 'juicefs'
 ```
 
+  </TabItem>
+</Tabs>
+
 You can use the `grep` command to filter different levels of logs for performance analysis or troubleshooting:
 
-```
-cat /var/log/syslog | grep 'juicefs' | grep '<INFO>'
-cat /var/log/syslog | grep 'juicefs' | grep '<WARNING>'
+```shell
 cat /var/log/syslog | grep 'juicefs' | grep '<ERROR>'
-cat /var/log/syslog | grep 'juicefs' | grep '<FATAL>'
 ```
 
 ### Kubernetes CSI Driver
diff --git a/docs/en/administration/metadata/postgresql_best_practices.md b/docs/en/administration/metadata/postgresql_best_practices.md
index a2d1cb31..238515a0 100644
--- a/docs/en/administration/metadata/postgresql_best_practices.md
+++ b/docs/en/administration/metadata/postgresql_best_practices.md
@@ -4,23 +4,23 @@ sidebar_position: 2
 ---
 # PostgreSQL Best Practices
 
-For distributed file systems where data and metadata are stored separately, the read and write performance of metadata directly affects the efficiency of the whole system, and the security of metadata is also directly related to the data security of the whole system.
+For distributed file systems where data and metadata are stored separately, the read and write performance and security of metadata directly affects the efficiency and data security of the whole system, respectively.
 
-In the production environment, it is recommended that you give priority to the hosted cloud database provided by the cloud computing platform with appropriate high availability architecture.
+In the production environment, it is recommended to select hosted cloud databases provided by cloud computing platforms first, and comebine it with appropriate high availability architecture to use.
 
-Whether you build it yourself or use a cloud database, you should always pay attention to the integrity and security of metadata when using JuiceFS.
+Please always pay attention to the integrity and security of metadata when using JuiceFS no matter whether databases is build on your own or in the cloud.
 
 ## Communication Security
 
 By default, JuiceFS clients will use SSL encryption to connect to PostgreSQL. If SSL encryption is not enabled on the database, you need to append the `sslmode=disable` parameter to the metadata URL.
 
-It is recommended to configure and always enable SSL encryption on the database server side.
+It is recommended to configure and keep SSL encryption enabled on the database server side all the time.
 
 ## Passing sensitive information via environment variables
 
-Although it is easy and convenient to set the database password directly in the metadata URL, JuiceFS hides it from logging and process list, but it's still possible to leak in other ways. For data security, it's better to pass the database password through an environment variable.
+Database password can be set directly through the metadata URL. Although it is easy and convenient, the password may leak during logging and process outputing processes. For the sake of security, it's better to pass the database password through an environment variable.
 
-`META_PASSWORD` is a predefined environment variable for it:
+`META_PASSWORD` is a predefined environment variable for the database password:
 
 ```shell
 export META_PASSWORD=mypassword
@@ -29,26 +29,26 @@ juicefs mount -d "postgres://user@192.168.1.6:5432/juicefs" /mnt/jfs
 
 ## Authentication methods
 
-Authentication with PostgreSQL is currently possible with "md5" method. The following section can be adapted in the pg_hba.conf of your PostgreSQL instance.
+PostgreSQL supports the md5 authentication method. The following section can be adapted in the pg_hba.conf of your PostgreSQL instance.
 
 ```
 # TYPE  DATABASE        USER            ADDRESS                 METHOD
 host    juicefs         juicefsuser     192.168.1.0/24          md5
 ```
 
-## Backup periodically
+## Periodic backups
 
-Please refer to the official manual [Chapter 26. Backup and Restore](https://www.postgresql.org/docs/current/backup.html) to learn how to backup and restore the database.
+Please refer to the official manual [Chapter 26. Backup and Restore](https://www.postgresql.org/docs/current/backup.html) to learn how to back up and restore databases.
 
-It is recommended to make a database backup plan and follow it periodically, and at the same time, try to restore the data in an experimental environment to confirm that the backup is valid.
+It is recommended to make a plan for regularly backing up your database, and at the same time, do some tests to restore the data in an experimental environment to confirm that the backup is valid.
 
-## Using connection pooling
+## Using connection pooler
 
-Connection pooling is an intermediate layer between the client and the database, which acts as an intermediary to improve connection efficiency and reduce the loss of short connections. Commonly used connection pools are [PgBouncer](https://www.pgbouncer.org/) and [Pgpool-II](https://www.pgpool.net/).
+Connection pooler is a middleware that works between client and database and reuses the earlier connection from the pool, which improve connection efficiency and reduce the loss of short connections. Commonly used connection poolers are [PgBouncer](https://www.pgbouncer.org/) and [Pgpool-II](https://www.pgpool.net/).
 
 ## High Availability
 
-The official PostgreSQL document [High Availability, Load Balancing, and Replication](https://www.postgresql.org/docs/current/different-replication-solutions.html) compares several common database high availability solutions, please choose the appropriate according to your needs.
+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.
diff --git a/docs/en/administration/metadata/redis_best_practices.md b/docs/en/administration/metadata/redis_best_practices.md
index 28d02ff1..39ca9717 100644
--- a/docs/en/administration/metadata/redis_best_practices.md
+++ b/docs/en/administration/metadata/redis_best_practices.md
@@ -11,8 +11,6 @@ This is a guide about Redis best practices. Redis is a critical component in Jui
 It's highly recommended to use Redis service managed by public cloud provider if possible. See ["Recommended Managed Redis Service"](#recommended-managed-redis-service) for more information.
 :::
 
-### Sentinel mode
-
 If you still want to operate Redis by yourself in production environment, please keep in mind that JuiceFS requires Redis version 4.0+. Moreover, it's recommended to pick an [official stable version](https://redis.io/download), and read the following contents before deploying Redis.
 
 :::note
@@ -40,11 +38,9 @@ used_memory_dataset_perc: 70.12%
 
 Among them, `used_memory_rss` is the total memory size actually used by Redis, which includes not only the size of data stored in Redis (that is, `used_memory_dataset` above), but also some Redis [system overhead](https://redis.io/commands/memory-stats) (that is, `used_memory_overhead` above). As mentioned earlier, the metadata of each file occupies about 300 bytes and is calculated by `used_memory_dataset`. If you find that the metadata of a single file in your JuiceFS file system occupies much more than 300 bytes, you can try to run [`juicefs gc`](../../reference/command_reference.md#juicefs-gc) command to clean up possible redundant data.
 
-## High Availability
+## High availability
 
-:::caution
-JuiceFS uses "[Redis transactions](https://redis.io/docs/manual/transactions)" to guarantee the atomicity of metadata operations. However, since Redis Cluster does not support transaction, Redis Cluster cannot be used as a JuiceFS metadata store. For high availability of Redis, please use Redis Sentinel.
-:::
+### Sentinel mode
 
 [Redis Sentinel](https://redis.io/docs/manual/sentinel) is the official high availability solution for Redis. It provides following capabilities:
 
@@ -81,19 +77,23 @@ Since JuiceFS v1.0.0, it is supported to connect only Redis replica nodes when m
 It should be noted that since the data of the Redis master node is asynchronously replicated to the replica nodes, it is possible that the metadata read may not be the latest.
 :::
 
-
 ### Cluster mode
-Juicefs also supports redis of cluster mode as meta engine, the `META-URL` format is `redis[s]://[[USER]:PASSWORD@]ADDR:PORT,[ADDR:PORT],[ADDR:PORT][/DB]`. For example:
+
+:::note
+This feature requires JuiceFS v1.0.0 or higher
+:::
+
+Juicefs also supports Redis Cluster as metadata engine, the `META-URL` format is `redis[s]://[[USER]:PASSWORD@]ADDR:PORT,[ADDR:PORT],[ADDR:PORT][/PREFIX]`. For example:
 
 ```shell
-juicefs format redis://127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002/1 ~/jfs
+juicefs format redis://127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002/jfs1 myjfs
 ```
 
-:::tip 
-Although Redis cluster does not support db isolation mechanism, when it is a metadata engine, Juicefs uses the function of redis cluster [Hash Tag](https://redis.io/docs/reference/cluster-spec/#hash-tags) to implement isolation and transaction assurance in cluster mode by using `{db}` as the prefix of key, so db in cluster mode still has an isolation effect.
+:::tip
+Redis Cluster does not support multiple databases like the standalone mode, instead it splits the key space into 16384 hash slots, and distributes the slots to several Redis master nodes. Based on Redis Cluster's [Hash Tag](https://redis.io/docs/reference/cluster-spec/#hash-tags) feature, JuiceFS adds `{prefix}` before all file system keys to ensure they will be hashed to the same hash slot, assuring that transactions can still work. Besides, one Redis Cluster can serve for multiple JuiceFS file systems as long as they use different prefixes.
 :::
 
-## Data Durability
+## Data durability
 
 Redis provides a different range of [persistence](https://redis.io/docs/manual/persistence) options:
 
@@ -115,7 +115,7 @@ Redis is very data backup friendly since you can copy RDB files while the databa
 
 Please read the [official documentation](https://redis.io/docs/manual/persistence) for more information.
 
-## Backing up Redis Data
+## Backing up Redis data
 
 **Make Sure to Backup Your Database.** Disks break, instances in the cloud disappear, and so forth.
 
@@ -131,7 +131,7 @@ This means that copying the RDB file is completely safe while the server is runn
 
 Please read the [official documentation](https://redis.io/docs/manual/persistence) for more information.
 
-## Restore Redis Data
+## Restore Redis data
 
 After generating the AOF or RDB backup file, you can restore the data by copying the backup file to the path corresponding to the `dir` configuration of the new Redis instance, which you can get by using the [`CONFIG GET dir`](https://redis.io/commands/config-get) command.
 
@@ -143,14 +143,14 @@ After recovering Redis data, you can continue to use the JuiceFS file system wit
 
 ## Recommended Managed Redis Service
 
+### Amazon MemoryDB for Redis
+
+[Amazon MemoryDB for Redis](https://aws.amazon.com/memorydb) is a durable, in-memory database service that delivers ultra-fast performance. MemoryDB is compatible with Redis, with MemoryDB, all of your data is stored in memory, which enables you to achieve microsecond read and single-digit millisecond write latency and high throughput. MemoryDB also stores data durably across multiple Availability Zones (AZs) using a Multi-AZ transactional log to enable fast failover, database recovery, and node restarts.
+
 ### Amazon ElastiCache for Redis
 
 [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis) is a fully managed, Redis-compatible in-memory data store built for the cloud. It provides [automatic failover](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/AutoFailover.html), [automatic backup](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/backups-automatic.html) features to ensure availability and durability.
 
-:::info
-Amazon ElastiCache for Redis has two type: cluster mode disabled and cluster mode enabled. Because JuiceFS uses [transaction](https://redis.io/docs/manual/transactions) to guarantee the atomicity of metadata operations, so you couldn't use "cluster mode enabled" type.
-:::
-
 ### Google Cloud Memorystore for Redis
 
 [Google Cloud Memorystore for Redis](https://cloud.google.com/memorystore/docs/redis) is a fully managed Redis service for the Google Cloud. Applications running on Google Cloud can achieve extreme performance by leveraging the highly scalable, available, secure Redis service without the burden of managing complex Redis deployments.
@@ -163,14 +163,6 @@ Amazon ElastiCache for Redis has two type: cluster mode disabled and cluster mod
 
 [Alibaba Cloud ApsaraDB for Redis](https://www.alibabacloud.com/product/apsaradb-for-redis) is a database service that is compatible with native Redis protocols. It supports a hybrid of memory and hard disks for data persistence. ApsaraDB for Redis provides a highly available hot standby architecture and can scale to meet requirements for high-performance and low-latency read/write operations.
 
-:::info
-ApsaraDB for Redis supports 3 type [architectures](https://www.alibabacloud.com/help/doc-detail/86132.htm): standard, cluster and read/write splitting. Because JuiceFS uses [transaction](https://redis.io/docs/manual/transactions) to guarantee the atomicity of metadata operations, so you couldn't use cluster type architecture.
-:::
-
 ### Tencent Cloud TencentDB for Redis
 
 [Tencent Cloud TencentDB for Redis](https://intl.cloud.tencent.com/product/crs) is a caching and storage service compatible with the Redis protocol. It features a rich variety of data structure options to help you develop different types of business scenarios, and offers a complete set of database services such as primary-secondary hot backup, automatic switchover for disaster recovery, data backup, failover, instance monitoring, online scaling and data rollback.
-
-:::info
-TencentDB for Redis supports 2 type [architectures](https://intl.cloud.tencent.com/document/product/239/3205): standard and cluster. Because JuiceFS uses [transaction](https://redis.io/docs/manual/transactions) to guarantee the atomicity of metadata operations, so you couldn't use cluster type architecture.
-:::
diff --git a/docs/en/development/contributing_guide.md b/docs/en/development/contributing_guide.md
index 929dadae..1d8bbfa1 100644
--- a/docs/en/development/contributing_guide.md
+++ b/docs/en/development/contributing_guide.md
@@ -2,7 +2,44 @@
 sidebar_label: Contributing Guide
 sidebar_position: 1
 ---
-# Contributing Guide
 
-:::note
-Working in progress.
\ No newline at end of file
+# Contributing to JuiceFS
+
+## Guidelines
+
+- Before starting work on a feature or bug fix, please search GitHub or reach out to us via GitHub, Slack etc. The purpose of this step is make sure no one else is already working on it and we'll ask you to open a GitHub issue if necessary.
+- We will use the GitHub issue to discuss the feature and come to agreement. This is to prevent your time being wasted, as well as ours.
+- If it is a major feature update, we highly recommend you also write a design document to help the community understand your motivation and solution.
+- A good way to find a project properly sized for a first time contributor is to search for open issues with the label ["kind/good-first-issue"](https://github.com/juicedata/juicefs/labels/kind%2Fgood-first-issue) or ["kind/help-wanted"](https://github.com/juicedata/juicefs/labels/kind%2Fhelp-wanted).
+
+## Coding Style
+
+- We're following ["Effective Go"](https://go.dev/doc/effective_go) and ["Go Code Review Comments"](https://github.com/golang/go/wiki/CodeReviewComments).
+- Use `go fmt` to format your code before committing. You can find information in editor support for Go tools in ["IDEs and Plugins for Go"](https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins).
+- If you see any code which clearly violates the style guide, please fix it and send a pull request.
+- Every new source file must begin with a license header.
+- Install [pre-commit](https://pre-commit.com) and use it to set up a pre-commit hook for static analysis. Just run `pre-commit install` in the root of the repo.
+
+## Sign the CLA
+
+Before you can contribute to JuiceFS, you will need to sign the [Contributor License Agreement](https://cla-assistant.io/juicedata/juicefs). There're a CLA assistant to guide you when you first time submit a pull request.
+
+## What is a Good PR
+
+- Presence of unit tests
+- Adherence to the coding style
+- Adequate in-line comments
+- Explanatory commit message
+
+## Contribution Flow
+
+This is a rough outline of what a contributor's workflow looks like:
+
+1. Create a topic branch from where to base the contribution. This is usually `main`.
+1. Make commits of logical units.
+1. Make sure commit messages are in the proper format.
+1. Push changes in a topic branch to a personal fork of the repository.
+1. Submit a pull request to [juicedata/juicefs](https://github.com/juicedata/juicefs/compare). The PR should link to one issue which either created by you or others.
+1. The PR must receive approval from at least one maintainer before it be merged.
+
+Happy hacking!
diff --git a/docs/en/faq.md b/docs/en/faq.md
index fdb11898..5cbcb99d 100644
--- a/docs/en/faq.md
+++ b/docs/en/faq.md
@@ -4,11 +4,9 @@
 
 JuiceFS already supported many object storage, please check [the list](guide/how_to_setup_object_storage.md#supported-object-storage) first. If this object storage is compatible with S3, you could treat it as S3. Otherwise, try reporting issue.
 
-## Can I use Redis Cluster as metadata engine?
+## Does support Redis in Sentinel or Cluster-mode as the metadata engine for JuiceFS?
 
-Yes. Since [v1.0.0 Beta3](https://github.com/juicedata/juicefs/releases/tag/v1.0.0-beta3) JuiceFS supports the use of [Redis Cluster](https://redis.io/docs/manual/scaling) as the metadata engine, but it should be noted that Redis Cluster requires that the keys of all operations in a transaction must be in the same hash slot, so a JuiceFS file system can only use one hash slot.
-
-See ["Redis Best Practices"](administration/metadata/redis_best_practices.md) for more information.
+Yes，There is also a [best practice document](administration/metadata/redis_best_practices.md) for Redis as the JuiceFS metadata engine for reference.
 
 ## What's the difference between JuiceFS and XXX?
 
@@ -20,13 +18,39 @@ JuiceFS is a distributed file system, the latency of metedata is determined by 1
 
 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).
 
+## Why can't I see the original files that have been stored in JuiceFS in object storage?
+
+While using JuiceFS, files will eventually be split into Chunks, Slices and Blocks and stored in object storage. Therefore, you may notice that the source files stored in JuiceFS cannot be found in the file browser of the object storage platform; instead, there are only a directory of chunks and a bunch of directories and files named by numbers in the bucket. Don't panic! That's exactly what makes JuiceFS a high-performance file system. For details, please refer to ["How JuiceFS Store Files"](introduction/architecture.md#how-juicefs-stores-files).
+
 ## Does JuiceFS support random read/write?
 
 Yes, including those issued using mmap. Currently JuiceFS is optimized for sequential reading/writing, and optimized for random reading/writing is work in progress. If you want better random reading performance, it's recommended to turn off compression ([`--compress none`](reference/command_reference.md#juicefs-format)).
 
+## What is the implementation principle of JuiceFS supporting random write?
+
+JuiceFS does not store the original file in the object storage, but splits it into N data blocks according to a certain size (4MiB by default), uploads it to the object storage, and stores the ID of the data block in the metadata engine. When writing randomly, it is logical to overwrite the original content. In fact, the metadata of the **data block to be overwritten** is marked as old data, and only the **new data block** generated during random writing is uploaded to the object storage, and update the metadata corresponding to the **new data block** to the metadata engine.
+
+When reading the data of the overwritten part, according to the **latest metadata**, it can be read from the **new data block** uploaded during random writing, and the **old data block** may be deleted by the background garbage collection tasks automatically clean up. This shifts the complexity of random writes to the complexity of reads.
+
+This is just a rough introduction to the implementation logic. The specific read and write process is very complicated. You can study the two documents ["JuiceFS Internals"](development/data_structures.md) and ["Data Processing Flow"](introduction/io_processing.md) and comb them together with the code.
+
+## Why do I delete files at the mount point, but there is no change or very little change in object storage footprint?
+
+The first reason is that you may have enabled the trash feature. In order to ensure data security, the trash is enabled by default. The deleted files are actually placed in the trash and are not actually deleted, so the size of the object storage will not change. trash retention time can be specified with `juicefs format` or modified with `juicefs config`. Please refer to the ["Trash"](security/trash.md) documentation for more information.
+
+The second reason is that JuiceFS deletes the data in the object storage asynchronously, so the space change of the object storage will be slower. If you need to immediately clean up the data in the object store that needs to be deleted, you can try running the [`juicefs gc`](reference/command_reference.md#juicefs-gc) command.
+
+## Why is the size displayed at the mount point different from the object storage footprint?
+
+From the answer to this question ["What is the implementation principle of JuiceFS supporting random write?"](#what-is-the-implementation-principle-of-juicefs-supporting-random-write), it can be inferred that the occupied space of object storage is greater than or equal to the actual size in most cases, especially after a large number of overwrites in a short period of time generate many file fragments. These fragments still occupy space in object storage until merges and reclamations are triggered. But don't worry about these fragments occupying space all the time, because every time you read/write a file, it will check and trigger the defragmentation of the file when necessary. Alternatively, you can manually trigger merges and recycles with the `juicefs gc --compact --delete` command.
+
+In addition, if the JuiceFS file system has compression enabled (not enabled by default), the objects stored on the object storage may be smaller than the actual file size (depending on the compression ratio of different types of files).
+
+If the above factors have been excluded, please check the [storage class](guide/how_to_setup_object_storage.md#storage-class) of the object storage you are using. The cloud service provider may set the minimum billable size for some storage classes. For example, the [minimum billable size](https://www.alibabacloud.com/help/en/object-storage-service/latest/storage-fees) of Alibaba Cloud OSS IA storage is 64KB. If a single file is smaller than 64KB, it will be calculated as 64KB.
+
 ## When my update will be visible to other clients?
 
-All the metadata updates are immediately visible to all others. JuiceFS guarantees close-to-open consistency, see ["Consistency"](guide/cache_management.md#consistency) for more information.
+All the metadata updates are immediately visible to all others. JuiceFS guarantees close-to-open consistency, see ["Consistency"](guide/cache_management.md#data-consistency) for more information.
 
 The new data written by `write()` will be buffered in kernel or client, visible to other processes on the same machine, not visible to other machines.
 
@@ -50,6 +74,10 @@ See ["Read Cache in Client"](guide/cache_management.md#read-cache-in-client) for
 
 Use [`juicefs umount`](reference/command_reference.md#juicefs-umount) command to unmount the volume.
 
+## `Resource busy -- try 'diskutil unmount'` error when unmounting the mount point
+
+This means that a file or directory under the mount point is in use and cannot be `umount` directly. You can check (such as through the `lsof` command) whether an open terminal is located in a directory on the JuiceFS mount point, or an application is processing files in the mount point. If so, exit the terminal or application and try to unmount the file system using the `juicefs umount` command.
+
 ## How to upgrade JuiceFS client?
 
 First unmount JuiceFS volume, then re-mount the volume with newer version client.
@@ -108,3 +136,41 @@ uid=1201(alice) gid=500(staff) groups=500(staff)
 ```
 
 Read ["Sync Accounts between Multiple Hosts"](administration/sync_accounts_between_multiple_hosts.md) to resolve this problem.
+
+## What other ways JuiceFS supports access to data besides mount?
+
+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).
+- 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).
+- WebDAV Gateway: Access JuiceFS via WebDAV protocol
+
+## Where is the JuiceFS log?
+
+Different types of JuiceFS clients have different ways to obtain logs. For details, please refer to ["Client log"](administration/fault_diagnosis_and_analysis.md#client-log) document.
+
+## How to destroy a file system?
+
+Destroy a file system with `juicefs destroy` command, which will clear the metadata engine and object storage related data. For details about the use of this command, please refer to the [document](administration/destroy.md).
+
+## Does JuiceFS Gateway support advanced features such as multi-user management?
+
+The built-in `gateway` subcommand of JuiceFS does not support functions such as multi-user management, and only provides basic S3 gateway functions. If you need to use these advanced features, please refer to the [documentation](deployment/s3_gateway.md#use-a-full-featured-s3-gateway).
+
+## Does JuiceFS support using a directory in object storage as the value of the `--bucket` option?
+
+As of the release of JuiceFS 1.0, this feature is not supported.
+
+## Does JuiceFS support accessing data that already exists in object storage?
+
+As of the release of JuiceFS 1.0, this feature is not supported.
+
+## Does JuiceFS currently support distributed caching?
+
+As of the release of JuiceFS 1.0, this feature is not supported.
+
+## Is there currently an SDK available for JuiceFS?
+
+As of the release of JuiceFS 1.0, the community has two SDKs, one is the [Java SDK](deployment/hadoop_java_sdk.md) that is highly compatible with the HDFS interface officially maintained by Juicedata, and the other is the [Python SDK](https://github.com/megvii-research/juicefs-python) maintained by community users.
diff --git a/docs/en/mount_at_boot.md b/docs/en/guide/mount_at_boot.md
similarity index 59%
rename from docs/en/mount_at_boot.md
rename to docs/en/guide/mount_at_boot.md
index 4abb7052..192f9b31 100644
--- a/docs/en/mount_at_boot.md
+++ b/docs/en/guide/mount_at_boot.md
@@ -1,4 +1,10 @@
-# Mount JuiceFS at Boot
+---
+sidebar_label: Mount JuiceFS at Boot Time
+sidebar_position: 2
+slug: /mount_juicefs_at_boot_time
+---
+
+# Mount JuiceFS at Boot Time
 
 This is a guide about how to mount JuiceFS automatically at boot.
 
@@ -10,23 +16,25 @@ Copy `juicefs` as `/sbin/mount.juicefs`, then edit `/etc/fstab` with following l
 <META-URL>    <MOUNTPOINT>       juicefs     _netdev[,<MOUNT-OPTIONS>]     0  0
 ```
 
-The format of `<META-URL>` is `redis://<user>:<password>@<host>:<port>/<db>`, e.g. `redis://localhost:6379/1`. And replace `<MOUNTPOINT>` with specific path you wanna mount JuiceFS to, e.g. `/jfs`. If you need set [mount options](reference/command_reference.md#juicefs-mount), replace `[,<MOUNT-OPTIONS>]` with comma separated options list. The following line is an example:
+For the format of `<META-URL>`, please refer to the ["How to Setup Metadata Engine"](how_to_setup_metadata_engine.md) document, such as `redis://localhost:6379/1`. Then replace `<MOUNTPOINT>` with the path you want JuiceFS to mount, e.g. `/jfs`. If you want to set [mount options](../reference/command_reference.md#juicefs-mount), separate the options list with commas and replace `[,<MOUNT-OPTIONS>]`. Here is an example:
 
 ```
-redis://localhost:6379/1    /jfs       juicefs     _netdev,max-uploads=50,writeback,cache-size=2048     0  0
+redis://localhost:6379/1    /jfs       juicefs     _netdev,max-uploads=50,writeback,cache-size=204800     0  0
 ```
 
-**Note: By default, CentOS 6 will NOT mount network file system after boot, run following command to enable it:**
+:::tip
+By default, CentOS 6 will NOT mount network file system after boot, run following command to enable it:
 
 ```bash
 sudo chkconfig --add netfs
 ```
+:::
 
 ## macOS
 
-Create a file named `io.juicefs.<NAME>.plist` under `~/Library/LaunchAgents`. Replace `<NAME>` with JuiceFS volume name. Add following contents to the file (again, replace `NAME`, `PATH-TO-JUICEFS`, `META-URL` and `MOUNTPOINT` with appropriate value):
+Create a file named `io.juicefs.<NAME>.plist` under `~/Library/LaunchAgents`. Replace `<NAME>` with JuiceFS file system name. Add following contents to the file (again, replace `NAME`, `PATH-TO-JUICEFS`, `META-URL`, `MOUNTPOINT` and `MOUNT-OPTIONS` with appropriate value):
 
-```xml
+```xml title="io.juicefs.<NAME>.plist"
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
@@ -39,6 +47,7 @@ Create a file named `io.juicefs.<NAME>.plist` under `~/Library/LaunchAgents`. Re
                 <string>mount</string>
                 <string>META-URL</string>
                 <string>MOUNTPOINT</string>
+                <string>MOUNT-OPTIONS</string>
         </array>
         <key>RunAtLoad</key>
         <true/>
@@ -46,7 +55,18 @@ Create a file named `io.juicefs.<NAME>.plist` under `~/Library/LaunchAgents`. Re
 </plist>
 ```
 
-Use following commands to load the file created in the previous step and test whether the loading is successful. **Please ensure Redis server is already running.**
+:::tip
+If there are multiple mount options, they can be set in multiple lines, for example:
+
+```xml
+                <string>--max-uploads</string>
+                <string>50</string>
+                <string>--cache-size</string>
+                <string>204800</string>
+```
+:::
+
+Use following commands to load the file created in the previous step and test whether the loading is successful. **Please make sure the metadata engine is running properly.**
 
 ```bash
 $ launchctl load ~/Library/LaunchAgents/io.juicefs.<NAME>.plist
@@ -56,7 +76,7 @@ $ ls <MOUNTPOINT>
 
 If mount failed, you can add following configuration to `io.juicefs.<NAME>.plist` file for debug purpose:
 
-```xml
+```xml title="io.juicefs.<NAME>.plist"
         <key>StandardOutPath</key>
         <string>/tmp/juicefs.out</string>
         <key>StandardErrorPath</key>
@@ -80,11 +100,10 @@ brew services start redis
 
 Then add following configuration to `io.juicefs.<NAME>.plist` file for ensure Redis server is loaded:
 
-```xml
+```xml title="io.juicefs.<NAME>.plist"
         <key>KeepAlive</key>
         <dict>
                 <key>OtherJobEnabled</key>
                 <string>homebrew.mxcl.redis</string>
         </dict>
 ```
-
diff --git a/docs/en/introduction/comparison/juicefs_vs_alluxio.md b/docs/en/introduction/comparison/juicefs_vs_alluxio.md
index 08fc8e0e..5b200e98 100644
--- a/docs/en/introduction/comparison/juicefs_vs_alluxio.md
+++ b/docs/en/introduction/comparison/juicefs_vs_alluxio.md
@@ -6,7 +6,7 @@ slug: /comparison/juicefs_vs_alluxio
 
 [Alluxio](https://www.alluxio.io) (/əˈlʌksio/) is a data access layer in the big data and machine learning ecosystem. Initially as research project "Tachyon", it was created at the University of California, Berkeley's [AMPLab](https://en.wikipedia.org/wiki/AMPLab) as creator's Ph.D. thesis in 2013. Alluxio was open sourced in 2014.
 
-The following table shows difference of main features between Alluxio and JuiceFS.
+The following table shows the differences of main features between Alluxio and JuiceFS.
 
 | Features                  | Alluxio            | JuiceFS            |
 | --------                  | -------            | -------            |
@@ -29,48 +29,48 @@ The following table shows difference of main features between Alluxio and JuiceF
 
 ### Storage format
 
-The [storage format](../../reference/how_juicefs_store_files.md) of one file in JuiceFS consists of three levels: chunk, slice and block. A file will be split into multiple blocks, and be compressed and encrypted (optional) store into object storage.
+A single file is [stored](../../reference/how_juicefs_store_files.md) in JuiceFS in three levels: chunk, slice and block. A file will be split into multiple blocks, and be compressed and encrypted (optional) before it is stored into object storage.
 
-Alluxio stores file as object to UFS. The file doesn't be split info blocks like JuiceFS does.
+Alluxio stores files as _objects_ into UFS. It doesn't split files info blocks like what JuiceFS does.
 
 ### Cache granularity
 
-The [default block size](../../reference/how_juicefs_store_files.md) of JuiceFS is 4MiB, compare to 64MiB of Alluxio, the granularity is smaller. The smaller block size is better for random read (e.g. Parquet and ORC) workload, i.e. cache management will be more efficiency.
+The [default block size](../../reference/how_juicefs_store_files.md) of JuiceFS is 4MiB, and thus its granularity is smaller compared to 64MiB of Alluxio. Smaller block size is better for random read (e.g. Parquet and ORC) workload, i.e. cache management will be more efficiency.
 
 ### Hadoop-compatible
 
-JuiceFS is [HDFS-compatible](../../deployment/hadoop_java_sdk.md). Not only compatible with Hadoop 2.x and Hadoop 3.x, but also variety of components in Hadoop ecosystem.
+JuiceFS is [HDFS-compatible](../../deployment/hadoop_java_sdk.md). Not only compatible with Hadoop 2.x and Hadoop 3.x, but also compatible for various components in Hadoop ecosystem.
 
 ### Kubernetes CSI Driver
 
-JuiceFS provides [Kubernetes CSI Driver](https://github.com/juicedata/juicefs-csi-driver) to help people who want to use JuiceFS in Kubernetes. Alluxio provides [Kubernetes CSI Driver](https://github.com/Alluxio/alluxio-csi) too, but this project seems like not active maintained and not official supported by Alluxio.
+JuiceFS provides [Kubernetes CSI Driver](https://github.com/juicedata/juicefs-csi-driver) to help people who want to use JuiceFS in Kubernetes. Alluxio provides [Kubernetes CSI Driver](https://github.com/Alluxio/alluxio-csi) too, but this project seems not active and is not officially supported by Alluxio.
 
 ### Fully POSIX-compatible
 
-JuiceFS is [fully POSIX-compatible](../../reference/posix_compatibility.md). One pjdfstest from [JD.com](https://www.slideshare.net/Alluxio/using-alluxio-posix-fuse-api-in-jdcom) shows that Alluxio didn't pass the POSIX compatibility test, e.g. Alluxio doesn't support symbolic link, truncate, fallocate, append, xattr, mkfifo, mknod and utimes. Besides the things covered by pjdfstest, JuiceFS also provides close-to-open consistency, atomic metadata operation, mmap, fallocate with punch hole, xattr, BSD locks (flock) and POSIX record locks (fcntl).
+JuiceFS is [fully POSIX-compatible](../../reference/posix_compatibility.md). A pjdfstest from [JD.com](https://www.slideshare.net/Alluxio/using-alluxio-posix-fuse-api-in-jdcom) shows that Alluxio didn't pass the POSIX compatibility test, e.g. Alluxio doesn't support symbolic link, truncate, fallocate, append, xattr, mkfifo, mknod and utimes. Besides the things covered by pjdfstest, JuiceFS also provides close-to-open consistency, atomic metadata operation, mmap, fallocate with punch hole, xattr, BSD locks (flock) and POSIX record locks (fcntl).
 
 ### Atomic metadata operation
 
-A metadata operation in Alluxio has two steps: the first step is modify state of Alluxio master, the second step is send request to UFS. As you can see, the metadata operation isn't atomic, its state is unpredictable when the operation is executing or any failure occurs. Alluxio relies on UFS to implement metadata operations, for example rename file operation will become copy and delete operations.
+A metadata operation in Alluxio consists of two steps: the first step is to modify the state of Alluxio master, and the second step is to send request to UFS. Thus, we can see that the metadata operation isn't atomic. The state is unpredictable when the operation is being executed or any failures happen. Alluxio requires UFS to implement metadata operations, for example, rename file operation will become copy and delete operations.
 
-Thanks to [Redis transaction](https://redis.io/topics/transactions), **most of metadata operations of JuiceFS are atomic**, e.g. rename file, delete file, rename directory. You don't have to worry about the consistency and performance.
+Thanks to [Redis transaction](https://redis.io/topics/transactions), **most of the metadata operations of JuiceFS are atomic**, e.g. rename file, delete file, rename directory. You don't have to worry about the consistency and performance.
 
 ### Consistency
 
-Alluxio loads metadata from the UFS as needed and it doesn't have information about UFS at startup. By default, Alluxio expects that all modifications to UFS occur through Alluxio. If changes are made to UFS directly, you need to sync metadata between Alluxio and UFS either manually or periodically. As ["Atomic metadata operation"](#atomic-metadata-operation) section says, the two steps of metadata operation may result in inconsistency.
+Alluxio loads metadata from the UFS as needed. It doesn't have information about UFS at startup. By default, Alluxio expects that all modifications on UFS are completed through Alluxio. If changes are made directly on UFS, you'll need to sync metadata between Alluxio and UFS either manually or periodically. As we have mentioned in ["Atomic metadata operation"](#atomic-metadata-operation) section, the two-step metadata operation may result in inconsistency.
 
-JuiceFS provides strong consistency, in both metadata and data. **The metadata service of JuiceFS is the single source of truth, not a mirror of UFS.** The metadata service doesn't rely on object storage to obtain metadata. Object storage just be treated as an unlimited block storage. There isn't any inconsistency between JuiceFS and object storage.
+JuiceFS provides strong consistency for both metadata and data. **The metadata service of JuiceFS is the single source of truth, not a mirror of UFS.** The metadata service doesn't rely on object storage to obtain metadata, and object storage is just treated as unlimited block storage. Thus, there will not be any inconsistency between JuiceFS and object storage.
 
 ### Data compression
 
-JuiceFS supports use [LZ4](https://lz4.github.io/lz4) or [Zstandard](https://facebook.github.io/zstd) to compress all your data. Alluxio doesn't have this feature.
+JuiceFS supports to use [LZ4](https://lz4.github.io/lz4) or [Zstandard](https://facebook.github.io/zstd) to compress all your data. Alluxio doesn't provide this feature.
 
 ### Data encryption
 
-JuiceFS supports data encryption in transit and at rest. Alluxio community edition doesn't have this feature, but [enterprise edition](https://docs.alluxio.io/ee/user/stable/en/operation/Security.html#end-to-end-data-encryption) has.
+JuiceFS supports data encryption in transit and at rest. Alluxio community edition doesn't provide this feature, while [enterprise edition](https://docs.alluxio.io/ee/user/stable/en/operation/Security.html#end-to-end-data-encryption) does.
 
 ### Zero-effort operation
 
-Alluxio's architecture can be divided into 3 components: master, worker and client. A typical cluster consists of a single leading master, standby masters, a job master, standby job masters, workers, and job workers. You need operation these masters and workers by yourself.
+Alluxio's architecture can be divided into 3 components: master, worker and client. A typical cluster consists of a single leading master, standby masters, a job master, standby job masters, workers, and job workers. You need to maintain all these masters and workers by yourself.
 
-JuiceFS uses Redis or [others](../../guide/how_to_setup_metadata_engine.md) as the metadata engine. You could use service managed by public cloud provider easily as JuiceFS's metadata engine. There isn't any operation needed.
+JuiceFS uses Redis or [others](../../guide/how_to_setup_metadata_engine.md) as the metadata engine. You could easily use the service managed by public cloud provider as JuiceFS's metadata engine without the need for operation.
diff --git a/docs/en/introduction/comparison/juicefs_vs_s3fs.md b/docs/en/introduction/comparison/juicefs_vs_s3fs.md
index 2f4c7aa9..cceb4402 100644
--- a/docs/en/introduction/comparison/juicefs_vs_s3fs.md
+++ b/docs/en/introduction/comparison/juicefs_vs_s3fs.md
@@ -4,35 +4,35 @@ slug: /comparison/juicefs_vs_s3fs
 
 # JuiceFS vs. S3FS
 
-[S3FS](https://github.com/s3fs-fuse/s3fs-fuse) is an open source tool developed in C++ that mounts S3 object storage locally via FUSE for read and write access as if it were a local disk. In addition to Amazon S3, it supports all S3 API-compatible object stores.
+[S3FS](https://github.com/s3fs-fuse/s3fs-fuse) is an open source tool developed in C++ that mounts S3 object storage locally via FUSE for read and write access as a local disk. In addition to Amazon S3, it supports all S3 API-compatible object stores.
 
-In terms of basic functionality, S3FS and JuiceFS can both mount object storage bucket locally via FUSE and use them with a POSIX interface. However, in terms of functional details and technical implementation, they are fundamentally different.
+In terms of basic functionality, S3FS and JuiceFS can both mount object storage bucket locally via FUSE and use them through POSIX interfaces. However, in terms of functional details and technical implementation, they are essentially different.
 
 ## Product positioning
 
-S3FS is a utility that makes it easy to mount object storage bucket locally, read and write in a way that is familiar to users, for general use scenarios that are not sensitive to performance and network latency.
+S3FS is a utility that allows users to mount object storage buckets locally and read and write in a way that the users used to. It targets the general use scenarios that are not sensitive to performance and network latency.
 
-JuiceFS is a distributed file system with a unique approach to data management and a series of technical optimizations for high performance, reliability and security, primarily addressing the storage needs of large volumes of data.
+JuiceFS is a distributed file system with a unique approach to data management and a series of technical optimizations for high performance, reliability and security, which primarily addresses the storage needs of large volumes of data.
 
 ## Architecture
 
-S3FS does not do special optimization for files, it acts like an access channel between local and object storage, with the local mount point seeing the same content as seen on the object storage browser, which makes it easy to use cloud storage locally. However, from another angle, this simple architecture makes S3FS' retrieval and reading and writing of files require direct interaction with the object store, and network latency can have a large impact on performance and user experience.
+S3FS does not do special optimization for files. It acts like an access channel between local and object storage, allowing the same content to be seen on the local mount point and the object storage browser, which makes it easy to use cloud storage locally. On the other hand, with this simple architecture, retrieving, reading and writing of files with S3FS require direct interaction with the object store, and network latency can impact strongly on performance and user experience.
 
-JuiceFS uses a technical architecture that separates data and metadata, where any file is first split into data blocks according to specific rules before being uploaded to the object store, and the corresponding metadata is stored in a separate database. The advantage of this is that retrieval of files and modification of metadata such as file names can be directly interacted with the more responsive database, bypassing the network latency impact of interacting with the object store.
+JuiceFS uses a technical architecture that separates data and metadata, where any file is first split into data blocks according to specific rules before being uploaded to the object storage, and the corresponding metadata is stored in a separated database. The advantage of this is that retrieval of files and modification of metadata such as file names can directly interact with the database with a faster response, bypassing the network latency impact of interacting with the object store.
 
-In addition, when it comes to large files, although S3FS can solve the problem of transferring large files by uploading them in chunks, the nature of object storage dictates that appending files requires rewriting the entire object. For large files of tens or hundreds of gigabytes or even terabytes, repeated uploads are bound to waste a lot of time and bandwidth resources.
+In addition, when processing large files, although S3FS can solve the problem of transferring large files by uploading them in chunks, the nature of object storage dictates that appending files requires rewriting the entire object. For large files of tens or hundreds of gigabytes or even terabytes, repeated uploads waste a lot of time and bandwidth resources.
 
-JuiceFS avoids such problems by splitting individual files into chunks locally according to specific rules (default 4MiB) before uploading, regardless of their size. Any rewriting and appending of files eventually becomes the generation of new data blocks instead of modifying already generated data blocks, greatly reducing the waste of time and bandwidth resources.
+JuiceFS avoids such problems by splitting individual files into chunks locally according to specific rules (default 4MiB) before uploading, regardless of their size. The rewriting and appending operations will eventually become new data blocks instead of modifying already generated data blocks, which greatly reduces the waste of time and bandwidth resources.
 
 For a detailed description of the architecture of JuiceFS, please refer to [documentation](../../introduction/architecture.md).
 
 ## Caching
 
-S3FS supports disk caching, but it is not enabled by default. Local caching can be enabled by specifying a cache path with `-o use_cache`. When caching is enabled, any file reads or writes will be written to the cache before the operation is performed. S3FS detects data changes via MD5 to ensure data correctness and reduce duplicate file downloads. Since all operations involved with S3FS require interaction with S3, whether or not caching is enabled has a significant impact on its application experience.
+S3FS supports disk caching, but it is disabled by default. Local caching can be enabled by specifying a cache path with `-o use_cache`. When caching is enabled, any file reads or writes will be written to the cache before the operation is actually performed. S3FS detects data changes via MD5 to ensure data correctness and reduce duplicate file downloads. Since all operations involved with S3FS require interactions with S3, whether the cache is enabled or not impacts significantly on its application experience.
 
-S3FS does not limit the cache space limit by default, for larger bucket may cause the cache to fill up the disk, you need to define the space reserved for disk by `-o ensure_diskfree`. In addition, S3FS does not have a cache expiration and cleanup mechanism, so users need to manually clean the cache periodically. Once the cache space is full, uncached file operations need to interact directly with the object storage, which will have some impact on handling large files.
+S3FS does not limit the cache capacity by default, which may cause the cache to fill up the disk when working with large buckets. You need to define the reserved disk space by `-o ensure_diskfree`. In addition, S3FS does not have a cache expiration and cleanup mechanism, so users need to manually clean up the cache periodically. Once the cache space is full, uncached file operations need to interact directly with the object storage, which will impact large file handling.
 
-JuiceFS uses a completely different caching approach than S3FS. First, JuiceFS guarantees data consistency. Secondly, JuiceFS defines a default disk cache usage limit of 100GiB, which can be freely adjusted by users as needed, and by default ensures that no more space is used when disk free space falls below 10%. When the cache usage limit is reached, JuiceFS will automatically clean up using an LRU-like algorithm to ensure that cache is always available for subsequent read and write operations.
+JuiceFS uses a completely different caching approach than S3FS. First, JuiceFS guarantees data consistency. Secondly, JuiceFS defines a default disk cache usage limit of 100GiB, which can be freely adjusted by users as needed, and by default ensures that no more space is used when disk free space falls below 10%. When the cache usage limit reaches the upper limit, JuiceFS will automatically do cleanup using an LRU-like algorithm to ensure that cache is always available for subsequent read and write operations.
 
 For more on JuiceFS caching, see [documentation](../../guide/cache_management.md).
 
diff --git a/docs/en/introduction/comparison/juicefs_vs_s3ql.md b/docs/en/introduction/comparison/juicefs_vs_s3ql.md
index 1e530a45..7eb1f1d4 100644
--- a/docs/en/introduction/comparison/juicefs_vs_s3ql.md
+++ b/docs/en/introduction/comparison/juicefs_vs_s3ql.md
@@ -4,21 +4,21 @@ slug: /comparison/juicefs_vs_s3ql
 
 # JuiceFS vs. S3QL
 
-Similar to JuiceFS, [S3QL](https://github.com/s3ql/s3ql) is also an open source network file system driven by object storage and database. All data will be split into blocks and stored in object storage services such as, Amazon S3, Backblaze B2, or OpenStack Swift, the corresponding metadata will be stored in the database.
+Similar to JuiceFS, [S3QL](https://github.com/s3ql/s3ql) is also an open source network file system driven by object storage and database. All data will be split into blocks and stored in object storage services such as Amazon S3, Backblaze B2, or OpenStack Swift, and the corresponding metadata will be stored in the database.
 
-## The same point
+## Common ground
 
-- All support the standard POSIX file system interface through the FUSE module, so that massive cloud storage can be mounted locally and used like local storage.
-- All can provide standard file system functions: hard links, symbolic links, extended attributes, file permissions.
-- All support data compression and encryption, but the algorithms used are different.
+- Both support the standard POSIX file system interface through the FUSE module, so that massive cloud storage can be mounted locally and used like local storage.
+- Both provide standard file system features: hard links, symbolic links, extended attributes, file permissions.
+- Both support data compression and encryption, but the algorithms used are different.
 
-## Different points
+## Differences
 
 - 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 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 provides remote synchronous backup of metadata. SQLite databases with metadata will be backed up asynchronously to object storage. JuiceFS mainly uses network databases such as Redis and MySQL, and does not directly provide SQLite database synchronization backup function, but JuiceFS supports metadata import and export, as well as various storage backend synchronization functions, users can easily backup metadata to objects Storage, also supports migration between different databases.
+- S3QL provides remote synchronous backup of metadata. SQLite databases with metadata will be backed up asynchronously to object storage. JuiceFS mainly uses network databases such as Redis and MySQL, and does not directly provide SQLite database synchronization backup function. However, JuiceFS supports metadata import and export as well as various storage backend synchronization functions, which allows users to back up metadata to object storage easily. JuiceFS also supports migration between different databases.
 
 |                           | **S3QL**              | **JuiceFS**                   |
 | :------------------------ | :-------------------- | :---------------------------- |
@@ -47,9 +47,9 @@ Similar to JuiceFS, [S3QL](https://github.com/s3ql/s3ql) is also an open source
 | Open source license       | GPLv3                 | Apache License 2.0                        |
 | Open source date          | 2011                  | 2021.1                        |
 
-## Usage
+## Usability
 
-This part mainly evaluates the ease of installation and use of the two products.
+This part mainly evaluates the ease of installing and using the two products.
 
 ### Installation
 
@@ -57,7 +57,7 @@ During the installation process, we use Rocky Linux 8.4 operating system (kernel
 
 #### S3QL
 
-S3QL is developed in Python and requires python-devel 3.7 and above to be installed. In addition, at least the following dependencies must be met: fuse3-devel, gcc, pyfuse3, sqlite-devel, cryptography, defusedxml, apsw, dugong. In addition, you need to pay special attention to Python's package dependencies and location issues.
+S3QL is developed in Python and requires python-devel 3.7 or higher to be installed. In addition, at least the following dependencies must be satisfied: fuse3-devel, gcc, pyfuse3, sqlite-devel, cryptography, defusedxml, apsw, dugong. In addition, you need to pay special attention to Python's package dependencies and location issues.
 
 S3QL will install 12 binary programs in the system, and each program provides an independent function, as shown in the figure below.
 
@@ -65,13 +65,13 @@ S3QL will install 12 binary programs in the system, and each program provides an
 
 #### JuiceFS
 
-JuiceFS is developed in Go and can be used directly by downloading the pre-compiled binary file. The JuiceFS client has only one binary program `juicefs`, just copy it to any executable path of the system, for example: `/usr/local/bin`.
+JuiceFS is developed in Go and can be used directly by downloading the pre-compiled binary file. The JuiceFS client has only one binary program `juicefs`. You can just copy it to any executable path of the system, for example: `/usr/local/bin`.
 
 ### Create and Mount a file system
 
-Both S3QL and JuiceFS use database to store metadata. S3QL only supports SQLite databases, and JuiceFS supports databases such as Redis, TiKV, MySQL, MariaDB, PostgreSQL, and SQLite.
+Both S3QL and JuiceFS use database to store metadata. S3QL only supports SQLite databases, while JuiceFS supports databases such as Redis, TiKV, MySQL, MariaDB, PostgreSQL, and SQLite.
 
-Here we use Minio object storage created locally and use them to create a file system separately:
+Here we create a file system using S3QL and JuiceFS separately with locally created Minio as object storage:
 
 #### S3QL
 
@@ -87,7 +87,7 @@ Mount a file system using `mount.s3ql`:
 mount.s3ql --compress none --backend-options no-ssl s3c://127.0.0.1:9000/s3ql/ mnt-s3ql
 ```
 
-S3QL needs to interactively provide the access key of the object storage API through the command line when creating and mounting a file system.
+S3QL needs the access key of the object storage API to be interactively provided through the command line when creating and mounting a file system.
 
 #### JuiceFS
 
@@ -112,6 +112,6 @@ JuiceFS only sets the object storage API access key when creating a file system,
 
 ## Summary
 
-**S3QL** adopts the storage structure of object storage + SQLite, and storing the 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, making S3QL very suitable for individuals to store files in cloud storage at a lower cost and more securely.
+**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. 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.
diff --git a/docs/zh_cn/administration/fault_diagnosis_and_analysis.md b/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
index ff431319..17ffdffb 100644
--- a/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
+++ b/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
@@ -4,6 +4,9 @@ sidebar_position: 5
 slug: /fault_diagnosis_and_analysis
 ---
 
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
 # 故障诊断和分析
 
 ## 客户端日志
@@ -14,42 +17,46 @@ JuiceFS 客户端在运行过程中会输出日志用于故障诊断，日志等
 
 ### 挂载点
 
-当挂载 JuiceFS 文件系统时加上了 [`-d` 选项](../reference/command_reference.md#juicefs-mount)（表示后台运行），日志会输出到系统日志和 `/var/log/juicefs.log`（需要 v0.15 及以上版本客户端，参见 [`--log` 选项](../reference/command_reference.md#juicefs-mount)）。取决于你使用的操作系统，你可以通过不同的命令获取日志：
+当挂载 JuiceFS 文件系统时加上了 [`-d` 选项](../reference/command_reference.md#juicefs-mount)（表示后台运行），日志会同时输出到系统日志和本地日志文件，取决于挂载文件系统时的运行用户，本地日志文件的路径稍有区别。root 用户对应的日志文件路径是 `/var/log/juicefs.log`，非 root 用户的日志文件路径是 `$HOME/.juicefs/juicefs.log`，具体请参见 [`--log` 选项](../reference/command_reference.md#juicefs-mount)。
+
+取决于你使用的操作系统，你可以通过不同的命令获取系统日志或直接读取本地日志文件：
+
+<Tabs>
+  <TabItem value="local-log-file" label="本地日志文件">
+
+```bash
+tail -n 100 /var/log/juicefs.log
+```
+
+  </TabItem>
+  <TabItem value="macos-syslog" label="macOS 系统日志">
 
-macOS
 ```bash
 syslog | grep 'juicefs'
 ```
 
-Debian based system
+  </TabItem>
+  <TabItem value="debian-syslog" label="Debian 系统日志">
+
 ```bash
 cat /var/log/syslog | grep 'juicefs'
 ```
 
-CentOS based system
+  </TabItem>
+  <TabItem value="centos-syslog" label="CentOS 系统日志">
+
 ```bash
 cat /var/log/messages | grep 'juicefs'
 ```
 
-All system (require v0.15+ JuiceFS)
-```bash
-tail -n 100 /var/log/juicefs.log
-```
+  </TabItem>
+</Tabs>
 
 你可以使用 `grep` 命令过滤显示不同等级的日志信息，从而进行性能统计和故障追踪，例如：
 
-```bash
-cat /var/log/syslog | grep 'juicefs' | grep '<INFO>'
-```
-```bash
-cat /var/log/syslog | grep 'juicefs' | grep '<WARNING>'
-```
-```bash
+```shell
 cat /var/log/syslog | grep 'juicefs' | grep '<ERROR>'
 ```
-```bash
-cat /var/log/syslog | grep 'juicefs' | grep '<FATAL>'
-```
 
 ### Kubernetes CSI 驱动
 
diff --git a/docs/zh_cn/administration/metadata/redis_best_practices.md b/docs/zh_cn/administration/metadata/redis_best_practices.md
index 6192d341..97999b26 100644
--- a/docs/zh_cn/administration/metadata/redis_best_practices.md
+++ b/docs/zh_cn/administration/metadata/redis_best_practices.md
@@ -40,11 +40,7 @@ used_memory_dataset_perc: 70.12%
 
 ## 数据可用性
 
-:::caution 注意
-JuiceFS 使用 「[Redis 事务](https://redis.io/docs/manual/transactions)」保证元数据操作的原子性。但由于 Redis Cluster 集群模式不支持事务（Transactions），因此 Redis 集群不可用作 JuiceFS 元数据存储。如有 Redis 高可用需求，请使用 Sentinel 哨兵模式。
-:::
-
-### Sentinel 模式
+### 哨兵模式
 
 [Redis 哨兵](https://redis.io/docs/manual/sentinel) 是 Redis 官方的高可用解决方案，它提供以下功能：
 
@@ -81,18 +77,22 @@ juicefs mount redis://:password@masterName,1.2.3.4,1.2.5.6:26379/2 ~/jfs
 需要注意由于 Redis 主节点的数据是异步复制到副本节点，因此有可能读到的元数据不是最新的。
 :::
 
-### Cluster 模式
-Juicefs 同样支持 cluster 模式的 redis 作为元数据引擎，redis cluster 模式的 `META-URL` 为 `redis[s]://[[USER]:PASSWORD@]ADDR:PORT,[ADDR:PORT],[ADDR:PORT][/DB]`，例如：
+### 集群模式
+
+:::note 注意
+此特性需要使用 1.0.0 及以上版本的 JuiceFS
+:::
+
+Juicefs 同样支持集群模式的 Redis 作为元数据引擎，Redis 集群模式的 `META-URL` 为 `redis[s]://[[USER]:PASSWORD@]ADDR:PORT,[ADDR:PORT],[ADDR:PORT][/PREFIX]`，例如：
 
 ```shell
-juicefs format redis://127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002 ~/jfs
+juicefs format redis://127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002/jfs1 myjfs
 ```
 
 :::tip 提示
-Redis cluster 虽然不支持 db 隔离机制，但是在其作为元数据引擎的时候，Juicefs 利用 redis cluster [Hash Tag](https://redis.io/docs/reference/cluster-spec/#hash-tags) 的特性，通过将 `{db}` 作为 key 的前缀实现了 cluster 模式下的隔离与事务保证，因此 cluster 模式下的 db 仍旧有隔离的效果。可以通过设置不同的 `db` 让一个 redis cluster 同时作为多个文件系统的元数据库。
+Redis 集群不再支持多数据库，而是将所有 keys 分散到 16384 个 hash slots 中，再将这些 hash slots 打散到多个 Redis master 节点来存储。JuiceFS 利用了 Redis 集群的 [Hash Tag](https://redis.io/docs/reference/cluster-spec/#hash-tags) 特性，通过将 `{prefix}` 作为 key 的前缀来将一个文件系统中的所有 keys 都存放在同一个 hash slot，以保证集群模式下操作的事务性。另外，通过设置不同的 `prefix` 可以让一个 Redis 集群同时作为多个 JuiceFS 的元数据库。
 :::
 
-
 ## 数据持久性
 
 Redis 提供了不同范围的[持久性](https://redis.io/docs/manual/persistence)选项：
@@ -143,14 +143,14 @@ Redis 对数据备份非常友好，因为您可以在数据库运行时复制 R
 
 ## 推荐的 Redis 托管服务
 
+### Amazon MemoryDB for Redis
+
+[Amazon MemoryDB for Redis](https://aws.amazon.com/memorydb) 是一种持久的内存数据库服务，可提供超快的性能。MemoryDB 与 Redis 兼容，使用 MemoryDB，你的所有数据都存储在内存中，这使你能够实现微秒级读取和数毫秒的写入延迟和高吞吐。MemoryDB 还使用多可用区事务日志跨多个可用区持久存储数据，以实现快速故障切换、数据库恢复和节点重启。
+
 ### Amazon ElastiCache for Redis
 
 [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis) 是为云构建的完全托管的、与 Redis 兼容的内存数据存储。它提供[自动故障切换](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/AutoFailover.html)、[自动备份](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/backups-automatic.html)等功能以确保可用性和持久性。
 
-:::info 说明
-Amazon ElastiCache for Redis 有两种类型：禁用集群模式和启用集群模式。因为 JuiceFS 使用[事务](https://redis.io/docs/manual/transactions)来保证元数据操作的原子性，所以不能使用「启用集群模式」类型。
-:::
-
 ### Google Cloud Memorystore for Redis
 
 [Google Cloud Memorystore for Redis](https://cloud.google.com/memorystore/docs/redis) 是针对 Google Cloud 的完全托管的 Redis 服务。通过利用高度可扩展、可用且安全的 Redis 服务，在 Google Cloud 上运行的应用程序可以实现卓越的性能，而无需管理复杂的 Redis 部署。
@@ -159,18 +159,10 @@ Amazon ElastiCache for Redis 有两种类型：禁用集群模式和启用集群
 
 [Azure Cache for Redis](https://azure.microsoft.com/en-us/services/cache) 是一个完全托管的内存缓存，支持高性能和可扩展的架构。使用它来创建云或混合部署，以亚毫秒级延迟处理每秒数百万个请求——所有这些都具有托管服务的配置、安全性和可用性优势。
 
-### 阿里云 ApsaraDB for Redis
+### 阿里云云数据库 Redis 版
 
-[阿里云 ApsaraDB for Redis](https://www.alibabacloud.com/product/apsaradb-for-redis) 是一种兼容原生 Redis 协议的数据库服务。它支持混合内存和硬盘以实现数据持久性。云数据库 Redis 版提供高可用的热备架构，可扩展以满足高性能、低延迟的读写操作需求。
+[阿里云云数据库 Redis 版](https://www.aliyun.com/product/kvstore)是一种兼容原生 Redis 协议的数据库服务。它支持混合内存和硬盘以实现数据持久性。云数据库 Redis 版提供高可用的热备架构，可扩展以满足高性能、低延迟的读写操作需求。
 
-:::info 说明
-ApsaraDB for Redis 支持 3 种类型的[架构](https://www.alibabacloud.com/help/doc-detail/86132.htm)：标准、集群和读写分离。因为 JuiceFS 使用[事务](https://redis.io/docs/manual/transactions)来保证元数据操作的原子性，所以不能使用集群类型架构。
-:::
+### 腾讯云云数据库 Redis
 
-### 腾讯云 TencentDB for Redis
-
-[腾讯云 TencentDB for Redis](https://intl.cloud.tencent.com/product/crs) 是一种兼容 Redis 协议的缓存和存储服务。丰富多样的数据结构选项，帮助您开发不同类型的业务场景，提供主从热备份、容灾自动切换、数据备份、故障转移、实例监控、在线等一整套数据库服务缩放和数据回滚。
-
-:::info 说明
-TencentDB for Redis 支持两种类型的[架构](https://intl.cloud.tencent.com/document/product/239/3205)：标准和集群。因为 JuiceFS 使用[事务](https://redis.io/docs/manual/transactions)来保证元数据操作的原子性，所以不能使用集群类型架构。
-:::
+[腾讯云云数据库 Redis](https://cloud.tencent.com/product/crs) 是一种兼容 Redis 协议的缓存和存储服务。丰富多样的数据结构选项，帮助您开发不同类型的业务场景，提供主从热备份、容灾自动切换、数据备份、故障转移、实例监控、在线等一整套数据库服务缩放和数据回滚。
diff --git a/docs/zh_cn/development/contributing_guide.md b/docs/zh_cn/development/contributing_guide.md
index f14ea707..a106b3fb 100644
--- a/docs/zh_cn/development/contributing_guide.md
+++ b/docs/zh_cn/development/contributing_guide.md
@@ -2,7 +2,44 @@
 sidebar_label: 贡献指南
 sidebar_position: 1
 ---
-# 贡献指南
 
-:::note 注意
-文档正在编写
\ No newline at end of file
+# JuiceFS 贡献指南
+
+## 基本准则
+
+- 在开始修复功能或错误之前，请先通过 GitHub、Slack 等渠道与我们沟通。此步骤的目的是确保没有其他人已经在处理它，如有必要，我们将要求您创建一个 GitHub issue。
+- 我们使用 GitHub issue 来讨论功能并达成一致。这是为了防止彼此的时间被浪费。
+- 如果这是一个重大的特性更新，我们强烈建议你写一份设计文档来帮助社区理解你的动机和解决方案。
+- 对于首次贡献者来说找到合适 issue 的好方法是使用标签 ["kind/good-first-issue"](https://github.com/juicedata/juicefs/labels/kind%2Fgood-first-issue) 或 ["kind/help-wanted"](https://github.com/juicedata/juicefs/labels/kind%2Fhelp-wanted) 搜索未解决的问题。
+
+## 代码风格
+
+- 我们遵循 ["Effective Go"](https://go.dev/doc/effective_go) 和 ["Go Code Review Comments"](https://github.com/golang/go/wiki/CodeReviewComments)。
+- 在提交前使用 `go fmt` 格式化你的代码。你可以在 [Go 的编辑器和 IDE](https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins) 中找到支持 Go 的相关工具的信息。
+- 如果你看到任何明显违反风格指南的代码，请修复它并提交一个 PR 。
+- 每个新的源文件都必须以许可证头开始。
+- 安装 [pre-commit](https://pre-commit.com) 并使用它来设置一个预提交钩子来进行静态分析。只需在仓库根目录下运行 `pre-commit install` 即可。
+
+## 签署 CLA
+
+在您为 JuiceFS 进行贡献之前，您需要签署[贡献者许可协议](https://cla-assistant.io/juicedata/juicefs)。当你第一次提交 PR 的时候，将有一个 CLA 助手指导你。
+
+## 什么是好的 PR
+
+- 足够的单元测试
+- 遵循编码风格
+- 足够的行内注释
+- 简要解释的提交内容
+
+## 贡献流程
+
+这是一个贡献者工作流程的大致轮廓：
+
+1. 基于主分支创建一个要贡献的主题分支。这个主分支通常是 `main` 分支；
+1. 提交代码；
+1. 确保提交消息的格式正确；
+1. 将主题分支中的更改推到个人 fork 的仓库；
+1. 提交一个 PR 到 [juicedata/juicefs](https://github.com/juicedata/juicefs/compare) 仓库。 这个 PR 应该链接到你或其他人创建的一个 issue；
+1. PR 在合并之前必须得到至少一个维护者的批准。
+
+尽情享受极客的快乐吧！
diff --git a/docs/zh_cn/development/data_structures.md b/docs/zh_cn/development/data_structures.md
index f65e698f..a973bbb3 100644
--- a/docs/zh_cn/development/data_structures.md
+++ b/docs/zh_cn/development/data_structures.md
@@ -84,7 +84,7 @@ type Format struct {
 - totalInodes：文件系统的已使用文件数
 
 - nextInode：下一个可用的 inode 号（Redis 中为当前已用的最大 inode 号）
-- nextChunk：下一个可用的 sliceID（Redis 中为当前已用的最大 sliceID）
+- nextChunk：下一个可用的 sliceId（Redis 中为当前已用的最大 sliceId）
 - nextSession：当前已用的最大 sid（sessionID）
 - nextTrash：当前已用的最大 trash inode 号
 - nextCleanupSlices：上一次检查清理残留 slices 的时间点
@@ -202,7 +202,7 @@ type Slice struct {
 记录 Slice 的引用计数，具体为：
 
 ```
-sliceID, size -> refs
+sliceId, size -> refs
 ```
 
 由于绝大部分 Slice 的引用计数均为 1，为减少数据库中相关 entry 数量，在 Redis 和 TKV 中以实际值减 1 作为存储的计数值。这样，大部分的 Slice 对应 refs 值为 0，则不必在数据库中创建相关 entry。
@@ -269,10 +269,10 @@ inode, length -> expire
 记录延迟删除的 Slices。当回收站功能开启时，因 Slice Compaction 功能删除的旧 Slices 会被保留与回收站配置相同的时间，以被在必要时可用来恢复数据。其内容为：
 
 ```
-sliceID, deleted -> []slice
+sliceId, deleted -> []slice
 ```
 
-其中 sliceID 为 compact 后新 Slice 的 ID，deleted 为 compact 完成的时间戳，映射值为被 compacted 的所有旧 slice 列表，每个 slice 仅编码了 ID 和 size 信息：
+其中 sliceId 为 compact 后新 Slice 的 ID，deleted 为 compact 完成的时间戳，映射值为被 compacted 的所有旧 slice 列表，每个 slice 仅编码了 ID 和 size 信息：
 
 ```go
 type slice struct {
@@ -366,7 +366,7 @@ Redis 中 Key 的通用格式为 `${prefix}${JFSKey}`，其中：
 - Key：`sliceRef`
 - Value Type：Hash
 - Value：所有需记录的 Slices 的计数值。在 Hash 中：
-  - Key：`k${sliceID}_${size}`
+  - Key：`k${sliceId}_${size}`
   - Value：此 Slice 的引用计数值减 1（若引用计数为 1，则一般不创建对应 entry）
 
 #### 3.2.10 Symlink
@@ -412,7 +412,7 @@ Redis 中 Key 的通用格式为 `${prefix}${JFSKey}`，其中：
 - Key：`delSlices`
 - Value Type：Hash
 - Value：所有待清理的 Slices。在 Hash 中：
-  - Key：`${sliceID}_${deleted}`
+  - Key：`${sliceId}_${deleted}`
   - Value：字节数组，其中每 12 字节对应一个 [slice](#3.1.15-DelSlices)
 
 #### 3.2.16 Sustained
@@ -514,15 +514,13 @@ Slices 是一段字节数组，每 24 字节对应一个 [Slice](#3.1.8-Chunk)
 #### 3.3.9 SliceRef
 
 ```go
-type chunkRef struct {
-	Chunkid uint64 `xorm:"pk"`
-	Size    uint32 `xorm:"notnull"`
-	Refs    int    `xorm:"notnull"`
+type sliceRef struct {
+	Id   uint64 `xorm:"pk chunkid"`
+	Size uint32 `xorm:"notnull"`
+	Refs int    `xorm:"notnull"`
 }
 ```
 
-Chunkid 实际等同 [3.1.9](#3.1.9-SliceRef) 里的 sliceID。
-
 #### 3.3.10 Symlink
 
 ```go
@@ -583,13 +581,13 @@ type delfile struct {
 
 ```go
 type delslices struct {
-	Chunkid uint64 `xorm:"pk"`
+	Id      uint64 `xorm:"pk chunkid"`
 	Deleted int64  `xorm:"notnull"`
 	Slices  []byte `xorm:"blob notnull"`
 }
 ```
 
-Chunkid 实际等同 [3.1.15](#3.1.15-DelSlices) 里的 sliceID。Slices 是一段字节数组，每 12 字节对应一个 [slice](#3.1.15-DelSlices)。
+Slices 是一段字节数组，每 12 字节对应一个 [slice](#3.1.15-DelSlices)。
 
 #### 3.3.16 Sustained
 
@@ -611,7 +609,7 @@ TKV（Transactional Key-Value Database）中 Key 的通用格式为 `${prefix}${
 在 TKV 的 Keys 中，所有整数都以编码后的二进制形式存储：
 
 - inode 和 counter value 占 8 个字节，使用**小端**编码
-- sid、sliceID 和 timestamp 占 8 个字节，使用**大端**编码
+- sid、sliceId 和 timestamp 占 8 个字节，使用**大端**编码
 
 #### 3.4.1 Setting
 
@@ -666,7 +664,7 @@ A${inode}C${index} -> Slices
 #### 3.4.9 SliceRef
 
 ```
-K${sliceID}${size} -> counter value
+K${sliceId}${size} -> counter value
 ```
 
 其中 size 占 4 个字节，使用**大端**编码。
@@ -729,7 +727,7 @@ D${inode}${length} -> timestamp
 #### 3.4.15 DelSlices
 
 ```
-L${timestamp}${sliceID} -> slices
+L${timestamp}${sliceId} -> slices
 ```
 
 其中 slices 是一段字节数组，每 12 字节对应一个 [slice](#3.1.15-DelSlices)。
@@ -767,7 +765,7 @@ Chunk: |<---        Chunk 0        --->|<---        Chunk 1        --->|<-- Chun
 在单机 Redis 中，这意味着有 3 个 [Chunk Keys](#3.1.8-Chunk)，分别为 `c100_0`， `c100_1` 和 `c100_2`，每个 Key 对应一个 Slices 列表。这些 Slices 主要在数据写入时生成，可能互相之间有覆盖，也可能未完全填充满 Chunk。因此，在使用前需要顺序遍历这个 Slices 列表，并重新构建出最新版的数据分布，做到：
 
 1. 有多个 Slice 覆盖的部分以最后加入的 Slice 为准
-2. 没有被 Slice 覆盖的部分自动补零，用 sliceID = 0 来表示
+2. 没有被 Slice 覆盖的部分自动补零，用 sliceId = 0 来表示
 3. 根据文件 size 截断 Chunk
 
 现假设 Chunk 0 中有 3 个 Slices，分别为：
@@ -809,19 +807,19 @@ Block 是 JuiceFS 管理数据的基本单元，其大小默认为 4 MiB，且
 - fsname 是文件系统名称
 - “chunks” 为固定字符串，代表 JuiceFS 的数据对象
 - hash 是根据 basename 算出来的哈希值，起到一定的隔离管理的作用
-- basename 是对象的有效名称，格式为 `${sliceID}_${index}_${size}`，其中：
-  - sliceID 为该对象所属 Slice 的 ID，JuiceFS 中每个 Slice 都有一个全局唯一的 ID
+- basename 是对象的有效名称，格式为 `${sliceId}_${index}_${size}`，其中：
+  - sliceId 为该对象所属 Slice 的 ID，JuiceFS 中每个 Slice 都有一个全局唯一的 ID
   - index 是该对象在所属 Slice 中的序号，默认一个 Slice 最多能拆成 16 个 Blocks，因此其取值范围为 [0, 16)
   - size 是该 Block 的大小，默认情况下其取值范围为 (0, 4 MiB]
 
-目前使用的 hash 算法有两种，以 basename 中的 sliceID 为参数，根据文件系统格式化时的 [HashPrefix](#3.1.1-Setting) 配置选择：
+目前使用的 hash 算法有两种，以 basename 中的 sliceId 为参数，根据文件系统格式化时的 [HashPrefix](#3.1.1-Setting) 配置选择：
 
 ```go
-func hash(sliceID int) string {
+func hash(sliceId int) string {
 	if HashPrefix {
-		return fmt.Sprintf("%02X/%d", sliceID%256, sliceID/1000/1000)
+		return fmt.Sprintf("%02X/%d", sliceId%256, sliceId/1000/1000)
 	}
-	return fmt.Sprintf("%d/%d", sliceID/1000/1000, sliceID/1000)
+	return fmt.Sprintf("%d/%d", sliceId/1000/1000, sliceId/1000)
 }
 ```
 
diff --git a/docs/zh_cn/faq.md b/docs/zh_cn/faq.md
index 7a69c974..54d4432d 100644
--- a/docs/zh_cn/faq.md
+++ b/docs/zh_cn/faq.md
@@ -4,11 +4,9 @@
 
 已经支持了绝大部分对象存储，参考这个[列表](guide/how_to_setup_object_storage.md#支持的存储服务)。如果它跟 S3 兼容的话，也可以当成 S3 来使用。否则，请创建一个 issue 来增加支持。
 
-## 是否可以使用 Redis 集群版作为元数据引擎？
+## 支持哨兵或者集群模式的 Redis 作为 JuiceFS 的元数据引擎吗？
 
-可以。自 [v1.0.0 Beta3](https://github.com/juicedata/juicefs/releases/tag/v1.0.0-beta3) 版本开始 JuiceFS 支持使用 [Redis 集群版](https://redis.io/docs/manual/scaling)作为元数据引擎，不过需要注意的是 Redis 集群版要求一个事务中所有操作的 key 必须在同一个 hash slot 中，因此一个 JuiceFS 文件系统只能使用一个 hash slot。
-
-请查看[「Redis 最佳实践」](administration/metadata/redis_best_practices.md)了解更多信息。
+支持，另外这里还有一篇 Redis 作为 JuiceFS 元数据引擎的[最佳实践文档](administration/metadata/redis_best_practices.md)可供参考。
 
 ## JuiceFS 与 XXX 的区别是什么？
 
@@ -20,13 +18,39 @@ JuiceFS 是一个分布式文件系统，元数据访问的延时取决于挂载
 
 JuiceFS 内置多级缓存（主动失效），一旦缓存预热好，访问的延时和吞吐量非常接近单机文件系统的性能（FUSE 会带来少量的开销）。
 
+## 为什么在对象存储中看不到那些已经存入 JuiceFS 的原始文件？
+
+使用 JuiceFS，文件最终会被拆分成 Chunks、Slices 和 Blocks 存储在对象存储。因此，你会发现在对象存储平台的文件浏览器中找不到存入 JuiceFS 的原始文件，存储桶中只有一个 chunks 目录和一堆数字编号的目录和文件。不要惊慌，这正是 JuiceFS 文件系统高效运作的秘诀！详情请参考[「JuiceFS 如何存储文件」](introduction/architecture.md#如何存储文件)。
+
 ## JuiceFS 支持随机读写吗？
 
 支持，包括通过 mmap 等进行的随机读写。目前 JuiceFS 主要是对顺序读写进行了大量优化，对随机读写的优化也在进行中。如果想要更好的随机读性能，建议关闭压缩（[`--compress none`](reference/command_reference.md#juicefs-format)）。
 
+## JuiceFS 支持随机写的实现原理是什么？
+
+JuiceFS 不将原始文件存入对象存储，而是将其按照某个大小（默认为 4MiB）拆分为 N 个数据块（Block）后，上传到对象存储，然后将数据块的 ID 存入元数据引擎。随机写的时候，逻辑上是要覆盖原本的内容，实际上是把**要覆盖的数据块**的元数据标记为旧数据，同时只上传随机写时产生的**新数据块**到对象存储，并将**新数据块**对应的元数据更新到元数据引擎中。
+
+当读取被覆盖部分的数据时，根据**最新的元数据**，从随机写时上传的**新数据块**读取即可，同时**旧数据块**可能会被后台运行的垃圾回收任务自动清理。这样就将随机写的复杂度转移到读的复杂度上，。
+
+这个只是很粗略的实现逻辑介绍，具体的读写流程非常复杂，可以研读[「JuiceFS 内部实现」](development/data_structures.md)与[「读写请求处理流程」](introduction/io_processing.md)这两篇文档并结合代码一起梳理。
+
+## 为什么我在挂载点删除了文件，但是对象存储占用空间没有变化或者变化很小？
+
+第一个原因是你可能开启了回收站特性。为了保证数据安全回收站默认开启，删除的文件其实被放到了回收站，实际并没有被删除，所以对象存储大小不会变化。回收站的保留时间可以通过 `juicefs format` 指定或者通过 `juicefs config` 修改。请参考[「回收站」](security/trash.md)文档了解更多信息。
+
+第二个原因是 JuiceFS 是异步删除对象存储中的数据，所以对象存储的空间变化会慢一点。如果你需要立即清理对象存储中需要被删除的数据，可以尝试运行 [`juicefs gc`](reference/command_reference.md#juicefs-gc) 命令。
+
+## 为什么挂载点显示的大小与对象存储占用空间存在差异？
+
+通过[「JuiceFS 支持随机写的实现原理是什么？」](#juicefs-支持随机写的实现原理是什么)这个问题的答案可以推断出，对象存储的占用空间大部分情况下是大于等于实际大小的，尤其是短时间内进行大量的覆盖写产生许多文件碎片后。这些碎片在未触发合并与回收前其仍旧占用着对象存储的空间。不过也不必担心这些碎片一直占用空间，因为在每次读／写文件的时候都会检查并在必要的时候触发该文件相关碎片的整理工作。另外你可以通过 `juicefs gc —-compact -—delete` 命令手动触发合并与回收。
+
+另外如果 JuiceFS 文件系统开启了压缩功能（默认不开启），那么对象存储上存储的对象有可能比实际文件大小更小（取决于不同类型文件的压缩比）。
+
+如果以上因素都已经排除，请检查你使用的对象存储的[存储类型](guide/how_to_setup_object_storage.md#存储类型)是什么，云服务商可能会针对某些存储类型设置最小计量单位。例如阿里云 OSS 低频访问存储的[最小计量单位](https://help.aliyun.com/document_detail/173534.html)是 64KB，如果单个文件小于 64KB 也会按照 64KB 计算。
+
 ## 数据更新什么时候会对其它客户端可见？
 
-所有的元数据更新都是立即对其它客户端可见。JuiceFS 保证关闭再打开（close-to-open）一致性，请查看[「一致性」](guide/cache_management.md#一致性)了解更多信息。
+所有的元数据更新都是立即对其它客户端可见。JuiceFS 保证关闭再打开（close-to-open）一致性，请查看[「一致性」](guide/cache_management.md#数据一致性)了解更多信息。
 
 通过 `write()` 新写入的数据会缓存在内核和客户端中，可以被当前机器的其它进程看到，其它机器暂时看不到。
 
@@ -50,6 +74,10 @@ JuiceFS 内置多级缓存（主动失效），一旦缓存预热好，访问的
 
 请使用 [`juicefs umount`](reference/command_reference.md#juicefs-umount) 命令卸载。
 
+## 卸载挂载点时报 `Resource busy -- try 'diskutil unmount'` 错误
+
+这代表挂载点下的某个文件或者目录正在被使用，无法直接 `umount`，可以检查（如通过 `lsof` 命令）是否有打开的终端正位于 JuiceFS 挂载点的某个目录，或者某个应用程序正在处理挂载点中的文件。如果有，则退出终端或应用程序后再尝试使用 `juicefs umount` 命令卸载文件系统。
+
 ## 怎么升级 JuiceFS 客户端？
 
 首先请卸载 JuiceFS 文件系统，然后使用新版本的客户端重新挂载。
@@ -108,3 +136,41 @@ uid=1201(alice) gid=500(staff) groups=500(staff)
 ```
 
 阅读文档[「多主机间同步账户」](administration/sync_accounts_between_multiple_hosts.md)解决这个问题。
+
+## JuiceFS 除了挂载外还支持哪些方式访问数据？
+
+除了挂载外，还支持以下几种方式：
+
+- Kuberenetes CSI 驱动：通过 Kubernetes CSI 驱动的方式将 JuiceFS 作为 Kubernetes 集群的存储层，详情请参考[「Kubernetes 使用 JuiceFS」](deployment/how_to_use_on_kubernetes.md)。
+- Hadoop Java SDK：方便在 Hadoop 体系中使用兼容 HDFS 接口的 Java 客户端访问 JuiceFS。详情请参考[「Hadoop 使用 JuiceFS」](deployment/hadoop_java_sdk.md)。
+- S3 网关：通过 S3 协议访问 JuiceFS，详情请参考[「配置 JuiceFS S3 网关」](deployment/s3_gateway.md)。
+- Docker Volume 插件：在 Docker 中方便使用 JuiceFS 的方式，详情请参考[「Docker 使用 JuiceFS」](deployment/juicefs_on_docker.md)。
+- WebDAV 网关：通过 WebDAV 协议访问 JuiceFS
+
+## JuiceFS 的日志在哪里？
+
+不同类型的 JuiceFS 客户端获取日志的方式也不同，详情请参考[「客户端日志」](administration/fault_diagnosis_and_analysis.md#客户端日志)文档。
+
+## 如何销毁一个文件系统？
+
+使用 `juicefs destroy` 命令销毁一个文件系统，该命令将会清空元数据引擎与对象存储中的相关数据。关于该命令的使用详情请参考[文档](administration/destroy.md)。
+
+## JuiceFS S3 网关支持多用户管理等高级功能吗？
+
+JuiceFS 内置的 `gateway` 子命令不支持多用户管理等功能，只提供基本的 S3 网关功能。如果需要使用这些高级功能，请参考[文档](deployment/s3_gateway.md#使用功能更完整的-s3-网关)。
+
+## JuiceFS 支持使用对象存储中的某个目录作为 `—-bucket` 选项的值吗？
+
+到 JuiceFS 1.0 为止，还不支持该功能。
+
+## JuiceFS 支持访问对象存储中已经存在的数据吗？
+
+到 JuiceFS 1.0 为止，还不支持该功能。
+
+## JuiceFS 目前支持分布式缓存吗？
+
+到 JuiceFS 1.0 为止，还不支持该功能。
+
+## JuiceFS 目前有 SDK 可以使用吗？
+
+截止到 JuiceFS 1.0 发布，社区有两个 SDK，一个是 Juicedata 官方维护的 HDFS 接口高度兼容的 [Java SDK](deployment/hadoop_java_sdk.md)，另一个是由社区用户维护的 [Python SDK](https://github.com/megvii-research/juicefs-python)。
diff --git a/docs/zh_cn/guide/mount_at_boot.md b/docs/zh_cn/guide/mount_at_boot.md
new file mode 100644
index 00000000..f9215d4d
--- /dev/null
+++ b/docs/zh_cn/guide/mount_at_boot.md
@@ -0,0 +1,109 @@
+---
+sidebar_label: 启动时自动挂载 JuiceFS
+sidebar_position: 2
+slug: /mount_juicefs_at_boot_time
+---
+
+# 启动时自动挂载 JuiceFS
+
+这是一篇关于如何在启动时自动挂载 JuiceFS 的指南。
+
+## Linux
+
+拷贝 `juicefs` 为 `/sbin/mount.juicefs`，然后按照下面的格式添加一行到 `/etc/fstab`：
+
+```
+<META-URL>    <MOUNTPOINT>       juicefs     _netdev[,<MOUNT-OPTIONS>]     0  0
+```
+
+`<META-URL>` 的格式请参考[「如何设置元数据引擎」](how_to_setup_metadata_engine.md)文档，比如 `redis://localhost:6379/1`。然后用你希望 JuiceFS 挂载的路径替换 `<MOUNTPOINT>` ，比如 `/jfs`。如果你想要设置[挂载选项](../reference/command_reference.md#juicefs-mount)，用逗号分隔选项列表并替换 `[,<MOUNT-OPTIONS>]` 。下面是一个示例：
+
+```
+redis://localhost:6379/1    /jfs       juicefs     _netdev,max-uploads=50,writeback,cache-size=204800     0  0
+```
+
+:::tip 提示
+默认情况下，CentOS 6 在启动后不会自动挂载网络文件系统，你可以使用下面的命令开启它：
+
+```bash
+sudo chkconfig --add netfs
+```
+:::
+
+## macOS
+
+在 `~/Library/LaunchAgents` 下创建名为 `io.juicefs.<NAME>.plist` 的文件。替换 `<NAME>` 为 JuiceFS 文件系统的名字。添加如下内容到文件中（再次替换 `NAME`、`PATH-TO-JUICEFS`、`META-URL`、`MOUNTPOINT` 和 `MOUNT-OPTIONS` 为适当的值）：
+
+```xml title="io.juicefs.<NAME>.plist"
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>Label</key>
+        <string>io.juicefs.NAME</string>
+        <key>ProgramArguments</key>
+        <array>
+                <string>PATH-TO-JUICEFS</string>
+                <string>mount</string>
+                <string>META-URL</string>
+                <string>MOUNTPOINT</string>
+                <string>MOUNT-OPTIONS</string>
+        </array>
+        <key>RunAtLoad</key>
+        <true/>
+</dict>
+</plist>
+```
+
+:::tip 提示
+如果有多个挂载选项可以分为多行依次设置，例如：
+
+```xml
+                <string>--max-uploads</string>
+                <string>50</string>
+                <string>--cache-size</string>
+                <string>204800</string>
+```
+:::
+
+使用以下命令加载上一步创建的文件，并测试加载是否成功。**请确保元数据引擎已正常运行。**
+
+```bash
+$ launchctl load ~/Library/LaunchAgents/io.juicefs.<NAME>.plist
+$ launchctl start ~/Library/LaunchAgents/io.juicefs.<NAME>
+$ ls <MOUNTPOINT>
+```
+
+如果挂载失败，可以将以下配置添加到 `io.juicefs.<NAME>.plist` 文件来调试：
+
+```xml title="io.juicefs.<NAME>.plist"
+        <key>StandardOutPath</key>
+        <string>/tmp/juicefs.out</string>
+        <key>StandardErrorPath</key>
+        <string>/tmp/juicefs.err</string>
+```
+
+使用以下命令重新加载最新的配置并检查输出：
+
+```bash
+$ launchctl unload ~/Library/LaunchAgents/io.juicefs.<NAME>.plist
+$ launchctl load ~/Library/LaunchAgents/io.juicefs.<NAME>.plist
+$ cat /tmp/juicefs.out
+$ cat /tmp/juicefs.err
+```
+
+如果你是使用 Homebrew 安装的 Redis 服务，你可以使用以下命令让其在机器启动时启动它：
+
+```bash
+brew services start redis
+```
+
+然后添加以下配置到 `io.juicefs.<NAME>.plist` 文件确保 Redis 服务已经启动：
+
+```xml title="io.juicefs.<NAME>.plist"
+        <key>KeepAlive</key>
+        <dict>
+                <key>OtherJobEnabled</key>
+                <string>homebrew.mxcl.redis</string>
+        </dict>
+```
diff --git a/docs/zh_cn/mount_at_boot.md b/docs/zh_cn/mount_at_boot.md
deleted file mode 100644
index bab25b31..00000000
--- a/docs/zh_cn/mount_at_boot.md
+++ /dev/null
@@ -1,90 +0,0 @@
-# Mount JuiceFS at Boot
-
-This is a guide about how to mount JuiceFS automatically at boot.
-
-## Linux
-
-Copy `juicefs` as `/sbin/mount.juicefs`, then edit `/etc/fstab` with following line:
-
-```
-<META-URL>    <MOUNTPOINT>       juicefs     _netdev[,<MOUNT-OPTIONS>]     0  0
-```
-
-The format of `<META-URL>` is `redis://<user>:<password>@<host>:<port>/<db>`, e.g. `redis://localhost:6379/1`. And replace `<MOUNTPOINT>` with specific path you wanna mount JuiceFS to, e.g. `/jfs`. If you need set [mount options](reference/command_reference.md#juicefs-mount), replace `[,<MOUNT-OPTIONS>]` with comma separated options list. The following line is an example:
-
-```
-redis://localhost:6379/1    /jfs       juicefs     _netdev,max-uploads=50,writeback,cache-size=2048     0  0
-```
-
-**Note: By default, CentOS 6 will NOT mount network file system after boot, run following command to enable it:**
-
-```bash
-sudo chkconfig --add netfs
-```
-
-## macOS
-
-Create a file named `io.juicefs.<NAME>.plist` under `~/Library/LaunchAgents`. Replace `<NAME>` with JuiceFS volume name. Add following contents to the file (again, replace `NAME`, `PATH-TO-JUICEFS`, `META-URL` and `MOUNTPOINT` with appropriate value):
-
-```xml
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-        <key>Label</key>
-        <string>io.juicefs.NAME</string>
-        <key>ProgramArguments</key>
-        <array>
-                <string>PATH-TO-JUICEFS</string>
-                <string>mount</string>
-                <string>META-URL</string>
-                <string>MOUNTPOINT</string>
-        </array>
-        <key>RunAtLoad</key>
-        <true/>
-</dict>
-</plist>
-```
-
-Use following commands to load the file created in the previous step and test whether the loading is successful. **Please ensure Redis server is already running.**
-
-```bash
-$ launchctl load ~/Library/LaunchAgents/io.juicefs.<NAME>.plist
-$ launchctl start ~/Library/LaunchAgents/io.juicefs.<NAME>
-$ ls <MOUNTPOINT>
-```
-
-If mount failed, you can add following configuration to `io.juicefs.<NAME>.plist` file for debug purpose:
-
-```xml
-        <key>StandardOutPath</key>
-        <string>/tmp/juicefs.out</string>
-        <key>StandardErrorPath</key>
-        <string>/tmp/juicefs.err</string>
-```
-
-Use following commands to reload the latest configuration and inspect the output:
-
-```bash
-$ launchctl unload ~/Library/LaunchAgents/io.juicefs.<NAME>.plist
-$ launchctl load ~/Library/LaunchAgents/io.juicefs.<NAME>.plist
-$ cat /tmp/juicefs.out
-$ cat /tmp/juicefs.err
-```
-
-If you install Redis server by Homebrew, you could use following command to start it at boot:
-
-```bash
-$ brew services start redis
-```
-
-Then add following configuration to `io.juicefs.<NAME>.plist` file for ensure Redis server is loaded:
-
-```xml
-        <key>KeepAlive</key>
-        <dict>
-                <key>OtherJobEnabled</key>
-                <string>homebrew.mxcl.redis</string>
-        </dict>
-```
-
diff --git a/pkg/chunk/cached_store.go b/pkg/chunk/cached_store.go
index 4bdf1b59..33985069 100644
--- a/pkg/chunk/cached_store.go
+++ b/pkg/chunk/cached_store.go
@@ -48,68 +48,68 @@ type pendingItem struct {
 	fpath string
 }
 
-// chunk for read only
-type rChunk struct {
+// slice for read and remove
+type rSlice struct {
 	id     uint64
 	length int
 	store  *cachedStore
 }
 
-func chunkForRead(id uint64, length int, store *cachedStore) *rChunk {
-	return &rChunk{id, length, store}
+func sliceForRead(id uint64, length int, store *cachedStore) *rSlice {
+	return &rSlice{id, length, store}
 }
 
-func (c *rChunk) blockSize(indx int) int {
-	bsize := c.length - indx*c.store.conf.BlockSize
-	if bsize > c.store.conf.BlockSize {
-		bsize = c.store.conf.BlockSize
+func (s *rSlice) blockSize(indx int) int {
+	bsize := s.length - indx*s.store.conf.BlockSize
+	if bsize > s.store.conf.BlockSize {
+		bsize = s.store.conf.BlockSize
 	}
 	return bsize
 }
 
-func (c *rChunk) key(indx int) string {
-	if c.store.conf.HashPrefix {
-		return fmt.Sprintf("chunks/%02X/%v/%v_%v_%v", c.id%256, c.id/1000/1000, c.id, indx, c.blockSize(indx))
+func (s *rSlice) key(indx int) string {
+	if s.store.conf.HashPrefix {
+		return fmt.Sprintf("chunks/%02X/%v/%v_%v_%v", s.id%256, s.id/1000/1000, s.id, indx, s.blockSize(indx))
 	}
-	return fmt.Sprintf("chunks/%v/%v/%v_%v_%v", c.id/1000/1000, c.id/1000, c.id, indx, c.blockSize(indx))
+	return fmt.Sprintf("chunks/%v/%v/%v_%v_%v", s.id/1000/1000, s.id/1000, s.id, indx, s.blockSize(indx))
 }
 
-func (c *rChunk) index(off int) int {
-	return off / c.store.conf.BlockSize
+func (s *rSlice) index(off int) int {
+	return off / s.store.conf.BlockSize
 }
 
-func (c *rChunk) keys() []string {
-	if c.length <= 0 {
+func (s *rSlice) keys() []string {
+	if s.length <= 0 {
 		return nil
 	}
-	lastIndx := (c.length - 1) / c.store.conf.BlockSize
+	lastIndx := (s.length - 1) / s.store.conf.BlockSize
 	keys := make([]string, lastIndx+1)
 	for i := 0; i <= lastIndx; i++ {
-		keys[i] = c.key(i)
+		keys[i] = s.key(i)
 	}
 	return keys
 }
 
-func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err error) {
+func (s *rSlice) ReadAt(ctx context.Context, page *Page, off int) (n int, err error) {
 	p := page.Data
 	if len(p) == 0 {
 		return 0, nil
 	}
-	if off >= c.length {
+	if off >= s.length {
 		return 0, io.EOF
 	}
 
-	indx := c.index(off)
-	boff := off % c.store.conf.BlockSize
-	blockSize := c.blockSize(indx)
+	indx := s.index(off)
+	boff := off % s.store.conf.BlockSize
+	blockSize := s.blockSize(indx)
 	if boff+len(p) > blockSize {
 		// read beyond currend page
 		var got int
 		for got < len(p) {
 			// aligned to current page
-			l := utils.Min(len(p)-got, c.blockSize(c.index(off))-off%c.store.conf.BlockSize)
+			l := utils.Min(len(p)-got, s.blockSize(s.index(off))-off%s.store.conf.BlockSize)
 			pp := page.Slice(got, l)
-			n, err = c.ReadAt(ctx, pp, off)
+			n, err = s.ReadAt(ctx, pp, off)
 			pp.Release()
 			if err != nil {
 				return got + n, err
@@ -123,17 +123,17 @@ func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err er
 		return got, nil
 	}
 
-	key := c.key(indx)
-	if c.store.conf.CacheSize > 0 {
+	key := s.key(indx)
+	if s.store.conf.CacheSize > 0 {
 		start := time.Now()
-		r, err := c.store.bcache.load(key)
+		r, err := s.store.bcache.load(key)
 		if err == nil {
 			n, err = r.ReadAt(p, int64(boff))
 			_ = r.Close()
 			if err == nil {
-				c.store.cacheHits.Add(1)
-				c.store.cacheHitBytes.Add(float64(n))
-				c.store.cacheReadHist.Observe(time.Since(start).Seconds())
+				s.store.cacheHits.Add(1)
+				s.store.cacheHitBytes.Add(float64(n))
+				s.store.cacheReadHist.Observe(time.Since(start).Seconds())
 				return n, nil
 			}
 			if f, ok := r.(*os.File); ok {
@@ -143,16 +143,16 @@ func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err er
 		}
 	}
 
-	c.store.cacheMiss.Add(1)
-	c.store.cacheMissBytes.Add(float64(len(p)))
+	s.store.cacheMiss.Add(1)
+	s.store.cacheMissBytes.Add(float64(len(p)))
 
-	if c.store.seekable && boff > 0 && len(p) <= blockSize/4 {
-		if c.store.downLimit != nil {
-			c.store.downLimit.Wait(int64(len(p)))
+	if s.store.seekable && boff > 0 && len(p) <= blockSize/4 {
+		if s.store.downLimit != nil {
+			s.store.downLimit.Wait(int64(len(p)))
 		}
 		// partial read
 		st := time.Now()
-		in, err := c.store.storage.Get(key, int64(boff), int64(len(p)))
+		in, err := s.store.storage.Get(key, int64(boff), int64(len(p)))
 		if err == nil {
 			n, err = io.ReadFull(in, p)
 			_ = in.Close()
@@ -162,17 +162,17 @@ func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err er
 		if used > SlowRequest {
 			logger.Infof("slow request: GET %s (%v, %.3fs)", key, err, used.Seconds())
 		}
-		c.store.objectDataBytes.WithLabelValues("GET").Add(float64(n))
-		c.store.objectReqsHistogram.WithLabelValues("GET").Observe(used.Seconds())
-		c.store.fetcher.fetch(key)
+		s.store.objectDataBytes.WithLabelValues("GET").Add(float64(n))
+		s.store.objectReqsHistogram.WithLabelValues("GET").Observe(used.Seconds())
+		s.store.fetcher.fetch(key)
 		if err == nil {
 			return n, nil
 		} else {
-			c.store.objectReqErrors.Add(1)
+			s.store.objectReqErrors.Add(1)
 		}
 	}
 
-	block, err := c.store.group.Execute(key, func() (*Page, error) {
+	block, err := s.store.group.Execute(key, func() (*Page, error) {
 		tmp := page
 		if boff > 0 || len(p) < blockSize {
 			tmp = NewOffPage(blockSize)
@@ -182,8 +182,8 @@ func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err er
 		tmp.Acquire()
 		err := utils.WithTimeout(func() error {
 			defer tmp.Release()
-			return c.store.load(key, tmp, c.store.shouldCache(blockSize), false)
-		}, c.store.conf.GetTimeout)
+			return s.store.load(key, tmp, s.store.shouldCache(blockSize), false)
+		}, s.store.conf.GetTimeout)
 		return tmp, err
 	})
 	defer block.Release()
@@ -196,48 +196,48 @@ func (c *rChunk) ReadAt(ctx context.Context, page *Page, off int) (n int, err er
 	return len(p), nil
 }
 
-func (c *rChunk) delete(indx int) error {
-	key := c.key(indx)
+func (s *rSlice) delete(indx int) error {
+	key := s.key(indx)
 	st := time.Now()
-	err := c.store.storage.Delete(key)
+	err := s.store.storage.Delete(key)
 	used := time.Since(st)
 	logger.Debugf("DELETE %v (%v, %.3fs)", key, err, used.Seconds())
 	if used > SlowRequest {
 		logger.Infof("slow request: DELETE %v (%v, %.3fs)", key, err, used.Seconds())
 	}
-	c.store.objectReqsHistogram.WithLabelValues("DELETE").Observe(used.Seconds())
+	s.store.objectReqsHistogram.WithLabelValues("DELETE").Observe(used.Seconds())
 	if err != nil {
-		c.store.objectReqErrors.Add(1)
+		s.store.objectReqErrors.Add(1)
 	}
 	return err
 }
 
-func (c *rChunk) Remove() error {
-	if c.length == 0 {
+func (s *rSlice) Remove() error {
+	if s.length == 0 {
 		// no block
 		return nil
 	}
 
-	lastIndx := (c.length - 1) / c.store.conf.BlockSize
+	lastIndx := (s.length - 1) / s.store.conf.BlockSize
 	for i := 0; i <= lastIndx; i++ {
 		// there could be multiple clients try to remove the same chunk in the same time,
 		// any of them should succeed if any blocks is removed
-		key := c.key(i)
-		c.store.removePending(key)
-		c.store.bcache.remove(key)
+		key := s.key(i)
+		s.store.removePending(key)
+		s.store.bcache.remove(key)
 	}
 
-	if c.store.conf.MaxDeletes == 0 {
+	if s.store.conf.MaxDeletes == 0 {
 		return errors.New("skip deleting objects because MaxDeletes is 0")
 	}
 	var err error
-	c.store.currentDelete <- struct{}{}
+	s.store.currentDelete <- struct{}{}
 	for i := 0; i <= lastIndx; i++ {
-		if e := c.delete(i); e != nil {
+		if e := s.delete(i); e != nil {
 			err = e
 		}
 	}
-	<-c.store.currentDelete
+	<-s.store.currentDelete
 	return err
 }
 
@@ -267,9 +267,9 @@ func freePage(p *Page) {
 	}
 }
 
-// chunk for write only
-type wChunk struct {
-	rChunk
+// slice for write only
+type wSlice struct {
+	rSlice
 	pages       [][]*Page
 	uploaded    int
 	errors      chan error
@@ -277,48 +277,48 @@ type wChunk struct {
 	pendings    int
 }
 
-func chunkForWrite(id uint64, store *cachedStore) *wChunk {
-	return &wChunk{
-		rChunk: rChunk{id, 0, store},
+func sliceForWrite(id uint64, store *cachedStore) *wSlice {
+	return &wSlice{
+		rSlice: rSlice{id, 0, store},
 		pages:  make([][]*Page, chunkSize/store.conf.BlockSize),
 		errors: make(chan error, chunkSize/store.conf.BlockSize),
 	}
 }
 
-func (c *wChunk) SetID(id uint64) {
-	c.id = id
+func (s *wSlice) SetID(id uint64) {
+	s.id = id
 }
 
-func (c *wChunk) WriteAt(p []byte, off int64) (n int, err error) {
+func (s *wSlice) WriteAt(p []byte, off int64) (n int, err error) {
 	if int(off)+len(p) > chunkSize {
 		return 0, fmt.Errorf("write out of chunk boudary: %d > %d", int(off)+len(p), chunkSize)
 	}
-	if off < int64(c.uploaded) {
-		return 0, fmt.Errorf("Cannot overwrite uploaded block: %d < %d", off, c.uploaded)
+	if off < int64(s.uploaded) {
+		return 0, fmt.Errorf("Cannot overwrite uploaded block: %d < %d", off, s.uploaded)
 	}
 
 	// Fill previous blocks with zeros
-	if c.length < int(off) {
-		zeros := make([]byte, int(off)-c.length)
-		_, _ = c.WriteAt(zeros, int64(c.length))
+	if s.length < int(off) {
+		zeros := make([]byte, int(off)-s.length)
+		_, _ = s.WriteAt(zeros, int64(s.length))
 	}
 
 	for n < len(p) {
-		indx := c.index(int(off) + n)
-		boff := (int(off) + n) % c.store.conf.BlockSize
+		indx := s.index(int(off) + n)
+		boff := (int(off) + n) % s.store.conf.BlockSize
 		var bs = pageSize
-		if indx > 0 || bs > c.store.conf.BlockSize {
-			bs = c.store.conf.BlockSize
+		if indx > 0 || bs > s.store.conf.BlockSize {
+			bs = s.store.conf.BlockSize
 		}
 		bi := boff / bs
 		bo := boff % bs
 		var page *Page
-		if bi < len(c.pages[indx]) {
-			page = c.pages[indx][bi]
+		if bi < len(s.pages[indx]) {
+			page = s.pages[indx][bi]
 		} else {
 			page = allocPage(bs)
 			page.Data = page.Data[:0]
-			c.pages[indx] = append(c.pages[indx], page)
+			s.pages[indx] = append(s.pages[indx], page)
 		}
 		left := len(p) - n
 		if bo+left > bs {
@@ -328,8 +328,8 @@ func (c *wChunk) WriteAt(p []byte, off int64) (n int, err error) {
 		}
 		n += copy(page.Data[bo:], p[n:])
 	}
-	if int(off)+n > c.length {
-		c.length = int(off) + n
+	if int(off)+n > s.length {
+		s.length = int(off) + n
 	}
 	return n, nil
 }
@@ -357,8 +357,8 @@ func (store *cachedStore) put(key string, p *Page) error {
 	}, store.conf.PutTimeout)
 }
 
-func (store *cachedStore) upload(key string, block *Page, c *wChunk) error {
-	sync := c != nil
+func (store *cachedStore) upload(key string, block *Page, s *wSlice) error {
+	sync := s != nil
 	blen := len(block.Data)
 	bufSize := store.compressor.CompressBound(blen)
 	var buf *Page
@@ -386,7 +386,7 @@ func (store *cachedStore) upload(key string, block *Page, c *wChunk) error {
 	}
 	for ; try < max; try++ {
 		time.Sleep(time.Second * time.Duration(try*try))
-		if c != nil && c.uploadError != nil {
+		if s != nil && s.uploadError != nil {
 			err = fmt.Errorf("(cancelled) upload block %s: %s (after %d tries)", key, err, try)
 			break
 		}
@@ -401,12 +401,12 @@ func (store *cachedStore) upload(key string, block *Page, c *wChunk) error {
 	return err
 }
 
-func (c *wChunk) upload(indx int) {
-	blen := c.blockSize(indx)
-	key := c.key(indx)
-	pages := c.pages[indx]
-	c.pages[indx] = nil
-	c.pendings++
+func (s *wSlice) upload(indx int) {
+	blen := s.blockSize(indx)
+	key := s.key(indx)
+	pages := s.pages[indx]
+	s.pages[indx] = nil
+	s.pendings++
 
 	go func() {
 		var block *Page
@@ -424,96 +424,96 @@ func (c *wChunk) upload(indx int) {
 		if off != blen {
 			panic(fmt.Sprintf("block length does not match: %v != %v", off, blen))
 		}
-		if c.store.conf.Writeback {
-			stagingPath, err := c.store.bcache.stage(key, block.Data, c.store.shouldCache(blen))
+		if s.store.conf.Writeback {
+			stagingPath, err := s.store.bcache.stage(key, block.Data, s.store.shouldCache(blen))
 			if err != nil {
 				logger.Warnf("write %s to disk: %s, upload it directly", stagingPath, err)
 			} else {
-				c.errors <- nil
-				if c.store.conf.UploadDelay == 0 {
+				s.errors <- nil
+				if s.store.conf.UploadDelay == 0 {
 					select {
-					case c.store.currentUpload <- true:
-						defer func() { <-c.store.currentUpload }()
-						if err = c.store.upload(key, block, nil); err == nil {
-							c.store.bcache.uploaded(key, blen)
+					case s.store.currentUpload <- true:
+						defer func() { <-s.store.currentUpload }()
+						if err = s.store.upload(key, block, nil); err == nil {
+							s.store.bcache.uploaded(key, blen)
 							if os.Remove(stagingPath) == nil {
-								if m, ok := c.store.bcache.(*cacheManager); ok {
+								if m, ok := s.store.bcache.(*cacheManager); ok {
 									m.stageBlocks.Sub(1)
 									m.stageBlockBytes.Sub(float64(blen))
 								}
 							}
 						} else { // add to delay list and wait for later scanning
-							c.store.addDelayedStaging(key, stagingPath, time.Now().Add(time.Second*30), false)
+							s.store.addDelayedStaging(key, stagingPath, time.Now().Add(time.Second*30), false)
 						}
 						return
 					default:
 					}
 				}
 				block.Release()
-				c.store.addDelayedStaging(key, stagingPath, time.Now(), c.store.conf.UploadDelay == 0)
+				s.store.addDelayedStaging(key, stagingPath, time.Now(), s.store.conf.UploadDelay == 0)
 				return
 			}
 		}
-		c.store.currentUpload <- true
-		defer func() { <-c.store.currentUpload }()
-		c.errors <- c.store.upload(key, block, c)
+		s.store.currentUpload <- true
+		defer func() { <-s.store.currentUpload }()
+		s.errors <- s.store.upload(key, block, s)
 	}()
 }
 
-func (c *wChunk) ID() uint64 {
-	return c.id
+func (s *wSlice) ID() uint64 {
+	return s.id
 }
 
-func (c *wChunk) Len() int {
-	return c.length
+func (s *wSlice) Len() int {
+	return s.length
 }
 
-func (c *wChunk) FlushTo(offset int) error {
-	if offset < c.uploaded {
-		panic(fmt.Sprintf("Invalid offset: %d < %d", offset, c.uploaded))
+func (s *wSlice) FlushTo(offset int) error {
+	if offset < s.uploaded {
+		panic(fmt.Sprintf("Invalid offset: %d < %d", offset, s.uploaded))
 	}
-	for i, block := range c.pages {
-		start := i * c.store.conf.BlockSize
-		end := start + c.store.conf.BlockSize
-		if start >= c.uploaded && end <= offset {
+	for i, block := range s.pages {
+		start := i * s.store.conf.BlockSize
+		end := start + s.store.conf.BlockSize
+		if start >= s.uploaded && end <= offset {
 			if block != nil {
-				c.upload(i)
+				s.upload(i)
 			}
-			c.uploaded = end
+			s.uploaded = end
 		}
 	}
 
 	return nil
 }
 
-func (c *wChunk) Finish(length int) error {
-	if c.length != length {
-		return fmt.Errorf("Length mismatch: %v != %v", c.length, length)
+func (s *wSlice) Finish(length int) error {
+	if s.length != length {
+		return fmt.Errorf("Length mismatch: %v != %v", s.length, length)
 	}
 
-	n := (length-1)/c.store.conf.BlockSize + 1
-	if err := c.FlushTo(n * c.store.conf.BlockSize); err != nil {
+	n := (length-1)/s.store.conf.BlockSize + 1
+	if err := s.FlushTo(n * s.store.conf.BlockSize); err != nil {
 		return err
 	}
-	for i := 0; i < c.pendings; i++ {
-		if err := <-c.errors; err != nil {
-			c.uploadError = err
+	for i := 0; i < s.pendings; i++ {
+		if err := <-s.errors; err != nil {
+			s.uploadError = err
 			return err
 		}
 	}
 	return nil
 }
 
-func (c *wChunk) Abort() {
-	for i := range c.pages {
-		for _, b := range c.pages[i] {
+func (s *wSlice) Abort() {
+	for i := range s.pages {
+		for _, b := range s.pages[i] {
 			freePage(b)
 		}
-		c.pages[i] = nil
+		s.pages[i] = nil
 	}
 	// delete uploaded blocks
-	c.length = c.uploaded
-	_ = c.Remove()
+	s.length = s.uploaded
+	_ = s.Remove()
 }
 
 // Config contains options for cachedStore
@@ -879,21 +879,21 @@ func (store *cachedStore) uploader() {
 	}
 }
 
-func (store *cachedStore) NewReader(chunkid uint64, length int) Reader {
-	return chunkForRead(chunkid, length, store)
+func (store *cachedStore) NewReader(id uint64, length int) Reader {
+	return sliceForRead(id, length, store)
 }
 
-func (store *cachedStore) NewWriter(chunkid uint64) Writer {
-	return chunkForWrite(chunkid, store)
+func (store *cachedStore) NewWriter(id uint64) Writer {
+	return sliceForWrite(id, store)
 }
 
-func (store *cachedStore) Remove(chunkid uint64, length int) error {
-	r := chunkForRead(chunkid, length, store)
+func (store *cachedStore) Remove(id uint64, length int) error {
+	r := sliceForRead(id, length, store)
 	return r.Remove()
 }
 
-func (store *cachedStore) FillCache(chunkid uint64, length uint32) error {
-	r := chunkForRead(chunkid, int(length), store)
+func (store *cachedStore) FillCache(id uint64, length uint32) error {
+	r := sliceForRead(id, int(length), store)
 	keys := r.keys()
 	var err error
 	for _, k := range keys {
diff --git a/pkg/chunk/chunk.go b/pkg/chunk/chunk.go
index 8ee75a1d..29bcff8e 100644
--- a/pkg/chunk/chunk.go
+++ b/pkg/chunk/chunk.go
@@ -28,16 +28,16 @@ type Reader interface {
 type Writer interface {
 	io.WriterAt
 	ID() uint64
-	SetID(chunkid uint64)
+	SetID(id uint64)
 	FlushTo(offset int) error
 	Finish(length int) error
 	Abort()
 }
 
 type ChunkStore interface {
-	NewReader(chunkid uint64, length int) Reader
-	NewWriter(chunkid uint64) Writer
-	Remove(chunkid uint64, length int) error
-	FillCache(chunkid uint64, length uint32) error
+	NewReader(id uint64, length int) Reader
+	NewWriter(id uint64) Writer
+	Remove(id uint64, length int) error
+	FillCache(id uint64, length uint32) error
 	UsedMemory() int64
 }
diff --git a/pkg/fuse/fuse.go b/pkg/fuse/fuse.go
index 4794c418..b3ce3fc6 100644
--- a/pkg/fuse/fuse.go
+++ b/pkg/fuse/fuse.go
@@ -75,9 +75,7 @@ func (fs *fileSystem) replyEntry(ctx *fuseContext, out *fuse.EntryOut, e *meta.E
 	} else {
 		out.SetEntryTimeout(fs.conf.EntryTimeout)
 	}
-	fs.replyAttr(ctx, e, &out.Attr, func(d time.Duration) {
-		out.SetAttrTimeout(d)
-	})
+	fs.replyAttr(ctx, e, &out.Attr, out.SetAttrTimeout)
 	return 0
 }
 
@@ -102,9 +100,7 @@ func (fs *fileSystem) GetAttr(cancel <-chan struct{}, in *fuse.GetAttrIn, out *f
 	if err != 0 {
 		return fuse.Status(err)
 	}
-	fs.replyAttr(ctx, entry, &out.Attr, func(d time.Duration) {
-		out.SetTimeout(d)
-	})
+	fs.replyAttr(ctx, entry, &out.Attr, out.SetTimeout)
 	return 0
 }
 
@@ -119,9 +115,7 @@ func (fs *fileSystem) SetAttr(cancel <-chan struct{}, in *fuse.SetAttrIn, out *f
 	if err != 0 {
 		return fuse.Status(err)
 	}
-	fs.replyAttr(ctx, entry, &out.Attr, func(d time.Duration) {
-		out.SetTimeout(d)
-	})
+	fs.replyAttr(ctx, entry, &out.Attr, out.SetTimeout)
 	return 0
 }
 
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index 0d9d2cee..a931a941 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -34,7 +34,7 @@ import (
 
 const (
 	inodeBatch    = 100
-	chunkIDBatch  = 1000
+	sliceIdBatch  = 1000
 	minUpdateTime = time.Millisecond * 10
 	nlocks        = 1024
 )
@@ -59,7 +59,7 @@ type engine interface {
 	doDeleteFileData(inode Ino, length uint64)
 	doCleanupSlices()
 	doCleanupDelayedSlices(edge int64, limit int) (int, error)
-	doDeleteSlice(chunkid uint64, size uint32) error
+	doDeleteSlice(id uint64, size uint32) error
 
 	doGetAttr(ctx Context, inode Ino, attr *Attr) syscall.Errno
 	doLookup(ctx Context, parent Ino, name string, inode *Ino, attr *Attr) syscall.Errno
@@ -101,7 +101,7 @@ type baseMeta struct {
 
 	freeMu     sync.Mutex
 	freeInodes freeID
-	freeChunks freeID
+	freeSlices freeID
 
 	usedSpaceG  prometheus.Gauge
 	usedInodesG prometheus.Gauge
@@ -288,8 +288,11 @@ func (m *baseMeta) refreshSession() {
 		}
 		m.en.doRefreshSession()
 		m.Unlock()
+		old := m.fmt.UUID
 		if _, err := m.Load(false); err != nil {
 			logger.Warnf("reload setting: %s", err)
+		} else if m.fmt.UUID != old {
+			logger.Fatalf("UUID changed from %s to %s", old, m.fmt.UUID)
 		}
 		if m.conf.NoBGJob {
 			continue
@@ -568,9 +571,9 @@ func (m *baseMeta) marshal(attr *Attr) []byte {
 	return w.Bytes()
 }
 
-func (m *baseMeta) encodeDelayedSlice(chunkid uint64, size uint32) []byte {
+func (m *baseMeta) encodeDelayedSlice(id uint64, size uint32) []byte {
 	w := utils.NewBuffer(8 + 4)
-	w.Put64(chunkid)
+	w.Put64(id)
 	w.Put32(size)
 	return w.Bytes()
 }
@@ -580,7 +583,7 @@ func (m *baseMeta) decodeDelayedSlices(buf []byte, ss *[]Slice) {
 		return
 	}
 	for rb := utils.FromBuffer(buf); rb.HasMore(); {
-		*ss = append(*ss, Slice{Chunkid: rb.Get64(), Size: rb.Get32()})
+		*ss = append(*ss, Slice{Id: rb.Get64(), Size: rb.Get32()})
 	}
 }
 
@@ -840,19 +843,19 @@ func (m *baseMeta) InvalidateChunkCache(ctx Context, inode Ino, indx uint32) sys
 	return 0
 }
 
-func (m *baseMeta) NewChunk(ctx Context, chunkid *uint64) syscall.Errno {
+func (m *baseMeta) NewSlice(ctx Context, id *uint64) syscall.Errno {
 	m.freeMu.Lock()
 	defer m.freeMu.Unlock()
-	if m.freeChunks.next >= m.freeChunks.maxid {
-		v, err := m.en.incrCounter("nextChunk", chunkIDBatch)
+	if m.freeSlices.next >= m.freeSlices.maxid {
+		v, err := m.en.incrCounter("nextChunk", sliceIdBatch)
 		if err != nil {
 			return errno(err)
 		}
-		m.freeChunks.next = uint64(v) - chunkIDBatch
-		m.freeChunks.maxid = uint64(v)
+		m.freeSlices.next = uint64(v) - sliceIdBatch
+		m.freeSlices.maxid = uint64(v)
 	}
-	*chunkid = m.freeChunks.next
-	m.freeChunks.next++
+	*id = m.freeSlices.next
+	m.freeSlices.next++
 	return 0
 }
 
@@ -955,16 +958,16 @@ func (m *baseMeta) tryDeleteFileData(inode Ino, length uint64) {
 	}
 }
 
-func (m *baseMeta) deleteSlice(chunkid uint64, size uint32) {
-	if chunkid == 0 {
+func (m *baseMeta) deleteSlice(id uint64, size uint32) {
+	if id == 0 {
 		return
 	}
-	if err := m.newMsg(DeleteChunk, chunkid, size); err == nil || strings.Contains(err.Error(), "NoSuchKey") || strings.Contains(err.Error(), "not found") {
-		if err = m.en.doDeleteSlice(chunkid, size); err != nil {
-			logger.Errorf("delete slice %d: %s", chunkid, err)
+	if err := m.newMsg(DeleteSlice, id, size); err == nil || strings.Contains(err.Error(), "NoSuchKey") || strings.Contains(err.Error(), "not found") {
+		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 chunk %d (%d bytes): %s", chunkid, size, err)
+		logger.Warnf("delete slice %d (%d bytes): %s", id, size, err)
 	}
 }
 
diff --git a/pkg/meta/dump.go b/pkg/meta/dump.go
index 34176db5..0c9fa854 100644
--- a/pkg/meta/dump.go
+++ b/pkg/meta/dump.go
@@ -71,7 +71,8 @@ type DumpedAttr struct {
 }
 
 type DumpedSlice struct {
-	Chunkid uint64 `json:"chunkid"`
+	Chunkid uint64 `json:"chunkid,omitempty"`
+	Id      uint64 `json:"id"`
 	Pos     uint32 `json:"pos,omitempty"`
 	Size    uint32 `json:"size"`
 	Off     uint32 `json:"off,omitempty"`
@@ -344,7 +345,7 @@ func loadEntries(r io.Reader, load func(*DumpedEntry), addChunk func(*chunkKey))
 		case "DelFiles":
 			err = dec.Decode(&dm.DelFiles)
 		case "FSTree":
-			_, err = decodeEntry(dec, 1, counters, parents, refs, bar, load, addChunk)
+			_, err = decodeEntry(dec, 0, counters, parents, refs, bar, load, addChunk)
 		case "Trash":
 			_, err = decodeEntry(dec, 1, counters, parents, refs, bar, load, addChunk)
 		}
@@ -375,6 +376,10 @@ func decodeEntry(dec *json.Decoder, parent Ino, cs *DumpedCounters, parents map[
 		case "attr":
 			err = dec.Decode(&e.Attr)
 			if err == nil {
+				if parent == 0 {
+					parent = 1
+					e.Attr.Inode = 1 // fix loading from subdir
+				}
 				inode := e.Attr.Inode
 				if typeFromString(e.Attr.Type) == TypeDirectory {
 					e.Attr.Nlink = 2
@@ -404,13 +409,17 @@ func decodeEntry(dec *json.Decoder, parent Ino, cs *DumpedCounters, parents map[
 			if err == nil && len(e.Parents) == 1 {
 				for _, c := range e.Chunks {
 					for _, s := range c.Slices {
-						ck := chunkKey{s.Chunkid, s.Size}
+						if s.Chunkid != 0 && s.Id == 0 {
+							s.Id = s.Chunkid
+							s.Chunkid = 0
+						}
+						ck := chunkKey{s.Id, s.Size}
 						refs[ck]++
 						if addChunk != nil && refs[ck] == 1 {
 							addChunk(&ck)
 						}
-						if cs.NextChunk <= int64(s.Chunkid) {
-							cs.NextChunk = int64(s.Chunkid) + 1
+						if cs.NextChunk <= int64(s.Id) {
+							cs.NextChunk = int64(s.Id) + 1
 						}
 					}
 				}
diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go
index 606a3f3c..6b52d126 100644
--- a/pkg/meta/interface.go
+++ b/pkg/meta/interface.go
@@ -34,8 +34,8 @@ import (
 const (
 	// ChunkSize is size of a chunk
 	ChunkSize = 1 << 26 // 64M
-	// DeleteChunk is a message to delete a chunk from object store.
-	DeleteChunk = 1000
+	// DeleteSlice is a message to delete a slice from object store.
+	DeleteSlice = 1000
 	// CompactChunk is a message to compact a chunk in object store.
 	CompactChunk = 1001
 	// Rmr is a message to remove a directory recursively.
@@ -195,10 +195,10 @@ type Entry struct {
 // Slice is a slice of a chunk.
 // Multiple slices could be combined together as a chunk.
 type Slice struct {
-	Chunkid uint64
-	Size    uint32
-	Off     uint32
-	Len     uint32
+	Id   uint64
+	Size uint32
+	Off  uint32
+	Len  uint32
 }
 
 // Summary represents the total number of files/directories and
@@ -308,9 +308,9 @@ type Meta interface {
 	// Close a file.
 	Close(ctx Context, inode Ino) syscall.Errno
 	// Read returns the list of slices on the given chunk.
-	Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) syscall.Errno
-	// NewChunk returns a new id for new data.
-	NewChunk(ctx Context, chunkid *uint64) syscall.Errno
+	Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) syscall.Errno
+	// NewSlice returns an id for new slice.
+	NewSlice(ctx Context, id *uint64) syscall.Errno
 	// Write put a slice of data on top of the given chunk.
 	Write(ctx Context, inode Ino, indx uint32, off uint32, slice Slice) syscall.Errno
 	// InvalidateChunkCache invalidate chunk cache
diff --git a/pkg/meta/metadata-sub.sample b/pkg/meta/metadata-sub.sample
index ee69ea67..3321f24d 100644
--- a/pkg/meta/metadata-sub.sample
+++ b/pkg/meta/metadata-sub.sample
@@ -35,13 +35,13 @@
       "big": {
         "attr": {"inode":6,"type":"regular","mode":420,"uid":501,"gid":0,"atime":1637150857,"mtime":1637150858,"ctime":1637150878,"atimensec":961503000,"mtimensec":961503000,"ctimensec":961503000,"nlink":1,"length":104857600},
         "chunks": [
-          {"index":0,"slices":[{"chunkid":5,"size":67108864,"len":67108864}]},
-          {"index":1,"slices":[{"chunkid":6,"size":37748736,"len":37748736}]}
+          {"index":0,"slices":[{"id":5,"size":67108864,"len":67108864}]},
+          {"index":1,"slices":[{"id":6,"size":37748736,"len":37748736}]}
         ]
       },
       "f11": {
         "attr": {"inode":4,"type":"regular","mode":420,"uid":501,"gid":20,"atime":1623746610,"mtime":1623746610,"ctime":1623746639,"atimensec":591590000,"mtimensec":591590000,"ctimensec":591590000,"nlink":2,"length":12},
-        "chunks": [{"index":0,"slices":[{"chunkid":2,"size":12,"len":12}]}]
+        "chunks": [{"index":0,"slices":[{"id":2,"size":12,"len":12}]}]
       }
     }
   }
diff --git a/pkg/meta/metadata.sample b/pkg/meta/metadata.sample
index a49cf548..87868412 100644
--- a/pkg/meta/metadata.sample
+++ b/pkg/meta/metadata.sample
@@ -55,24 +55,24 @@
           "big": {
             "attr": {"inode":6,"type":"regular","mode":420,"uid":501,"gid":0,"atime":1637150857,"mtime":1637150858,"ctime":1637150878,"atimensec":961503000,"mtimensec":961503000,"ctimensec":961503000,"nlink":1,"length":104857600},
             "chunks": [
-              {"index":0,"slices":[{"chunkid":5,"size":67108864,"len":67108864}]},
-              {"index":1,"slices":[{"chunkid":6,"size":37748736,"len":37748736}]}
+              {"index":0,"slices":[{"id":5,"size":67108864,"len":67108864}]},
+              {"index":1,"slices":[{"id":6,"size":37748736,"len":37748736}]}
             ]
           },
           "f11": {
             "attr": {"inode":4,"type":"regular","mode":420,"uid":501,"gid":20,"atime":1623746610,"mtime":1623746610,"ctime":1623746639,"atimensec":591590000,"mtimensec":591590000,"ctimensec":591590000,"nlink":2,"length":12},
-            "chunks": [{"index":0,"slices":[{"chunkid":2,"size":12,"len":12}]}]
+            "chunks": [{"index":0,"slices":[{"id":2,"size":12,"len":12}]}]
           }
         }
       },
       "f1": {
         "attr": {"inode":2,"type":"regular","mode":420,"uid":501,"gid":20,"atime":1623746580,"mtime":1623746661,"ctime":1623746661,"atimensec":219686000,"mtimensec":219686000,"ctimensec":219686000,"nlink":1,"length":24},
         "xattrs": [{"name":"k","value":"v"}],
-        "chunks": [{"index":0,"slices":[{"chunkid":1,"size":6,"len":6},{"chunkid":2,"size":12,"len":12},{"chunkid":4,"size":24,"len":24}]}]
+        "chunks": [{"index":0,"slices":[{"id":1,"size":6,"len":6},{"id":2,"size":12,"len":12},{"id":4,"size":24,"len":24}]}]
       },
       "l1": {
         "attr": {"inode":4,"type":"regular","mode":420,"uid":501,"gid":20,"atime":1623746610,"mtime":1623746610,"ctime":1623746639,"atimensec":591590000,"mtimensec":591590000,"ctimensec":591590000,"nlink":2,"length":12},
-        "chunks": [{"index":0,"slices":[{"chunkid":2,"size":12,"len":12}]}]
+        "chunks": [{"index":0,"slices":[{"id":2,"size":12,"len":12}]}]
       },
       "s1": {
         "attr": {"inode":5,"type":"symlink","mode":420,"uid":501,"gid":20,"atime":1623746645,"mtime":1623746645,"ctime":1623746645,"atimensec":984144000,"mtimensec":984144000,"ctimensec":984144000,"nlink":1,"length":0},
@@ -92,7 +92,7 @@
         "entries": {
           "1-27-tf1": {
             "attr": {"inode":27,"type":"regular","mode":420,"uid":501,"gid":0,"atime":1638437852,"mtime":1638437852,"ctime":1638437877,"atimensec":28186000,"mtimensec":28186000,"ctimensec":28186000,"nlink":1,"length":11},
-            "chunks": [{"index":0,"slices":[{"chunkid":7,"size":11,"len":11}]}]
+            "chunks": [{"index":0,"slices":[{"id":7,"size":11,"len":11}]}]
           },
           "1-28-td1": {
             "attr": {"inode":28,"type":"directory","mode":493,"uid":501,"gid":0,"atime":1638437856,"mtime":1638437879,"ctime":1638437879,"atimensec":59246000,"mtimensec":59246000,"ctimensec":59246000,"nlink":2,"length":0},
@@ -101,7 +101,7 @@
           },
           "28-29-tdf1": {
             "attr": {"inode":29,"type":"regular","mode":420,"uid":501,"gid":0,"atime":1638437873,"mtime":1638437873,"ctime":1638437879,"atimensec":449880000,"mtimensec":449880000,"ctimensec":449880000,"nlink":1,"length":10485760},
-            "chunks": [{"index":0,"slices":[{"chunkid":8,"size":10485760,"len":10485760}]}]
+            "chunks": [{"index":0,"slices":[{"id":8,"size":10485760,"len":10485760}]}]
           }
         }
       }
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 5bbb0ddb..b239efc9 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -61,7 +61,7 @@ import (
 	locked: locked$sid -> { lockf$inode or lockp$inode }
 
 	Removed files: delfiles -> [$inode:$length -> seconds]
-	Slices refs: k$chunkid_$size -> refcount
+	Slices refs: k$sliceId_$size -> refcount
 
 	Redis features:
 	  Sorted Set: 1.2+
@@ -210,8 +210,8 @@ func (m *redisMeta) Shutdown() error {
 	return m.rdb.Close()
 }
 
-func (m *redisMeta) doDeleteSlice(chunkid uint64, size uint32) error {
-	return m.rdb.HDel(Background, m.sliceRefs(), m.sliceKey(chunkid, size)).Err()
+func (m *redisMeta) doDeleteSlice(id uint64, size uint32) error {
+	return m.rdb.HDel(Background, m.sliceRefs(), m.sliceKey(id, size)).Err()
 }
 
 func (m *redisMeta) Name() string {
@@ -495,9 +495,9 @@ func (m *redisMeta) chunkKey(inode Ino, indx uint32) string {
 	return m.prefix + "c" + inode.String() + "_" + strconv.FormatInt(int64(indx), 10)
 }
 
-func (m *redisMeta) sliceKey(chunkid uint64, size uint32) string {
+func (m *redisMeta) sliceKey(id uint64, size uint32) string {
 	// inside hashset
-	return "k" + strconv.FormatUint(chunkid, 10) + "_" + strconv.FormatUint(uint64(size), 10)
+	return "k" + strconv.FormatUint(id, 10) + "_" + strconv.FormatUint(uint64(size), 10)
 }
 
 func (m *redisMeta) xattrKey(inode Ino) string {
@@ -802,6 +802,7 @@ func (m *redisMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64,
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFF) }()
 	var newSpace int64
 	err := m.txn(ctx, func(tx *redis.Tx) error {
+		newSpace = 0
 		var t Attr
 		a, err := tx.Get(ctx, m.inodeKey(inode)).Bytes()
 		if err != nil {
@@ -916,6 +917,7 @@ func (m *redisMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, si
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFF) }()
 	var newSpace int64
 	err := m.txn(ctx, func(tx *redis.Tx) error {
+		newSpace = 0
 		var t Attr
 		a, err := tx.Get(ctx, m.inodeKey(inode)).Bytes()
 		if err != nil {
@@ -948,7 +950,8 @@ func (m *redisMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, si
 		t.Ctimensec = uint32(now.Nanosecond())
 		_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
 			pipe.Set(ctx, m.inodeKey(inode), m.marshal(&t), 0)
-			if mode&(fallocZeroRange|fallocPunchHole) != 0 {
+			if mode&(fallocZeroRange|fallocPunchHole) != 0 && off < old {
+				off, size := off, size
 				if off+size > old {
 					size = old - off
 				}
@@ -1184,19 +1187,21 @@ func (m *redisMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, m
 }
 
 func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
-	var _type uint8
 	var trash, inode Ino
-	keys := []string{m.entryKey(parent), m.inodeKey(parent)}
 	if st := m.checkTrash(parent, &trash); st != 0 {
 		return st
 	}
 	if trash == 0 {
 		defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFE) }()
 	}
+	var _type uint8
 	var opened bool
 	var attr Attr
 	var newSpace, newInode int64
 	err := m.txn(ctx, func(tx *redis.Tx) error {
+		opened = false
+		attr = Attr{}
+		newSpace, newInode = 0, 0
 		buf, err := tx.HGet(ctx, m.entryKey(parent), name).Bytes()
 		if err == redis.Nil && m.conf.CaseInsensi {
 			if e := m.resolveCase(ctx, parent, name); e != nil {
@@ -1233,8 +1238,6 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 			pattr.Ctimensec = uint32(now.Nanosecond())
 			updateParent = true
 		}
-		attr = Attr{}
-		opened = false
 		if rs[1] != nil {
 			m.parseAttr([]byte(rs[1].(string)), &attr)
 			if ctx.Uid() != 0 && pattr.Mode&01000 != 0 && ctx.Uid() != pattr.Uid && ctx.Uid() != attr.Uid {
@@ -1302,7 +1305,7 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 		})
 
 		return err
-	}, keys...)
+	}, m.entryKey(parent), m.inodeKey(parent))
 	if err == nil && trash == 0 {
 		if _type == TypeFile && attr.Nlink == 0 {
 			m.fileDeleted(opened, inode, attr.Length)
@@ -1313,9 +1316,7 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 }
 
 func (m *redisMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno {
-	var typ uint8
-	var trash, inode Ino
-	keys := []string{m.inodeKey(parent), m.entryKey(parent)}
+	var trash Ino
 	if st := m.checkTrash(parent, &trash); st != 0 {
 		return st
 	}
@@ -1331,7 +1332,7 @@ func (m *redisMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno
 		if err != nil {
 			return err
 		}
-		typ, inode = m.parseEntry(buf)
+		typ, inode := m.parseEntry(buf)
 		if typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
@@ -1394,7 +1395,7 @@ func (m *redisMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno
 			return nil
 		})
 		return err
-	}, keys...)
+	}, m.inodeKey(parent), m.entryKey(parent))
 	if err == nil && trash == 0 {
 		m.updateStats(-align4K(0), -1)
 	}
@@ -1403,13 +1404,16 @@ func (m *redisMeta) doRmdir(ctx Context, parent Ino, name string) syscall.Errno
 
 func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst Ino, nameDst string, flags uint32, inode *Ino, attr *Attr) syscall.Errno {
 	exchange := flags == RenameExchange
-	keys := []string{m.entryKey(parentSrc), m.inodeKey(parentSrc), m.entryKey(parentDst), m.inodeKey(parentDst)}
 	var opened bool
 	var trash, dino Ino
 	var dtyp uint8
 	var tattr Attr
 	var newSpace, newInode int64
 	err := m.txn(ctx, func(tx *redis.Tx) error {
+		opened = false
+		dino, dtyp = 0, 0
+		tattr = Attr{}
+		newSpace, newInode = 0, 0
 		buf, err := tx.HGet(ctx, m.entryKey(parentSrc), nameSrc).Bytes()
 		if err == redis.Nil && m.conf.CaseInsensi {
 			if e := m.resolveCase(ctx, parentSrc, nameSrc); e != nil {
@@ -1428,7 +1432,7 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 			}
 			return nil
 		}
-		keys = []string{m.inodeKey(ino)}
+		keys := []string{m.inodeKey(ino)}
 
 		dbuf, err := tx.HGet(ctx, m.entryKey(parentDst), nameDst).Bytes()
 		if err == redis.Nil && m.conf.CaseInsensi {
@@ -1460,7 +1464,7 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 			return err
 		}
 
-		keys := []string{m.inodeKey(parentSrc), m.inodeKey(parentDst), m.inodeKey(ino)}
+		keys = []string{m.inodeKey(parentSrc), m.inodeKey(parentDst), m.inodeKey(ino)}
 		if dino > 0 {
 			keys = append(keys, m.inodeKey(dino))
 		}
@@ -1481,8 +1485,6 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 
 		var supdate, dupdate bool
 		now := time.Now()
-		tattr = Attr{}
-		opened = false
 		if dino > 0 {
 			if rs[3] == nil {
 				logger.Warnf("no attribute for inode %d (%d, %s)", dino, parentDst, nameDst)
@@ -1535,7 +1537,6 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 			if exchange {
 				return syscall.ENOENT
 			}
-			dino, dtyp = 0, 0
 		}
 		if ctx.Uid() != 0 && sattr.Mode&01000 != 0 && ctx.Uid() != sattr.Uid && ctx.Uid() != iattr.Uid {
 			return syscall.EACCES
@@ -1641,7 +1642,7 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 			return nil
 		})
 		return err
-	}, keys...)
+	}, m.entryKey(parentSrc), m.inodeKey(parentSrc), m.entryKey(parentDst), m.inodeKey(parentDst))
 	if err == nil && !exchange && trash == 0 {
 		if dino > 0 && dtyp == TypeFile && tattr.Nlink == 0 {
 			m.fileDeleted(opened, dino, tattr.Length)
@@ -1932,14 +1933,14 @@ func (m *redisMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	return err
 }
 
-func (m *redisMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) syscall.Errno {
+func (m *redisMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) syscall.Errno {
 	f := m.of.find(inode)
 	if f != nil {
 		f.RLock()
 		defer f.RUnlock()
 	}
-	if cs, ok := m.of.ReadChunk(inode, indx); ok {
-		*chunks = cs
+	if ss, ok := m.of.ReadChunk(inode, indx); ok {
+		*slices = ss
 		return 0
 	}
 	defer m.timeit(time.Now())
@@ -1948,9 +1949,9 @@ func (m *redisMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) s
 		return errno(err)
 	}
 	ss := readSlices(vals)
-	*chunks = buildSlice(ss)
-	m.of.CacheChunk(inode, indx, *chunks)
-	if !m.conf.ReadOnly && (len(vals) >= 5 || len(*chunks) >= 5) {
+	*slices = buildSlice(ss)
+	m.of.CacheChunk(inode, indx, *slices)
+	if !m.conf.ReadOnly && (len(vals) >= 5 || len(*slices) >= 5) {
 		go m.compactChunk(inode, indx, false)
 	}
 	return 0
@@ -1967,6 +1968,7 @@ func (m *redisMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice
 	var newSpace int64
 	var needCompact bool
 	err := m.txn(ctx, func(tx *redis.Tx) error {
+		newSpace = 0
 		var attr Attr
 		a, err := tx.Get(ctx, m.inodeKey(inode)).Bytes()
 		if err != nil {
@@ -1992,9 +1994,9 @@ func (m *redisMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice
 
 		var rpush *redis.IntCmd
 		_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
-			rpush = pipe.RPush(ctx, m.chunkKey(inode, indx), marshalSlice(off, slice.Chunkid, slice.Size, slice.Off, slice.Len))
+			rpush = pipe.RPush(ctx, m.chunkKey(inode, indx), marshalSlice(off, slice.Id, slice.Size, slice.Off, slice.Len))
 			// most of chunk are used by single inode, so use that as the default (1 == not exists)
-			// pipe.Incr(ctx, r.sliceKey(slice.Chunkid, slice.Size))
+			// pipe.Incr(ctx, r.sliceKey(slice.ID, slice.Size))
 			pipe.Set(ctx, m.inodeKey(inode), m.marshal(&attr), 0)
 			if newSpace > 0 {
 				pipe.IncrBy(ctx, m.usedSpaceKey(), newSpace)
@@ -2025,6 +2027,7 @@ func (m *redisMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino,
 	var newSpace int64
 	defer func() { m.of.InvalidateChunk(fout, 0xFFFFFFFF) }()
 	err := m.txn(ctx, func(tx *redis.Tx) error {
+		newSpace = 0
 		rs, err := tx.MGet(ctx, m.inodeKey(fin), m.inodeKey(fout)).Result()
 		if err != nil {
 			return err
@@ -2041,6 +2044,7 @@ func (m *redisMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino,
 			*copied = 0
 			return nil
 		}
+		size := size
 		if offIn+size > sattr.Length {
 			size = sattr.Length - offIn
 		}
@@ -2099,20 +2103,20 @@ func (m *redisMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino,
 						indx := uint32(doff / ChunkSize)
 						dpos := uint32(doff % ChunkSize)
 						if dpos+s.Len > ChunkSize {
-							pipe.RPush(ctx, m.chunkKey(fout, indx), marshalSlice(dpos, s.Chunkid, s.Size, s.Off, ChunkSize-dpos))
-							if s.Chunkid > 0 {
-								pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(s.Chunkid, s.Size), 1)
+							pipe.RPush(ctx, m.chunkKey(fout, indx), marshalSlice(dpos, s.Id, s.Size, s.Off, ChunkSize-dpos))
+							if s.Id > 0 {
+								pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(s.Id, s.Size), 1)
 							}
 
 							skip := ChunkSize - dpos
-							pipe.RPush(ctx, m.chunkKey(fout, indx+1), marshalSlice(0, s.Chunkid, s.Size, s.Off+skip, s.Len-skip))
-							if s.Chunkid > 0 {
-								pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(s.Chunkid, s.Size), 1)
+							pipe.RPush(ctx, m.chunkKey(fout, indx+1), marshalSlice(0, s.Id, s.Size, s.Off+skip, s.Len-skip))
+							if s.Id > 0 {
+								pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(s.Id, s.Size), 1)
 							}
 						} else {
-							pipe.RPush(ctx, m.chunkKey(fout, indx), marshalSlice(dpos, s.Chunkid, s.Size, s.Off, s.Len))
-							if s.Chunkid > 0 {
-								pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(s.Chunkid, s.Size), 1)
+							pipe.RPush(ctx, m.chunkKey(fout, indx), marshalSlice(dpos, s.Id, s.Size, s.Off, s.Len))
+							if s.Id > 0 {
+								pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(s.Id, s.Size), 1)
 							}
 						}
 					}
@@ -2206,10 +2210,10 @@ func (m *redisMeta) doCleanupSlices() {
 			if strings.HasPrefix(val, "-") { // < 0
 				ps := strings.Split(key, "_")
 				if len(ps) == 2 {
-					chunkid, _ := strconv.ParseUint(ps[0][1:], 10, 64)
+					id, _ := strconv.ParseUint(ps[0][1:], 10, 64)
 					size, _ := strconv.ParseUint(ps[1], 10, 32)
-					if chunkid > 0 && size > 0 {
-						m.deleteSlice(chunkid, uint32(size))
+					if id > 0 && size > 0 {
+						m.deleteSlice(id, uint32(size))
 					}
 				}
 			} else if val == "0" {
@@ -2318,20 +2322,20 @@ func (m *redisMeta) deleteChunk(inode Ino, indx uint32) error {
 			if err == redis.Nil {
 				return nil
 			}
-			slices = nil
-			rs = nil
+			slices = slices[:0]
+			rs = rs[:0]
 			_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
 				for _, v := range vals {
 					pipe.LPop(ctx, key)
 					rb := utils.ReadBuffer([]byte(v))
 					_ = rb.Get32() // pos
-					chunkid := rb.Get64()
-					if chunkid == 0 {
+					id := rb.Get64()
+					if id == 0 {
 						continue
 					}
 					size := rb.Get32()
-					slices = append(slices, &slice{chunkid: chunkid, size: size})
-					rs = append(rs, pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(chunkid, size), -1))
+					slices = append(slices, &slice{id: id, size: size})
+					rs = append(rs, pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(id, size), -1))
 				}
 				return nil
 			})
@@ -2342,7 +2346,7 @@ func (m *redisMeta) deleteChunk(inode Ino, indx uint32) error {
 		}
 		for i, s := range slices {
 			if rs[i].Val() < 0 {
-				m.deleteSlice(s.chunkid, s.size)
+				m.deleteSlice(s.id, s.size)
 			}
 		}
 		if len(slices) < 100 {
@@ -2414,13 +2418,13 @@ func (r *redisMeta) doCleanupDelayedSlices(edge int64, limit int) (int, error) {
 			}
 
 			if err := r.txn(ctx, func(tx *redis.Tx) error {
+				ss, rs = ss[:0], rs[:0]
 				val, e := tx.HGet(ctx, r.delSlices(), key).Result()
 				if e == redis.Nil {
 					return nil
 				} else if e != nil {
 					return e
 				}
-				ss, rs = ss[:0], rs[:0]
 				buf := []byte(val)
 				r.decodeDelayedSlices(buf, &ss)
 				if len(ss) == 0 {
@@ -2428,7 +2432,7 @@ func (r *redisMeta) doCleanupDelayedSlices(edge int64, limit int) (int, error) {
 				}
 				_, e = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {
 					for _, s := range ss {
-						rs = append(rs, pipe.HIncrBy(ctx, r.sliceRefs(), r.sliceKey(s.Chunkid, s.Size), -1))
+						rs = append(rs, pipe.HIncrBy(ctx, r.sliceRefs(), r.sliceKey(s.Id, s.Size), -1))
 					}
 					pipe.HDel(ctx, r.delSlices(), key)
 					return nil
@@ -2440,7 +2444,7 @@ func (r *redisMeta) doCleanupDelayedSlices(edge int64, limit int) (int, error) {
 			}
 			for i, s := range ss {
 				if rs[i].Err() == nil && rs[i].Val() < 0 {
-					r.deleteSlice(s.Chunkid, s.Size)
+					r.deleteSlice(s.Id, s.Size)
 					count++
 				}
 			}
@@ -2483,18 +2487,18 @@ func (m *redisMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	ss := readSlices(vals)
 	skipped := skipSome(ss)
 	ss = ss[skipped:]
-	pos, size, chunks := compactChunk(ss)
+	pos, size, slices := compactChunk(ss)
 	if len(ss) < 2 || size == 0 {
 		return
 	}
 
-	var chunkid uint64
-	st := m.NewChunk(ctx, &chunkid)
+	var id uint64
+	st := m.NewSlice(ctx, &id)
 	if st != 0 {
 		return
 	}
 	logger.Debugf("compact %d:%d: skipped %d slices (%d bytes) %d slices (%d bytes)", inode, indx, skipped, pos, len(ss), size)
-	err = m.newMsg(CompactChunk, chunks, chunkid)
+	err = m.newMsg(CompactChunk, slices, id)
 	if err != nil {
 		if !strings.Contains(err.Error(), "not exist") && !strings.Contains(err.Error(), "not found") {
 			logger.Warnf("compact %d %d with %d slices: %s", inode, indx, len(ss), err)
@@ -2506,8 +2510,8 @@ func (m *redisMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	trash := m.toTrash(0)
 	if trash {
 		for _, s := range ss {
-			if s.chunkid > 0 {
-				buf = append(buf, m.encodeDelayedSlice(s.chunkid, s.size)...)
+			if s.id > 0 {
+				buf = append(buf, m.encodeDelayedSlice(s.id, s.size)...)
 			}
 		}
 	} else {
@@ -2530,19 +2534,19 @@ func (m *redisMeta) compactChunk(inode Ino, indx uint32, force bool) {
 
 		_, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {
 			pipe.LTrim(ctx, key, int64(len(vals)), -1)
-			pipe.LPush(ctx, key, marshalSlice(pos, chunkid, size, 0, size))
+			pipe.LPush(ctx, key, marshalSlice(pos, id, size, 0, size))
 			for i := skipped; i > 0; i-- {
 				pipe.LPush(ctx, key, vals[i-1])
 			}
-			pipe.HSet(ctx, m.sliceRefs(), m.sliceKey(chunkid, size), "0") // create the key to tracking it
+			pipe.HSet(ctx, m.sliceRefs(), m.sliceKey(id, size), "0") // create the key to tracking it
 			if trash {
 				if len(buf) > 0 {
-					pipe.HSet(ctx, m.delSlices(), fmt.Sprintf("%d_%d", chunkid, time.Now().Unix()), buf)
+					pipe.HSet(ctx, m.delSlices(), fmt.Sprintf("%d_%d", id, time.Now().Unix()), buf)
 				}
 			} else {
 				for i, s := range ss {
-					if s.chunkid > 0 {
-						rs[i] = pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(s.chunkid, s.size), -1)
+					if s.id > 0 {
+						rs[i] = pipe.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(s.id, s.size), -1)
 					}
 				}
 			}
@@ -2552,7 +2556,7 @@ func (m *redisMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	}, key))
 	// there could be false-negative that the compaction is successful, double-check
 	if errno != 0 && errno != syscall.EINVAL {
-		if e := m.rdb.HGet(ctx, m.sliceRefs(), m.sliceKey(chunkid, size)).Err(); e == redis.Nil {
+		if e := m.rdb.HGet(ctx, m.sliceRefs(), m.sliceKey(id, size)).Err(); e == redis.Nil {
 			errno = syscall.EINVAL // failed
 		} else if e == nil {
 			errno = 0 // successful
@@ -2560,16 +2564,16 @@ func (m *redisMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	}
 
 	if errno == syscall.EINVAL {
-		m.rdb.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(chunkid, size), -1)
-		logger.Infof("compaction for %d:%d is wasted, delete slice %d (%d bytes)", inode, indx, chunkid, size)
-		m.deleteSlice(chunkid, size)
+		m.rdb.HIncrBy(ctx, m.sliceRefs(), m.sliceKey(id, size), -1)
+		logger.Infof("compaction for %d:%d is wasted, delete slice %d (%d bytes)", inode, indx, id, size)
+		m.deleteSlice(id, size)
 	} else if errno == 0 {
 		m.of.InvalidateChunk(inode, indx)
-		m.cleanupZeroRef(m.sliceKey(chunkid, size))
+		m.cleanupZeroRef(m.sliceKey(id, size))
 		if !trash {
 			for i, s := range ss {
-				if s.chunkid > 0 && rs[i].Err() == nil && rs[i].Val() < 0 {
-					m.deleteSlice(s.chunkid, s.size)
+				if s.id > 0 && rs[i].Err() == nil && rs[i].Val() < 0 {
+					m.deleteSlice(s.id, s.size)
 				}
 			}
 		}
@@ -2744,8 +2748,8 @@ func (m *redisMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool,
 			vals := cmd.(*redis.StringSliceCmd).Val()
 			ss := readSlices(vals)
 			for _, s := range ss {
-				if s.chunkid > 0 {
-					slices[Ino(inode)] = append(slices[Ino(inode)], Slice{Chunkid: s.chunkid, Size: s.size})
+				if s.id > 0 {
+					slices[Ino(inode)] = append(slices[Ino(inode)], Slice{Id: s.id, Size: s.size})
 					if showProgress != nil {
 						showProgress()
 					}
@@ -2769,7 +2773,7 @@ func (m *redisMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool,
 				}
 			}
 			for _, s := range ss {
-				if s.Chunkid > 0 {
+				if s.Id > 0 {
 					slices[1] = append(slices[1], s)
 				}
 			}
@@ -2806,12 +2810,11 @@ func (m *redisMeta) ListXattr(ctx Context, inode Ino, names *[]byte) syscall.Err
 }
 
 func (m *redisMeta) doSetXattr(ctx Context, inode Ino, name string, value []byte, flags uint32) syscall.Errno {
-	c := Background
 	key := m.xattrKey(inode)
 	return errno(m.txn(ctx, func(tx *redis.Tx) error {
 		switch flags {
 		case XattrCreate:
-			ok, err := tx.HSetNX(c, key, name, value).Result()
+			ok, err := tx.HSetNX(ctx, key, name, value).Result()
 			if err != nil {
 				return err
 			}
@@ -2820,7 +2823,7 @@ func (m *redisMeta) doSetXattr(ctx Context, inode Ino, name string, value []byte
 			}
 			return nil
 		case XattrReplace:
-			if ok, err := tx.HExists(c, key, name).Result(); err != nil {
+			if ok, err := tx.HExists(ctx, key, name).Result(); err != nil {
 				return err
 			} else if !ok {
 				return ENOATTR
@@ -2950,6 +2953,7 @@ func (m *redisMeta) dumpEntries(es ...*DumpedEntry) error {
 			}
 			switch typ {
 			case TypeFile:
+				e.Chunks = e.Chunks[:0]
 				if attr.Length > 0 {
 					vals, err := cr[i].Result()
 					if err != nil {
@@ -2959,7 +2963,7 @@ func (m *redisMeta) dumpEntries(es ...*DumpedEntry) error {
 						ss := readSlices(vals)
 						slices := make([]*DumpedSlice, 0, len(ss))
 						for _, s := range ss {
-							slices = append(slices, &DumpedSlice{Chunkid: s.chunkid, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
+							slices = append(slices, &DumpedSlice{Id: s.id, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
 						}
 						e.Chunks = append(e.Chunks, &DumpedChunk{0, slices})
 					}
@@ -3013,7 +3017,7 @@ func (m *redisMeta) dumpEntries(es ...*DumpedEntry) error {
 					ss := readSlices(vals)
 					slices := make([]*DumpedSlice, 0, len(ss))
 					for _, s := range ss {
-						slices = append(slices, &DumpedSlice{Chunkid: s.chunkid, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
+						slices = append(slices, &DumpedSlice{Id: s.id, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
 					}
 					e := es[lcs[i].i]
 					e.Chunks = append(e.Chunks, &DumpedChunk{lcs[i].indx, slices})
@@ -3259,7 +3263,7 @@ func (m *redisMeta) loadEntry(e *DumpedEntry, p redis.Pipeliner, tryExec func())
 			}
 			slices := make([]string, 0, len(c.Slices))
 			for _, s := range c.Slices {
-				slices = append(slices, string(marshalSlice(s.Pos, s.Chunkid, s.Size, s.Off, s.Len)))
+				slices = append(slices, string(marshalSlice(s.Pos, s.Id, s.Size, s.Off, s.Len)))
 				if len(slices) > batch {
 					p.RPush(ctx, m.chunkKey(inode, c.Index), slices)
 					tryExec()
diff --git a/pkg/meta/slice.go b/pkg/meta/slice.go
index 09efb006..7ebf6e1b 100644
--- a/pkg/meta/slice.go
+++ b/pkg/meta/slice.go
@@ -19,22 +19,22 @@ package meta
 import "github.com/juicedata/juicefs/pkg/utils"
 
 type slice struct {
-	chunkid uint64
-	size    uint32
-	off     uint32
-	len     uint32
-	pos     uint32
-	left    *slice
-	right   *slice
+	id    uint64
+	size  uint32
+	off   uint32
+	len   uint32
+	pos   uint32
+	left  *slice
+	right *slice
 }
 
-func newSlice(pos uint32, chunkid uint64, cleng, off, len uint32) *slice {
+func newSlice(pos uint32, id uint64, cleng, off, len uint32) *slice {
 	if len == 0 {
 		return nil
 	}
 	s := &slice{}
 	s.pos = pos
-	s.chunkid = chunkid
+	s.id = id
 	s.size = cleng
 	s.off = off
 	s.len = len
@@ -46,7 +46,7 @@ func newSlice(pos uint32, chunkid uint64, cleng, off, len uint32) *slice {
 func (s *slice) read(buf []byte) {
 	rb := utils.ReadBuffer(buf)
 	s.pos = rb.Get32()
-	s.chunkid = rb.Get64()
+	s.id = rb.Get64()
 	s.size = rb.Get32()
 	s.off = rb.Get32()
 	s.len = rb.Get32()
@@ -64,7 +64,7 @@ func (s *slice) cut(pos uint32) (left, right *slice) {
 		return left, s
 	} else if pos < s.pos+s.len {
 		l := pos - s.pos
-		right = newSlice(pos, s.chunkid, s.size, s.off+l, s.len-l)
+		right = newSlice(pos, s.id, s.size, s.off+l, s.len-l)
 		right.right = s.right
 		s.len = l
 		s.right = nil
@@ -90,10 +90,10 @@ func (s *slice) visit(f func(*slice)) {
 
 const sliceBytes = 24
 
-func marshalSlice(pos uint32, chunkid uint64, size, off, len uint32) []byte {
+func marshalSlice(pos uint32, id uint64, size, off, len uint32) []byte {
 	w := utils.NewBuffer(sliceBytes)
 	w.Put32(pos)
-	w.Put64(chunkid)
+	w.Put64(id)
 	w.Put32(size)
 	w.Put32(off)
 	w.Put32(len)
@@ -144,7 +144,7 @@ func buildSlice(ss []*slice) []Slice {
 			chunk = append(chunk, Slice{Size: s.pos - pos, Len: s.pos - pos})
 			pos = s.pos
 		}
-		chunk = append(chunk, Slice{Chunkid: s.chunkid, Size: s.size, Off: s.off, Len: s.len})
+		chunk = append(chunk, Slice{Id: s.id, Size: s.size, Off: s.off, Len: s.len})
 		pos += s.len
 	})
 	return chunk
@@ -153,7 +153,7 @@ func buildSlice(ss []*slice) []Slice {
 func compactChunk(ss []*slice) (uint32, uint32, []Slice) {
 	var chunk = buildSlice(ss)
 	var pos uint32
-	if len(chunk) > 0 && chunk[0].Chunkid == 0 {
+	if len(chunk) > 0 && chunk[0].Id == 0 {
 		pos = chunk[0].Len
 		chunk = chunk[1:]
 	}
@@ -176,7 +176,7 @@ func skipSome(chunk []*slice) int {
 			break
 		}
 		isFirst := func(pos uint32, s Slice) bool {
-			return pos == first.pos && s.Chunkid == first.chunkid && s.Off == first.off && s.Len == first.len
+			return pos == first.pos && s.Id == first.id && s.Off == first.off && s.Len == first.len
 		}
 		if !isFirst(pos, c[0]) {
 			// it's not the first slice, compact it
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index c3726707..7b8192a3 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -90,14 +90,18 @@ type chunk struct {
 	Slices []byte `xorm:"blob notnull"`
 }
 
-type chunkRef struct {
-	Chunkid uint64 `xorm:"pk"`
-	Size    uint32 `xorm:"notnull"`
-	Refs    int    `xorm:"notnull"`
+type sliceRef struct {
+	Id   uint64 `xorm:"pk chunkid"`
+	Size uint32 `xorm:"notnull"`
+	Refs int    `xorm:"notnull"`
+}
+
+func (c *sliceRef) TableName() string {
+	return "jfs_chunk_ref"
 }
 
 type delslices struct {
-	Chunkid uint64 `xorm:"pk"`
+	Id      uint64 `xorm:"pk chunkid"`
 	Deleted int64  `xorm:"notnull"` // timestamp
 	Slices  []byte `xorm:"blob notnull"`
 }
@@ -220,9 +224,9 @@ func (m *dbMeta) Name() string {
 	return m.db.DriverName()
 }
 
-func (m *dbMeta) doDeleteSlice(chunkid uint64, size uint32) error {
+func (m *dbMeta) doDeleteSlice(id uint64, size uint32) error {
 	return m.txn(func(s *xorm.Session) error {
-		_, err := s.Exec("delete from jfs_chunk_ref where chunkid=?", chunkid)
+		_, err := s.Exec("delete from jfs_chunk_ref where chunkid=?", id)
 		return err
 	})
 }
@@ -245,7 +249,7 @@ func (m *dbMeta) Init(format Format, force bool) error {
 	if err := m.syncTable(new(node), new(symlink), new(xattr)); err != nil {
 		return fmt.Errorf("create table node, symlink, xattr: %s", err)
 	}
-	if err := m.syncTable(new(chunk), new(chunkRef), new(delslices)); err != nil {
+	if err := m.syncTable(new(chunk), new(sliceRef), new(delslices)); err != nil {
 		return fmt.Errorf("create table chunk, chunk_ref, delslices: %s", err)
 	}
 	if err := m.syncTable(new(session2), new(sustained), new(delfile)); err != nil {
@@ -335,7 +339,7 @@ func (m *dbMeta) Init(format Format, force bool) error {
 func (m *dbMeta) Reset() error {
 	return m.db.DropTables(&setting{}, &counter{},
 		&node{}, &edge{}, &symlink{}, &xattr{},
-		&chunk{}, &chunkRef{}, &delslices{},
+		&chunk{}, &sliceRef{}, &delslices{},
 		&session{}, &session2{}, &sustained{}, &delfile{},
 		&flock{}, &plock{})
 }
@@ -903,6 +907,7 @@ func (m *dbMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, at
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFF) }()
 	var newSpace int64
 	err := m.txn(func(s *xorm.Session) error {
+		newSpace = 0
 		var n = node{Inode: inode}
 		ok, err := s.ForUpdate().Get(&n)
 		if err != nil {
@@ -993,6 +998,7 @@ func (m *dbMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFF) }()
 	var newSpace int64
 	err := m.txn(func(s *xorm.Session) error {
+		newSpace = 0
 		var n = node{Inode: inode}
 		ok, err := s.ForUpdate().Get(&n)
 		if err != nil {
@@ -1026,7 +1032,8 @@ func (m *dbMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 		if _, err := s.Cols("length", "mtime", "ctime").Update(&n, &node{Inode: inode}); err != nil {
 			return err
 		}
-		if mode&(fallocZeroRange|fallocPunchHole) != 0 {
+		if mode&(fallocZeroRange|fallocPunchHole) != 0 && off < old {
+			off, size := off, size
 			if off+size > old {
 				size = old - off
 			}
@@ -1195,10 +1202,12 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 	if st := m.checkTrash(parent, &trash); st != 0 {
 		return st
 	}
-	var newSpace, newInode int64
 	var n node
 	var opened bool
+	var newSpace, newInode int64
 	err := m.txn(func(s *xorm.Session) error {
+		opened = false
+		newSpace, newInode = 0, 0
 		var pn = node{Inode: parent}
 		ok, err := s.ForUpdate().Get(&pn)
 		if err != nil {
@@ -1236,7 +1245,6 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 			return err
 		}
 		now := time.Now().UnixNano() / 1e3
-		opened = false
 		if ok {
 			if ctx.Uid() != 0 && pn.Mode&01000 != 0 && ctx.Uid() != pn.Uid && ctx.Uid() != n.Uid {
 				return syscall.EACCES
@@ -1445,6 +1453,9 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 	var dn node
 	var newSpace, newInode int64
 	err := m.txn(func(s *xorm.Session) error {
+		opened = false
+		dino = 0
+		newSpace, newInode = 0, 0
 		var spn = node{Inode: parentSrc}
 		var dpn = node{Inode: parentDst}
 		err := m.getNodesForUpdate(s, &spn, &dpn)
@@ -1500,7 +1511,6 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		}
 		var supdate, dupdate bool
 		now := time.Now().UnixNano() / 1e3
-		opened = false
 		dn = node{Inode: de.Inode}
 		if ok {
 			if flags == RenameNoReplace {
@@ -1560,7 +1570,6 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			if exchange {
 				return syscall.ENOENT
 			}
-			dino = 0
 		}
 		if ctx.Uid() != 0 && spn.Mode&01000 != 0 && ctx.Uid() != spn.Uid && ctx.Uid() != sn.Uid {
 			return syscall.EACCES
@@ -1893,6 +1902,8 @@ func (m *dbMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	var n = node{Inode: inode}
 	var newSpace int64
 	err := m.txn(func(s *xorm.Session) error {
+		newSpace = 0
+		n = node{Inode: inode}
 		ok, err := s.ForUpdate().Get(&n)
 		if err != nil {
 			return err
@@ -1918,14 +1929,14 @@ func (m *dbMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	return err
 }
 
-func (m *dbMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) syscall.Errno {
+func (m *dbMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) syscall.Errno {
 	f := m.of.find(inode)
 	if f != nil {
 		f.RLock()
 		defer f.RUnlock()
 	}
-	if cs, ok := m.of.ReadChunk(inode, indx); ok {
-		*chunks = cs
+	if ss, ok := m.of.ReadChunk(inode, indx); ok {
+		*slices = ss
 		return 0
 	}
 	defer m.timeit(time.Now())
@@ -1941,9 +1952,9 @@ func (m *dbMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) sysc
 	if ss == nil {
 		return syscall.EIO
 	}
-	*chunks = buildSlice(ss)
-	m.of.CacheChunk(inode, indx, *chunks)
-	if !m.conf.ReadOnly && (len(c.Slices)/sliceBytes >= 5 || len(*chunks) >= 5) {
+	*slices = buildSlice(ss)
+	m.of.CacheChunk(inode, indx, *slices)
+	if !m.conf.ReadOnly && (len(c.Slices)/sliceBytes >= 5 || len(*slices) >= 5) {
 		go m.compactChunk(inode, indx, false)
 	}
 	return 0
@@ -1960,6 +1971,7 @@ func (m *dbMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 	var newSpace int64
 	var needCompact bool
 	err := m.txn(func(s *xorm.Session) error {
+		newSpace = 0
 		var n = node{Inode: inode}
 		ok, err := s.ForUpdate().Get(&n)
 		if err != nil {
@@ -1988,7 +2000,7 @@ func (m *dbMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 		if err != nil {
 			return err
 		}
-		buf := marshalSlice(off, slice.Chunkid, slice.Size, slice.Off, slice.Len)
+		buf := marshalSlice(off, slice.Id, slice.Size, slice.Off, slice.Len)
 		if ok {
 			if err := m.appendSlice(s, inode, indx, buf); err != nil {
 				return err
@@ -1998,7 +2010,7 @@ func (m *dbMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 				return err
 			}
 		}
-		if err = mustInsert(s, chunkRef{slice.Chunkid, slice.Size, 1}); err != nil {
+		if err = mustInsert(s, sliceRef{slice.Id, slice.Size, 1}); err != nil {
 			return err
 		}
 		_, err = s.Cols("length", "mtime", "ctime").Update(&n, &node{Inode: inode})
@@ -2026,6 +2038,7 @@ func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 	var newSpace int64
 	defer func() { m.of.InvalidateChunk(fout, 0xFFFFFFFF) }()
 	err := m.txn(func(s *xorm.Session) error {
+		newSpace = 0
 		var nin = node{Inode: fin}
 		var nout = node{Inode: fout}
 		err := m.getNodesForUpdate(s, &nin, &nout)
@@ -2039,6 +2052,7 @@ func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 			*copied = 0
 			return nil
 		}
+		size := size
 		if offIn+size > nin.Length {
 			size = nin.Length - offIn
 		}
@@ -2069,12 +2083,12 @@ func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 		}
 
 		ses := s
-		updateSlices := func(indx uint32, buf []byte, chunkid uint64, size uint32) error {
+		updateSlices := func(indx uint32, buf []byte, id uint64, size uint32) error {
 			if err := m.appendSlice(ses, fout, indx, buf); err != nil {
 				return err
 			}
-			if chunkid > 0 {
-				if _, err := ses.Exec("update jfs_chunk_ref set refs=refs+1 where chunkid = ? AND size = ?", chunkid, size); err != nil {
+			if id > 0 {
+				if _, err := ses.Exec("update jfs_chunk_ref set refs=refs+1 where chunkid = ? AND size = ?", id, size); err != nil {
 					return err
 				}
 			}
@@ -2106,15 +2120,15 @@ func (m *dbMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 					indx := uint32(doff / ChunkSize)
 					dpos := uint32(doff % ChunkSize)
 					if dpos+s.Len > ChunkSize {
-						if err := updateSlices(indx, marshalSlice(dpos, s.Chunkid, s.Size, s.Off, ChunkSize-dpos), s.Chunkid, s.Size); err != nil {
+						if err := updateSlices(indx, marshalSlice(dpos, s.Id, s.Size, s.Off, ChunkSize-dpos), s.Id, s.Size); err != nil {
 							return err
 						}
 						skip := ChunkSize - dpos
-						if err := updateSlices(indx+1, marshalSlice(0, s.Chunkid, s.Size, s.Off+skip, s.Len-skip), s.Chunkid, s.Size); err != nil {
+						if err := updateSlices(indx+1, marshalSlice(0, s.Id, s.Size, s.Off+skip, s.Len-skip), s.Id, s.Size); err != nil {
 							return err
 						}
 					} else {
-						if err := updateSlices(indx, marshalSlice(dpos, s.Chunkid, s.Size, s.Off, s.Len), s.Chunkid, s.Size); err != nil {
+						if err := updateSlices(indx, marshalSlice(dpos, s.Id, s.Size, s.Off, s.Len), s.Id, s.Size); err != nil {
 							return err
 						}
 					}
@@ -2166,19 +2180,20 @@ func (m *dbMeta) doFindDeletedFiles(ts int64, limit int) (map[Ino]uint64, error)
 }
 
 func (m *dbMeta) doCleanupSlices() {
-	var cks []chunkRef
+	var cks []sliceRef
 	_ = m.roTxn(func(s *xorm.Session) error {
 		cks = nil
 		return s.Where("refs <= 0").Find(&cks)
 	})
 	for _, ck := range cks {
-		m.deleteSlice(ck.Chunkid, ck.Size)
+		m.deleteSlice(ck.Id, ck.Size)
 	}
 }
 
 func (m *dbMeta) deleteChunk(inode Ino, indx uint32) error {
 	var ss []*slice
 	err := m.txn(func(s *xorm.Session) error {
+		ss = ss[:0]
 		var c = chunk{Inode: inode, Indx: indx}
 		ok, err := s.ForUpdate().MustCols("indx").Get(&c)
 		if err != nil {
@@ -2189,10 +2204,10 @@ func (m *dbMeta) deleteChunk(inode Ino, indx uint32) error {
 		}
 		ss = readSliceBuf(c.Slices)
 		for _, sc := range ss {
-			if sc.chunkid == 0 {
+			if sc.id == 0 {
 				continue
 			}
-			_, err = s.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? AND size=?", sc.chunkid, sc.size)
+			_, err = s.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? AND size=?", sc.id, sc.size)
 			if err != nil {
 				return err
 			}
@@ -2208,10 +2223,10 @@ func (m *dbMeta) deleteChunk(inode Ino, indx uint32) error {
 		return fmt.Errorf("delete slice from chunk %s fail: %s, retry later", inode, err)
 	}
 	for _, s := range ss {
-		if s.chunkid == 0 {
+		if s.id == 0 {
 			continue
 		}
-		var ref = chunkRef{Chunkid: s.chunkid}
+		var ref = sliceRef{Id: s.id}
 		err := m.roTxn(func(s *xorm.Session) error {
 			ok, err := s.Get(&ref)
 			if err == nil && !ok {
@@ -2220,7 +2235,7 @@ func (m *dbMeta) deleteChunk(inode Ino, indx uint32) error {
 			return err
 		})
 		if err == nil && ref.Refs <= 0 {
-			m.deleteSlice(s.chunkid, s.size)
+			m.deleteSlice(s.id, s.size)
 		}
 	}
 	return nil
@@ -2256,30 +2271,30 @@ func (m *dbMeta) doCleanupDelayedSlices(edge int64, limit int) (int, error) {
 	var ss []Slice
 	for _, ds := range result {
 		if err := m.txn(func(ses *xorm.Session) error {
-			ds := delslices{Chunkid: ds.Chunkid}
+			ss = ss[:0]
+			ds := delslices{Id: ds.Id}
 			if ok, e := ses.ForUpdate().Get(&ds); e != nil {
 				return e
 			} else if !ok {
 				return nil
 			}
-			ss = ss[:0]
 			m.decodeDelayedSlices(ds.Slices, &ss)
 			if len(ss) == 0 {
-				return fmt.Errorf("invalid value for delayed slices %d: %v", ds.Chunkid, ds.Slices)
+				return fmt.Errorf("invalid value for delayed slices %d: %v", ds.Id, ds.Slices)
 			}
 			for _, s := range ss {
-				if _, e := ses.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? and size=?", s.Chunkid, s.Size); e != nil {
+				if _, e := ses.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? and size=?", s.Id, s.Size); e != nil {
 					return e
 				}
 			}
-			_, e := ses.Delete(&delslices{Chunkid: ds.Chunkid})
+			_, e := ses.Delete(&delslices{Id: ds.Id})
 			return e
 		}); err != nil {
-			logger.Warnf("Cleanup delayed slices %d: %s", ds.Chunkid, err)
+			logger.Warnf("Cleanup delayed slices %d: %s", ds.Id, err)
 			continue
 		}
 		for _, s := range ss {
-			var ref = chunkRef{Chunkid: s.Chunkid}
+			var ref = sliceRef{Id: s.Id}
 			err := m.roTxn(func(s *xorm.Session) error {
 				ok, err := s.Get(&ref)
 				if err == nil && !ok {
@@ -2288,7 +2303,7 @@ func (m *dbMeta) doCleanupDelayedSlices(edge int64, limit int) (int, error) {
 				return err
 			})
 			if err == nil && ref.Refs <= 0 {
-				m.deleteSlice(s.Chunkid, s.Size)
+				m.deleteSlice(s.Id, s.Size)
 				count++
 			}
 		}
@@ -2329,18 +2344,18 @@ func (m *dbMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	ss := readSliceBuf(c.Slices)
 	skipped := skipSome(ss)
 	ss = ss[skipped:]
-	pos, size, chunks := compactChunk(ss)
+	pos, size, slices := compactChunk(ss)
 	if len(ss) < 2 || size == 0 {
 		return
 	}
 
-	var chunkid uint64
-	st := m.NewChunk(Background, &chunkid)
+	var id uint64
+	st := m.NewSlice(Background, &id)
 	if st != 0 {
 		return
 	}
 	logger.Debugf("compact %d:%d: skipped %d slices (%d bytes) %d slices (%d bytes)", inode, indx, skipped, pos, len(ss), size)
-	err = m.newMsg(CompactChunk, chunks, chunkid)
+	err = m.newMsg(CompactChunk, slices, id)
 	if err != nil {
 		if !strings.Contains(err.Error(), "not exist") && !strings.Contains(err.Error(), "not found") {
 			logger.Warnf("compact %d %d with %d slices: %s", inode, indx, len(ss), err)
@@ -2351,8 +2366,8 @@ func (m *dbMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	trash := m.toTrash(0)
 	if trash {
 		for _, s := range ss {
-			if s.chunkid > 0 {
-				buf = append(buf, m.encodeDelayedSlice(s.chunkid, s.size)...)
+			if s.id > 0 {
+				buf = append(buf, m.encodeDelayedSlice(s.id, s.size)...)
 			}
 		}
 	}
@@ -2367,26 +2382,26 @@ func (m *dbMeta) compactChunk(inode Ino, indx uint32, force bool) {
 			return syscall.EINVAL
 		}
 
-		c2.Slices = append(append(c2.Slices[:skipped*sliceBytes], marshalSlice(pos, chunkid, size, 0, size)...), c2.Slices[len(c.Slices):]...)
+		c2.Slices = append(append(c2.Slices[:skipped*sliceBytes], marshalSlice(pos, id, size, 0, size)...), c2.Slices[len(c.Slices):]...)
 		if _, err := s.Where("Inode = ? AND indx = ?", inode, indx).Update(c2); err != nil {
 			return err
 		}
 		// create the key to tracking it
-		if err = mustInsert(s, chunkRef{chunkid, size, 1}); err != nil {
+		if err = mustInsert(s, sliceRef{id, size, 1}); err != nil {
 			return err
 		}
 		if trash {
 			if len(buf) > 0 {
-				if err = mustInsert(s, &delslices{chunkid, time.Now().Unix(), buf}); err != nil {
+				if err = mustInsert(s, &delslices{id, time.Now().Unix(), buf}); err != nil {
 					return err
 				}
 			}
 		} else {
 			for _, s_ := range ss {
-				if s_.chunkid == 0 {
+				if s_.id == 0 {
 					continue
 				}
-				if _, err := s.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? and size=?", s_.chunkid, s_.size); err != nil {
+				if _, err := s.Exec("update jfs_chunk_ref set refs=refs-1 where chunkid=? and size=?", s_.id, s_.size); err != nil {
 					return err
 				}
 			}
@@ -2395,7 +2410,7 @@ func (m *dbMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	})
 	// there could be false-negative that the compaction is successful, double-check
 	if err != nil {
-		var c = chunkRef{Chunkid: chunkid}
+		var c = sliceRef{Id: id}
 		var ok bool
 		e := m.roTxn(func(s *xorm.Session) error {
 			var e error
@@ -2406,23 +2421,23 @@ func (m *dbMeta) compactChunk(inode Ino, indx uint32, force bool) {
 			if ok {
 				err = nil
 			} else {
-				logger.Infof("compacted chunk %d was not used", chunkid)
+				logger.Infof("compacted chunk %d was not used", id)
 				err = syscall.EINVAL
 			}
 		}
 	}
 
 	if errno, ok := err.(syscall.Errno); ok && errno == syscall.EINVAL {
-		logger.Infof("compaction for %d:%d is wasted, delete slice %d (%d bytes)", inode, indx, chunkid, size)
-		m.deleteSlice(chunkid, size)
+		logger.Infof("compaction for %d:%d is wasted, delete slice %d (%d bytes)", inode, indx, id, size)
+		m.deleteSlice(id, size)
 	} else if err == nil {
 		m.of.InvalidateChunk(inode, indx)
 		if !trash {
 			for _, s := range ss {
-				if s.chunkid == 0 {
+				if s.id == 0 {
 					continue
 				}
-				var ref = chunkRef{Chunkid: s.chunkid}
+				var ref = sliceRef{Id: s.id}
 				var ok bool
 				err := m.roTxn(func(s *xorm.Session) error {
 					var e error
@@ -2430,7 +2445,7 @@ func (m *dbMeta) compactChunk(inode Ino, indx uint32, force bool) {
 					return e
 				})
 				if err == nil && ok && ref.Refs <= 0 {
-					m.deleteSlice(s.chunkid, s.size)
+					m.deleteSlice(s.id, s.size)
 				}
 			}
 		}
@@ -2487,8 +2502,8 @@ func (m *dbMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, sh
 		for _, c := range cs {
 			ss := readSliceBuf(c.Slices)
 			for _, s := range ss {
-				if s.chunkid > 0 {
-					slices[c.Inode] = append(slices[c.Inode], Slice{Chunkid: s.chunkid, Size: s.size})
+				if s.id > 0 {
+					slices[c.Inode] = append(slices[c.Inode], Slice{Id: s.id, Size: s.size})
 					if showProgress != nil {
 						showProgress()
 					}
@@ -2525,7 +2540,7 @@ func (m *dbMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, sh
 				}
 			}
 			for _, s := range ss {
-				if s.Chunkid > 0 {
+				if s.Id > 0 {
 					slices[1] = append(slices[1], s)
 				}
 			}
@@ -2656,7 +2671,7 @@ func (m *dbMeta) dumpEntry(s *xorm.Session, inode Ino, typ uint8) (*DumpedEntry,
 			ss := readSliceBuf(c.Slices)
 			slices := make([]*DumpedSlice, 0, len(ss))
 			for _, s := range ss {
-				slices = append(slices, &DumpedSlice{Chunkid: s.chunkid, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
+				slices = append(slices, &DumpedSlice{Id: s.id, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
 			}
 			e.Chunks = append(e.Chunks, &DumpedChunk{indx, slices})
 		}
@@ -2708,7 +2723,7 @@ func (m *dbMeta) dumpEntryFast(s *xorm.Session, inode Ino, typ uint8) *DumpedEnt
 			ss := readSliceBuf(c.Slices)
 			slices := make([]*DumpedSlice, 0, len(ss))
 			for _, s := range ss {
-				slices = append(slices, &DumpedSlice{Chunkid: s.chunkid, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
+				slices = append(slices, &DumpedSlice{Id: s.id, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
 			}
 			e.Chunks = append(e.Chunks, &DumpedChunk{indx, slices})
 		}
@@ -3005,7 +3020,7 @@ func (m *dbMeta) loadEntry(e *DumpedEntry, chs []chan interface{}) {
 			}
 			slices := make([]byte, 0, sliceBytes*len(c.Slices))
 			for _, s := range c.Slices {
-				slices = append(slices, marshalSlice(s.Pos, s.Chunkid, s.Size, s.Off, s.Len)...)
+				slices = append(slices, marshalSlice(s.Pos, s.Id, s.Size, s.Off, s.Len)...)
 			}
 			chs[2] <- &chunk{Inode: inode, Indx: c.Index, Slices: slices}
 		}
@@ -3048,7 +3063,7 @@ func (m *dbMeta) LoadMeta(r io.Reader) error {
 	if err = m.syncTable(new(node), new(edge), new(symlink), new(xattr)); err != nil {
 		return fmt.Errorf("create table node, edge, symlink, xattr: %s", err)
 	}
-	if err = m.syncTable(new(chunk), new(chunkRef), new(delslices)); err != nil {
+	if err = m.syncTable(new(chunk), new(sliceRef), new(delslices)); err != nil {
 		return fmt.Errorf("create table chunk, chunk_ref, delslices: %s", err)
 	}
 	if err = m.syncTable(new(session2), new(sustained), new(delfile)); err != nil {
@@ -3109,7 +3124,7 @@ func (m *dbMeta) LoadMeta(r io.Reader) error {
 
 	dm, counters, parents, refs, err := loadEntries(r,
 		func(e *DumpedEntry) { m.loadEntry(e, chs) },
-		func(ck *chunkKey) { chs[3] <- &chunkRef{ck.id, ck.size, 1} })
+		func(ck *chunkKey) { chs[3] <- &sliceRef{ck.id, ck.size, 1} })
 	if err != nil {
 		return err
 	}
@@ -3133,7 +3148,7 @@ func (m *dbMeta) LoadMeta(r io.Reader) error {
 	if err = m.txn(func(s *xorm.Session) error {
 		for k, v := range refs {
 			if v > 1 {
-				if _, e := s.Cols("refs").Update(&chunkRef{Refs: int(v)}, &chunkRef{Chunkid: k.id}); e != nil {
+				if _, e := s.Cols("refs").Update(&sliceRef{Refs: int(v)}, &sliceRef{Id: k.id}); e != nil {
 					return e
 				}
 			}
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index dc7b638c..25b43ef5 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -100,8 +100,8 @@ func (m *kvMeta) Name() string {
 	return m.client.name()
 }
 
-func (m *kvMeta) doDeleteSlice(chunkid uint64, size uint32) error {
-	return m.deleteKeys(m.sliceKey(chunkid, size))
+func (m *kvMeta) doDeleteSlice(id uint64, size uint32) error {
+	return m.deleteKeys(m.sliceKey(id, size))
 }
 
 func (m *kvMeta) keyLen(args ...interface{}) int {
@@ -147,12 +147,12 @@ func (m *kvMeta) fmtKey(args ...interface{}) []byte {
 }
 
 /**
-  Ino iiiiiiii
-  Length llllllll
-  Indx nnnn
-  name ...
-  chunkid cccccccc
-  session  ssssssss
+  Ino     iiiiiiii
+  Length  llllllll
+  Indx    nnnn
+  name    ...
+  sliceId cccccccc
+  session ssssssss
 
 All keys:
   setting            format
@@ -190,12 +190,12 @@ func (m *kvMeta) chunkKey(inode Ino, indx uint32) []byte {
 	return m.fmtKey("A", inode, "C", indx)
 }
 
-func (m *kvMeta) sliceKey(chunkid uint64, size uint32) []byte {
-	return m.fmtKey("K", chunkid, size)
+func (m *kvMeta) sliceKey(id uint64, size uint32) []byte {
+	return m.fmtKey("K", id, size)
 }
 
-func (m *kvMeta) delSliceKey(ts int64, chunkid uint64) []byte {
-	return m.fmtKey("L", uint64(ts), chunkid)
+func (m *kvMeta) delSliceKey(ts int64, id uint64) []byte {
+	return m.fmtKey("L", uint64(ts), id)
 }
 
 func (m *kvMeta) symKey(inode Ino) []byte {
@@ -816,6 +816,7 @@ func (m *kvMeta) Truncate(ctx Context, inode Ino, flags uint8, length uint64, at
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFF) }()
 	var newSpace int64
 	err := m.txn(func(tx kvTxn) error {
+		newSpace = 0
 		var t Attr
 		a := tx.get(m.inodeKey(inode))
 		if a == nil {
@@ -897,6 +898,7 @@ func (m *kvMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 	defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFF) }()
 	var newSpace int64
 	err := m.txn(func(tx kvTxn) error {
+		newSpace = 0
 		var t Attr
 		a := tx.get(m.inodeKey(inode))
 		if a == nil {
@@ -928,7 +930,8 @@ func (m *kvMeta) Fallocate(ctx Context, inode Ino, mode uint8, off uint64, size
 		t.Ctime = now.Unix()
 		t.Ctimensec = uint32(now.Nanosecond())
 		tx.set(m.inodeKey(inode), m.marshal(&t))
-		if mode&(fallocZeroRange|fallocPunchHole) != 0 {
+		if mode&(fallocZeroRange|fallocPunchHole) != 0 && off < old {
+			off, size := off, size
 			if off+size > old {
 				size = old - off
 			}
@@ -1085,6 +1088,9 @@ func (m *kvMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 	var opened bool
 	var newSpace, newInode int64
 	err := m.txn(func(tx kvTxn) error {
+		opened = false
+		attr = Attr{}
+		newSpace, newInode = 0, 0
 		buf := tx.get(m.entryKey(parent, name))
 		if buf == nil && m.conf.CaseInsensi {
 			if e := m.resolveCase(ctx, parent, name); e != nil {
@@ -1108,8 +1114,6 @@ func (m *kvMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
-		attr = Attr{}
-		opened = false
 		now := time.Now()
 		if rs[1] != nil {
 			m.parseAttr(rs[1], &attr)
@@ -1275,6 +1279,10 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 	var tattr Attr
 	var newSpace, newInode int64
 	err := m.txn(func(tx kvTxn) error {
+		opened = false
+		dino, dtyp = 0, 0
+		tattr = Attr{}
+		newSpace, newInode = 0, 0
 		buf := tx.get(m.entryKey(parentSrc, nameSrc))
 		if buf == nil && m.conf.CaseInsensi {
 			if e := m.resolveCase(ctx, parentSrc, nameSrc); e != nil {
@@ -1316,8 +1324,6 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		}
 		var supdate, dupdate bool
 		now := time.Now()
-		tattr = Attr{}
-		opened = false
 		if dbuf != nil {
 			if flags == RenameNoReplace {
 				return syscall.EEXIST
@@ -1371,7 +1377,6 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			if exchange {
 				return syscall.ENOENT
 			}
-			dino, dtyp = 0, 0
 		}
 		if ctx.Uid() != 0 && sattr.Mode&01000 != 0 && ctx.Uid() != sattr.Uid && ctx.Uid() != iattr.Uid {
 			return syscall.EACCES
@@ -1612,6 +1617,7 @@ func (m *kvMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	var attr Attr
 	var newSpace int64
 	err := m.txn(func(tx kvTxn) error {
+		newSpace = 0
 		a := tx.get(m.inodeKey(inode))
 		if a == nil {
 			return nil
@@ -1630,14 +1636,14 @@ func (m *kvMeta) doDeleteSustainedInode(sid uint64, inode Ino) error {
 	return err
 }
 
-func (m *kvMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) syscall.Errno {
+func (m *kvMeta) Read(ctx Context, inode Ino, indx uint32, slices *[]Slice) syscall.Errno {
 	f := m.of.find(inode)
 	if f != nil {
 		f.RLock()
 		defer f.RUnlock()
 	}
-	if cs, ok := m.of.ReadChunk(inode, indx); ok {
-		*chunks = cs
+	if ss, ok := m.of.ReadChunk(inode, indx); ok {
+		*slices = ss
 		return 0
 	}
 	defer m.timeit(time.Now())
@@ -1649,9 +1655,9 @@ func (m *kvMeta) Read(ctx Context, inode Ino, indx uint32, chunks *[]Slice) sysc
 	if ss == nil {
 		return syscall.EIO
 	}
-	*chunks = buildSlice(ss)
-	m.of.CacheChunk(inode, indx, *chunks)
-	if !m.conf.ReadOnly && (len(val)/sliceBytes >= 5 || len(*chunks) >= 5) {
+	*slices = buildSlice(ss)
+	m.of.CacheChunk(inode, indx, *slices)
+	if !m.conf.ReadOnly && (len(val)/sliceBytes >= 5 || len(*slices) >= 5) {
 		go m.compactChunk(inode, indx, false)
 	}
 	return 0
@@ -1668,6 +1674,7 @@ func (m *kvMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 	var newSpace int64
 	var needCompact bool
 	err := m.txn(func(tx kvTxn) error {
+		newSpace = 0
 		var attr Attr
 		a := tx.get(m.inodeKey(inode))
 		if a == nil {
@@ -1690,7 +1697,7 @@ func (m *kvMeta) Write(ctx Context, inode Ino, indx uint32, off uint32, slice Sl
 		attr.Mtimensec = uint32(now.Nanosecond())
 		attr.Ctime = now.Unix()
 		attr.Ctimensec = uint32(now.Nanosecond())
-		val := tx.append(m.chunkKey(inode, indx), marshalSlice(off, slice.Chunkid, slice.Size, slice.Off, slice.Len))
+		val := tx.append(m.chunkKey(inode, indx), marshalSlice(off, slice.Id, slice.Size, slice.Off, slice.Len))
 		tx.set(m.inodeKey(inode), m.marshal(&attr))
 		needCompact = (len(val)/sliceBytes)%100 == 99
 		return nil
@@ -1714,6 +1721,7 @@ func (m *kvMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 	}
 	defer func() { m.of.InvalidateChunk(fout, 0xFFFFFFFF) }()
 	err := m.txn(func(tx kvTxn) error {
+		newSpace = 0
 		rs := tx.gets(m.inodeKey(fin), m.inodeKey(fout))
 		if rs[0] == nil || rs[1] == nil {
 			return syscall.ENOENT
@@ -1727,6 +1735,7 @@ func (m *kvMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 			*copied = 0
 			return nil
 		}
+		size := size
 		if offIn+size > sattr.Length {
 			size = sattr.Length - offIn
 		}
@@ -1784,19 +1793,19 @@ func (m *kvMeta) CopyFileRange(ctx Context, fin Ino, offIn uint64, fout Ino, off
 					indx := uint32(doff / ChunkSize)
 					dpos := uint32(doff % ChunkSize)
 					if dpos+s.Len > ChunkSize {
-						tx.append(m.chunkKey(fout, indx), marshalSlice(dpos, s.Chunkid, s.Size, s.Off, ChunkSize-dpos))
-						if s.Chunkid > 0 {
-							tx.incrBy(m.sliceKey(s.Chunkid, s.Size), 1)
+						tx.append(m.chunkKey(fout, indx), marshalSlice(dpos, s.Id, s.Size, s.Off, ChunkSize-dpos))
+						if s.Id > 0 {
+							tx.incrBy(m.sliceKey(s.Id, s.Size), 1)
 						}
 						skip := ChunkSize - dpos
-						tx.append(m.chunkKey(fout, indx+1), marshalSlice(0, s.Chunkid, s.Size, s.Off+skip, s.Len-skip))
-						if s.Chunkid > 0 {
-							tx.incrBy(m.sliceKey(s.Chunkid, s.Size), 1)
+						tx.append(m.chunkKey(fout, indx+1), marshalSlice(0, s.Id, s.Size, s.Off+skip, s.Len-skip))
+						if s.Id > 0 {
+							tx.incrBy(m.sliceKey(s.Id, s.Size), 1)
 						}
 					} else {
-						tx.append(m.chunkKey(fout, indx), marshalSlice(dpos, s.Chunkid, s.Size, s.Off, s.Len))
-						if s.Chunkid > 0 {
-							tx.incrBy(m.sliceKey(s.Chunkid, s.Size), 1)
+						tx.append(m.chunkKey(fout, indx), marshalSlice(dpos, s.Id, s.Size, s.Off, s.Len))
+						if s.Id > 0 {
+							tx.incrBy(m.sliceKey(s.Id, s.Size), 1)
 						}
 					}
 				}
@@ -1853,13 +1862,13 @@ func (m *kvMeta) doCleanupSlices() {
 	})
 	for k, v := range vals {
 		rb := utils.FromBuffer([]byte(k)[1:])
-		chunkid := rb.Get64()
+		id := rb.Get64()
 		size := rb.Get32()
 		refs := parseCounter(v)
 		if refs < 0 {
-			m.deleteSlice(chunkid, size)
+			m.deleteSlice(id, size)
 		} else {
-			m.cleanupZeroRef(chunkid, size)
+			m.cleanupZeroRef(id, size)
 		}
 	}
 }
@@ -1868,11 +1877,12 @@ func (m *kvMeta) deleteChunk(inode Ino, indx uint32) error {
 	key := m.chunkKey(inode, indx)
 	var todel []*slice
 	err := m.txn(func(tx kvTxn) error {
+		todel = todel[:0]
 		buf := tx.get(key)
 		slices := readSliceBuf(buf)
 		tx.dels(key)
 		for _, s := range slices {
-			if s.chunkid > 0 && tx.incrBy(m.sliceKey(s.chunkid, s.size), -1) < 0 {
+			if s.id > 0 && tx.incrBy(m.sliceKey(s.id, s.size), -1) < 0 {
 				todel = append(todel, s)
 			}
 		}
@@ -1882,18 +1892,18 @@ func (m *kvMeta) deleteChunk(inode Ino, indx uint32) error {
 		return err
 	}
 	for _, s := range todel {
-		m.deleteSlice(s.chunkid, s.size)
+		m.deleteSlice(s.id, s.size)
 	}
 	return nil
 }
 
-func (r *kvMeta) cleanupZeroRef(chunkid uint64, size uint32) {
+func (r *kvMeta) cleanupZeroRef(id uint64, size uint32) {
 	_ = r.txn(func(tx kvTxn) error {
-		v := tx.incrBy(r.sliceKey(chunkid, size), 0)
+		v := tx.incrBy(r.sliceKey(id, size), 0)
 		if v != 0 {
 			return syscall.EINVAL
 		}
-		tx.dels(r.sliceKey(chunkid, size))
+		tx.dels(r.sliceKey(id, size))
 		return nil
 	})
 }
@@ -1936,17 +1946,17 @@ func (m *kvMeta) doCleanupDelayedSlices(edge int64, limit int) (int, error) {
 	var rs []int64
 	for _, key := range keys {
 		if err := m.txn(func(tx kvTxn) error {
+			ss, rs = ss[:0], rs[:0]
 			buf := tx.get(key)
 			if len(buf) == 0 {
 				return nil
 			}
-			ss, rs = ss[:0], rs[:0]
 			m.decodeDelayedSlices(buf, &ss)
 			if len(ss) == 0 {
 				return fmt.Errorf("invalid value for delayed slices %s: %v", key, buf)
 			}
 			for _, s := range ss {
-				rs = append(rs, tx.incrBy(m.sliceKey(s.Chunkid, s.Size), -1))
+				rs = append(rs, tx.incrBy(m.sliceKey(s.Id, s.Size), -1))
 			}
 			tx.dels(key)
 			return nil
@@ -1956,7 +1966,7 @@ func (m *kvMeta) doCleanupDelayedSlices(edge int64, limit int) (int, error) {
 		}
 		for i, s := range ss {
 			if rs[i] < 0 {
-				m.deleteSlice(s.Chunkid, s.Size)
+				m.deleteSlice(s.Id, s.Size)
 				count++
 			}
 		}
@@ -1996,18 +2006,18 @@ func (m *kvMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	ss := readSliceBuf(buf)
 	skipped := skipSome(ss)
 	ss = ss[skipped:]
-	pos, size, chunks := compactChunk(ss)
+	pos, size, slices := compactChunk(ss)
 	if len(ss) < 2 || size == 0 {
 		return
 	}
 
-	var chunkid uint64
-	st := m.NewChunk(Background, &chunkid)
+	var id uint64
+	st := m.NewSlice(Background, &id)
 	if st != 0 {
 		return
 	}
 	logger.Debugf("compact %d:%d: skipped %d slices (%d bytes) %d slices (%d bytes)", inode, indx, skipped, pos, len(ss), size)
-	err = m.newMsg(CompactChunk, chunks, chunkid)
+	err = m.newMsg(CompactChunk, slices, id)
 	if err != nil {
 		if !strings.Contains(err.Error(), "not exist") && !strings.Contains(err.Error(), "not found") {
 			logger.Warnf("compact %d %d with %d slices: %s", inode, indx, len(ss), err)
@@ -2018,8 +2028,8 @@ func (m *kvMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	trash := m.toTrash(0)
 	if trash {
 		for _, s := range ss {
-			if s.chunkid > 0 {
-				dsbuf = append(dsbuf, m.encodeDelayedSlice(s.chunkid, s.size)...)
+			if s.id > 0 {
+				dsbuf = append(dsbuf, m.encodeDelayedSlice(s.id, s.size)...)
 			}
 		}
 	}
@@ -2030,18 +2040,18 @@ func (m *kvMeta) compactChunk(inode Ino, indx uint32, force bool) {
 			return syscall.EINVAL
 		}
 
-		buf2 = append(append(buf2[:skipped*sliceBytes], marshalSlice(pos, chunkid, size, 0, size)...), buf2[len(buf):]...)
+		buf2 = append(append(buf2[:skipped*sliceBytes], marshalSlice(pos, id, size, 0, size)...), buf2[len(buf):]...)
 		tx.set(m.chunkKey(inode, indx), buf2)
 		// create the key to tracking it
-		tx.set(m.sliceKey(chunkid, size), make([]byte, 8))
+		tx.set(m.sliceKey(id, size), make([]byte, 8))
 		if trash {
 			if len(dsbuf) > 0 {
-				tx.set(m.delSliceKey(time.Now().Unix(), chunkid), dsbuf)
+				tx.set(m.delSliceKey(time.Now().Unix(), id), dsbuf)
 			}
 		} else {
 			for _, s := range ss {
-				if s.chunkid > 0 {
-					tx.incrBy(m.sliceKey(s.chunkid, s.size), -1)
+				if s.id > 0 {
+					tx.incrBy(m.sliceKey(s.id, s.size), -1)
 				}
 			}
 		}
@@ -2050,31 +2060,31 @@ func (m *kvMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	// there could be false-negative that the compaction is successful, double-check
 	if err != nil {
 		logger.Warnf("compact %d:%d failed: %s", inode, indx, err)
-		refs, e := m.get(m.sliceKey(chunkid, size))
+		refs, e := m.get(m.sliceKey(id, size))
 		if e == nil {
 			if len(refs) > 0 {
 				err = nil
 			} else {
-				logger.Infof("compacted chunk %d was not used", chunkid)
+				logger.Infof("compacted chunk %d was not used", id)
 				err = syscall.EINVAL
 			}
 		}
 	}
 
 	if errno, ok := err.(syscall.Errno); ok && errno == syscall.EINVAL {
-		logger.Infof("compaction for %d:%d is wasted, delete slice %d (%d bytes)", inode, indx, chunkid, size)
-		m.deleteSlice(chunkid, size)
+		logger.Infof("compaction for %d:%d is wasted, delete slice %d (%d bytes)", inode, indx, id, size)
+		m.deleteSlice(id, size)
 	} else if err == nil {
 		m.of.InvalidateChunk(inode, indx)
-		m.cleanupZeroRef(chunkid, size)
+		m.cleanupZeroRef(id, size)
 		if !trash {
 			var refs int64
 			for _, s := range ss {
-				if s.chunkid > 0 && m.client.txn(func(tx kvTxn) error {
-					refs = tx.incrBy(m.sliceKey(s.chunkid, s.size), 0)
+				if s.id > 0 && m.client.txn(func(tx kvTxn) error {
+					refs = tx.incrBy(m.sliceKey(s.id, s.size), 0)
 					return nil
 				}) == nil && refs < 0 {
-					m.deleteSlice(s.chunkid, s.size)
+					m.deleteSlice(s.id, s.size)
 				}
 			}
 		}
@@ -2133,8 +2143,8 @@ func (m *kvMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, sh
 		inode := m.decodeInode([]byte(key)[1:9])
 		ss := readSliceBuf(value)
 		for _, s := range ss {
-			if s.chunkid > 0 {
-				slices[inode] = append(slices[inode], Slice{Chunkid: s.chunkid, Size: s.size})
+			if s.id > 0 {
+				slices[inode] = append(slices[inode], Slice{Id: s.id, Size: s.size})
 				if showProgress != nil {
 					showProgress()
 				}
@@ -2164,7 +2174,7 @@ func (m *kvMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, sh
 			}
 		}
 		for _, s := range ss {
-			if s.Chunkid > 0 {
+			if s.Id > 0 {
 				slices[1] = append(slices[1], s)
 			}
 		}
@@ -2263,6 +2273,7 @@ func (m *kvMeta) dumpEntry(inode Ino, e *DumpedEntry) error {
 		}
 
 		if attr.Typ == TypeFile {
+			e.Chunks = e.Chunks[:0]
 			vals := tx.scanRange(m.chunkKey(inode, 0), m.chunkKey(inode, uint32(attr.Length/ChunkSize)+1))
 			for indx := uint32(0); uint64(indx)*ChunkSize < attr.Length; indx++ {
 				v, ok := vals[string(m.chunkKey(inode, indx))]
@@ -2272,7 +2283,7 @@ func (m *kvMeta) dumpEntry(inode Ino, e *DumpedEntry) error {
 				ss := readSliceBuf(v)
 				slices := make([]*DumpedSlice, 0, len(ss))
 				for _, s := range ss {
-					slices = append(slices, &DumpedSlice{Chunkid: s.chunkid, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
+					slices = append(slices, &DumpedSlice{Id: s.id, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
 				}
 				e.Chunks = append(e.Chunks, &DumpedChunk{indx, slices})
 			}
@@ -2419,7 +2430,7 @@ func (m *kvMeta) DumpMeta(w io.Writer, root Ino) (err error) {
 					ss := readSliceBuf(value)
 					slices := make([]*DumpedSlice, 0, len(ss))
 					for _, s := range ss {
-						slices = append(slices, &DumpedSlice{Chunkid: s.chunkid, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
+						slices = append(slices, &DumpedSlice{Id: s.id, Pos: s.pos, Size: s.size, Off: s.off, Len: s.len})
 					}
 					e.Chunks = append(e.Chunks, &DumpedChunk{indx, slices})
 				case 'D':
@@ -2583,7 +2594,7 @@ func (m *kvMeta) loadEntry(e *DumpedEntry, kv chan *pair) {
 			}
 			slices := make([]byte, 0, sliceBytes*len(c.Slices))
 			for _, s := range c.Slices {
-				slices = append(slices, marshalSlice(s.Pos, s.Chunkid, s.Size, s.Off, s.Len)...)
+				slices = append(slices, marshalSlice(s.Pos, s.Id, s.Size, s.Off, s.Len)...)
 			}
 			kv <- &pair{m.chunkKey(inode, c.Index), slices}
 		}
diff --git a/pkg/object/hdfs.go b/pkg/object/hdfs.go
index 5fa22838..1819ab67 100644
--- a/pkg/object/hdfs.go
+++ b/pkg/object/hdfs.go
@@ -244,14 +244,6 @@ func (h *hdfsclient) ListAll(prefix, marker string) (<-chan Object, error) {
 				}
 				return err
 			}
-
-			if !strings.HasSuffix(prefix, "/") && !strings.HasPrefix(info.Name(), prefix) {
-				if info.IsDir() && root != path {
-					return filepath.SkipDir
-				}
-				return nil
-			}
-
 			key := path[1:]
 			if !strings.HasPrefix(key, prefix) || key < marker {
 				if info.IsDir() && !strings.HasPrefix(prefix, key) && !strings.HasPrefix(marker, key) {
diff --git a/pkg/object/redis.go b/pkg/object/redis.go
index a251d1ea..700740ee 100644
--- a/pkg/object/redis.go
+++ b/pkg/object/redis.go
@@ -30,6 +30,7 @@ import (
 	"os"
 	"sort"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/go-redis/redis/v8"
@@ -80,28 +81,44 @@ func (r *redisStore) Delete(key string) error {
 }
 
 func (t *redisStore) ListAll(prefix, marker string) (<-chan Object, error) {
+	var scanCli []redis.UniversalClient
+	var m sync.Mutex
+	if c, ok := t.rdb.(*redis.ClusterClient); ok {
+		err := c.ForEachMaster(context.TODO(), func(ctx context.Context, client *redis.Client) error {
+			m.Lock()
+			defer m.Unlock()
+			scanCli = append(scanCli, client)
+			return nil
+		})
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		scanCli = append(scanCli, t.rdb)
+	}
 	batch := 1000
 	var objs = make(chan Object, batch)
 	var keyList []string
 	var cursor uint64
-	for {
-		// FIXME: this will be really slow for many objects
-		keys, c, err := t.rdb.Scan(context.TODO(), cursor, prefix+"*", int64(batch)).Result()
-		if err != nil {
-			logger.Warnf("redis scan error, coursor %d: %s", cursor, err)
-			return nil, err
-		}
-		for _, key := range keys {
-			if key > marker {
-				keyList = append(keyList, key)
+	for _, mCli := range scanCli {
+		for {
+			// FIXME: this will be really slow for many objects
+			keys, c, err := mCli.Scan(context.TODO(), cursor, prefix+"*", int64(batch)).Result()
+			if err != nil {
+				logger.Warnf("redis scan error, coursor %d: %s", cursor, err)
+				return nil, err
 			}
+			for _, key := range keys {
+				if key > marker {
+					keyList = append(keyList, key)
+				}
+			}
+			if c == 0 {
+				break
+			}
+			cursor = c
 		}
-		if c == 0 {
-			break
-		}
-		cursor = c
 	}
-
 	sort.Strings(keyList)
 
 	go func() {
diff --git a/pkg/vfs/compact.go b/pkg/vfs/compact.go
index fb93ce18..4dbd18e0 100644
--- a/pkg/vfs/compact.go
+++ b/pkg/vfs/compact.go
@@ -37,7 +37,7 @@ var (
 func readSlice(store chunk.ChunkStore, s *meta.Slice, page *chunk.Page, off int) error {
 	buf := page.Data
 	read := 0
-	reader := store.NewReader(s.Chunkid, int(s.Size))
+	reader := store.NewReader(s.Id, int(s.Size))
 	for read < len(buf) {
 		p := page.Slice(read, len(buf)-read)
 		n, err := reader.ReadAt(context.Background(), p, off+int(s.Off))
@@ -51,7 +51,7 @@ func readSlice(store chunk.ChunkStore, s *meta.Slice, page *chunk.Page, off int)
 	return nil
 }
 
-func Compact(conf chunk.Config, store chunk.ChunkStore, slices []meta.Slice, chunkid uint64) error {
+func Compact(conf chunk.Config, store chunk.ChunkStore, slices []meta.Slice, id uint64) error {
 	for utils.AllocMemory()-store.UsedMemory() > int64(conf.BufferSize)*3/2 {
 		time.Sleep(time.Millisecond * 100)
 	}
@@ -60,13 +60,13 @@ func Compact(conf chunk.Config, store chunk.ChunkStore, slices []meta.Slice, chu
 		size += s.Len
 	}
 	compactSizeHistogram.Observe(float64(size))
-	logger.Debugf("compact %d slices (%d bytes) to chunk %d", len(slices), size, chunkid)
+	logger.Debugf("compact %d slices (%d bytes) to new slice %d", len(slices), size, id)
 
-	writer := store.NewWriter(chunkid)
+	writer := store.NewWriter(id)
 
 	var pos int
 	for i, s := range slices {
-		if s.Chunkid == 0 {
+		if s.Id == 0 {
 			_, err := writer.WriteAt(make([]byte, int(s.Len)), int64(pos))
 			if err != nil {
 				writer.Abort()
@@ -80,7 +80,7 @@ func Compact(conf chunk.Config, store chunk.ChunkStore, slices []meta.Slice, chu
 			l := utils.Min(conf.BlockSize, int(s.Len)-read)
 			p := chunk.NewOffPage(l)
 			if err := readSlice(store, &slices[i], p, read); err != nil {
-				logger.Debugf("can't compact chunk %d, retry later, read %d: %s", chunkid, i, err)
+				logger.Debugf("can't compact to slice %d, retry later, read %d: %s", id, i, err)
 				p.Release()
 				writer.Abort()
 				return err
@@ -88,7 +88,7 @@ func Compact(conf chunk.Config, store chunk.ChunkStore, slices []meta.Slice, chu
 			_, err := writer.WriteAt(p.Data, int64(pos+read))
 			p.Release()
 			if err != nil {
-				logger.Errorf("can't compact chunk %d, retry later, write: %s", chunkid, err)
+				logger.Errorf("can't compact to slice %d, retry later, write: %s", id, err)
 				writer.Abort()
 				return err
 			}
diff --git a/pkg/vfs/fill.go b/pkg/vfs/fill.go
index f9812f28..2233e3c5 100644
--- a/pkg/vfs/fill.go
+++ b/pkg/vfs/fill.go
@@ -188,8 +188,8 @@ func (v *VFS) fillInode(ctx meta.Context, inode Ino, size uint64, bytes *uint64)
 			if bytes != nil {
 				atomic.AddUint64(bytes, uint64(s.Size))
 			}
-			if err := v.Store.FillCache(s.Chunkid, s.Size); err != nil {
-				return fmt.Errorf("Failed to cache inode %d slice %d: %s", inode, s.Chunkid, err)
+			if err := v.Store.FillCache(s.Id, s.Size); err != nil {
+				return fmt.Errorf("Failed to cache inode %d slice %d: %s", inode, s.Id, err)
 			}
 			if ctx.Canceled() {
 				return syscall.EINTR
diff --git a/pkg/vfs/internal.go b/pkg/vfs/internal.go
index dcf9aed8..81d14fa5 100644
--- a/pkg/vfs/internal.go
+++ b/pkg/vfs/internal.go
@@ -313,9 +313,9 @@ func (v *VFS) handleInternalMsg(ctx meta.Context, cmd uint32, r *utils.Buffer, d
 				_ = v.Meta.Read(ctx, inode, uint32(indx), &cs)
 				for _, c := range cs {
 					if raw {
-						fmt.Fprintf(w, "\t%d:\t%d\t%d\t%d\t%d\n", indx, c.Chunkid, c.Size, c.Off, c.Len)
+						fmt.Fprintf(w, "\t%d:\t%d\t%d\t%d\t%d\n", indx, c.Id, c.Size, c.Off, c.Len)
 					} else {
-						for _, o := range v.caclObjects(c.Chunkid, c.Size, c.Off, c.Len) {
+						for _, o := range v.caclObjects(c.Id, c.Size, c.Off, c.Len) {
 							fmt.Fprintf(w, "\t%d:\t%s\t%d\t%d\t%d\n", indx, o.key, o.size, o.off, o.len)
 						}
 					}
diff --git a/pkg/vfs/reader.go b/pkg/vfs/reader.go
index ae45f294..c9606171 100644
--- a/pkg/vfs/reader.go
+++ b/pkg/vfs/reader.go
@@ -171,8 +171,8 @@ func (s *sliceReader) run() {
 	f.Lock()
 	length := f.length
 	f.Unlock()
-	var chunks []meta.Slice
-	err := f.r.m.Read(meta.Background, inode, indx, &chunks)
+	var slices []meta.Slice
+	err := f.r.m.Read(meta.Background, inode, indx, &slices)
 	f.Lock()
 	if s.state != BUSY || f.err != 0 || f.closing {
 		s.done(0, 0)
@@ -204,7 +204,7 @@ func (s *sliceReader) run() {
 	defer p.Release()
 	var n int
 	ctx := context.TODO()
-	n = f.r.Read(ctx, p, chunks, (uint32(s.block.off))%meta.ChunkSize)
+	n = f.r.Read(ctx, p, slices, (uint32(s.block.off))%meta.ChunkSize)
 
 	f.Lock()
 	if s.state != BUSY || f.shouldStop() {
@@ -782,7 +782,7 @@ func (r *dataReader) Invalidate(inode Ino, off, length uint64) {
 func (r *dataReader) readSlice(ctx context.Context, s *meta.Slice, page *chunk.Page, off int) error {
 	buf := page.Data
 	read := 0
-	if s.Chunkid == 0 {
+	if s.Id == 0 {
 		for read < len(buf) {
 			buf[read] = 0
 			read++
@@ -790,14 +790,14 @@ func (r *dataReader) readSlice(ctx context.Context, s *meta.Slice, page *chunk.P
 		return nil
 	}
 
-	reader := r.store.NewReader(s.Chunkid, int(s.Size))
+	reader := r.store.NewReader(s.Id, int(s.Size))
 	for read < len(buf) {
 		p := page.Slice(read, len(buf)-read)
 		n, err := reader.ReadAt(ctx, p, off+int(s.Off))
 		p.Release()
 		if n == 0 && err != nil {
-			logger.Warningf("fail to read chunkid %d (off:%d, size:%d, clen: %d): %s",
-				s.Chunkid, off+int(s.Off), len(buf)-read, s.Size, err)
+			logger.Warningf("fail to read sliceId %d (off:%d, size:%d, clen: %d): %s",
+				s.Id, off+int(s.Off), len(buf)-read, s.Size, err)
 			return err
 		}
 		read += n
@@ -806,9 +806,9 @@ func (r *dataReader) readSlice(ctx context.Context, s *meta.Slice, page *chunk.P
 	return nil
 }
 
-func (r *dataReader) Read(ctx context.Context, page *chunk.Page, chunks []meta.Slice, offset uint32) int {
-	if len(chunks) > 16 {
-		return r.readManyChunks(ctx, page, chunks, offset)
+func (r *dataReader) Read(ctx context.Context, page *chunk.Page, slices []meta.Slice, offset uint32) int {
+	if len(slices) > 16 {
+		return r.readManySlices(ctx, page, slices, offset)
 	}
 	read := 0
 	var pos uint32
@@ -816,18 +816,18 @@ func (r *dataReader) Read(ctx context.Context, page *chunk.Page, chunks []meta.S
 	waits := 0
 	buf := page.Data
 	size := len(buf)
-	for i := 0; i < len(chunks); i++ {
-		if read < size && offset < pos+chunks[i].Len {
-			toread := utils.Min(size-read, int(pos+chunks[i].Len-offset))
+	for i := 0; i < len(slices); i++ {
+		if read < size && offset < pos+slices[i].Len {
+			toread := utils.Min(size-read, int(pos+slices[i].Len-offset))
 			go func(s *meta.Slice, p *chunk.Page, off, pos uint32) {
 				defer p.Release()
 				errs <- r.readSlice(ctx, s, p, int(off))
-			}(&chunks[i], page.Slice(read, toread), offset-pos, pos)
+			}(&slices[i], page.Slice(read, toread), offset-pos, pos)
 			read += toread
 			offset += uint32(toread)
 			waits++
 		}
-		pos += chunks[i].Len
+		pos += slices[i].Len
 	}
 	for read < size {
 		buf[read] = 0
@@ -847,7 +847,7 @@ func (r *dataReader) Read(ctx context.Context, page *chunk.Page, chunks []meta.S
 	return read
 }
 
-func (r *dataReader) readManyChunks(ctx context.Context, page *chunk.Page, chunks []meta.Slice, offset uint32) int {
+func (r *dataReader) readManySlices(ctx context.Context, page *chunk.Page, slices []meta.Slice, offset uint32) int {
 	read := 0
 	var pos uint32
 	var err error
@@ -857,10 +857,10 @@ func (r *dataReader) readManyChunks(ctx context.Context, page *chunk.Page, chunk
 	size := len(buf)
 	concurrency := make(chan byte, 16)
 
-CHUNKS:
-	for i := 0; i < len(chunks); i++ {
-		if read < size && offset < pos+chunks[i].Len {
-			toread := utils.Min(size-read, int(pos+chunks[i].Len-offset))
+SLICES:
+	for i := 0; i < len(slices); i++ {
+		if read < size && offset < pos+slices[i].Len {
+			toread := utils.Min(size-read, int(pos+slices[i].Len-offset))
 		WAIT:
 			for {
 				select {
@@ -870,7 +870,7 @@ CHUNKS:
 					waits--
 					if e != nil {
 						err = e
-						break CHUNKS
+						break SLICES
 					}
 				}
 			}
@@ -878,13 +878,13 @@ CHUNKS:
 				defer p.Release()
 				errs <- r.readSlice(ctx, s, p, off)
 				<-concurrency
-			}(&chunks[i], page.Slice(read, toread), int(offset-pos), pos)
+			}(&slices[i], page.Slice(read, toread), int(offset-pos), pos)
 
 			read += toread
 			offset += uint32(toread)
 			waits++
 		}
-		pos += chunks[i].Len
+		pos += slices[i].Len
 	}
 	// wait for all jobs done, otherwise they may access invalid memory
 	for waits > 0 {
diff --git a/pkg/vfs/writer.go b/pkg/vfs/writer.go
index c8cb0301..c6e6f15e 100644
--- a/pkg/vfs/writer.go
+++ b/pkg/vfs/writer.go
@@ -69,7 +69,7 @@ func (s *sliceWriter) prepareID(ctx meta.Context, retry bool) {
 	for s.id == 0 {
 		var id uint64
 		f.Unlock()
-		st := f.w.m.NewChunk(ctx, &id)
+		st := f.w.m.NewSlice(ctx, &id)
 		f.Lock()
 		if st != 0 && st != syscall.EIO {
 			s.err = st
@@ -197,7 +197,7 @@ func (c *chunkWriter) commitThread() {
 		f.Unlock()
 
 		if err == 0 {
-			var ss = meta.Slice{Chunkid: s.id, Size: s.length, Off: s.soff, Len: s.slen}
+			var ss = meta.Slice{Id: s.id, Size: s.length, Off: s.soff, Len: s.slen}
 			err = f.w.m.Write(meta.Background, f.inode, c.indx, s.off, ss)
 			f.w.reader.Invalidate(f.inode, uint64(c.indx)*meta.ChunkSize+uint64(s.off), uint64(ss.Len))
 		}
diff --git a/sdk/java/libjfs/Makefile b/sdk/java/libjfs/Makefile
index dc10454e..a174d56c 100644
--- a/sdk/java/libjfs/Makefile
+++ b/sdk/java/libjfs/Makefile
@@ -21,6 +21,9 @@ ARCHNAME := amd64
 ifeq ($(uname_m), aarch64)
     ARCHNAME = arm64
 endif
+ifeq ($(uname_m), arm64)
+    ARCHNAME = arm64
+endif
 
 LIBFILE := libjfs-$(ARCHNAME).so
 ifeq ($(uname_S), Windows)
diff --git a/sdk/java/libjfs/guid.go b/sdk/java/libjfs/guid.go
index 2292e379..f562e103 100644
--- a/sdk/java/libjfs/guid.go
+++ b/sdk/java/libjfs/guid.go
@@ -137,10 +137,18 @@ func (m *mapping) update(uids []pwent, gids []pwent) {
 	m.Lock()
 	defer m.Unlock()
 	for _, u := range uids {
+		oldId := m.usernames[u.name]
+		oldName := m.userIDs[u.id]
+		delete(m.userIDs, oldId)
+		delete(m.usernames, oldName)
 		m.usernames[u.name] = u.id
 		m.userIDs[u.id] = u.name
 	}
 	for _, g := range gids {
+		oldId := m.groups[g.name]
+		oldName := m.groupIDs[g.id]
+		delete(m.groupIDs, oldId)
+		delete(m.groups, oldName)
 		m.groups[g.name] = g.id
 		m.groupIDs[g.id] = g.name
 	}
diff --git a/sdk/java/libjfs/main.go b/sdk/java/libjfs/main.go
index 6c86e904..815a047a 100644
--- a/sdk/java/libjfs/main.go
+++ b/sdk/java/libjfs/main.go
@@ -472,15 +472,15 @@ func jfs_init(cname, jsonConf, user, group, superuser, supergroup *C.char) uintp
 			chunkConf.CacheDir = strings.Join(ds, string(os.PathListSeparator))
 		}
 		store := chunk.NewCachedStore(blob, chunkConf, registerer)
-		m.OnMsg(meta.DeleteChunk, func(args ...interface{}) error {
-			chunkid := args[0].(uint64)
+		m.OnMsg(meta.DeleteSlice, func(args ...interface{}) error {
+			id := args[0].(uint64)
 			length := args[1].(uint32)
-			return store.Remove(chunkid, int(length))
+			return store.Remove(id, int(length))
 		})
 		m.OnMsg(meta.CompactChunk, func(args ...interface{}) error {
 			slices := args[0].([]meta.Slice)
-			chunkid := args[1].(uint64)
-			return vfs.Compact(chunkConf, store, slices, chunkid)
+			id := args[1].(uint64)
+			return vfs.Compact(chunkConf, store, slices, id)
 		})
 		err = m.NewSession()
 		if err != nil {
@@ -571,8 +571,12 @@ func jfs_update_uid_grouping(h uintptr, uidstr *C.char, grouping *C.char) {
 
 	if w.isSuperuser(w.user, groups) {
 		w.ctx = meta.NewContext(uint32(os.Getpid()), 0, []uint32{0})
-	} else if len(groups) > 0 {
-		w.ctx = meta.NewContext(uint32(os.Getpid()), w.lookupUid(w.user), w.lookupGids(strings.Join(groups, ",")))
+	} else {
+		gids := w.ctx.Gids()
+		if len(groups) > 0 {
+			gids = w.lookupGids(strings.Join(groups, ","))
+		}
+		w.ctx = meta.NewContext(uint32(os.Getpid()), w.lookupUid(w.user), gids)
 	}
 }
 


Test patch:
diff --git a/cmd/integration_test.go b/cmd/integration_test.go
index a537e433..6ea98fff 100644
--- a/cmd/integration_test.go
+++ b/cmd/integration_test.go
@@ -26,10 +26,10 @@ import (
 	"github.com/go-redis/redis/v8"
 )
 
-const gatewayMeta = "redis://127.0.0.1:6379/11"
+const gatewayMeta = "redis://127.0.0.1:6379/14"
 const gatewayVolume = "gateway-volume"
 const gatewayAddr = "localhost:9008"
-const webdavMeta = "redis://127.0.0.1:6379/12"
+const webdavMeta = "redis://127.0.0.1:6379/15"
 const webdavVolume = "webdav-volume"
 const webdavAddr = "localhost:9009"
 
diff --git a/cmd/mdtest.go b/cmd/mdtest.go
index 7b25f40b..0ad0d666 100644
--- a/cmd/mdtest.go
+++ b/cmd/mdtest.go
@@ -46,15 +46,15 @@ func createFile(jfs *fs.FileSystem, bar *utils.Bar, np int, root string, d int,
 		}
 		if bytes > 0 {
 			for indx := 0; indx*meta.ChunkSize < bytes; indx++ {
-				var chunkid uint64
-				if st := m.NewChunk(ctx, &chunkid); st != 0 {
+				var id uint64
+				if st := m.NewSlice(ctx, &id); st != 0 {
 					return fmt.Errorf("writechunk %s: %s", fn, st)
 				}
 				size := meta.ChunkSize
 				if bytes < (indx+1)*meta.ChunkSize {
 					size = bytes - indx*meta.ChunkSize
 				}
-				if st := m.Write(ctx, f.Inode(), uint32(indx), 0, meta.Slice{Chunkid: chunkid, Size: uint32(size), Len: uint32(size)}); st != 0 {
+				if st := m.Write(ctx, f.Inode(), uint32(indx), 0, meta.Slice{Id: id, Size: uint32(size), Len: uint32(size)}); st != 0 {
 					return fmt.Errorf("writeend %s: %s", fn, st)
 				}
 			}
diff --git a/pkg/chunk/cached_store_test.go b/pkg/chunk/cached_store_test.go
index 70c1e8b8..dfdbc2d6 100644
--- a/pkg/chunk/cached_store_test.go
+++ b/pkg/chunk/cached_store_test.go
@@ -29,8 +29,8 @@ import (
 	"github.com/juicedata/juicefs/pkg/object"
 )
 
-func forgeChunk(store ChunkStore, chunkid uint64, size int) error {
-	w := store.NewWriter(chunkid)
+func forgetSlice(store ChunkStore, sliceId uint64, size int) error {
+	w := store.NewWriter(sliceId)
 	buf := bytes.Repeat([]byte{0x41}, size)
 	if _, err := w.WriteAt(buf, 0); err != nil {
 		return err
@@ -74,13 +74,13 @@ func testStore(t *testing.T, store ChunkStore) {
 	bsize := defaultConf.BlockSize / 2
 	errs := make(chan error, 3)
 	for i := 2; i < 5; i++ {
-		go func(chunkid uint64) {
-			if err := forgeChunk(store, chunkid, bsize); err != nil {
+		go func(sliceId uint64) {
+			if err := forgetSlice(store, sliceId, bsize); err != nil {
 				errs <- err
 				return
 			}
 			time.Sleep(time.Millisecond * 100) // waiting for flush
-			errs <- store.Remove(chunkid, bsize)
+			errs <- store.Remove(sliceId, bsize)
 		}(uint64(i))
 	}
 	for i := 0; i < 3; i++ {
@@ -193,8 +193,8 @@ func TestStoreDelayed(t *testing.T) {
 	store := NewCachedStore(mem, conf, nil)
 	time.Sleep(time.Second) // waiting for cache scanned
 	testStore(t, store)
-	if err := forgeChunk(store, 10, 1024); err != nil {
-		t.Fatalf("forge chunk 10 1024: %s", err)
+	if err := forgetSlice(store, 10, 1024); err != nil {
+		t.Fatalf("forge slice 10 1024: %s", err)
 	}
 	defer store.Remove(10, 1024)
 	time.Sleep(time.Second) // waiting for upload
@@ -217,13 +217,13 @@ func TestFillCache(t *testing.T) {
 	conf.CacheSize = 10
 	_ = os.RemoveAll(conf.CacheDir)
 	store := NewCachedStore(mem, conf, nil)
-	if err := forgeChunk(store, 10, 1024); err != nil {
-		t.Fatalf("forge chunk 10 1024: %s", err)
+	if err := forgetSlice(store, 10, 1024); err != nil {
+		t.Fatalf("forge slice 10 1024: %s", err)
 	}
 	defer store.Remove(10, 1024)
 	bsize := conf.BlockSize
-	if err := forgeChunk(store, 11, bsize); err != nil {
-		t.Fatalf("forge chunk 11 %d: %s", bsize, err)
+	if err := forgetSlice(store, 11, bsize); err != nil {
+		t.Fatalf("forge slice 11 %d: %s", bsize, err)
 	}
 	defer store.Remove(11, bsize)
 
diff --git a/pkg/fuse/fuse_test.go b/pkg/fuse/fuse_test.go
index b0b79333..9415ca18 100644
--- a/pkg/fuse/fuse_test.go
+++ b/pkg/fuse/fuse_test.go
@@ -93,8 +93,8 @@ func mount(url, mp string) {
 
 	m.OnMsg(meta.CompactChunk, meta.MsgCallback(func(args ...interface{}) error {
 		slices := args[0].([]meta.Slice)
-		chunkid := args[1].(uint64)
-		return vfs.Compact(chunkConf, store, slices, chunkid)
+		sliceId := args[1].(uint64)
+		return vfs.Compact(chunkConf, store, slices, sliceId)
 	}))
 
 	conf := &vfs.Config{
diff --git a/pkg/meta/base_test.go b/pkg/meta/base_test.go
index 4aa770f9..43ae398e 100644
--- a/pkg/meta/base_test.go
+++ b/pkg/meta/base_test.go
@@ -80,7 +80,7 @@ func testMeta(t *testing.T, m Meta) {
 }
 
 func testMetaClient(t *testing.T, m Meta) {
-	m.OnMsg(DeleteChunk, func(args ...interface{}) error { return nil })
+	m.OnMsg(DeleteSlice, func(args ...interface{}) error { return nil })
 	ctx := Background
 	var attr = &Attr{}
 	if st := m.GetAttr(ctx, 1, attr); st != 0 || attr.Mode != 0777 { // getattr of root always succeed
@@ -366,7 +366,7 @@ func testMetaClient(t *testing.T, m Meta) {
 	}
 
 	// data
-	var chunkid uint64
+	var sliceId uint64
 	// try to open a file that does not exist
 	if st := m.Open(ctx, 99999, syscall.O_RDWR, &Attr{}); st != syscall.ENOENT {
 		t.Fatalf("open not exist inode got %d, expected %d", st, syscall.ENOENT)
@@ -375,19 +375,19 @@ func testMetaClient(t *testing.T, m Meta) {
 		t.Fatalf("open f: %s", st)
 	}
 	_ = m.Close(ctx, inode)
-	if st := m.NewChunk(ctx, &chunkid); st != 0 {
+	if st := m.NewSlice(ctx, &sliceId); st != 0 {
 		t.Fatalf("write chunk: %s", st)
 	}
-	var s = Slice{Chunkid: chunkid, Size: 100, Len: 100}
+	var s = Slice{Id: sliceId, Size: 100, Len: 100}
 	if st := m.Write(ctx, inode, 0, 100, s); st != 0 {
 		t.Fatalf("write end: %s", st)
 	}
-	var chunks []Slice
-	if st := m.Read(ctx, inode, 0, &chunks); st != 0 {
+	var slices []Slice
+	if st := m.Read(ctx, inode, 0, &slices); st != 0 {
 		t.Fatalf("read chunk: %s", st)
 	}
-	if len(chunks) != 2 || chunks[0].Chunkid != 0 || chunks[0].Size != 100 || chunks[1].Chunkid != chunkid || chunks[1].Size != 100 {
-		t.Fatalf("chunks: %v", chunks)
+	if len(slices) != 2 || slices[0].Id != 0 || slices[0].Size != 100 || slices[1].Id != sliceId || slices[1].Size != 100 {
+		t.Fatalf("slices: %v", slices)
 	}
 	if st := m.Fallocate(ctx, inode, fallocPunchHole|fallocKeepSize, 100, 50); st != 0 {
 		t.Fatalf("fallocate: %s", st)
@@ -410,11 +410,11 @@ func testMetaClient(t *testing.T, m Meta) {
 	if st := m.Fallocate(ctx, parent, fallocPunchHole|fallocKeepSize, 100, 50); st != syscall.EPERM {
 		t.Fatalf("fallocate dir: %s", st)
 	}
-	if st := m.Read(ctx, inode, 0, &chunks); st != 0 {
+	if st := m.Read(ctx, inode, 0, &slices); st != 0 {
 		t.Fatalf("read chunk: %s", st)
 	}
-	if len(chunks) != 3 || chunks[1].Chunkid != 0 || chunks[1].Len != 50 || chunks[2].Chunkid != chunkid || chunks[2].Len != 50 {
-		t.Fatalf("chunks: %v", chunks)
+	if len(slices) != 3 || slices[1].Id != 0 || slices[1].Len != 50 || slices[2].Id != sliceId || slices[2].Len != 50 {
+		t.Fatalf("slices: %v", slices)
 	}
 
 	// xattr
@@ -511,11 +511,11 @@ func testMetaClient(t *testing.T, m Meta) {
 		t.Fatalf("unlink f3: %s", st)
 	}
 	time.Sleep(time.Millisecond * 100) // wait for delete
-	if st := m.Read(ctx, inode, 0, &chunks); st != 0 {
+	if st := m.Read(ctx, inode, 0, &slices); st != 0 {
 		t.Fatalf("read chunk: %s", st)
 	}
-	if len(chunks) != 0 {
-		t.Fatalf("chunks: %v", chunks)
+	if len(slices) != 0 {
+		t.Fatalf("slices: %v", slices)
 	}
 	if st := m.Rmdir(ctx, 1, "d"); st != 0 {
 		t.Fatalf("rmdir d: %s", st)
@@ -812,10 +812,10 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 	}
 	var l sync.Mutex
 	deleted := make(map[uint64]int)
-	m.OnMsg(DeleteChunk, func(args ...interface{}) error {
+	m.OnMsg(DeleteSlice, func(args ...interface{}) error {
 		l.Lock()
-		chunkid := args[0].(uint64)
-		deleted[chunkid] = 1
+		sliceId := args[0].(uint64)
+		deleted[sliceId] = 1
 		l.Unlock()
 		return nil
 	})
@@ -834,13 +834,13 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 	}()
 
 	// random write
-	var chunkid uint64
-	m.NewChunk(ctx, &chunkid)
-	_ = m.Write(ctx, inode, 1, uint32(0), Slice{Chunkid: chunkid, Size: 64 << 20, Len: 64 << 20})
-	m.NewChunk(ctx, &chunkid)
-	_ = m.Write(ctx, inode, 1, uint32(30<<20), Slice{Chunkid: chunkid, Size: 8, Len: 8})
-	m.NewChunk(ctx, &chunkid)
-	_ = m.Write(ctx, inode, 1, uint32(40<<20), Slice{Chunkid: chunkid, Size: 8, Len: 8})
+	var sliceId uint64
+	m.NewSlice(ctx, &sliceId)
+	_ = m.Write(ctx, inode, 1, uint32(0), Slice{Id: sliceId, Size: 64 << 20, Len: 64 << 20})
+	m.NewSlice(ctx, &sliceId)
+	_ = m.Write(ctx, inode, 1, uint32(30<<20), Slice{Id: sliceId, Size: 8, Len: 8})
+	m.NewSlice(ctx, &sliceId)
+	_ = m.Write(ctx, inode, 1, uint32(40<<20), Slice{Id: sliceId, Size: 8, Len: 8})
 	var cs1 []Slice
 	_ = m.Read(ctx, inode, 1, &cs1)
 	if len(cs1) != 5 {
@@ -858,9 +858,9 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 	// append
 	var size uint32 = 100000
 	for i := 0; i < 200; i++ {
-		var chunkid uint64
-		m.NewChunk(ctx, &chunkid)
-		if st := m.Write(ctx, inode, 0, uint32(i)*size, Slice{Chunkid: chunkid, Size: size, Len: size}); st != 0 {
+		var sliceId uint64
+		m.NewSlice(ctx, &sliceId)
+		if st := m.Write(ctx, inode, 0, uint32(i)*size, Slice{Id: sliceId, Size: size, Len: size}); st != 0 {
 			t.Fatalf("write %d: %s", i, st)
 		}
 		time.Sleep(time.Millisecond)
@@ -868,15 +868,15 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 	if c, ok := m.(compactor); ok {
 		c.compactChunk(inode, 0, true)
 	}
-	var chunks []Slice
-	if st := m.Read(ctx, inode, 0, &chunks); st != 0 {
+	var slices []Slice
+	if st := m.Read(ctx, inode, 0, &slices); st != 0 {
 		t.Fatalf("read 0: %s", st)
 	}
-	if len(chunks) >= 10 {
-		t.Fatalf("inode %d should be compacted, but have %d slices", inode, len(chunks))
+	if len(slices) >= 10 {
+		t.Fatalf("inode %d should be compacted, but have %d slices", inode, len(slices))
 	}
 	var total uint32
-	for _, s := range chunks {
+	for _, s := range slices {
 		total += s.Len
 	}
 	if total != size*200 {
@@ -889,8 +889,8 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 		t.Fatalf("compactall: %s", st)
 	}
 	p.Done()
-	slices := make(map[Ino][]Slice)
-	if st := m.ListSlices(ctx, slices, false, nil); st != 0 {
+	sliceMap := make(map[Ino][]Slice)
+	if st := m.ListSlices(ctx, sliceMap, false, nil); st != 0 {
 		t.Fatalf("list all slices: %s", st)
 	}
 
@@ -899,10 +899,10 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 	l.Unlock()
 	if trash {
 		if deletes > 10 {
-			t.Fatalf("deleted chunks %d is greater than 10", deletes)
+			t.Fatalf("deleted slices %d is greater than 10", deletes)
 		}
-		if len(slices[1]) < 200 {
-			t.Fatalf("list delayed slices %d is less than 200", len(slices[1]))
+		if len(sliceMap[1]) < 200 {
+			t.Fatalf("list delayed slices %d is less than 200", len(sliceMap[1]))
 		}
 		m.(engine).doCleanupDelayedSlices(time.Now().Unix()+1, 1000)
 		l.Lock()
@@ -910,12 +910,12 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 		l.Unlock()
 	}
 	if deletes < 200 {
-		t.Fatalf("deleted chunks %d is less than 200", deletes)
+		t.Fatalf("deleted slices %d is less than 200", deletes)
 	}
 }
 
 func testConcurrentWrite(t *testing.T, m Meta) {
-	m.OnMsg(DeleteChunk, func(args ...interface{}) error {
+	m.OnMsg(DeleteSlice, func(args ...interface{}) error {
 		return nil
 	})
 	m.OnMsg(CompactChunk, func(args ...interface{}) error {
@@ -939,9 +939,9 @@ func testConcurrentWrite(t *testing.T, m Meta) {
 		go func(indx uint32) {
 			defer g.Done()
 			for j := 0; j < 100; j++ {
-				var chunkid uint64
-				m.NewChunk(ctx, &chunkid)
-				var slice = Slice{Chunkid: chunkid, Size: 100, Len: 100}
+				var sliceId uint64
+				m.NewSlice(ctx, &sliceId)
+				var slice = Slice{Id: sliceId, Size: 100, Len: 100}
 				st := m.Write(ctx, inode, indx, 0, slice)
 				if st != 0 {
 					errno = st
@@ -957,7 +957,7 @@ func testConcurrentWrite(t *testing.T, m Meta) {
 }
 
 func testTruncateAndDelete(t *testing.T, m Meta) {
-	m.OnMsg(DeleteChunk, func(args ...interface{}) error {
+	m.OnMsg(DeleteSlice, func(args ...interface{}) error {
 		return nil
 	})
 	// remove quota
@@ -976,11 +976,11 @@ func testTruncateAndDelete(t *testing.T, m Meta) {
 		t.Fatalf("create file %s", st)
 	}
 	defer m.Unlink(ctx, 1, "f")
-	var cid uint64
-	if st := m.NewChunk(ctx, &cid); st != 0 {
+	var sliceId uint64
+	if st := m.NewSlice(ctx, &sliceId); st != 0 {
 		t.Fatalf("new chunk: %s", st)
 	}
-	if st := m.Write(ctx, inode, 0, 100, Slice{cid, 100, 0, 100}); st != 0 {
+	if st := m.Write(ctx, inode, 0, 100, Slice{sliceId, 100, 0, 100}); st != 0 {
 		t.Fatalf("write file %s", st)
 	}
 	if st := m.Truncate(ctx, inode, 0, 200<<20, attr); st != 0 {
@@ -1000,7 +1000,7 @@ func testTruncateAndDelete(t *testing.T, m Meta) {
 		totalSlices += len(ss)
 	}
 	if totalSlices != 1 {
-		t.Fatalf("number of chunks: %d != 1, %+v", totalSlices, slices)
+		t.Fatalf("number of slices: %d != 1, %+v", totalSlices, slices)
 	}
 	_ = m.Close(ctx, inode)
 	if st := m.Unlink(ctx, 1, "f"); st != 0 {
@@ -1016,12 +1016,12 @@ func testTruncateAndDelete(t *testing.T, m Meta) {
 	}
 	// the last chunk could be found and deleted
 	if totalSlices > 1 {
-		t.Fatalf("number of chunks: %d > 1, %+v", totalSlices, slices)
+		t.Fatalf("number of slices: %d > 1, %+v", totalSlices, slices)
 	}
 }
 
 func testCopyFileRange(t *testing.T, m Meta) {
-	m.OnMsg(DeleteChunk, func(args ...interface{}) error {
+	m.OnMsg(DeleteSlice, func(args ...interface{}) error {
 		return nil
 	})
 	_ = m.Init(Format{Name: "test"}, false)
@@ -1051,23 +1051,23 @@ func testCopyFileRange(t *testing.T, m Meta) {
 	if copied != expected {
 		t.Fatalf("expect copy %d bytes, but got %d", expected, copied)
 	}
-	var expectedChunks = [][]Slice{
+	var expectedSlices = [][]Slice{
 		{{0, 30 << 20, 0, 30 << 20}, {10, 200, 50, 50}, {0, 0, 200, ChunkSize - 30<<20 - 50}},
 		{{0, 0, 150 + (ChunkSize - 30<<20), 30<<20 - 150}, {0, 0, 0, 100 << 10}, {11, 40 << 20, 0, (34 << 20) + 150 - (100 << 10)}},
 		{{11, 40 << 20, (34 << 20) + 150 - (100 << 10), 6<<20 - 150 + 100<<10}, {0, 0, 40<<20 + 100<<10, ChunkSize - 40<<20 - 100<<10}, {0, 0, 0, 150 + (ChunkSize - 30<<20)}},
 		{{0, 0, 150 + (ChunkSize - 30<<20), 30<<20 - 150}, {12, 63 << 20, 10 << 20, (8 << 20) + 150}},
 	}
 	for i := uint32(0); i < 4; i++ {
-		var chunks []Slice
-		if st := m.Read(ctx, iout, i, &chunks); st != 0 {
+		var slices []Slice
+		if st := m.Read(ctx, iout, i, &slices); st != 0 {
 			t.Fatalf("read chunk %d: %s", i, st)
 		}
-		if len(chunks) != len(expectedChunks[i]) {
-			t.Fatalf("expect chunk %d: %+v, but got %+v", i, expectedChunks[i], chunks)
+		if len(slices) != len(expectedSlices[i]) {
+			t.Fatalf("expect chunk %d: %+v, but got %+v", i, expectedSlices[i], slices)
 		}
-		for j, s := range chunks {
-			if s != expectedChunks[i][j] {
-				t.Fatalf("expect slice %d,%d: %+v, but got %+v", i, j, expectedChunks[i][j], s)
+		for j, s := range slices {
+			if s != expectedSlices[i][j] {
+				t.Fatalf("expect slice %d,%d: %+v, but got %+v", i, j, expectedSlices[i][j], s)
 			}
 		}
 	}
diff --git a/pkg/meta/benchmarks_test.go b/pkg/meta/benchmarks_test.go
index 3b2e03a2..c88c757d 100644
--- a/pkg/meta/benchmarks_test.go
+++ b/pkg/meta/benchmarks_test.go
@@ -508,9 +508,9 @@ func benchReadlink(b *testing.B, m Meta) {
 
 func benchNewChunk(b *testing.B, m Meta) {
 	ctx := Background
-	var chunkid uint64
+	var sliceId uint64
 	for i := 0; i < b.N; i++ {
-		if err := m.NewChunk(ctx, &chunkid); err != 0 {
+		if err := m.NewSlice(ctx, &sliceId); err != 0 {
 			b.Fatalf("newchunk: %s", err)
 		}
 	}
@@ -527,16 +527,16 @@ func benchWrite(b *testing.B, m Meta) {
 		b.Fatalf("create: %s", err)
 	}
 	var (
-		chunkid uint64
+		sliceId uint64
 		offset  uint32
 		step    uint32 = 1024
 	)
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		if err := m.NewChunk(ctx, &chunkid); err != 0 {
+		if err := m.NewSlice(ctx, &sliceId); err != 0 {
 			b.Fatalf("newchunk: %s", err)
 		}
-		if err := m.Write(ctx, inode, 0, offset, Slice{Chunkid: chunkid, Size: step, Len: step}); err != 0 {
+		if err := m.Write(ctx, inode, 0, offset, Slice{Id: sliceId, Size: step, Len: step}); err != 0 {
 			b.Fatalf("write: %s", err)
 		}
 		offset += step
@@ -556,13 +556,13 @@ func benchRead(b *testing.B, m Meta, n int) {
 	if err := m.Create(ctx, parent, "file", 0644, 022, 0, &inode, nil); err != 0 {
 		b.Fatalf("create: %s", err)
 	}
-	var chunkid uint64
+	var sliceId uint64
 	var step uint32 = 1024
 	for j := 0; j < n; j++ {
-		if err := m.NewChunk(ctx, &chunkid); err != 0 {
+		if err := m.NewSlice(ctx, &sliceId); err != 0 {
 			b.Fatalf("newchunk: %s", err)
 		}
-		if err := m.Write(ctx, inode, 0, uint32(j)*step, Slice{Chunkid: chunkid, Size: step, Len: step}); err != 0 {
+		if err := m.Write(ctx, inode, 0, uint32(j)*step, Slice{Id: sliceId, Size: step, Len: step}); err != 0 {
 			b.Fatalf("write: %s", err)
 		}
 	}
@@ -612,7 +612,7 @@ func benchmarkLink(b *testing.B, m Meta) {
 }
 
 func benchmarkData(b *testing.B, m Meta) {
-	m.OnMsg(DeleteChunk, func(args ...interface{}) error { return nil })
+	m.OnMsg(DeleteSlice, func(args ...interface{}) error { return nil })
 	m.OnMsg(CompactChunk, func(args ...interface{}) error { return nil })
 	b.Run("newchunk", func(b *testing.B) { benchNewChunk(b, m) })
 	b.Run("write", func(b *testing.B) { benchWrite(b, m) })
diff --git a/pkg/meta/load_dump_test.go b/pkg/meta/load_dump_test.go
index 5490d05f..896e3d8c 100644
--- a/pkg/meta/load_dump_test.go
+++ b/pkg/meta/load_dump_test.go
@@ -126,12 +126,12 @@ func testLoad(t *testing.T, uri, fname string) Meta {
 	if attr.Nlink != 1 || attr.Length != 24 {
 		t.Fatalf("nlink: %d, length: %d", attr.Nlink, attr.Length)
 	}
-	var chunks []Slice
-	if st := m.Read(ctx, 2, 0, &chunks); st != 0 {
+	var slices []Slice
+	if st := m.Read(ctx, 2, 0, &slices); st != 0 {
 		t.Fatalf("read chunk: %s", st)
 	}
-	if len(chunks) != 1 || chunks[0].Chunkid != 4 || chunks[0].Size != 24 {
-		t.Fatalf("chunks: %v", chunks)
+	if len(slices) != 1 || slices[0].Id != 4 || slices[0].Size != 24 {
+		t.Fatalf("slices: %v", slices)
 	}
 	if st := m.GetAttr(ctx, 4, attr); st != 0 || attr.Nlink != 2 { // hard link
 		t.Fatalf("getattr: %s, %d", st, attr.Nlink)
@@ -160,6 +160,34 @@ func testLoad(t *testing.T, uri, fname string) Meta {
 	return m
 }
 
+func testLoadSub(t *testing.T, uri, fname string) {
+	m := NewClient(uri, &Config{Retries: 10, Strict: true})
+	if err := m.Reset(); err != nil {
+		t.Fatalf("reset meta: %s", err)
+	}
+	fp, err := os.Open(fname)
+	if err != nil {
+		t.Fatalf("open file: %s", fname)
+	}
+	defer fp.Close()
+	if err = m.LoadMeta(fp); err != nil {
+		t.Fatalf("load meta: %s", err)
+	}
+
+	var entries []*Entry
+	if st := m.Readdir(Background, 1, 0, &entries); st != 0 {
+		t.Fatalf("readdir: %s", st)
+	} else if len(entries) != 4 {
+		t.Fatalf("entries: %d", len(entries))
+	}
+	for _, entry := range entries {
+		fname := string(entry.Name)
+		if fname != "." && fname != ".." && fname != "big" && fname != "f11" {
+			t.Fatalf("invalid entry name: %s", fname)
+		}
+	}
+}
+
 func testDump(t *testing.T, m Meta, root Ino, expect, result string) {
 	fp, err := os.OpenFile(result, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
 	if err != nil {
@@ -186,6 +214,8 @@ func testLoadDump(t *testing.T, name, addr string) {
 		m = NewClient(addr, &Config{Retries: 10, Strict: true, Subdir: "d1"})
 		testDump(t, m, 1, subSampleFile, "test_subdir.dump")
 		testDump(t, m, 0, sampleFile, "test.dump")
+		_ = m.Shutdown()
+		testLoadSub(t, addr, subSampleFile)
 	})
 }
 
@@ -217,5 +247,7 @@ func TestLoadDump_MemKV(t *testing.T) {
 		}
 		testDump(t, m, 1, subSampleFile, "test_subdir.dump")
 		testDump(t, m, 0, sampleFile, "test.dump")
+		_ = os.Remove(settingPath)
+		testLoadSub(t, "memkv://user:pass@test/jfs", subSampleFile)
 	})
 }
diff --git a/pkg/object/object_storage_test.go b/pkg/object/object_storage_test.go
index 7eb1c85f..829b4c22 100644
--- a/pkg/object/object_storage_test.go
+++ b/pkg/object/object_storage_test.go
@@ -23,10 +23,12 @@ import (
 	"crypto/rsa"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io/ioutil"
 	"math"
 	"os"
 	"reflect"
+	"sort"
 	"strings"
 	"testing"
 	"time"
@@ -170,6 +172,33 @@ func testStorage(t *testing.T, s ObjectStorage) {
 		}
 	}
 
+	// test redis cluster list all api
+	keyTotal := 100
+	var sortedKeys []string
+	for i := 0; i < keyTotal; i++ {
+		k := fmt.Sprintf("hashKey%d", i)
+		sortedKeys = append(sortedKeys, k)
+		if err := s.Put(k, bytes.NewReader(br)); err != nil {
+			t.Fatalf("PUT failed: %s", err.Error())
+		}
+	}
+	sort.Strings(sortedKeys)
+	defer func() {
+		for i := 0; i < keyTotal; i++ {
+			_ = s.Delete(fmt.Sprintf("hashKey%d", i))
+		}
+	}()
+	objs, err := listAll(s, "hashKey", "", int64(keyTotal))
+	if err != nil {
+		t.Fatalf("list4 failed: %s", err.Error())
+	} else {
+		for i := 0; i < keyTotal; i++ {
+			if objs[i].Key() != sortedKeys[i] {
+				t.Fatal("The result for list4 is incorrect")
+			}
+		}
+	}
+
 	f, _ := ioutil.TempFile("", "test")
 	f.Write([]byte("this is a file"))
 	f.Seek(0, 0)
diff --git a/pkg/vfs/compact_test.go b/pkg/vfs/compact_test.go
index 2d6190f5..ca5e96f6 100644
--- a/pkg/vfs/compact_test.go
+++ b/pkg/vfs/compact_test.go
@@ -56,7 +56,7 @@ func TestCompact(t *testing.T) {
 		if e := w.Finish(len(buf)); e != nil {
 			t.Fatalf("flush chunk %d: %s", cid, e)
 		}
-		slices = append(slices, meta.Slice{Chunkid: cid, Size: uint32(len(buf)), Len: uint32(len(buf))})
+		slices = append(slices, meta.Slice{Id: cid, Size: uint32(len(buf)), Len: uint32(len(buf))})
 	}
 
 	// compact
diff --git a/pkg/vfs/fill_test.go b/pkg/vfs/fill_test.go
index cf5b849b..967f5769 100644
--- a/pkg/vfs/fill_test.go
+++ b/pkg/vfs/fill_test.go
@@ -42,7 +42,7 @@ func TestFill(t *testing.T) {
 	var slices []meta.Slice
 	_ = v.Meta.Read(meta.Background, fe.Inode, 0, &slices)
 	for _, s := range slices {
-		_ = v.Store.Remove(s.Chunkid, int(s.Size))
+		_ = v.Store.Remove(s.Id, int(s.Size))
 	}
 	// bad cases
 	v.fillCache(meta.Background, []string{"/test/file", "/sym2", "/sym3", "/.stats", "/not_exists"}, 2, nil, nil)
diff --git a/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java b/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
index cd02431e..9d216a14 100644
--- a/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
+++ b/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
@@ -622,4 +622,26 @@ public class JuiceFileSystemTest extends TestCase {
     assertEquals(FsPermission.createImmutable((short) 0666), newFs.getFileStatus(new Path("/test_umask/dir/f")).getPermission());
     newFs.close();
   }
+
+  public void testGuidMapping() throws Exception {
+    Configuration newConf = new Configuration(cfg);
+
+    FSDataOutputStream ou = fs.create(new Path("/etc/users"));
+    ou.write("foo:10000\n".getBytes());
+    ou.close();
+    newConf.set("juicefs.users", "/etc/users");
+
+    FileSystem fooFs = createNewFs(newConf, "foo", new String[]{"nogrp"});
+    Path f = new Path("/test_foo");
+    fooFs.create(f).close();
+    assertEquals("foo", fooFs.getFileStatus(f).getOwner());
+
+    ou = fs.create(new Path("/etc/users"));
+    ou.write("foo:10001\n".getBytes());
+    ou.close();
+    FileSystem newFS = FileSystem.newInstance(newConf);
+    assertEquals("10000", fooFs.getFileStatus(f).getOwner());
+
+    fooFs.delete(f, false);
+  }
 }