Repository: juicedata/juicefs
Pull request #2995 (2022-11-20 03:56:17)

cache_management.md improvements
* better introduce cache parameters
* consolidate content, better content organization
* simplify language here and there


Patch:
diff --git a/cmd/debug.go b/cmd/debug.go
index d6008636..4a0757b7 100644
--- a/cmd/debug.go
+++ b/cmd/debug.go
@@ -107,13 +107,13 @@ func copyFileOnWindows(srcPath, destPath string) error {
 	return nil
 }
 
-func copyFile(srcPath, destPath string, rootPrivileges bool) error {
+func copyFile(srcPath, destPath string, requireRootPrivileges bool) error {
 	if runtime.GOOS == "windows" {
 		return copyFileOnWindows(srcPath, destPath)
 	}
 
 	var copyArgs []string
-	if rootPrivileges {
+	if requireRootPrivileges {
 		copyArgs = append(copyArgs, "sudo")
 	}
 	copyArgs = append(copyArgs, "/bin/sh", "-c", fmt.Sprintf("cat %s > %s", srcPath, destPath))
@@ -121,7 +121,7 @@ func copyFile(srcPath, destPath string, rootPrivileges bool) error {
 }
 
 func getCmdMount(mp string) (uid, pid, cmd string, err error) {
-	psArgs := []string{"/bin/sh", "-c", "ps -ef | grep -v grep | grep 'juicefs mount' | grep " + mp}
+	psArgs := []string{"/bin/sh", "-c", fmt.Sprintf("ps -ef | grep -v grep | grep -E \"%s mount | /sbin/mount.juicefs\" | grep %s", os.Args[0], mp)}
 	ret, err := exec.Command(psArgs[0], psArgs[1:]...).CombinedOutput()
 	if err != nil {
 		return "", "", "", fmt.Errorf("failed to execute command `%s`: %v", strings.Join(psArgs, " "), err)
@@ -149,16 +149,16 @@ func getCmdMount(mp string) (uid, pid, cmd string, err error) {
 	return uid, pid, cmd, nil
 }
 
-func getDefaultLogDir(rootPrivileges bool) (string, error) {
+func getDefaultLogDir(requireRootPrivileges bool) (string, error) {
 	var defaultLogDir = "/var/log"
 	switch runtime.GOOS {
 	case "linux":
-		if rootPrivileges {
+		if os.Getuid() == 0 {
 			break
 		}
 		fallthrough
 	case "darwin":
-		if rootPrivileges {
+		if requireRootPrivileges {
 			defaultLogDir = "/var/root/.juicefs"
 			break
 		}
@@ -173,13 +173,13 @@ func getDefaultLogDir(rootPrivileges bool) (string, error) {
 
 var logArg = regexp.MustCompile(`--log(\s*=?\s*)(\S+)`)
 
-func getLogPath(cmd string, rootPrivileges bool) (string, error) {
+func getLogPath(cmd string, requireRootPrivileges bool) (string, error) {
 	var logPath string
 	tmp := logArg.FindStringSubmatch(cmd)
 	if len(tmp) == 3 {
 		logPath = tmp[2]
 	} else {
-		defaultLogDir, err := getDefaultLogDir(rootPrivileges)
+		defaultLogDir, err := getDefaultLogDir(requireRootPrivileges)
 		if err != nil {
 			return "", err
 		}
@@ -195,9 +195,9 @@ func closeFile(file *os.File) {
 	}
 }
 
-func copyLogFile(logPath, retLogPath string, limit uint64, rootPrivileges bool) error {
+func copyLogFile(logPath, retLogPath string, limit uint64, requireRootPrivileges bool) error {
 	var copyArgs []string
-	if rootPrivileges {
+	if requireRootPrivileges {
 		copyArgs = append(copyArgs, "sudo")
 	}
 	copyArgs = append(copyArgs, "/bin/sh", "-c")
@@ -209,7 +209,7 @@ func copyLogFile(logPath, retLogPath string, limit uint64, rootPrivileges bool)
 	return exec.Command(copyArgs[0], copyArgs[1:]...).Run()
 }
 
-func getPprofPort(pid, amp string, rootPrivileges bool) (int, error) {
+func getPprofPort(pid, amp string, requireRootPrivileges bool) (int, error) {
 	content, err := os.ReadFile(filepath.Join(amp, ".config"))
 	if err != nil {
 		logger.Warnf("failed to read config file: %v", err)
@@ -229,7 +229,7 @@ func getPprofPort(pid, amp string, rootPrivileges bool) (int, error) {
 	}
 
 	var lsofArgs []string
-	if rootPrivileges {
+	if requireRootPrivileges {
 		lsofArgs = append(lsofArgs, "sudo")
 	}
 	lsofArgs = append(lsofArgs, "/bin/sh", "-c", "lsof -i -nP | grep -v grep | grep LISTEN | grep "+pid)
@@ -395,7 +395,7 @@ func geneZipFile(srcPath, destPath string) error {
 	})
 }
 
-func collectPprof(ctx *cli.Context, cmd string, pid string, amp string, rootPrivileges bool, currDir string, wg *sync.WaitGroup) error {
+func collectPprof(ctx *cli.Context, cmd string, pid string, amp string, requireRootPrivileges bool, currDir string, wg *sync.WaitGroup) error {
 	if !checkAgent(cmd) {
 		logger.Warnf("No agent found, the pprof metrics will not be collected")
 		return nil
@@ -406,7 +406,7 @@ func collectPprof(ctx *cli.Context, cmd string, pid string, amp string, rootPriv
 		return nil
 	}
 
-	port, err := getPprofPort(pid, amp, rootPrivileges)
+	port, err := getPprofPort(pid, amp, requireRootPrivileges)
 	if err != nil {
 		return fmt.Errorf("failed to get pprof port: %v", err)
 	}
@@ -450,12 +450,12 @@ func collectPprof(ctx *cli.Context, cmd string, pid string, amp string, rootPriv
 	return nil
 }
 
-func collectLog(ctx *cli.Context, cmd string, rootPrivileges bool, currDir string) error {
+func collectLog(ctx *cli.Context, cmd string, requireRootPrivileges bool, currDir string) error {
 	if !isUnix() {
 		logger.Warnf("Collecting log currently only support Linux/macOS")
 		return nil
 	}
-	logPath, err := getLogPath(cmd, rootPrivileges)
+	logPath, err := getLogPath(cmd, requireRootPrivileges)
 	if err != nil {
 		return fmt.Errorf("failed to get log path: %v", err)
 	}
@@ -463,7 +463,7 @@ func collectLog(ctx *cli.Context, cmd string, rootPrivileges bool, currDir strin
 	retLogPath := filepath.Join(currDir, "juicefs.log")
 
 	logger.Infof("Log %s is being collected", logPath)
-	return copyLogFile(logPath, retLogPath, limit, rootPrivileges)
+	return copyLogFile(logPath, retLogPath, limit, requireRootPrivileges)
 }
 
 func collectSysInfo(ctx *cli.Context, currDir string) error {
@@ -492,9 +492,9 @@ JuiceFS Version:
 	return nil
 }
 
-func collectSpecialFile(ctx *cli.Context, amp string, currDir string, rootPrivileges bool, wg *sync.WaitGroup) error {
+func collectSpecialFile(ctx *cli.Context, amp string, currDir string, requireRootPrivileges bool, wg *sync.WaitGroup) error {
 	configName := ".config"
-	if err := copyFile(filepath.Join(amp, configName), filepath.Join(currDir, "config.txt"), rootPrivileges); err != nil {
+	if err := copyFile(filepath.Join(amp, configName), filepath.Join(currDir, "config.txt"), requireRootPrivileges); err != nil {
 		return fmt.Errorf("failed to get volume config %s: %v", configName, err)
 	}
 
@@ -505,14 +505,14 @@ func collectSpecialFile(ctx *cli.Context, amp string, currDir string, rootPrivil
 		defer wg.Done()
 		srcPath := filepath.Join(amp, statsName)
 		destPath := filepath.Join(currDir, "stats.txt")
-		if err := copyFile(srcPath, destPath, rootPrivileges); err != nil {
+		if err := copyFile(srcPath, destPath, requireRootPrivileges); err != nil {
 			logger.Errorf("Failed to get volume config %s: %v", statsName, err)
 		}
 
 		logger.Infof("Stats metrics are being sampled, sampling duration: %ds", stats)
 		time.Sleep(time.Second * time.Duration(stats))
 		destPath = filepath.Join(currDir, fmt.Sprintf("stats.%ds.txt", stats))
-		if err := copyFile(srcPath, destPath, rootPrivileges); err != nil {
+		if err := copyFile(srcPath, destPath, requireRootPrivileges); err != nil {
 			logger.Errorf("Failed to get volume config %s: %v", statsName, err)
 		}
 	}()
@@ -552,22 +552,22 @@ func debug(ctx *cli.Context) error {
 	}
 	fmt.Printf("\nMount Command:\n%s\n\n", cmd)
 
-	rootPrivileges := false
+	requireRootPrivileges := false
 	if (uid == "0" || uid == "root") && os.Getuid() != 0 {
 		fmt.Println("Mount point is mounted by the root user, may ask for root privilege...")
-		rootPrivileges = true
+		requireRootPrivileges = true
 	}
 
 	var wg sync.WaitGroup
-	if err := collectSpecialFile(ctx, amp, currDir, rootPrivileges, &wg); err != nil {
+	if err := collectSpecialFile(ctx, amp, currDir, requireRootPrivileges, &wg); err != nil {
 		return err
 	}
 
-	if err := collectLog(ctx, cmd, rootPrivileges, currDir); err != nil {
+	if err := collectLog(ctx, cmd, requireRootPrivileges, currDir); err != nil {
 		return err
 	}
 
-	if err := collectPprof(ctx, cmd, pid, amp, rootPrivileges, currDir, &wg); err != nil {
+	if err := collectPprof(ctx, cmd, pid, amp, requireRootPrivileges, currDir, &wg); err != nil {
 		return err
 	}
 
diff --git a/cmd/gc.go b/cmd/gc.go
index 3eb33d41..78c418fe 100644
--- a/cmd/gc.go
+++ b/cmd/gc.go
@@ -111,7 +111,7 @@ func gc(ctx *cli.Context) error {
 	// Scan all chunks first and do compaction if necessary
 	progress := utils.NewProgress(false, false)
 	if ctx.Bool("compact") {
-		bar := progress.AddCountBar("Scanned chunks", 0)
+		bar := progress.AddCountBar("Compacted chunks", 0)
 		spin := progress.AddDoubleSpinner("Compacted slices")
 		m.OnMsg(meta.DeleteSlice, func(args ...interface{}) error {
 			return store.Remove(args[0].(uint64), int(args[1].(uint32)))
@@ -124,7 +124,7 @@ func gc(ctx *cli.Context) error {
 			}
 			return err
 		})
-		if st := m.CompactAll(meta.Background, bar); st == 0 {
+		if st := m.CompactAll(meta.Background, ctx.Int("threads"), bar); st == 0 {
 			bar.Done()
 			spin.Done()
 			if progress.Quiet {
diff --git a/cmd/mount.go b/cmd/mount.go
index 59c33a3b..60bbe816 100644
--- a/cmd/mount.go
+++ b/cmd/mount.go
@@ -463,7 +463,7 @@ func tryToInstallMountExec() error {
 	if _, err := os.Stat("/sbin/mount.juicefs"); err == nil {
 		return nil
 	}
-	src, err := filepath.Abs(os.Args[0])
+	src, err := os.Executable()
 	if err != nil {
 		return err
 	}
diff --git a/docs/en/administration/fault_diagnosis_and_analysis.md b/docs/en/administration/fault_diagnosis_and_analysis.md
index 1407612c..c55d2084 100644
--- a/docs/en/administration/fault_diagnosis_and_analysis.md
+++ b/docs/en/administration/fault_diagnosis_and_analysis.md
@@ -11,15 +11,15 @@ import TabItem from '@theme/TabItem';
 
 ## Client log
 
-JuiceFS client will output logs for troubleshooting while running. The level of logs in terms of fatality follows DEBUG < INFO < WARNING < ERROR < FATAL. Since DEBUG logs are not printed by default, you need to explicitly enable it if needed, e.g. by adding the `--debug` option when running the JuiceFS client. 
+JuiceFS client will output logs for troubleshooting while running. The level of logs in terms of fatality follows DEBUG < INFO < WARNING < ERROR < FATAL. Since DEBUG logs are not printed by default, you need to explicitly enable it if needed, e.g. by adding the `--debug` option when running the JuiceFS client.
 
 Different JuiceFS clients print logs in different ways, which are described as follows.
 
 ### Mount point
 
-When a JuiceFS file system is mounted with the [`-d` option](../reference/command_reference.md#juicefs-mount) (indicating running in the background), it will print logs to the system log file and local log file simultaneously. Depending on which user is running when mounting the file system, the paths of the local log files are slightly different. For root, the local log file locates at `/var/log/juicefs.log`, while it locates at `$HOME/.juicefs/juicefs.log` for non-root users. Please refer to [`--log` option](../reference/command_reference.md#juicefs-mount) for details.
+When a JuiceFS file system is mounted with the [`-d` option](../reference/command_reference.md#mount) (indicating running in the background), it will print logs to the system log file and local log file simultaneously. Depending on which user is running when mounting the file system, the paths of the local log files are slightly different. For root, the local log file locates at `/var/log/juicefs.log`, while it locates at `$HOME/.juicefs/juicefs.log` for non-root users. Please refer to [`--log` option](../reference/command_reference.md#mount) for details.
 
-Depending on the operating system, there are different commands to retrieve system logs or read local log files directly. 
+Depending on the operating system, there are different commands to retrieve system logs or read local log files directly.
 
 <Tabs>
   <TabItem value="local-log-file" label="Local log file">
@@ -149,7 +149,7 @@ By default, pprof listens on port numbers ranging from 6060 to 6099. That's why
 - Goroutine stack information: `http://localhost:<port>/debug/pprof/goroutine?debug=1`
 - CPU performance statistics: `http://localhost:<port>/debug/pprof/profile?seconds=30`
 - Memory allocation statistics: `http://localhost:<port>/debug/pprof/heap`
-- 
+-
 :::tip
 You can also use the debug command to automatically collect these runtime information and save it locally. By default, it is saved to the debug directory under the current directory, for example:
 ```bash
diff --git a/docs/en/administration/monitoring.md b/docs/en/administration/monitoring.md
index 00a03d55..1fdb54a2 100644
--- a/docs/en/administration/monitoring.md
+++ b/docs/en/administration/monitoring.md
@@ -76,7 +76,7 @@ There are different ways to collect monitoring metrics depending on how JuiceFS
 
 ### Mount point
 
-When the JuiceFS file system is mounted via the [`juicefs mount`](../reference/command_reference.md#juicefs-mount) command, you can collect monitoring metrics via the address `http://localhost:9567/metrics`, or you can customize it via the `--metrics` option. For example:
+When the JuiceFS file system is mounted via the [`juicefs mount`](../reference/command_reference.md#mount) command, you can collect monitoring metrics via the address `http://localhost:9567/metrics`, or you can customize it via the `--metrics` option. For example:
 
 ```shell
 juicefs mount --metrics localhost:9567 ...
diff --git a/docs/en/deployment/juicefs_on_docker.md b/docs/en/deployment/juicefs_on_docker.md
index f5be49a9..a65c7ef6 100644
--- a/docs/en/deployment/juicefs_on_docker.md
+++ b/docs/en/deployment/juicefs_on_docker.md
@@ -269,7 +269,7 @@ services:
 
 You can adjust the parameters of the format and mount commands in the above code as needed. For example, when there is some latency in the network connection between local and object storage and local storage is reliable, you can mount the file system by adding the `--writeback` option so that files can be stored to the local cache first and then uploaded to the object storage asynchronously, see [client-side write cache](./cache_management/#client-write-data-cache) for details.
 
-For more file system creation and mounting parameters, please see [command reference](./command_reference#juicefs-mount).
+For more file system creation and mounting parameters, please see [command reference](../reference/command_reference.md#mount).
 
 #### Deployment and Usage
 
diff --git a/docs/en/faq.md b/docs/en/faq.md
index 9afff3fe..0875b1ab 100644
--- a/docs/en/faq.md
+++ b/docs/en/faq.md
@@ -99,7 +99,7 @@ This is just a rough introduction to the implementation logic. The specific read
 
 ### How to copy a large number of small files into JuiceFS quickly?
 
-You could mount JuiceFS with [`--writeback` option](reference/command_reference.md#juicefs-mount), which will write the small files into local disks first, then upload them to object storage in background, this could speedup coping many small files into JuiceFS.
+You could mount JuiceFS with [`--writeback` option](reference/command_reference.md#mount), which will write the small files into local disks first, then upload them to object storage in background, this could speedup coping many small files into JuiceFS.
 
 See ["Write Cache in Client"](guide/cache_management.md#write-cache-in-client) for more information.
 
diff --git a/docs/en/getting-started/README.md b/docs/en/getting-started/README.md
index d767fe31..854670ed 100644
--- a/docs/en/getting-started/README.md
+++ b/docs/en/getting-started/README.md
@@ -69,7 +69,7 @@ Since no storage-related options are specified in this example, the local disk i
 
 ### Basic Concept
 
-JuiceFS client provides a command [`mount`](../reference/command_reference.md#juicefs-mount) to mount file systems in the following format.
+JuiceFS client provides a command [`mount`](../reference/command_reference.md#mount) to mount file systems in the following format.
 
 ```shell
 juicefs mount [command options] META-URL MOUNTPOINT
diff --git a/docs/en/guide/cache_management.md b/docs/en/guide/cache_management.md
index f1255974..fda7a8f3 100644
--- a/docs/en/guide/cache_management.md
+++ b/docs/en/guide/cache_management.md
@@ -10,20 +10,21 @@ For a file system driven by a combination of object storage and database, cache
 JuiceFS provides various caching mechanisms including metadata caching, data read/write caching, etc.
 
 :::tip Does my application really need cache?
-Cache improve performance, only when application needs to repeatedly read files, so if you know for sure you're in a scenario where data is only accessed once (e.g. data cleansing during ETL), you can safely turn off cache to prevent overhead.
+Data caching will effectively improve the performance of random reads. For applications that require high random read performance (e.g. Elasticsearch, ClickHouse), it is recommended to use faster storage medium and allocate more space for cache.
+
+Meanwhile, cache improve performance only when application needs to repeatedly read files, so if you know for sure you're in a scenario where data is only accessed once (e.g. data cleansing during ETL), you can safely turn off cache to prevent overhead.
 :::
 
 ## Data consistency
 
 JuiceFS provides a "close-to-open" consistency guarantee, which means that when two or more clients read and write the same file at the same time, the changes made by client A may not be immediately visible to client B. However, once the file is closed by client A, any client re-opened it afterwards is guaranteed to see the latest data, no matter it is on the same node with A or not.
 
-"Close-to-open" is the minimum consistency guarantee provided by JuiceFS, and in some cases it may not be necessary to reopen the file to access the latest written data. For example, multiple applications use the same JuiceFS client to access the same file (where file changes are immediately visible), or to view the latest data on different nodes with the `tail -f` command.
+"Close-to-open" is the minimum consistency guarantee provided by JuiceFS, and in some cases it may not be necessary to reopen the file to access the latest written data. For example:
 
-## Cache read mechanism
+* Multiple applications use the same JuiceFS client to access the same file (where file changes are immediately visible)
+* View the latest data on different nodes with the `tail -f` command (require use Linux)
 
-JuiceFS provides a multi-level cache to improve the performance of frequently accessed data. Read requests will try kernel page cache, the pre-read buffer of the JuiceFS process, and the local disk cache in turn. If the data requested is not found in any level of the cache, it will be read from the object storage, and also be written into every level of the cache asynchronously to improve the performance of subsequent accesses.
-
-![](../images/juicefs-cache.png)
+As for object storage, JuiceFS divides files into data blocks (default 4MiB), assigns unique IDs and saves them on object storage. Any modification operation of the file will generate a new data block, and the original block remains unchanged, including the cached data on the local disk. So don't worry about the consistency of the data cache, because once the file is modified, JuiceFS will read the new data block from the object storage, and will not read the data block (which will be deleted later) corresponding to the overwritten part of the file.
 
 ## Metadata cache
 
@@ -31,7 +32,7 @@ JuiceFS supports caching metadata both in kernel and in client memory (i.e. Juic
 
 ### Kernel metadata cache {#kernel-metadata-cache}
 
-There are three kinds of metadata which can be cached in kernel: **attribute**, **entry** and **directory**. The TTL of them can be specified by the following [mount options](../reference/command_reference.md#juicefs-mount):
+There are three kinds of metadata which can be cached in kernel: **attribute**, **entry** and **directory**. The TTL of them can be specified by the following [mount options](../reference/command_reference.md#mount):
 
 ```
 --attr-cache value       attributes cache TTL in seconds (default: 1)
@@ -39,7 +40,7 @@ There are three kinds of metadata which can be cached in kernel: **attribute**,
 --dir-entry-cache value  dir entry cache TTL in seconds (default: 1)
 ```
 
-JuiceFS caches attributes, file entries, and directory entries in kernel for 1 second by default to improve the performance of `lookup` and `getattr`. When clients on multiple nodes are sharing the same file system, the metadata cached in kernel will only expire due to TTL exceeding. That is, in an edge case, it may happen that the metadata modification (e.g., `chown`) on node A cannot be seen immediately on node B. Of course, all nodes will eventually be able to see the changes made by A after the expiry of the cache.
+Attribute, entry and direntries are cached for 1 second by default, this speeds up lookup and getattr operations. When clients on multiple nodes are sharing the same file system, the metadata cached in kernel will only expire by TTL. So in edge cases, it's possible that metadata modifications (e.g., `chown`) on node A cannot be seen immediately on node B. Nevertheless, all nodes will eventually be able to see the changes made by A after cache expiration.
 
 ### In-memory client metadata cache
 
@@ -47,19 +48,17 @@ JuiceFS caches attributes, file entries, and directory entries in kernel for 1 s
 This feature requires JuiceFS >= 0.15.2
 :::
 
-When a JuiceFS client `open()` a file, the attributes of the file are automatically cached in client memory. If the [`--open-cache`](../reference/command_reference.md#juicefs-mount) option is set to a value greater than 0 when mounting the file system, subsequent `getattr()` and `open()` operations will return the result from the in-memory cache immediately, as long as the cache has not expired.
+When a JuiceFS client `open()` a file, the attributes of the file are automatically cached in client memory. If the [`--open-cache`](../reference/command_reference.md#mount) option is set to a value greater than 0 when mounting the file system, subsequent `getattr()` and `open()` operations will return the result from the in-memory cache immediately, as long as the cache has not expired.
 
-When doing `read()` on a file, the chunk and slice information of the file is automatically cached in the client memory. Reading the chunk again during the cache lifetime will return the slice information from the in-memory cache immediately.
+When doing `read()` on a file, the chunk and slice information of the file is automatically cached in the client memory. Reading the chunk again during the cache lifetime will return the slice information from the in-memory cache immediately (check ["How JuiceFS Stores Files"](../introduction/architecture.md#how-juicefs-stores-files) to know what chunk and slice are).
 
-:::tip
-You can check ["How JuiceFS Stores Files"](../reference/how_juicefs_store_files.md) to know what chunk and slice are.
-:::
-
-By default, if there is a file whose metadata has been cached in memory and not been accessed by any process for more than 1 hour, all its metadata cache will be automatically deleted.
+To ensure consistency, `--open-cache` is disabled by default, and every time you open a file, the client need to directly access the metadata engine. However, if the file is rarely modified, or in a read-only scenario (such as AI model training), it is recommended to set `--open-cache` according to the situation to further improve the read performance.
 
 ## Data cache
 
-To improve performance, JuiceFS also provides various caching mechanisms for data, including page cache in the kernel, local file system cache in client host, and read/write buffer in client process itself.
+JuiceFS provides a multi-level cache to improve the performance of frequently accessed data. Read requests will try kernel page cache, the pre-read buffer of the JuiceFS process, and the local disk cache in turn. If the data requested is not found in any level of the cache, it will be read from the object storage, and also be written into every level of the cache asynchronously to improve the performance of subsequent accesses.
+
+![](../images/juicefs-cache.png)
 
 ### Read/Write Buffer {#buffer-size}
 
@@ -79,41 +78,39 @@ For a file that has already been read, the kernel automatically caches its conte
 
 Thanks to the kernel cache, repeated reads of the same file in JuiceFS can be extremely fast, with latencies as low as a few microseconds and throughput up to several GiBs per second.
 
-JuiceFS clients currently do not have kernel write caching enabled by default. Starting with [Linux kernel 3.15](https://github.com/torvalds/linux/commit/4d99ff8f12e), FUSE supports ["writeback-cache mode"]( https://www.kernel.org/doc/Documentation/filesystems/fuse-io.txt), which means that the `write()` system call can be done very quickly. You can set the [`-o writeback_cache`](../reference/fuse_mount_options.md#writeback_cache) option at [mounting file system](../reference/command_reference.md#juicefs-mount) to enable writeback-cache mode. It is recommended to enable this mount option when very small data (e.g. around 100 bytes) needs to be written frequently.
+JuiceFS clients currently do not have kernel write caching enabled by default. Starting with [Linux kernel 3.15](https://github.com/torvalds/linux/commit/4d99ff8f12e), FUSE supports ["writeback-cache mode"]( https://www.kernel.org/doc/Documentation/filesystems/fuse-io.txt), which means that the `write()` system call can be done very quickly. You can set the [`-o writeback_cache`](../reference/fuse_mount_options.md#writeback_cache) option at [mounting file system](../reference/command_reference.md#mount) to enable writeback-cache mode. It is recommended to enable this mount option when very small data (e.g. around 100 bytes) needs to be written frequently.
 
 ### Client read data cache {#client-read-cache}
 
-The JuiceFS client automatically prefetches data into the cache based on the read pattern, thus improving sequential read performance. By default, it prefetches 1 block concurrently and caches it locally when reading. The local cache can be set up on any local file system based on HDD, SSD or memory.
+Local cache can be set up on any local file system based on HDD, SSD or memory.
 
-:::tip Hint
-You can check ["How JuiceFS Stores Files"](../reference/how_juicefs_store_files.md) to learn what a block is.
-:::
+Data downloaded from object storage, as well as small data (smaller than a single block) uploaded to object storage will be cached by JuiceFS Client, without compression or encryption.
 
-The local cache can be adjusted at [mounting file system](../reference/command_reference.md#juicefs-mount) with the following options.
+Below are some important options for cache configuration (see [`juicefs mount`](../reference/command_reference.md#mount) for complete reference):
 
-```
---cache-dir value         directory paths of local cache, use colon to separate multiple paths (default: "$HOME/.juicefs/cache" or "/var/jfsCache")
---cache-size value        size of cached object for read in MiB (default: 102400)
---free-space-ratio value  minimum free space (ratio) (default: 0.1)
---cache-partial-only      cache only random/small read (default: false)
-```
+* `--prefetch`
+
+  Prefetch N blocks concurrently (default to 1), prefetch mechanism works like this: when reading a block at arbitrary position, the whole block is asynchronously scheduled for download. Prefetch often improves random read performance, but if your scenario cannot effectively utilize prefetched data (for example, reading large files randomly and sparsely), prefetch will bring read amplification, consider set to 0 to disable it.
 
-Specifically, there are two ways to store the local cache of JuiceFS in memory, i.e., setting `--cache-dir` to `memory` or `/dev/shm/<cache-dir>`. The difference between these two approaches is that the former deletes the cache data after remounting the JuiceFS file system, while the latter retains the cache data. Still, there is not much difference in performance between them.
+  JuiceFS is equipped with another internal similar mechanism called "readahead": when doing sequential reads, client will download nearby blocks in advance, improving sequential performance. The concurrency of readahead is affected by the size of ["Read/Write Buffer"](#buffer-size), the larger the read-write buffer, the higher the concurrency.
 
-The JuiceFS client writes data downloaded from the object storage (including new uploaded data containing less than 1 block) to the cache directory as fast as possible, without compression or encryption. **Because JuiceFS generates unique names for all block objects written to the object storage, and all block objects are not modified, there is no need to worry about the invalidation of the cached data when the file content is updated.**
+* `--cache-dir`
 
-Data caching can effectively improve the performance of random reads. For applications like Elasticsearch, ClickHouse, etc. that require higher random read performance, it is recommended to point the cache path to a faster storage medium and allocate more cache space.
+  Cache directory, default to `/var/jfsCache` or `$HOME/.juicefs/cache`. Please read ["Cache directory"](#cache-dir) for more information.
 
-#### Cache life cycle
+  If you are in urgent need to free up disk space, you can manually delete data under the cache directory, which is usually `<cache-dir>/<UUID>/raw/`.
 
-When the cache usage space reaches the upper limit (that is, the cache size is greater than or equal to `--cache-size`) or the disk is almost full (that is, the free disk space ratio is less than `--free-space-ratio`), the JuiceFS client will automatically expire cache using random two algorithm, i.e. pick two blocks and expire the least accessed one.
+* `--cache-size` and `--free-space-ratio`
 
-JuiceFS allocates 100GiB cache space by default, but this does not mean that you have to have a disk with a capacity more than that. This value represents the maximum capacity that a JuiceFS client may use if the disk capacity allows. When the remaining space on the disk is less than 100GiB, ensure that the remaining space is not less than 10% by default.
+  Cache size (in MiB, default to 102400) and minimum ratio of free size (default 0.1). Both parameters is able to control cache size, if any of the two criteria is met, JuiceFS Client will expire cache usage using an algorithm similar to LRU, i.e. remove older and less used blocks.
 
-For example, if you set `--cache-dir` to a partition with a capacity of 50GiB, then if `--cache-size` is set to 100GiB, JuiceFS will always keep the cache capacity around 45GiB, which means that more than 10% of the partition is reserved.
+  Actual cache size may exceed configured value, because it is difficult to calculate the exact disk space taken by cache. Currently, JuiceFS takes the sum of all cached objects sizes using a minimum 4 KiB size, which is often different from the result of `du`.
 
+* `--cache-partial-only`
 
-JuiceFS currently estimates the size of cached objects by adding up the size of all cached objects with a minimum size of 4KiB, which is usually different from the `du` results.
+  Only cache small files and random small reads, do not cache whole block. This applies to conditions where object storage throughput is higher than the local cache device. Default value is false.
+
+  There are two main read patterns, sequential read and random read. Sequential read usually demands higher throughput while random reads needs lower latency. When local disk throughput is lower than object storage, consider enable `--cache-partial-only` so that sequential reads do not cache the whole block, but rather, only small reads (like footer of Parquet / ORC file) are cached. This allows JuiceFS to take advantage of low latency provided by local disk, and high throughput provided by object storage, at the same time.
 
 ### Client write data cache {#writeback}
 
@@ -181,7 +178,7 @@ Then run the warm up command.
 juicefs warmup -f warm.txt
 ```
 
-## Cache directory
+## Cache directory {#cache-dir}
 
 Depending on the operating system, the default cache path for JuiceFS is as follows:
 
@@ -228,10 +225,14 @@ Then, using that path as a cache, mount the filesystem.
 juicefs mount --cache-dir /dev/shm/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs
 ```
 
+Another way to use memory for cache is set `--cache-dir` option to `memory`, this puts cache directly in client process memory, which is simpler compared to `/dev/shm`, but obviously cache will be lost after process restart, use this for tests and evaluations.
+
 ### Shared folders
 
 Shared directories created via SMB or NFS can also be used as cache for JuiceFS. For the case where multiple devices on the LAN mount the same JuiceFS file system, using shared directories on the LAN as cache paths can effectively relieve the bandwidth pressure of duplicate caches for multiple devices.
 
+But special attention needs to be paid. Usually, when the file system where the cache directory is located fails to work properly, the JuiceFS client can immediately return an error and downgrade to direct access to object storage. If the abnormality of the shared directory shows that the read operation is stuck (such as some network file system in kernel mode), then JuiceFS will also be stuck together. This requires you to tune the underlying file system behavior of the shared directory to achieve rapid failure.
+
 Using SMB/CIFS as an example, mount the shared directories on the LAN by using the tools provided by the `cifs-utils` package.
 
 ```shell
@@ -246,14 +247,12 @@ sudo juicefs mount --cache-dir /mnt/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs
 
 ### Multiple cache directories
 
-JuiceFS supports setting multiple cache directories at the same time, thus avoiding the problem of insufficient cache space by splitting multiple paths using `:` (Linux, macOS) or `;` (Windows), e.g.:
+JuiceFS supports setting multiple cache directories at the same time, thus avoiding the problem of insufficient cache space by separating multiple paths using `:` (Linux, macOS) or `;` (Windows), e.g.:
 
 ```shell
 sudo juicefs mount --cache-dir ~/jfscache:/mnt/jfscache:/dev/shm/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs
 ```
 
-When multiple cache paths are set, the client will write data evenly to each cache path using hash policy.
-
-When multiple cache directories are set, the `--cache-size` option represents the total size of data in all cache directories. It is recommended to ensure that the free space of different cache directories are close to each other, otherwise, the space of a cache directory may not be fully utilized.
+When multiple cache directories are set, or multiple devices are used as cache disks, the `--cache-size` option represents the total size of data in all cache directories. The client will use the hash strategy to evenly write data to each cache path, and cannot perform special tuning for multiple cache disks with different capacities or performances.
 
-For example, `--cache-dir` is `/data1:/data2`, where `/data1` has a free space of 1GiB, `/data2` has a free space of 2GiB, `--cache-size` is 3GiB, `--free-space-ratio` is 0.1. Because the cache write strategy is to write evenly, the maximum space allocated to each cache directory is `3GiB / 2 = 1.5GiB`, resulting in a maximum of 1.5GiB cache space in the `/data2` directory instead of `2GiB * 0.9 = 1.8GiB`.
+Therefore, it is recommended that the available space of different cache directories/cache disks be consistent, otherwise it may cause the situation that the space of a certain cache directory cannot be fully utilized. For example, `--cache-dir` is `/data1:/data2`, where `/data1` has a free space of 1GiB, `/data2` has a free space of 2GiB, `--cache-size` is 3GiB, `--free-space-ratio` is 0.1. Because the cache write strategy is to write evenly, the maximum space allocated to each cache directory is `3GiB / 2 = 1.5GiB`, resulting in a maximum of 1.5GiB cache space in the `/data2` directory instead of `2GiB * 0.9 = 1.8GiB`.
diff --git a/docs/en/guide/mount_at_boot.md b/docs/en/guide/mount_at_boot.md
index 053d59c6..a37f0ee4 100644
--- a/docs/en/guide/mount_at_boot.md
+++ b/docs/en/guide/mount_at_boot.md
@@ -16,7 +16,7 @@ Copy `juicefs` as `/sbin/mount.juicefs`, then edit `/etc/fstab` with following l
 <META-URL>    <MOUNTPOINT>       juicefs     _netdev[,<MOUNT-OPTIONS>]     0  0
 ```
 
-For the format of `<META-URL>`, please refer to the ["How to Set Up Metadata Engine"](how_to_set_up_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:
+For the format of `<META-URL>`, please refer to the ["How to Set Up Metadata Engine"](how_to_set_up_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#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=204800     0  0
diff --git a/docs/en/reference/fuse_mount_options.md b/docs/en/reference/fuse_mount_options.md
index cf09ecfe..5ddd4868 100644
--- a/docs/en/reference/fuse_mount_options.md
+++ b/docs/en/reference/fuse_mount_options.md
@@ -5,7 +5,7 @@ slug: /fuse_mount_options
 ---
 # FUSE Mount Options
 
-This guide lists important FUSE mount options. These mount options are specified by the option `-o`  when execute the command [`juicefs mount`](../reference/command_reference.md#juicefs-mount) (use comma to separate multiple options). For example:
+This guide lists important FUSE mount options. These mount options are specified by the option `-o`  when execute the command [`juicefs mount`](../reference/command_reference.md#mount) (use comma to separate multiple options). For example:
 
 ```bash
 juicefs mount -d -o allow_other,writeback_cache localhost ~/jfs
diff --git a/docs/en/tutorials/aws.md b/docs/en/tutorials/aws.md
index 32a992ca..aa9ed091 100644
--- a/docs/en/tutorials/aws.md
+++ b/docs/en/tutorials/aws.md
@@ -182,7 +182,7 @@ sudo juicefs mount -d redis://[<redis-username>]:<redis-password>@<redis-url>:63
 
 > **Note**: When mounting a file system, only the database address is required but not the file system name. The default cache path is `/var/jfsCache`. Please make sure the current user has enough read/write permissions.
 
-You can optimize JuiceFS by adjusting the [mount options](../reference/command_reference.md#juicefs-mount), for example by `--cache-size` to change cache to 20GB.
+You can optimize JuiceFS by adjusting the [mount options](../reference/command_reference.md#mount), for example by `--cache-size` to change cache to 20GB.
 
 ```shell
 sudo juicefs mount --cache-size 20480 -d redis://herald-demo.abcdefg.0001.apse1.cache.amazonaws.com:6379/1  /mnt/jfs
diff --git a/docs/zh_cn/administration/fault_diagnosis_and_analysis.md b/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
index ab17dd20..477c020f 100644
--- a/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
+++ b/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
@@ -17,7 +17,7 @@ JuiceFS 客户端在运行过程中会输出日志用于故障诊断，日志等
 
 ### 挂载点
 
-当挂载 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)。
+当挂载 JuiceFS 文件系统时加上了 [`-d` 选项](../reference/command_reference.md#mount)（表示后台运行），日志会同时输出到系统日志和本地日志文件，取决于挂载文件系统时的运行用户，本地日志文件的路径稍有区别。root 用户对应的日志文件路径是 `/var/log/juicefs.log`，非 root 用户的日志文件路径是 `$HOME/.juicefs/juicefs.log`，具体请参见 [`--log` 选项](../reference/command_reference.md#mount)。
 
 取决于你使用的操作系统，你可以通过不同的命令获取系统日志或直接读取本地日志文件：
 
diff --git a/docs/zh_cn/administration/monitoring.md b/docs/zh_cn/administration/monitoring.md
index 18e81f33..da53e272 100644
--- a/docs/zh_cn/administration/monitoring.md
+++ b/docs/zh_cn/administration/monitoring.md
@@ -76,7 +76,7 @@ scrape_configs:
 
 ### 挂载点
 
-当通过 [`juicefs mount`](../reference/command_reference.md#juicefs-mount) 命令挂载 JuiceFS 文件系统后，可以通过 `http://localhost:9567/metrics` 这个地址收集监控指标，你也可以通过 `--metrics` 选项自定义。如：
+当通过 [`juicefs mount`](../reference/command_reference.md#mount) 命令挂载 JuiceFS 文件系统后，可以通过 `http://localhost:9567/metrics` 这个地址收集监控指标，你也可以通过 `--metrics` 选项自定义。如：
 
 ```shell
 juicefs mount --metrics localhost:9567 ...
diff --git a/docs/zh_cn/deployment/juicefs_on_docker.md b/docs/zh_cn/deployment/juicefs_on_docker.md
index 2a4edb3b..fd18b202 100644
--- a/docs/zh_cn/deployment/juicefs_on_docker.md
+++ b/docs/zh_cn/deployment/juicefs_on_docker.md
@@ -269,7 +269,7 @@ services:
 
 可以根据需要调整上述代码中 format 和 mount 命令的参数，例如，当本地与对象存储的网络连接存在一定延迟且本地存储相对可靠时，可以通过添加 `--writeback` 选项挂载文件系统，让文件可以先存储到本地缓存，再异步上传到对象存储，详情参考[客户端写缓存](./cache_management/#客户端写缓存)。
 
-更多文件系统创建和挂载参数请查看[命令参考](./command_reference#juicefs-mount)。
+更多文件系统创建和挂载参数请查看[命令参考](../reference/command_reference.md#mount)。
 
 #### 部署和使用
 
diff --git a/docs/zh_cn/faq.md b/docs/zh_cn/faq.md
index 929938d8..5f7b0b15 100644
--- a/docs/zh_cn/faq.md
+++ b/docs/zh_cn/faq.md
@@ -102,7 +102,7 @@ JuiceFS 不将原始文件存入对象存储，而是将其按照某个大小（
 
 ### 怎么快速地拷贝大量小文件到 JuiceFS？
 
-请在挂载时加上 [`--writeback` 选项](reference/command_reference.md#juicefs-mount)，它会先把数据写入本机的缓存，然后再异步上传到对象存储，会比直接上传到对象存储快很多倍。
+请在挂载时加上 [`--writeback` 选项](reference/command_reference.md#mount)，它会先把数据写入本机的缓存，然后再异步上传到对象存储，会比直接上传到对象存储快很多倍。
 
 请查看[「客户端写缓存」](guide/cache_management.md#客户端写缓存)了解更多信息。
 
diff --git a/docs/zh_cn/getting-started/README.md b/docs/zh_cn/getting-started/README.md
index b60c1246..0fbd53b9 100644
--- a/docs/zh_cn/getting-started/README.md
+++ b/docs/zh_cn/getting-started/README.md
@@ -69,7 +69,7 @@ juicefs format sqlite3://myjfs.db myjfs
 
 ### 基本概念
 
-挂载文件系统使用客户端提供的 [`mount`](../reference/command_reference.md#juicefs-mount) 命令，一般格式为：
+挂载文件系统使用客户端提供的 [`mount`](../reference/command_reference.md#mount) 命令，一般格式为：
 
 ```shell
 juicefs mount [command options] META-URL MOUNTPOINT
diff --git a/docs/zh_cn/guide/cache_management.md b/docs/zh_cn/guide/cache_management.md
index fb10779d..62ab38e3 100644
--- a/docs/zh_cn/guide/cache_management.md
+++ b/docs/zh_cn/guide/cache_management.md
@@ -9,21 +9,22 @@ slug: /cache_management
 
 JuiceFS 提供包括元数据缓存、数据读写缓存等多种缓存机制。
 
-:::tip 我的场景真的需要缓存吗
-缓存能提升性能的前提是，你的应用需要反复读取同一批文件。如果你确定你的应用对数据是“读取一次，然后再也不需要”的访问模式（比如大数据的数据清洗常常就是这样），可以关闭缓存功能，省去缓存不断的建立，又白白淘汰的开销。
+:::tip 我的场景真的需要缓存吗？
+数据缓存可以有效地提高随机读的性能，对于像 Elasticsearch、ClickHouse 等对随机读性能要求更高的应用，建议将缓存路径设置在速度更快的存储介质上并分配更大的缓存空间。
+
+然而缓存能提升性能的前提是，你的应用需要反复读取同一批文件。如果你确定你的应用对数据是「读取一次，然后再也不需要」的访问模式（比如大数据的数据清洗常常就是这样），可以关闭缓存功能，省去缓存不断建立，又反复淘汰的开销。
 :::
 
-## 数据一致性
+## 数据一致性 {#consistency}
 
 JuiceFS 提供「关闭再打开（close-to-open）」一致性保证，即当两个及以上客户端同时读写相同的文件时，客户端 A 的修改在客户端 B 不一定能立即看到。但是，一旦这个文件在客户端 A 写入完成并关闭，之后在任何一个客户端重新打开该文件都可以保证能访问到最新写入的数据，不论是否在同一个节点。
 
-「关闭再打开」是 JuiceFS 提供的最低限度一致性保证，在某些情况下可能也不需要重新打开文件才能访问到最新写入的数据。例如多个应用程序使用同一个 JuiceFS 客户端访问相同的文件（文件变更立即可见），或者在不同节点上通过 `tail -f` 命令查看最新数据。
-
-## 读缓存机制
+「关闭再打开」是 JuiceFS 提供的最低限度一致性保证，在某些情况下可能也不需要重新打开文件才能访问到最新写入的数据：
 
-当访问 JuiceFS 中的文件时，会有多级缓存给经常访问的数据提供更好的性能，读请求会依次尝试内核分页缓存、JuiceFS 进程的预读缓冲区、本地磁盘缓存，当缓存中没找到对应数据时才会从对象存储读取，并且会异步写入各级缓存保证下一次访问的性能。
+* 多个应用程序使用同一个 JuiceFS 客户端访问相同的文件时，文件变更立即对所有进程可见。
+* 在不同节点上通过 `tail -f` 命令查看最新数据（需使用 Linux 系统）
 
-![](../images/juicefs-cache.png)
+至于对象存储，JuiceFS 将文件分成一个个数据块（默认 4MiB），赋予唯一 ID 并保存在对象存储上。文件的任何修改操作都将生成新的数据块，原有块保持不变，包括本地磁盘上的缓存数据。所以不用担心数据缓存的一致性问题，因为一旦文件被修改过了，JuiceFS 会从对象存储读取新的数据块，不会再读取文件中被覆盖的部分对应的数据块（之后会被删除掉）。
 
 ## 元数据缓存
 
@@ -31,7 +32,7 @@ JuiceFS 支持在内核和客户端内存（即 JuiceFS 进程）中缓存元数
 
 ### 内核元数据缓存 {#kernel-metadata-cache}
 
-内核中可以缓存三种元数据：**属性（attribute)**、**文件项（entry）**和**目录项（direntry）**，可以通过以下[挂载参数](../reference/command_reference.md#juicefs-mount)控制缓存时间：
+内核中可以缓存三种元数据：**属性（attribute)**、**文件项（entry）**和**目录项（direntry）**，可以通过以下[挂载参数](../reference/command_reference.md#mount)控制缓存时间：
 
 ```
 --attr-cache value       属性缓存时长，单位秒 (默认值: 1)
@@ -47,19 +48,17 @@ JuiceFS 默认会在内核中缓存属性、文件项和目录项，缓存时长
 此特性需要使用 0.15.2 及以上版本的 JuiceFS
 :::
 
-JuiceFS 客户端在 `open()` 操作即打开一个文件时，其文件属性（attribute）会被自动缓存在客户端内存中。如果在挂载文件系统时设置了 [`--open-cache`](../reference/command_reference.md#juicefs-mount) 选项且值大于 0，只要缓存尚未超时失效，随后执行的 `getattr()` 和 `open()` 操作会从内存缓存中立即返回结果。
+JuiceFS 客户端在 `open()` 操作即打开一个文件时，其文件属性（attribute）会被自动缓存在客户端内存中。如果在挂载文件系统时设置了 [`--open-cache`](../reference/command_reference.md#mount) 选项且值大于 0，只要缓存尚未超时失效，随后执行的 `getattr()` 和 `open()` 操作会从内存缓存中立即返回结果。
 
-执行 `read()` 操作即读取一个文件时，文件的 chunk 和 slice 信息会被自动缓存在客户端内存。在缓存有效期内，再次读取 chunk 会从内存缓存中立即返回 slice 信息。
+执行 `read()` 操作即读取一个文件时，文件的 chunk 和 slice 信息会被自动缓存在客户端内存。在缓存有效期内，再次读取 chunk 会从内存缓存中立即返回 slice 信息（查阅[「JuiceFS 如何存储文件」](../introduction/architecture.md#如何存储文件)以了解 chunk 和 slice 是什么）。
 
-:::tip 提示
-您可以查阅[「JuiceFS 如何存储文件」](../reference/how_juicefs_store_files.md)了解 chunk 和 slice 是什么。
-:::
-
-默认情况下，对于一个元数据已经被缓存在内存的文件，超过 1 小时没有被任何进程访问，其所有元数据缓存会被自动删除。
+为保强一致性，`--open-cache` 默认关闭，每次打开文件都需直接访问元数据引擎。但如果文件很少发生修改，或者只读场景下（例如 AI 模型训练），则推荐根据情况设置 `--open-cache`，进一步提高读性能。
 
 ## 数据缓存
 
-JuiceFS 对数据也提供多种缓存机制来提高性能，包括内核中的页缓存和客户端所在机器的本地缓存，以及客户端自身的内存读写缓冲区。
+当访问 JuiceFS 中的文件时，会有多级缓存给经常访问的数据提供更好的性能，读请求会依次尝试内核分页缓存、JuiceFS 进程的预读缓冲区、本地磁盘缓存，当缓存中没找到对应数据时才会从对象存储读取，并且会异步写入各级缓存保证下一次访问的性能。
+
+![](../images/juicefs-cache.png)
 
 ### 读写缓冲区 {#buffer-size}
 
@@ -79,41 +78,39 @@ JuiceFS 对数据也提供多种缓存机制来提高性能，包括内核中的
 
 得益于内核缓存，重复读取 JuiceFS 中相同文件的速度会非常快，延时可低至微秒，吞吐量可以到每秒数 GiB。
 
-JuiceFS 客户端目前还未默认启用内核的写入缓存功能，从 [Linux 内核 3.15](https://github.com/torvalds/linux/commit/4d99ff8f12e) 开始，FUSE 支持[「writeback-cache 模式」](https://www.kernel.org/doc/Documentation/filesystems/fuse-io.txt)，这意味着可以非常快速地完成 `write()` 系统调用。你可以在[挂载文件系统](../reference/command_reference.md#juicefs-mount)时设置 [`-o writeback_cache`](../reference/fuse_mount_options.md#writeback_cache) 选项开启 writeback-cache 模式。当需要频繁写入非常小的数据（如 100 字节左右）时，建议启用此挂载选项。
+JuiceFS 客户端目前还未默认启用内核的写入缓存功能，从 [Linux 内核 3.15](https://github.com/torvalds/linux/commit/4d99ff8f12e) 开始，FUSE 支持[「writeback-cache 模式」](https://www.kernel.org/doc/Documentation/filesystems/fuse-io.txt)，这意味着可以非常快速地完成 `write()` 系统调用。你可以在[挂载文件系统](../reference/command_reference.md#mount)时设置 [`-o writeback_cache`](../reference/fuse_mount_options.md#writeback_cache) 选项开启 writeback-cache 模式。当需要频繁写入非常小的数据（如 100 字节左右）时，建议启用此挂载选项。
 
-### 客户端读缓存
+### 客户端读缓存 {#client-read-cache}
 
-JuiceFS 客户端会根据读取模式自动预读数据放入缓存，从而提高顺序读的性能。默认情况下，会在读取数据时并发预读 1 个 block 缓存在本地。本地缓存可以设置在基于机械硬盘、SSD 或内存的任意本地文件系统。
+本地缓存可以设置在基于机械硬盘、SSD 或内存的任意本地文件系统。
 
-:::tip 提示
-您可以查阅[「JuiceFS 如何存储文件」](../reference/how_juicefs_store_files.md)了解 block 是什么。
-:::
+JuiceFS 客户端会把从对象存储下载的数据，以及新上传的小于 1 个 block 大小的数据写入到缓存目录中，不做压缩和加密。
 
-本地缓存可以在[挂载文件系统](../reference/command_reference.md#juicefs-mount)时通过以下选项调整：
+以下是缓存配置的关键参数（完整参数列表见 [`juicefs mount`](../reference/command_reference.md#mount)）：
 
-```
---prefetch value          并发预读 N 个块 (默认: 1)
---cache-dir value         本地缓存目录路径；使用冒号隔离多个路径 (默认: "$HOME/.juicefs/cache" 或 "/var/jfsCache")
---cache-size value        缓存对象的总大小；单位为 MiB (默认: 102400)
---free-space-ratio value  最小剩余空间比例 (默认: 0.1)
---cache-partial-only      仅缓存随机小块读 (默认: false)
-```
+* `--prefetch`
 
-特别地，如果希望将 JuiceFS 的本地缓存存储在内存中有两种方式，一种是将 `--cache-dir` 设置为 `memory`，另一种是将其设置为 `/dev/shm/<cache-dir>`。这两种方式的区别是前者在重新挂载 JuiceFS 文件系统之后缓存数据就清空了，而后者还会保留，性能上两者没有太大差别。
+  并发预读 N 个块（默认 1）。所谓预读（prefetch），就是随机读取文件任意一小段，都会触发对应的整个对象存储块异步完整下载。预读往往能改善随机读性能，但如果你的场景的文件访问模式无法利用到预读数据（比如 offset 跨度极大的大文件随机访问），预读会带来比较明显的读放大，可以考虑设为 0 以禁用预读特性。
 
-JuiceFS 客户端会尽可能快地把从对象存储下载的数据（包括新上传的小于 1 个 block 大小的数据）写入到缓存目录中，不做压缩和加密。**因为 JuiceFS 会为所有写入对象存储的 block 对象生成唯一的名字，而且所有 block 对象不会被修改，因此当文件内容更新时，不用担心缓存的数据失效问题。**
+  JuiceFS 还内置着另一种类似的预读机制：在顺序读时，会提前下载临近的对象存储块，这在 JuiceFS 内称为 readahead 机制，能有效提高顺序读性能。Readahead 的并发度受[「读写缓冲区」](#buffer-size)的大小影响，读写缓冲区越大并发度越高。
 
-数据缓存可以有效地提高随机读的性能，对于像 Elasticsearch、ClickHouse 等对随机读性能要求更高的应用，建议将缓存路径设置在速度更快的存储介质上并分配更大的缓存空间。
+* `--cache-dir`
 
-#### 缓存生命周期
+  缓存目录，默认为 `/var/jfsCache` 或 `$HOME/.juicefs/cache`。请阅读[「缓存位置」](#cache-dir)了解更多信息。
 
-在缓存使用空间到达上限（即缓存大小大于等于 `--cache-size`）或磁盘将被存满（即磁盘可用空间比例小于 `--free-space-ratio`）时，JuiceFS 客户端会自动清理缓存。JuiceFS 客户端会采用类 LRU 的算法对缓存进行清理，即尽量清理较早且较少使用的缓存。
+  如果急需释放磁盘空间，你可以手动清理缓存目录下的文件，缓存路径通常为 `<cache-dir>/<UUID>/raw/`。
 
-JuiceFS 默认会分配 100GiB 的缓存空间，但这并不代表你要使用超过该容量的磁盘。这个值代表的是在磁盘容量允许的情况下 JuiceFS 客户端可能使用的最大容量。当磁盘的剩余空间不足 100GiB 时，默认情况下会保证剩余空间不低于 10%。
+* `--cache-size` 与 `--free-space-ratio`
 
-例如，你将 `--cache-dir` 设置在了容量为 50GiB 的分区，此时如果 `--cache-size` 设置为 100GiB，JuiceFS 的缓存容量始终保持在 45GiB 左右，即保留该分区有 10% 以上的剩余空间。
+  缓存空间大小（单位 MiB，默认 102400）与缓存盘的最少剩余空间占比（默认 0.1）。这两个参数任意一个达到阈值，均会自动触发缓存淘汰，使用的是类似于 LRU 的策略，即尽量清理较早且较少使用的缓存。
 
-对于总量相同的缓存数据，在不同的文件系统上会有不同的容量计算规则。JuiceFS 目前是通过累加所有被缓存对象的大小（最小 4KiB）来估算得到的，这与 `du` 命令得到的数值并不完全一致。
+  实际缓存数据占用空间大小可能会略微超过设置值，这是因为对同样一批缓存数据，很难精确计算它们在不同的本地文件系统上所占用的存储空间，JuiceFS 累加所有被缓存对象大小时会按照 4KiB 的最小值来计算，因此与 `du` 得到的数值往往不一致。
+
+* `--cache-partial-only`
+
+  只缓存小文件和随机读的部分，适合对象存储的吞吐比缓存盘还高的情况。默认为 false。
+
+  读一般有两种模式，连续读和随机读。对于连续读，一般需要较高的吞吐。对于随机读，一般需要较低的时延。当本地磁盘的吞吐反而比不上对象存储时，可以考虑启用 `--cache-partial-only`，这样一来，连续读虽然会将一整个对象块读取下来，但并不会被缓存。而随机读（例如读 Parquet 或者 ORC 文件的 footer）所读取的字节数比较小，不会读取整个对象块，此类读取就会被缓存。充分地利用了本地磁盘低时延和网络高吞吐的优势。
 
 ### 客户端写缓存 {#writeback}
 
@@ -181,7 +178,7 @@ juicefs warmup /mnt/jfs/dataset-1
 juicefs warmup -f warm.txt
 ```
 
-## 缓存位置
+## 缓存位置 {#cache-dir}
 
 取决于操作系统，JuiceFS 的默认缓存路径如下：
 
@@ -229,10 +226,14 @@ sudo mount -o size=32000M -o remount /dev/shm
 juicefs mount --cache-dir /dev/shm/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs
 ```
 
+除此之外，还可以将 `--cache-dir` 选项设置为 `memory` 来直接使用进程内存作为缓存，与 `/dev/shm` 相比，好处是简单不依赖外部设备，但相应地也无法持久化，一般在测试评估的时候使用。
+
 ### 共享目录
 
 SMB、NFS 等共享目录也可以用作 JuiceFS 的缓存，对于局域网有多个设备挂载了相同 JuiceFS 文件系统的情况，将局域网中的共享目录作为缓存路径，可以有效缓解多个设备重复预热缓存的带宽压力。
 
+但是需要特别注意，通常情况下当缓存目录所在的文件系统无法正常工作时 JuiceFS 客户都能立刻返回错误，并降级成直接访问对象存储。如果共享目录异常时体现为读操作卡死（如某些内核态的网络文件系统），那么 JuiceFS 也会随之一起卡住，这就要求你对共享目录底层的文件系统行为进行调优，做到快速失败。
+
 以 SMB/CIFS 共享为例，使用 `cifs-utils` 包提供的工具挂载局域网中的共享目录：
 
 ```shell
@@ -247,12 +248,12 @@ sudo juicefs mount --cache-dir /mnt/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs
 
 ### 多缓存目录
 
-JuiceFS 支持同时设置多个缓存目录，从而解决缓存空间不足的问题，使用 `:`（Linux、macOS）或 `;`（Windows）分割多个路径，例如：
+JuiceFS 支持同时设置多个缓存目录，从而解决缓存空间不足的问题，使用 `:`（Linux、macOS）或 `;`（Windows）字符分隔多个路径，例如：
 
 ```shell
 sudo juicefs mount --cache-dir ~/jfscache:/mnt/jfscache:/dev/shm/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs
 ```
 
-设置了多个缓存路径时，客户端会采用 hash 策略向各个缓存路径中均匀地写入数据。当设置了多个缓存目录时，`--cache-size` 选项表示所有缓存目录中的数据总大小。
+当设置了多个缓存目录，或者使用多块设备作为缓存盘，`--cache-size` 选项表示所有缓存目录中的数据总大小。客户端会采用哈希策略向各个缓存路径中均匀地写入数据，无法对多块容量或性能不同的缓存盘进行特殊调优。
 
-建议不同缓存目录的可用空间保持一致，否则可能造成不能充分利用某个缓存目录空间的情况。例如 `--cache-dir` 为 `/data1:/data2`，其中 `/data1` 的可用空间为 1GiB，`/data2` 的可用空间为 2GiB，`--cache-size` 为 3GiB，`--free-space-ratio` 为 0.1。因为缓存的写入策略是均匀写入，所以分配给每个缓存目录的最大空间是 `3GiB / 2 = 1.5GiB`，会造成 `/data2` 目录的缓存空间最大为 1.5GiB，而不是 `2GiB * 0.9 = 1.8GiB`。
+因此建议不同缓存目录／缓存盘的可用空间保持一致，否则可能造成不能充分利用某个缓存目录空间的情况。例如 `--cache-dir` 为 `/data1:/data2`，其中 `/data1` 的可用空间为 1GiB，`/data2` 的可用空间为 2GiB，`--cache-size` 为 3GiB，`--free-space-ratio` 为 0.1。因为缓存的写入策略是均匀写入，所以分配给每个缓存目录的最大空间是 `3GiB / 2 = 1.5GiB`，会造成 `/data2` 目录的缓存空间最大为 1.5GiB，而不是 `2GiB * 0.9 = 1.8GiB`。
diff --git a/docs/zh_cn/guide/mount_at_boot.md b/docs/zh_cn/guide/mount_at_boot.md
index cb3d1553..2d8498be 100644
--- a/docs/zh_cn/guide/mount_at_boot.md
+++ b/docs/zh_cn/guide/mount_at_boot.md
@@ -16,7 +16,7 @@ slug: /mount_juicefs_at_boot_time
 <META-URL>    <MOUNTPOINT>       juicefs     _netdev[,<MOUNT-OPTIONS>]     0  0
 ```
 
-`<META-URL>` 的格式请参考[「如何设置元数据引擎」](how_to_set_up_metadata_engine.md)文档，比如 `redis://localhost:6379/1`。然后用你希望 JuiceFS 挂载的路径替换 `<MOUNTPOINT>` ，比如 `/jfs`。如果你想要设置[挂载选项](../reference/command_reference.md#juicefs-mount)，用逗号分隔选项列表并替换 `[,<MOUNT-OPTIONS>]` 。下面是一个示例：
+`<META-URL>` 的格式请参考[「如何设置元数据引擎」](how_to_set_up_metadata_engine.md)文档，比如 `redis://localhost:6379/1`。然后用你希望 JuiceFS 挂载的路径替换 `<MOUNTPOINT>` ，比如 `/jfs`。如果你想要设置[挂载选项](../reference/command_reference.md#mount)，用逗号分隔选项列表并替换 `[,<MOUNT-OPTIONS>]` 。下面是一个示例：
 
 ```
 redis://localhost:6379/1    /jfs       juicefs     _netdev,max-uploads=50,writeback,cache-size=204800     0  0
diff --git a/docs/zh_cn/reference/fuse_mount_options.md b/docs/zh_cn/reference/fuse_mount_options.md
index 945bafbb..48c5d655 100644
--- a/docs/zh_cn/reference/fuse_mount_options.md
+++ b/docs/zh_cn/reference/fuse_mount_options.md
@@ -5,7 +5,7 @@ slug: /fuse_mount_options
 ---
 # FUSE 挂载选项
 
-本指南列出了重要的 FUSE 挂载选项。当执行 [`juicefs mount`](../reference/command_reference.md#juicefs-mount) 命令时，这些安装选项由 `-o` 选项指定，多个选项使用半角逗号分隔。例如：
+本指南列出了重要的 FUSE 挂载选项。当执行 [`juicefs mount`](../reference/command_reference.md#mount) 命令时，这些安装选项由 `-o` 选项指定，多个选项使用半角逗号分隔。例如：
 
 ```bash
 juicefs mount -d -o allow_other,writeback_cache localhost ~/jfs
diff --git a/docs/zh_cn/tutorials/aws.md b/docs/zh_cn/tutorials/aws.md
index 6243fbb1..1b6a3e7b 100644
--- a/docs/zh_cn/tutorials/aws.md
+++ b/docs/zh_cn/tutorials/aws.md
@@ -183,7 +183,7 @@ sudo juicefs mount -d redis://[<redis-username>]:<redis-password>@<redis-url>:63
 
 > **注意**：挂载文件系统时，只需填写数据库地址，不需要文件系统名称。默认的缓存路径为 `/var/jfsCache`，请确保当前用户有足够的读写权限。
 
-你可以通过调整[挂载参数](../reference/command_reference.md#juicefs-mount)，对 JuiceFS 进行优化，比如可以通过 `--cache-size` 将缓存修改为 20GB：
+你可以通过调整[挂载参数](../reference/command_reference.md#mount)，对 JuiceFS 进行优化，比如可以通过 `--cache-size` 将缓存修改为 20GB：
 
 ```shell
 sudo juicefs mount --cache-size 20480 -d redis://herald-demo.abcdefg.0001.apse1.cache.amazonaws.com:6379/1  /mnt/jfs
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index 66e8b35c..e4d6d2ed 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -58,6 +58,8 @@ type engine interface {
 	doFindStaleSessions(limit int) ([]uint64, error) // limit < 0 means all
 	doCleanStaleSession(sid uint64) error
 
+	scanAllChunks(ctx Context, ch chan<- cchunk, bar *utils.Bar) error
+	compactChunk(inode Ino, indx uint32, force bool)
 	doDeleteSustainedInode(sid uint64, inode Ino) error
 	doFindDeletedFiles(ts int64, limit int) (map[Ino]uint64, error) // limit < 0 means all
 	doDeleteFileData(inode Ino, length uint64)
@@ -94,6 +96,13 @@ type fsStat struct {
 	usedInodes int64
 }
 
+// chunk for compaction
+type cchunk struct {
+	inode  Ino
+	indx   uint32
+	slices int
+}
+
 type baseMeta struct {
 	sync.Mutex
 	addr string
@@ -111,6 +120,7 @@ type baseMeta struct {
 	symlinks     *sync.Map
 	msgCallbacks *msgCallbacks
 	umounting    bool
+	sesMu        sync.Mutex
 
 	*fsStat
 
@@ -297,13 +307,14 @@ func (m *baseMeta) expireTime() int64 {
 func (m *baseMeta) refreshSession() {
 	for {
 		utils.SleepWithJitter(m.conf.Heartbeat)
-		m.Lock()
+		m.sesMu.Lock()
 		if m.umounting {
-			m.Unlock()
+			m.sesMu.Unlock()
 			return
 		}
 		m.en.doRefreshSession()
-		m.Unlock()
+		m.sesMu.Unlock()
+
 		old := m.fmt.UUID
 		if _, err := m.Load(false); err != nil {
 			logger.Warnf("reload setting: %s", err)
@@ -343,9 +354,9 @@ func (m *baseMeta) CloseSession() error {
 	if m.conf.ReadOnly {
 		return nil
 	}
-	m.Lock()
+	m.sesMu.Lock()
 	m.umounting = true
-	m.Unlock()
+	m.sesMu.Unlock()
 	logger.Infof("close session %d: %s", m.sid, m.en.doCleanStaleSession(m.sid))
 	return nil
 }
@@ -894,9 +905,12 @@ func (m *baseMeta) NewSlice(ctx Context, id *uint64) syscall.Errno {
 func (m *baseMeta) Close(ctx Context, inode Ino) syscall.Errno {
 	if m.of.Close(inode) {
 		m.Lock()
-		defer m.Unlock()
-		if m.removedFiles[inode] {
+		_, removed := m.removedFiles[inode]
+		if removed {
 			delete(m.removedFiles, inode)
+		}
+		m.Unlock()
+		if removed {
 			_ = m.en.doDeleteSustainedInode(m.sid, inode)
 		}
 	}
@@ -1186,6 +1200,31 @@ func (m *baseMeta) Check(ctx Context, fpath string, repair bool, recursive bool)
 	return
 }
 
+func (m *baseMeta) CompactAll(ctx Context, threads int, bar *utils.Bar) syscall.Errno {
+	var wg sync.WaitGroup
+	ch := make(chan cchunk, 1000000)
+	for i := 0; i < threads; i++ {
+		wg.Add(1)
+		go func() {
+			for c := range ch {
+				logger.Debugf("Compacting chunk %d:%d (%d slices)", c.inode, c.indx, c.slices)
+				m.en.compactChunk(c.inode, c.indx, true)
+				bar.Increment()
+			}
+			wg.Done()
+		}()
+	}
+
+	err := m.en.scanAllChunks(ctx, ch, bar)
+	close(ch)
+	wg.Wait()
+	if err != nil {
+		logger.Warnf("Scan chunks: %s", err)
+		return errno(err)
+	}
+	return 0
+}
+
 func (m *baseMeta) fileDeleted(opened bool, inode Ino, length uint64) {
 	if opened {
 		m.Lock()
diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go
index bdee9181..7b7146a8 100644
--- a/pkg/meta/interface.go
+++ b/pkg/meta/interface.go
@@ -346,7 +346,7 @@ type Meta interface {
 	Setlk(ctx Context, inode Ino, owner uint64, block bool, ltype uint32, start, end uint64, pid uint32) syscall.Errno
 
 	// Compact all the chunks by merge small slices together
-	CompactAll(ctx Context, bar *utils.Bar) syscall.Errno
+	CompactAll(ctx Context, threads int, bar *utils.Bar) syscall.Errno
 	// ListSlices returns all slices used by all files.
 	ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, showProgress func()) syscall.Errno
 	// Remove all files and directories recursively.
@@ -418,7 +418,7 @@ func NewClient(uri string, conf *Config) Meta {
 	}
 	m, err := f(driver, uri[p+3:], conf)
 	if err != nil {
-		logger.Fatalf("Meta %s is not available: %s", uri, err)
+		logger.Fatalf("Meta %s is not available: %s", utils.RemovePassword(uri), err)
 	}
 	return m
 }
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 01514564..82b91dac 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -2638,17 +2638,16 @@ func (m *redisMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	}
 }
 
-func (m *redisMeta) CompactAll(ctx Context, bar *utils.Bar) syscall.Errno {
+func (m *redisMeta) scanAllChunks(ctx Context, ch chan<- cchunk, bar *utils.Bar) error {
 	p := m.rdb.Pipeline()
-	return errno(m.scan(ctx, "c*_*", func(keys []string) error {
-		bar.IncrTotal(int64(len(keys)))
+	return m.scan(ctx, "c*_*", func(keys []string) error {
 		for _, key := range keys {
 			_ = p.LLen(ctx, key)
 		}
 		cmds, err := p.Exec(ctx)
 		if err != nil {
 			logger.Warnf("list slices: %s", err)
-			return errno(err)
+			return err
 		}
 		for i, cmd := range cmds {
 			cnt := cmd.(*redis.IntCmd).Val()
@@ -2657,14 +2656,13 @@ func (m *redisMeta) CompactAll(ctx Context, bar *utils.Bar) syscall.Errno {
 				var indx uint32
 				n, err := fmt.Sscanf(keys[i], m.prefix+"c%d_%d", &inode, &indx)
 				if err == nil && n == 2 {
-					logger.Debugf("compact chunk %d:%d (%d slices)", inode, indx, cnt)
-					m.compactChunk(Ino(inode), indx, true)
+					bar.IncrTotal(1)
+					ch <- cchunk{Ino(inode), indx, int(cnt)}
 				}
 			}
-			bar.Increment()
 		}
 		return nil
-	}))
+	})
 }
 
 func (m *redisMeta) cleanupLeakedInodes(delete bool) {
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index f3410259..4f42048c 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -2543,23 +2543,17 @@ func dup(b []byte) []byte {
 	return r
 }
 
-func (m *dbMeta) CompactAll(ctx Context, bar *utils.Bar) syscall.Errno {
-	var cs []chunk
-	err := m.roTxn(func(s *xorm.Session) error {
-		cs = nil
-		return s.Where("length(slices) >= ?", sliceBytes*2).Cols("inode", "indx").Find(&cs)
+func (m *dbMeta) scanAllChunks(ctx Context, ch chan<- cchunk, bar *utils.Bar) error {
+	return m.roTxn(func(s *xorm.Session) error {
+		return s.Table(&chunk{}).Iterate(new(chunk), func(idx int, bean interface{}) error {
+			c := bean.(*chunk)
+			if len(c.Slices) > sliceBytes {
+				bar.IncrTotal(1)
+				ch <- cchunk{c.Inode, c.Indx, len(c.Slices) / sliceBytes}
+			}
+			return nil
+		})
 	})
-	if err != nil {
-		return errno(err)
-	}
-
-	bar.IncrTotal(int64(len(cs)))
-	for _, c := range cs {
-		logger.Debugf("compact chunk %d:%d (%d slices)", c.Inode, c.Indx, len(c.Slices)/sliceBytes)
-		m.compactChunk(c.Inode, c.Indx, true)
-		bar.Increment()
-	}
-	return 0
 }
 
 func (m *dbMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, showProgress func()) syscall.Errno {
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index 64b5d039..6c5add92 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -2153,27 +2153,19 @@ func (m *kvMeta) compactChunk(inode Ino, indx uint32, force bool) {
 	}
 }
 
-func (r *kvMeta) CompactAll(ctx Context, bar *utils.Bar) syscall.Errno {
+func (m *kvMeta) scanAllChunks(ctx Context, ch chan<- cchunk, bar *utils.Bar) error {
 	// AiiiiiiiiCnnnn     file chunks
 	klen := 1 + 8 + 1 + 4
-	result, err := r.scanValues(r.fmtKey("A"), -1, func(k, v []byte) bool {
-		return len(k) == klen && k[1+8] == 'C' && len(v) > sliceBytes
+	return m.client.scan(m.fmtKey("A"), func(k, v []byte) {
+		if len(k) == klen && k[1+8] == 'C' && len(v) > sliceBytes {
+			bar.IncrTotal(1)
+			ch <- cchunk{
+				inode:  m.decodeInode(k[1:9]),
+				indx:   binary.BigEndian.Uint32(k[10:]),
+				slices: len(v) / sliceBytes,
+			}
+		}
 	})
-	if err != nil {
-		logger.Warnf("scan chunks: %s", err)
-		return errno(err)
-	}
-
-	bar.IncrTotal(int64(len(result)))
-	for k, value := range result {
-		key := []byte(k[1:])
-		inode := r.decodeInode(key[:8])
-		indx := binary.BigEndian.Uint32(key[9:])
-		logger.Debugf("compact chunk %d:%d (%d slices)", inode, indx, len(value)/sliceBytes)
-		r.compactChunk(inode, indx, true)
-		bar.Increment()
-	}
-	return 0
 }
 
 func (m *kvMeta) ListSlices(ctx Context, slices map[Ino][]Slice, delete bool, showProgress func()) syscall.Errno {
diff --git a/pkg/meta/tkv_mem.go b/pkg/meta/tkv_mem.go
index 869554b3..6afcf611 100644
--- a/pkg/meta/tkv_mem.go
+++ b/pkg/meta/tkv_mem.go
@@ -277,7 +277,9 @@ func (c *memKV) scan(prefix []byte, handler func(key []byte, value []byte)) erro
 		if end != "" && it.key >= end {
 			return false
 		}
+		c.Unlock()
 		handler([]byte(it.key), it.value)
+		c.Lock()
 		return true
 	})
 	return nil
diff --git a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
index 29def786..0ea020a5 100644
--- a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
+++ b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
@@ -17,7 +17,6 @@ package io.juicefs;
 
 import com.kenai.jffi.internal.StubLoader;
 import io.juicefs.metrics.JuiceFSInstrumentation;
-import io.juicefs.utils.BufferPool;
 import io.juicefs.utils.ConsistentHash;
 import io.juicefs.utils.NodesFetcher;
 import io.juicefs.utils.NodesFetcherBuilder;
@@ -42,6 +41,7 @@ import org.apache.hadoop.io.MD5Hash;
 import org.apache.hadoop.security.AccessControlException;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.hadoop.util.DataChecksum;
+import org.apache.hadoop.util.DirectBufferPool;
 import org.apache.hadoop.util.Progressable;
 import org.apache.hadoop.util.VersionInfo;
 import org.json.JSONObject;
@@ -93,6 +93,8 @@ public class JuiceFileSystemImpl extends FileSystem {
   private ScheduledExecutorService nodesFetcherThread;
   private ScheduledExecutorService refreshUidThread;
   private Map<String, FileStatus> lastFileStatus = new HashMap<>();
+  private static final DirectBufferPool directBufferPool = new DirectBufferPool();
+
   private boolean metricsEnable = false;
 
   /*
@@ -753,7 +755,7 @@ public class JuiceFileSystemImpl extends FileSystem {
     public FileInputStream(Path f, int fd, int size) throws IOException {
       path = f;
       this.fd = fd;
-      buf = BufferPool.getBuffer(size);
+      buf = directBufferPool.getBuffer(size);
       buf.limit(0);
       position = 0;
     }
@@ -835,12 +837,14 @@ public class JuiceFileSystemImpl extends FileSystem {
       if (!buf.hasRemaining() && b.remaining() <= buf.capacity() && !refill()) {
         return -1;
       }
-      int got = 0;
-      while (b.hasRemaining() && buf.hasRemaining()) {
-        b.put(buf.get());
-        got++;
+      ByteBuffer srcBuf = buf.duplicate();
+      int got = Math.min(b.remaining(), srcBuf.remaining());
+      if (got > 0) {
+        srcBuf.limit(srcBuf.position() + got);
+        b.put(srcBuf);
+        buf.position(srcBuf.position());
+        statistics.incrementBytesRead(got);
       }
-      statistics.incrementBytesRead(got);
       int more = read(position, b);
       if (more <= 0)
         return got > 0 ? got : -1;
@@ -906,7 +910,7 @@ public class JuiceFileSystemImpl extends FileSystem {
       if (buf == null) {
         return; // already closed
       }
-      BufferPool.returnBuffer(buf);
+      directBufferPool.returnBuffer(buf);
       buf = null;
       int r = lib.jfs_close(Thread.currentThread().getId(), fd);
       fd = 0;


Test patch:
diff --git a/pkg/meta/base_test.go b/pkg/meta/base_test.go
index aec7d9a5..1ca41da8 100644
--- a/pkg/meta/base_test.go
+++ b/pkg/meta/base_test.go
@@ -904,7 +904,7 @@ func testCompaction(t *testing.T, m Meta, trash bool) {
 
 	// TODO: check result if that's predictable
 	p, bar := utils.MockProgress()
-	if st := m.CompactAll(ctx, bar); st != 0 {
+	if st := m.CompactAll(ctx, 8, bar); st != 0 {
 		t.Fatalf("compactall: %s", st)
 	}
 	p.Done()
diff --git a/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java b/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
index 4e238f75..31c826cd 100644
--- a/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
+++ b/sdk/java/src/test/java/io/juicefs/JuiceFileSystemTest.java
@@ -33,9 +33,9 @@ import java.io.OutputStream;
 import java.net.URI;
 import java.nio.ByteBuffer;
 import java.security.PrivilegedExceptionAction;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 
 import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_TRASH_CHECKPOINT_INTERVAL_KEY;
 import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_TRASH_INTERVAL_KEY;
@@ -725,4 +725,35 @@ public class JuiceFileSystemTest extends TestCase {
     FileSystem newFs = FileSystem.newInstance(newConf);
     assertEquals(256 << 20, newFs.getDefaultBlockSize(new Path("/")));
   }
+
+  public void testReadSpeed() throws Exception {
+    int read = (128 << 10) ;
+    Path speedFile = new Path("/tmp/speedFile");
+    fs.delete(speedFile, false);
+    FSDataOutputStream ou = fs.create(speedFile);
+    int fileSize = 128 << 20;
+    ou.write(new byte[fileSize]);
+    ou.close();
+    FSDataInputStream open = fs.open(speedFile);
+    AtomicLong counter = new AtomicLong(0L);
+    AtomicBoolean finished = new AtomicBoolean(false);
+    TimerTask timerTask = new TimerTask() {
+      @Override
+      public void run() {
+        System.out.printf("read method calls: %d\n", counter.get());
+        finished.set(true);
+      }
+    };
+    Timer timer = new Timer();
+    timer.schedule(timerTask, 1000);
+
+    ByteBuffer readArray = ByteBuffer.allocateDirect(read);
+    while (!finished.get()) {
+      open.seek(0);
+      readArray.position(0);
+      readArray.limit(read);
+      open.read(readArray);
+      counter.getAndIncrement();
+    }
+  }
 }