Repository: juicedata/juicefs
Pull request #1961 (2022-05-06 09:12:30)

meta/tkv: dump metadata using snapshot
Currently, we use transaction to scan all the metadata, which is heavy (bookkeeping) and easy to fail (timeout or others), this  PR change it to iterator on read-only snapshot (based on timestamp or revision).


Patch:
diff --git a/.github/workflows/fio_benchmark.yml b/.github/workflows/fio_benchmark.yml
index 53038cd0..989e8a6e 100644
--- a/.github/workflows/fio_benchmark.yml
+++ b/.github/workflows/fio_benchmark.yml
@@ -42,7 +42,8 @@ jobs:
       - name: Fio Benchmark 
         run: |
           default_size=1G
-          warning_value=90
+          disk_warning_value=90
+          decrease_ratio=0.8
           fio_jobs=(
             "big-file-sequential-read:      --rw=read --refill_buffers --bs=256k --size=${default_size}" \
             "big-file-sequential-write:     --rw=write --refill_buffers --bs=256k --size=${default_size}" \
@@ -85,25 +86,33 @@ jobs:
             "small-file-multi-write-8:      --rw=write --file_service_type=sequential --bs=4k --filesize=4k --nrfiles=1000 --numjobs=8" \
             "small-file-multi-write-16:     --rw=write --file_service_type=sequential --bs=4k --filesize=4k --nrfiles=1000 --numjobs=16" \
           )
+          csv_file=/var/lib/mysql-files/result.csv
+          test -f $csv_file  && rm $csv_file -f
           urls=("juicefs")
           urls+=($(curl -s https://api.github.com/repos/juicedata/juicefs/releases | grep browser_download_url | grep linux-amd64.tar.gz | awk -F\" '{print $4}' | head -${{ steps.vars.outputs.BACKWARD_VERSIONS }}))          
           for url in "${urls[@]}"; do
-            csv_file=/var/lib/mysql-files/result.csv
-            test -f $csv_file  && rm $csv_file -f
             if [[ $url == http* ]]; then  
-              rm juicefs 
               echo download url is: $url
               wget -q $url
-              tar -zxf $(basename $url)
+              bin_name=$(echo $(basename $url) | sed 's/.tar.gz//g')
+              mkdir jfs || true
+              tar -zxf $(basename $url) -C jfs
+              mv jfs/juicefs $bin_name
               rm $(basename $url)
+              rm jfs -rf
             fi
-            juicefs_version=$(./juicefs -V|cut -b 17-)
-            echo juicefs version: $(./juicefs -V)
-            for job in "${fio_jobs[@]}"
-            do
-              echo job is:, $job
+          done
+
+          fail=false
+          for job in "${fio_jobs[@]}"; do
+            echo job is:: $job
+            bw_list=()
+            for url in "${urls[@]}"; do
+              echo url is: $url
+              bin_name=$(echo $(basename $url) | sed 's/.tar.gz//g') 
+              juicefs_version=$(./$bin_name -V|cut -b 17-)
+              echo juicefs version: $(./$bin_name -V)
               if [ -n "$job" ]; then
-                set -x
                 name=$(echo $job | awk -F: '{print $1}' | xargs)
                 fio_arg=$(echo $job | awk -F: '{print $2}' | xargs)
                 mount_arg=$(echo $job | awk -F: '{print $3}' | xargs)
@@ -112,39 +121,75 @@ jobs:
                 rm /var/jfsCache/ -rf
                 while true; do
                   used=$(df -h /mnt/minio/data1  | awk 'NR>1 {gsub(/%/,""); print $5}') 
-                  if [ $used -gt $warning_value ]; then
+                  if [ $used -gt $disk_warning_value ]; then
                     printf "disk used %s%%, sleep 10s\n" "$used"
                     sleep 10s
                   else
                     break
                   fi
                 done
-                ./juicefs format --trash-days 0 --storage minio --bucket  $minio_url --access-key minioadmin --secret-key ${{ secrets.MINIO_SECRET_KEY }}  ${{ steps.vars.outputs.META_URL }} fio-benchmark
-                ./juicefs mount -d ${{ steps.vars.outputs.META_URL }} ${{ steps.vars.outputs.MOUNT_POINT }} --no-usage-report $mount_arg &
+                rev=$(./$bin_name -V | awk -F- '{print $1}' | awk -F ' ' '{print $3}')
+                if [[ "$rev" < "1.0.0" ]]; then
+                  trash_day=""
+                else
+                  trash_day="--trash-days 0"
+                fi
+                ./$bin_name format $trash_day --storage minio --bucket  $minio_url --access-key minioadmin --secret-key ${{ secrets.MINIO_SECRET_KEY }}  ${{ steps.vars.outputs.META_URL }} fio-benchmark
+                ./$bin_name mount -d ${{ steps.vars.outputs.META_URL }} ${{ steps.vars.outputs.MOUNT_POINT }} --no-usage-report $mount_arg &
                 if [[ "$name" =~ ^big-file-rand-read.* ]]; then
                   block_size=$(echo $name | awk -F- '{print $NF}' | xargs)
                   echo block_size is $block_size
                   fio --name=big-file-rand-read-preload --directory=${{ steps.vars.outputs.MOUNT_POINT }} --rw=randread --refill_buffers --size=${default_size} --filename=randread.bin --bs=$block_size --pre_read=1
                   sudo sync && echo 3 > /proc/sys/vm/drop_caches
                 fi
+
+                echo "start fio"
                 fio --name=$name --directory=${{ steps.vars.outputs.MOUNT_POINT }} $fio_arg > result.log
-                bw=$(tail -1 result.log | awk '{print $2}' | awk -F '=' '{print $2}' | cut -b 1,-3)
-                echo $bw, $(date +"%Y-%m-%d %H:%M:%S"), ${{github.sha}}, https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}, $juicefs_version, $name, ${{ github.ref_name }}, $default_size, redis, minio | tee -a $csv_file
-                
-                ./juicefs umount -f ${{ steps.vars.outputs.MOUNT_POINT }}
+                echo "finish fio"
+
+                bw_str=$(tail -1 result.log | awk '{print $2}' | awk -F '=' '{print $2}' )
+                bw=$(echo $bw_str | sed 's/.iB.*//g') 
+                if [[ $bw_str == *KiB* ]]; then
+                  bw=$(echo "scale=2; $bw/1024.0" | bc -l)
+                elif [[ $bw_str == *GiB* ]]; then
+                  bw=$(echo "scale=2; $bw*1024.0" | bc -l)
+                fi
+                bw_list+=($bw)
+
+                echo $name, $bw, $juicefs_version, $default_size, ${{ github.ref_name }}, $(date +"%Y-%m-%d %H:%M:%S"), ${{github.sha}}, https://github.com/${{github.repository}}/actions/runs/${{github.run_id}},  redis, minio | tee -a $csv_file
+              
+                ./$bin_name umount -f ${{ steps.vars.outputs.MOUNT_POINT }}
                 UUID=$(./juicefs status ${{ steps.vars.outputs.META_URL }} | grep UUID | cut -d '"' -f 4)
                 if [ -n "$UUID" ];then
                   sudo ./juicefs destroy --force ${{ steps.vars.outputs.META_URL }} $UUID
                 fi
                 mc rb  myminio/$name --force --dangerous || printf "Warining; remove bucket failed: %s, exit code: %s" "myminio/$name" "$?"
-                set +x
               fi
             done
-            mysql -h 172.27.0.1 -u root -p${{secrets.MYSQL_PASSWORD}} -e "use  test_result; LOAD DATA INFILE '/var/lib/mysql-files/result.csv' INTO TABLE fio_test FIELDS TERMINATED BY ',' (bandwidth, created_date, github_revision, workflow_url, juicefs_version, name, ref_name, size, meta_engine, storage); " 
+            
+            new=${bw_list[0]}
+            old=$(expr ${bw_list[1]}*$decrease_ratio | bc)
+            echo new is $new, old is $old
+            if (( $(echo "$new < $old" |bc -l) )); then
+              printf "Fatal: bandwidth decreased too much: new is: %s, old is: %s\n" ${bw_list[0]} ${bw_list[1]} 
+              fail=true
+            fi
           done
-          
+          mysql -h 172.27.0.1 -u root -p${{secrets.MYSQL_PASSWORD}} -e "use  test_result; LOAD DATA INFILE '/var/lib/mysql-files/result.csv' INTO TABLE fio_test FIELDS TERMINATED BY ',' (name, bandwidth,  juicefs_version, size, ref_name, created_date, github_revision, workflow_url, meta_engine, storage); " 
+          if $fail; then
+            echo "Fatal: fio bandwidth decreased by 20% compared with last version, please check the result file"
+            exit 1
+          fi
+
+      - name: Save test reuslt
+        if: ${{always()}}
+        uses: actions/upload-artifact@v2
+        with: 
+          name: result.csv
+          path: /var/lib/mysql-files/result.csv
 
       - name: Clean After
+        if: ${{always()}}
         run: |
           if [ -d ${{ steps.vars.outputs.MOUNT_POINT }} ]; then
             ./juicefs umount ${{ steps.vars.outputs.MOUNT_POINT }} || true
diff --git a/.github/workflows/version_compatible.yml b/.github/workflows/version_compatible.yml
index cb09bde0..31a79346 100644
--- a/.github/workflows/version_compatible.yml
+++ b/.github/workflows/version_compatible.yml
@@ -187,3 +187,7 @@ jobs:
         with:
           channel-id: "${{ secrets.SLACK_CHANNEL_ID_FOR_PR_CHECK_NOTIFY }}"
           slack_bot_token: "${{ secrets.SLACK_BOT_TOKEN }}"  
+          
+      - name: Setup upterm session
+        if: ${{ failure() }}
+        uses: lhotari/action-upterm@v1
\ No newline at end of file
diff --git a/cmd/flags.go b/cmd/flags.go
index dfeb6a13..95216151 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -46,6 +46,10 @@ func globalFlags() []cli.Flag {
 			Name:  "no-agent",
 			Usage: "disable pprof (:6060) and gops (:6070) agent",
 		},
+		&cli.StringFlag{
+			Name:  "pyroscope",
+			Usage: "pyroscope address",
+		},
 		&cli.BoolFlag{
 			Name:  "no-color",
 			Usage: "disable colors",
diff --git a/cmd/main.go b/cmd/main.go
index ceb57c2f..00d4d769 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -18,18 +18,18 @@ package main
 
 import (
 	"fmt"
-	"net/http"
-	_ "net/http/pprof"
-	"os"
-	"strings"
-
 	"github.com/erikdubbelboer/gspt"
 	"github.com/google/gops/agent"
-	"github.com/sirupsen/logrus"
-
 	"github.com/juicedata/juicefs/pkg/utils"
 	"github.com/juicedata/juicefs/pkg/version"
+	"github.com/pyroscope-io/client/pyroscope"
+	"github.com/sirupsen/logrus"
 	"github.com/urfave/cli/v2"
+	"net/http"
+	_ "net/http/pprof"
+	"os"
+	"strconv"
+	"strings"
 )
 
 var logger = utils.GetLogger("juicefs")
@@ -255,6 +255,30 @@ func setup(c *cli.Context, n int) {
 			}
 		}()
 	}
+
+	if c.IsSet("pyroscope") {
+		tags := make(map[string]string)
+		appName := fmt.Sprintf("juicefs.%s", c.Command.Name)
+		if c.Command.Name == "mount" {
+			tags["mountpoint"] = c.Args().Get(1)
+		}
+		if hostname, err := os.Hostname(); err == nil {
+			tags["hostname"] = hostname
+		}
+		tags["pid"] = strconv.Itoa(os.Getpid())
+		tags["version"] = version.Version()
+
+		if _, err := pyroscope.Start(pyroscope.Config{
+			ApplicationName: appName,
+			ServerAddress:   c.String("pyroscope"),
+			Logger:          logger,
+			Tags:            tags,
+			AuthToken:       os.Getenv("PYROSCOPE_AUTH_TOKEN"),
+			ProfileTypes:    pyroscope.DefaultProfileTypes,
+		}); err != nil {
+			logger.Errorf("start pyroscope agent: %v", err)
+		}
+	}
 }
 
 func removePassword(uri string) {
diff --git a/docs/en/administration/fault_diagnosis_and_analysis.md b/docs/en/administration/fault_diagnosis_and_analysis.md
index 515bf8d6..77fe7e67 100644
--- a/docs/en/administration/fault_diagnosis_and_analysis.md
+++ b/docs/en/administration/fault_diagnosis_and_analysis.md
@@ -172,3 +172,21 @@ $ go tool pprof -pdf 'http://localhost:<port>/debug/pprof/heap' > juicefs.heap.p
 ```
 
 For more information about pprof, please see the [official documentation](https://github.com/google/pprof/blob/master/doc/README.md).
+
+### Profiling with the Pyroscope
+
+[pyroscope](https://github.com/pyroscope-io/pyroscope) Pyroscope is an open source continuous profiling platform. It will help you:
+
++ Find performance issues and bottlenecks in your code
++ Resolve issues with high CPU utilization
++ Understand the call tree of your application
++ Track changes over time
+
+JuiceFS supports using the `--pyroscope` parameter to pass in the pyroscope server address, and metrics are pushed to the server every 10s.
+If permission verification is enabled on the server, the verification information API Key can be passed in through the environment variable `PYROSCOPE_AUTH_TOKEN`
+
+```bash
+$ export PYROSCOPE_AUTH_TOKEN=xxxxxxxxxxxxxxxx
+$ juicefs mount --pyroscope http://localhost:4040 redis://localhost /mnt/jfs
+$ juicefs dump --pyroscope http://localhost:4040 redis://localhost dump.json
+```
diff --git a/docs/zh_cn/administration/fault_diagnosis_and_analysis.md b/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
index 019b35a6..34fa61ec 100644
--- a/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
+++ b/docs/zh_cn/administration/fault_diagnosis_and_analysis.md
@@ -172,3 +172,20 @@ $ go tool pprof -pdf 'http://localhost:<port>/debug/pprof/heap' > juicefs.heap.p
 ```
 
 关于 pprof 的更多信息，请查看[官方文档](https://github.com/google/pprof/blob/master/doc/README.md)。
+
+### 使用 Pyroscope 进行性能剖析
+
+[pyroscope](https://github.com/pyroscope-io/pyroscope) 是一个开源的持续性能剖析平台。它能够帮你： 
++ 找出源代码中的性能问题和瓶颈 
++ 解决 CPU 利用率高的问题 
++ 理解应用程序的调用树（call tree） 
++ 追踪随一段时间内变化的情况
+
+JuiceFS 支持使用 `--pyroscope` 参数传入 pyroscope 服务端地址，指标以每隔 10s 的频率推送到服务端。
+如果服务端开启了权限校验，校验信息 API Key 可以通过环境变量 `PYROSCOPE_AUTH_TOKEN` 传入
+
+```bash
+$ export PYROSCOPE_AUTH_TOKEN=xxxxxxxxxxxxxxxx
+$ juicefs mount --pyroscope http://localhost:4040 redis://localhost /mnt/jfs
+$ juicefs dump --pyroscope http://localhost:4040 redis://localhost dump.json
+```
diff --git a/go.mod b/go.mod
index 0125c261..4a1542c1 100644
--- a/go.mod
+++ b/go.mod
@@ -50,6 +50,7 @@ require (
 	github.com/prometheus/client_golang v1.11.0
 	github.com/prometheus/client_model v0.2.0
 	github.com/prometheus/common v0.26.0
+	github.com/pyroscope-io/client v0.2.1
 	github.com/qingstor/qingstor-sdk-go/v4 v4.4.0
 	github.com/qiniu/go-sdk/v7 v7.11.1
 	github.com/satori/go.uuid v1.2.0
diff --git a/go.sum b/go.sum
index 5944867a..f76ae497 100644
--- a/go.sum
+++ b/go.sum
@@ -716,6 +716,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/pyroscope-io/client v0.2.1 h1:GUyrncANOCuFhnmgURnpJ90o9z/uwZMx+1l9RMJUuKk=
+github.com/pyroscope-io/client v0.2.1/go.mod h1:zRdQXIGxy0H2QbKEkCmZBR6KOLLIFYLWsdzVI0MRm2E=
 github.com/qingstor/qingstor-sdk-go/v4 v4.4.0 h1:tbItWtGB1TDfYzqK8dtm6tV+xWU5iYMwL37C6AL5dDs=
 github.com/qingstor/qingstor-sdk-go/v4 v4.4.0/go.mod h1:mDVFtA7+bXQ5xoELTWkoFy1Ad13wtp8jtlnl/RU+zzM=
 github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
diff --git a/pkg/meta/base.go b/pkg/meta/base.go
index ece2abbd..fe0f25a1 100644
--- a/pkg/meta/base.go
+++ b/pkg/meta/base.go
@@ -31,8 +31,9 @@ import (
 )
 
 const (
-	inodeBatch   = 100
-	chunkIDBatch = 1000
+	inodeBatch    = 100
+	chunkIDBatch  = 1000
+	minUpdateTime = time.Millisecond * 10
 )
 
 type engine interface {
diff --git a/pkg/meta/redis.go b/pkg/meta/redis.go
index 05cbaedf..46fabf1e 100644
--- a/pkg/meta/redis.go
+++ b/pkg/meta/redis.go
@@ -1132,14 +1132,21 @@ func (m *redisMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, m
 			return syscall.EEXIST
 		}
 
+		var updateParent bool
 		now := time.Now()
-		if _type == TypeDirectory {
-			pattr.Nlink++
+		if parent != TrashInode {
+			if _type == TypeDirectory {
+				pattr.Nlink++
+				updateParent = true
+			}
+			if updateParent || now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
+				pattr.Mtime = now.Unix()
+				pattr.Mtimensec = uint32(now.Nanosecond())
+				pattr.Ctime = now.Unix()
+				pattr.Ctimensec = uint32(now.Nanosecond())
+				updateParent = true
+			}
 		}
-		pattr.Mtime = now.Unix()
-		pattr.Mtimensec = uint32(now.Nanosecond())
-		pattr.Ctime = now.Unix()
-		pattr.Ctimensec = uint32(now.Nanosecond())
 		attr.Atime = now.Unix()
 		attr.Atimensec = uint32(now.Nanosecond())
 		attr.Mtime = now.Unix()
@@ -1155,7 +1162,7 @@ func (m *redisMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, m
 
 		_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
 			pipe.HSet(ctx, m.entryKey(parent), name, m.packEntry(_type, ino))
-			if parent != TrashInode {
+			if updateParent {
 				pipe.Set(ctx, m.inodeKey(parent), m.marshal(&pattr), 0)
 			}
 			pipe.Set(ctx, m.inodeKey(ino), m.marshal(attr), 0)
@@ -1215,11 +1222,15 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		var updateParent bool
 		now := time.Now()
-		pattr.Mtime = now.Unix()
-		pattr.Mtimensec = uint32(now.Nanosecond())
-		pattr.Ctime = now.Unix()
-		pattr.Ctimensec = uint32(now.Nanosecond())
+		if !isTrash(parent) && now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
+			pattr.Mtime = now.Unix()
+			pattr.Mtimensec = uint32(now.Nanosecond())
+			pattr.Ctime = now.Unix()
+			pattr.Ctimensec = uint32(now.Nanosecond())
+			updateParent = true
+		}
 		attr = Attr{}
 		opened = false
 		if rs[1] != nil {
@@ -1243,7 +1254,7 @@ func (m *redisMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno
 		}
 		_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
 			pipe.HDel(ctx, m.entryKey(parent), name)
-			if !isTrash(parent) {
+			if updateParent {
 				pipe.Set(ctx, m.inodeKey(parent), m.marshal(&pattr), 0)
 			}
 			if attr.Nlink > 0 {
@@ -1456,6 +1467,7 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 		}
 		m.parseAttr([]byte(rs[2].(string)), &iattr)
 
+		var supdate, dupdate bool
 		now := time.Now()
 		tattr = Attr{}
 		opened = false
@@ -1472,6 +1484,7 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 				if dtyp == TypeDirectory && parentSrc != parentDst {
 					dattr.Nlink--
 					sattr.Nlink++
+					supdate, dupdate = true, true
 				}
 			} else {
 				if dtyp == TypeDirectory {
@@ -1483,6 +1496,7 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 						return syscall.ENOTEMPTY
 					}
 					dattr.Nlink--
+					dupdate = true
 					if trash > 0 {
 						tattr.Parent = trash
 					}
@@ -1511,21 +1525,28 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 			return syscall.EACCES
 		}
 
-		sattr.Mtime = now.Unix()
-		sattr.Mtimensec = uint32(now.Nanosecond())
-		sattr.Ctime = now.Unix()
-		sattr.Ctimensec = uint32(now.Nanosecond())
-		dattr.Mtime = now.Unix()
-		dattr.Mtimensec = uint32(now.Nanosecond())
-		dattr.Ctime = now.Unix()
-		dattr.Ctimensec = uint32(now.Nanosecond())
-		iattr.Parent = parentDst
-		iattr.Ctime = now.Unix()
-		iattr.Ctimensec = uint32(now.Nanosecond())
 		if typ == TypeDirectory && parentSrc != parentDst {
 			sattr.Nlink--
 			dattr.Nlink++
+			supdate, dupdate = true, true
+		}
+		if supdate || now.Sub(time.Unix(sattr.Mtime, int64(sattr.Mtimensec))) >= minUpdateTime {
+			sattr.Mtime = now.Unix()
+			sattr.Mtimensec = uint32(now.Nanosecond())
+			sattr.Ctime = now.Unix()
+			sattr.Ctimensec = uint32(now.Nanosecond())
+			supdate = true
+		}
+		if dupdate || now.Sub(time.Unix(dattr.Mtime, int64(dattr.Mtimensec))) >= minUpdateTime {
+			dattr.Mtime = now.Unix()
+			dattr.Mtimensec = uint32(now.Nanosecond())
+			dattr.Ctime = now.Unix()
+			dattr.Ctimensec = uint32(now.Nanosecond())
+			dupdate = true
 		}
+		iattr.Parent = parentDst
+		iattr.Ctime = now.Unix()
+		iattr.Ctimensec = uint32(now.Nanosecond())
 		if inode != nil {
 			*inode = ino
 		}
@@ -1569,12 +1590,14 @@ func (m *redisMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentD
 					}
 				}
 			}
-			if parentDst != parentSrc && !isTrash(parentSrc) {
+			if parentDst != parentSrc && !isTrash(parentSrc) && supdate {
 				pipe.Set(ctx, m.inodeKey(parentSrc), m.marshal(&sattr), 0)
 			}
 			pipe.Set(ctx, m.inodeKey(ino), m.marshal(&iattr), 0)
 			pipe.HSet(ctx, m.entryKey(parentDst), nameDst, buf)
-			pipe.Set(ctx, m.inodeKey(parentDst), m.marshal(&dattr), 0)
+			if dupdate {
+				pipe.Set(ctx, m.inodeKey(parentDst), m.marshal(&dattr), 0)
+			}
 			return nil
 		})
 		return err
@@ -1602,11 +1625,15 @@ func (m *redisMeta) doLink(ctx Context, inode, parent Ino, name string, attr *At
 		if pattr.Typ != TypeDirectory {
 			return syscall.ENOTDIR
 		}
+		var updateParent bool
 		now := time.Now()
-		pattr.Mtime = now.Unix()
-		pattr.Mtimensec = uint32(now.Nanosecond())
-		pattr.Ctime = now.Unix()
-		pattr.Ctimensec = uint32(now.Nanosecond())
+		if now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
+			pattr.Mtime = now.Unix()
+			pattr.Mtimensec = uint32(now.Nanosecond())
+			pattr.Ctime = now.Unix()
+			pattr.Ctimensec = uint32(now.Nanosecond())
+			updateParent = true
+		}
 		m.parseAttr([]byte(rs[1].(string)), &iattr)
 		if iattr.Typ == TypeDirectory {
 			return syscall.EPERM
@@ -1626,7 +1653,9 @@ func (m *redisMeta) doLink(ctx Context, inode, parent Ino, name string, attr *At
 
 		_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
 			pipe.HSet(ctx, m.entryKey(parent), name, m.packEntry(iattr.Typ, inode))
-			pipe.Set(ctx, m.inodeKey(parent), m.marshal(&pattr), 0)
+			if updateParent {
+				pipe.Set(ctx, m.inodeKey(parent), m.marshal(&pattr), 0)
+			}
 			pipe.Set(ctx, m.inodeKey(inode), m.marshal(&iattr), 0)
 			return nil
 		})
diff --git a/pkg/meta/sql.go b/pkg/meta/sql.go
index 6c1af8b0..322344f2 100644
--- a/pkg/meta/sql.go
+++ b/pkg/meta/sql.go
@@ -1048,12 +1048,19 @@ func (m *dbMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 			return syscall.EEXIST
 		}
 
+		var updateParent bool
 		now := time.Now().UnixNano() / 1e3
-		if _type == TypeDirectory {
-			pn.Nlink++
+		if parent != TrashInode {
+			if _type == TypeDirectory {
+				pn.Nlink++
+				updateParent = true
+			}
+			if updateParent || time.Duration(now-pn.Mtime)*1e3 >= minUpdateTime {
+				pn.Mtime = now
+				pn.Ctime = now
+				updateParent = true
+			}
 		}
-		pn.Mtime = now
-		pn.Ctime = now
 		n.Atime = now
 		n.Mtime = now
 		n.Ctime = now
@@ -1067,7 +1074,7 @@ func (m *dbMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 		if err = mustInsert(s, &edge{Parent: parent, Name: []byte(name), Inode: ino, Type: _type}, &n); err != nil {
 			return err
 		}
-		if parent != TrashInode {
+		if updateParent {
 			if _, err := s.Cols("nlink", "mtime", "ctime").Update(&pn, &node{Inode: pn.Inode}); err != nil {
 				return err
 			}
@@ -1152,13 +1159,17 @@ func (m *dbMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 		}
 		defer func() { m.of.InvalidateChunk(e.Inode, 0xFFFFFFFE) }()
 
-		pn.Mtime = now
-		pn.Ctime = now
+		var updateParent bool
+		if !isTrash(parent) && time.Duration(now-pn.Mtime)*1e3 >= minUpdateTime {
+			pn.Mtime = now
+			pn.Ctime = now
+			updateParent = true
+		}
 
 		if _, err := s.Delete(&edge{Parent: parent, Name: e.Name}); err != nil {
 			return err
 		}
-		if !isTrash(parent) {
+		if updateParent {
 			if _, err = s.Cols("mtime", "ctime").Update(&pn, &node{Inode: pn.Inode}); err != nil {
 				return err
 			}
@@ -1389,6 +1400,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				de.Name = e.Name
 			}
 		}
+		var supdate, dupdate bool
 		now := time.Now().UnixNano() / 1e3
 		opened = false
 		dn = node{Inode: de.Inode}
@@ -1411,6 +1423,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				if de.Type == TypeDirectory && parentSrc != parentDst {
 					dpn.Nlink--
 					spn.Nlink++
+					supdate, dupdate = true, true
 				}
 			} else {
 				if de.Type == TypeDirectory {
@@ -1422,6 +1435,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 						return syscall.ENOTEMPTY
 					}
 					dpn.Nlink--
+					dupdate = true
 					if trash > 0 {
 						dn.Parent = trash
 					}
@@ -1450,16 +1464,23 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			return syscall.EACCES
 		}
 
-		spn.Mtime = now
-		spn.Ctime = now
-		dpn.Mtime = now
-		dpn.Ctime = now
-		sn.Parent = parentDst
-		sn.Ctime = now
 		if se.Type == TypeDirectory && parentSrc != parentDst {
 			spn.Nlink--
 			dpn.Nlink++
+			supdate, dupdate = true, true
+		}
+		if supdate || time.Duration(now-spn.Mtime)*1e3 >= minUpdateTime {
+			spn.Mtime = now
+			spn.Ctime = now
+			supdate = true
 		}
+		if dupdate || time.Duration(now-dpn.Mtime)*1e3 >= minUpdateTime {
+			dpn.Mtime = now
+			dpn.Ctime = now
+			dupdate = true
+		}
+		sn.Parent = parentDst
+		sn.Ctime = now
 		if inode != nil {
 			*inode = sn.Inode
 		}
@@ -1535,7 +1556,7 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				return err
 			}
 		}
-		if parentDst != parentSrc && !isTrash(parentSrc) {
+		if parentDst != parentSrc && !isTrash(parentSrc) && supdate {
 			if _, err := s.Cols("nlink", "mtime", "ctime").Update(&spn, &node{Inode: parentSrc}); err != nil {
 				return err
 			}
@@ -1543,8 +1564,10 @@ func (m *dbMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 		if _, err := s.Cols("ctime", "parent").Update(&sn, &node{Inode: sn.Inode}); err != nil {
 			return err
 		}
-		if _, err := s.Cols("nlink", "mtime", "ctime").Update(&dpn, &node{Inode: parentDst}); err != nil {
-			return err
+		if dupdate {
+			if _, err := s.Cols("nlink", "mtime", "ctime").Update(&dpn, &node{Inode: parentDst}); err != nil {
+				return err
+			}
 		}
 		return err
 	})
@@ -1591,17 +1614,23 @@ func (m *dbMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 			return syscall.EPERM
 		}
 
+		var updateParent bool
 		now := time.Now().UnixNano() / 1e3
-		pn.Mtime = now
-		pn.Ctime = now
+		if time.Duration(now-pn.Mtime)*1e3 >= minUpdateTime {
+			pn.Mtime = now
+			pn.Ctime = now
+			updateParent = true
+		}
 		n.Nlink++
 		n.Ctime = now
 
 		if err = mustInsert(s, &edge{Parent: parent, Name: []byte(name), Inode: inode, Type: n.Type}); err != nil {
 			return err
 		}
-		if _, err := s.Cols("mtime", "ctime").Update(&pn, &node{Inode: parent}); err != nil {
-			return err
+		if updateParent {
+			if _, err := s.Cols("mtime", "ctime").Update(&pn, &node{Inode: parent}); err != nil {
+				return err
+			}
 		}
 		if _, err := s.Cols("nlink", "ctime").Update(&n, node{Inode: inode}); err != nil {
 			return err
diff --git a/pkg/meta/tkv.go b/pkg/meta/tkv.go
index 04c1a09e..d8bf6b4d 100644
--- a/pkg/meta/tkv.go
+++ b/pkg/meta/tkv.go
@@ -43,7 +43,6 @@ type kvTxn interface {
 	get(key []byte) []byte
 	gets(keys ...[]byte) [][]byte
 	scanRange(begin, end []byte) map[string][]byte
-	scan(prefix []byte, handler func(key, value []byte))
 	scanKeys(prefix []byte) [][]byte
 	scanValues(prefix []byte, limit int, filter func(k, v []byte) bool) map[string][]byte
 	exist(prefix []byte) bool
@@ -56,6 +55,7 @@ type kvTxn interface {
 type tkvClient interface {
 	name() string
 	txn(f func(kvTxn) error) error
+	scan(prefix []byte, handler func(key, value []byte)) error
 	reset(prefix []byte) error
 	close() error
 	shouldRetry(err error) bool
@@ -1024,14 +1024,21 @@ func (m *kvMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 			return syscall.EEXIST
 		}
 
+		var updateParent bool
 		now := time.Now()
-		if _type == TypeDirectory {
-			pattr.Nlink++
+		if parent != TrashInode {
+			if _type == TypeDirectory {
+				pattr.Nlink++
+				updateParent = true
+			}
+			if updateParent || now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
+				pattr.Mtime = now.Unix()
+				pattr.Mtimensec = uint32(now.Nanosecond())
+				pattr.Ctime = now.Unix()
+				pattr.Ctimensec = uint32(now.Nanosecond())
+				updateParent = true
+			}
 		}
-		pattr.Mtime = now.Unix()
-		pattr.Mtimensec = uint32(now.Nanosecond())
-		pattr.Ctime = now.Unix()
-		pattr.Ctimensec = uint32(now.Nanosecond())
 		attr.Atime = now.Unix()
 		attr.Atimensec = uint32(now.Nanosecond())
 		attr.Mtime = now.Unix()
@@ -1046,7 +1053,7 @@ func (m *kvMeta) doMknod(ctx Context, parent Ino, name string, _type uint8, mode
 		}
 
 		tx.set(m.entryKey(parent, name), m.packEntry(_type, ino))
-		if parent != TrashInode {
+		if updateParent {
 			tx.set(m.inodeKey(parent), m.marshal(&pattr))
 		}
 		tx.set(m.inodeKey(ino), m.marshal(attr))
@@ -1118,13 +1125,17 @@ func (m *kvMeta) doUnlink(ctx Context, parent Ino, name string) syscall.Errno {
 			trash = 0
 		}
 		defer func() { m.of.InvalidateChunk(inode, 0xFFFFFFFE) }()
-		pattr.Mtime = now.Unix()
-		pattr.Mtimensec = uint32(now.Nanosecond())
-		pattr.Ctime = now.Unix()
-		pattr.Ctimensec = uint32(now.Nanosecond())
+		var updateParent bool
+		if !isTrash(parent) && now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
+			pattr.Mtime = now.Unix()
+			pattr.Mtimensec = uint32(now.Nanosecond())
+			pattr.Ctime = now.Unix()
+			pattr.Ctimensec = uint32(now.Nanosecond())
+			updateParent = true
+		}
 
 		tx.dels(m.entryKey(parent, name))
-		if !isTrash(parent) {
+		if updateParent {
 			tx.set(m.inodeKey(parent), m.marshal(&pattr))
 		}
 		if attr.Nlink > 0 {
@@ -1287,6 +1298,7 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				dbuf = m.packEntry(e.Attr.Typ, e.Inode)
 			}
 		}
+		var supdate, dupdate bool
 		now := time.Now()
 		tattr = Attr{}
 		opened = false
@@ -1308,6 +1320,7 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				if dtyp == TypeDirectory && parentSrc != parentDst {
 					dattr.Nlink--
 					sattr.Nlink++
+					supdate, dupdate = true, true
 				}
 			} else {
 				if dtyp == TypeDirectory {
@@ -1315,6 +1328,7 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 						return syscall.ENOTEMPTY
 					}
 					dattr.Nlink--
+					dupdate = true
 					if trash > 0 {
 						tattr.Parent = trash
 					}
@@ -1343,21 +1357,28 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 			return syscall.EACCES
 		}
 
-		sattr.Mtime = now.Unix()
-		sattr.Mtimensec = uint32(now.Nanosecond())
-		sattr.Ctime = now.Unix()
-		sattr.Ctimensec = uint32(now.Nanosecond())
-		dattr.Mtime = now.Unix()
-		dattr.Mtimensec = uint32(now.Nanosecond())
-		dattr.Ctime = now.Unix()
-		dattr.Ctimensec = uint32(now.Nanosecond())
-		iattr.Parent = parentDst
-		iattr.Ctime = now.Unix()
-		iattr.Ctimensec = uint32(now.Nanosecond())
 		if typ == TypeDirectory && parentSrc != parentDst {
 			sattr.Nlink--
 			dattr.Nlink++
+			supdate, dupdate = true, true
+		}
+		if supdate || now.Sub(time.Unix(sattr.Mtime, int64(sattr.Mtimensec))) >= minUpdateTime {
+			sattr.Mtime = now.Unix()
+			sattr.Mtimensec = uint32(now.Nanosecond())
+			sattr.Ctime = now.Unix()
+			sattr.Ctimensec = uint32(now.Nanosecond())
+			supdate = true
+		}
+		if dupdate || now.Sub(time.Unix(dattr.Mtime, int64(dattr.Mtimensec))) >= minUpdateTime {
+			dattr.Mtime = now.Unix()
+			dattr.Mtimensec = uint32(now.Nanosecond())
+			dattr.Ctime = now.Unix()
+			dattr.Ctimensec = uint32(now.Nanosecond())
+			dupdate = true
 		}
+		iattr.Parent = parentDst
+		iattr.Ctime = now.Unix()
+		iattr.Ctimensec = uint32(now.Nanosecond())
 		if inode != nil {
 			*inode = ino
 		}
@@ -1397,12 +1418,14 @@ func (m *kvMeta) doRename(ctx Context, parentSrc Ino, nameSrc string, parentDst
 				}
 			}
 		}
-		if parentDst != parentSrc && !isTrash(parentSrc) {
+		if parentDst != parentSrc && !isTrash(parentSrc) && supdate {
 			tx.set(m.inodeKey(parentSrc), m.marshal(&sattr))
 		}
 		tx.set(m.inodeKey(ino), m.marshal(&iattr))
 		tx.set(m.entryKey(parentDst, nameDst), buf)
-		tx.set(m.inodeKey(parentDst), m.marshal(&dattr))
+		if dupdate {
+			tx.set(m.inodeKey(parentDst), m.marshal(&dattr))
+		}
 		return nil
 	})
 	if err == nil && !exchange && trash == 0 {
@@ -1434,16 +1457,22 @@ func (m *kvMeta) doLink(ctx Context, inode, parent Ino, name string, attr *Attr)
 			return syscall.EEXIST
 		}
 
+		var updateParent bool
 		now := time.Now()
-		pattr.Mtime = now.Unix()
-		pattr.Mtimensec = uint32(now.Nanosecond())
-		pattr.Ctime = now.Unix()
-		pattr.Ctimensec = uint32(now.Nanosecond())
+		if now.Sub(time.Unix(pattr.Mtime, int64(pattr.Mtimensec))) >= minUpdateTime {
+			pattr.Mtime = now.Unix()
+			pattr.Mtimensec = uint32(now.Nanosecond())
+			pattr.Ctime = now.Unix()
+			pattr.Ctimensec = uint32(now.Nanosecond())
+			updateParent = true
+		}
 		iattr.Ctime = now.Unix()
 		iattr.Ctimensec = uint32(now.Nanosecond())
 		iattr.Nlink++
 		tx.set(m.entryKey(parent, name), m.packEntry(iattr.Typ, inode))
-		tx.set(m.inodeKey(parent), m.marshal(&pattr))
+		if updateParent {
+			tx.set(m.inodeKey(parent), m.marshal(&pattr))
+		}
 		tx.set(m.inodeKey(inode), m.marshal(&iattr))
 		if attr != nil {
 			*attr = iattr
@@ -2280,26 +2309,26 @@ func (m *kvMeta) DumpMeta(w io.Writer, root Ino) (err error) {
 			defer func() { m.snap = nil }()
 			m.snap = &memKV{items: btree.New(2), temp: &kvItem{}}
 			bar := progress.AddCountBar("Snapshot keys", 0)
-			if err = m.txn(func(tx kvTxn) error {
-				used := parseCounter(tx.get(m.counterKey(usedSpace)))
-				inodeTotal := parseCounter(tx.get(m.counterKey(totalInodes)))
-				var guessKeyTotal int64 = 3 // setting, nextInode, nextChunk
-				if inodeTotal > 0 {
-					guessKeyTotal += int64(math.Ceil((float64(used/inodeTotal/(64*1024*1024)) + float64(3)) * float64(inodeTotal)))
+			bUsed, _ := m.get(m.counterKey(usedSpace))
+			bInodes, _ := m.get(m.counterKey(totalInodes))
+			used := parseCounter(bUsed)
+			inodeTotal := parseCounter(bInodes)
+			var guessKeyTotal int64 = 3 // setting, nextInode, nextChunk
+			if inodeTotal > 0 {
+				guessKeyTotal += int64(math.Ceil((float64(used/inodeTotal/(64*1024*1024)) + float64(3)) * float64(inodeTotal)))
+			}
+			bar.SetCurrent(0) // Reset
+			bar.SetTotal(guessKeyTotal)
+			threshold := 0.1
+			err := m.client.scan(nil, func(key, value []byte) {
+				m.snap.set(string(key), value)
+				if bar.Current() > int64(math.Ceil(float64(guessKeyTotal)*(1-threshold))) {
+					guessKeyTotal += int64(math.Ceil(float64(guessKeyTotal) * threshold))
+					bar.SetTotal(guessKeyTotal)
 				}
-				bar.SetCurrent(0) // Reset
-				bar.SetTotal(guessKeyTotal)
-				threshold := 0.1
-				tx.scan(nil, func(key, value []byte) {
-					m.snap.set(string(key), value)
-					if bar.Current() > int64(math.Ceil(float64(guessKeyTotal)*(1-threshold))) {
-						guessKeyTotal += int64(math.Ceil(float64(guessKeyTotal) * threshold))
-						bar.SetTotal(guessKeyTotal)
-					}
-					bar.Increment()
-				})
-				return nil
-			}); err != nil {
+				bar.Increment()
+			})
+			if err != nil {
 				return err
 			}
 			bar.Done()
diff --git a/pkg/meta/tkv_badger.go b/pkg/meta/tkv_badger.go
index 5f2c730d..f4a1c6fd 100644
--- a/pkg/meta/tkv_badger.go
+++ b/pkg/meta/tkv_badger.go
@@ -32,23 +32,6 @@ type badgerTxn struct {
 	c *badger.DB
 }
 
-func (tx *badgerTxn) scan(prefix []byte, handler func(key []byte, value []byte)) {
-	it := tx.t.NewIterator(badger.IteratorOptions{
-		Prefix:         prefix,
-		PrefetchValues: true,
-		PrefetchSize:   1024,
-	})
-	defer it.Close()
-	for it.Rewind(); it.Valid(); it.Next() {
-		item := it.Item()
-		value, err := item.ValueCopy(nil)
-		if err != nil {
-			panic(err)
-		}
-		handler(it.Item().Key(), value)
-	}
-}
-
 func (tx *badgerTxn) get(key []byte) []byte {
 	item, err := tx.t.Get(key)
 	if err == badger.ErrKeyNotFound {
@@ -223,6 +206,26 @@ func (c *badgerClient) txn(f func(kvTxn) error) (err error) {
 	return txn.t.Commit()
 }
 
+func (c *badgerClient) scan(prefix []byte, handler func(key []byte, value []byte)) error {
+	tx := c.client.NewTransaction(false)
+	defer tx.Discard()
+	it := tx.NewIterator(badger.IteratorOptions{
+		Prefix:         prefix,
+		PrefetchValues: true,
+		PrefetchSize:   10240,
+	})
+	defer it.Close()
+	for it.Rewind(); it.Valid(); it.Next() {
+		item := it.Item()
+		value, err := item.ValueCopy(nil)
+		if err != nil {
+			return err
+		}
+		handler(it.Item().Key(), value)
+	}
+	return nil
+}
+
 func (c *badgerClient) reset(prefix []byte) error {
 	for {
 		tx := c.client.NewTransaction(true)
diff --git a/pkg/meta/tkv_etcd.go b/pkg/meta/tkv_etcd.go
index 84e25327..8aa7826d 100644
--- a/pkg/meta/tkv_etcd.go
+++ b/pkg/meta/tkv_etcd.go
@@ -127,20 +127,6 @@ func (tx *etcdTxn) scanRange(begin_, end_ []byte) map[string][]byte {
 	return ret
 }
 
-func (tx *etcdTxn) scan(prefix []byte, handler func(key []byte, value []byte)) {
-	resp, err := tx.kv.Get(tx.ctx,
-		string(prefix),
-		etcd.WithPrefix(),
-		etcd.WithSerializable())
-	if err != nil {
-		panic(fmt.Errorf("get prefix %v: %s", string(prefix), err))
-	}
-	for _, kv := range resp.Kvs {
-		tx.observed[string(kv.Key)] = kv.ModRevision
-		handler(kv.Key, kv.Value)
-	}
-}
-
 func (tx *etcdTxn) scanKeys(prefix []byte) [][]byte {
 	resp, err := tx.kv.Get(tx.ctx, string(prefix), etcd.WithPrefix(), etcd.WithKeysOnly())
 	if err != nil {
@@ -290,6 +276,40 @@ func (c *etcdClient) txn(f func(kvTxn) error) (err error) {
 
 var conflicted = errors.New("conflicted transaction")
 
+func (c *etcdClient) scan(prefix []byte, handler func(key []byte, value []byte)) error {
+	var start = prefix
+	var end = string(nextKey(prefix))
+	resp, err := c.client.Get(context.Background(), "anything")
+	if err != nil {
+		return err
+	}
+	currentRev := resp.Header.Revision
+	var following bool
+	for {
+		resp, err := c.client.Get(context.Background(),
+			string(start),
+			etcd.WithRange(end),
+			etcd.WithLimit(1024),
+			etcd.WithMaxModRev(currentRev),
+			etcd.WithSerializable())
+		if err != nil {
+			return fmt.Errorf("get start %v: %s", string(start), err)
+		}
+		if following && len(resp.Kvs) > 0 {
+			resp.Kvs = resp.Kvs[1:]
+		}
+		if len(resp.Kvs) == 0 {
+			break
+		}
+		for _, kv := range resp.Kvs {
+			handler(kv.Key, kv.Value)
+		}
+		start = resp.Kvs[len(resp.Kvs)-1].Key
+		following = true
+	}
+	return nil
+}
+
 func (c *etcdClient) reset(prefix []byte) error {
 	_, err := c.kv.Delete(context.Background(), string(prefix), etcd.WithPrefix())
 	return err
diff --git a/pkg/meta/tkv_mem.go b/pkg/meta/tkv_mem.go
index b996b3c2..8b63c0b0 100644
--- a/pkg/meta/tkv_mem.go
+++ b/pkg/meta/tkv_mem.go
@@ -98,22 +98,6 @@ func (tx *memTxn) scanRange(begin_, end_ []byte) map[string][]byte {
 	return ret
 }
 
-func (tx *memTxn) scan(prefix []byte, handler func(key []byte, value []byte)) {
-	tx.store.Lock()
-	defer tx.store.Unlock()
-	begin := string(prefix)
-	end := string(nextKey(prefix))
-	tx.store.items.AscendGreaterOrEqual(&kvItem{key: begin}, func(i btree.Item) bool {
-		it := i.(*kvItem)
-		if it.key >= end {
-			return false
-		}
-		tx.observed[it.key] = it.ver
-		handler([]byte(it.key), it.value)
-		return true
-	})
-}
-
 func nextKey(key []byte) []byte {
 	if len(key) == 0 {
 		return nil
@@ -286,6 +270,22 @@ func (c *memKV) txn(f func(kvTxn) error) error {
 	return nil
 }
 
+func (c *memKV) scan(prefix []byte, handler func(key []byte, value []byte)) error {
+	c.Lock()
+	defer c.Unlock()
+	begin := string(prefix)
+	end := string(nextKey(prefix))
+	c.items.AscendGreaterOrEqual(&kvItem{key: begin}, func(i btree.Item) bool {
+		it := i.(*kvItem)
+		if it.key >= end {
+			return false
+		}
+		handler([]byte(it.key), it.value)
+		return true
+	})
+	return nil
+}
+
 func (c *memKV) reset(prefix []byte) error {
 	if len(prefix) == 0 {
 		c.Lock()
@@ -295,10 +295,9 @@ func (c *memKV) reset(prefix []byte) error {
 		return nil
 	}
 	return c.txn(func(kt kvTxn) error {
-		kt.scan(prefix, func(key, value []byte) {
+		return c.scan(prefix, func(key, value []byte) {
 			kt.dels(key)
 		})
-		return nil
 	})
 }
 
diff --git a/pkg/meta/tkv_prefix.go b/pkg/meta/tkv_prefix.go
index 1a1c2ded..9ee06398 100644
--- a/pkg/meta/tkv_prefix.go
+++ b/pkg/meta/tkv_prefix.go
@@ -53,11 +53,7 @@ func (tx *prefixTxn) scanRange(begin_, end_ []byte) map[string][]byte {
 	}
 	return m
 }
-func (tx *prefixTxn) scan(prefix []byte, handler func(key, value []byte)) {
-	tx.kvTxn.scan(tx.realKey(prefix), func(key, value []byte) {
-		handler(tx.origKey(key), value)
-	})
-}
+
 func (tx *prefixTxn) scanKeys(prefix []byte) [][]byte {
 	keys := tx.kvTxn.scanKeys(tx.realKey(prefix))
 	for i, k := range keys {
@@ -114,6 +110,15 @@ func (c *prefixClient) txn(f func(kvTxn) error) error {
 	})
 }
 
+func (c *prefixClient) scan(prefix []byte, handler func(key, value []byte)) error {
+	k := make([]byte, len(c.prefix)+len(prefix))
+	copy(k, c.prefix)
+	copy(k[len(c.prefix):], prefix)
+	return c.tkvClient.scan(k, func(key, value []byte) {
+		handler(key[len(c.prefix):], value)
+	})
+}
+
 func (c *prefixClient) reset(prefix []byte) error {
 	if prefix != nil {
 		return fmt.Errorf("prefix must be nil, but got %v", prefix)
diff --git a/pkg/meta/tkv_tikv.go b/pkg/meta/tkv_tikv.go
index bf06ca41..ea9cd56c 100644
--- a/pkg/meta/tkv_tikv.go
+++ b/pkg/meta/tkv_tikv.go
@@ -31,6 +31,7 @@ import (
 	tikverr "github.com/tikv/client-go/v2/error"
 	"github.com/tikv/client-go/v2/tikv"
 	"github.com/tikv/client-go/v2/txnkv"
+	"github.com/tikv/client-go/v2/txnkv/txnutil"
 )
 
 func init() {
@@ -136,20 +137,6 @@ func (tx *tikvTxn) scanRange(begin, end []byte) map[string][]byte {
 	return tx.scanRange0(begin, end, -1, nil)
 }
 
-func (tx *tikvTxn) scan(prefix []byte, handler func(key, value []byte)) {
-	it, err := tx.Iter(prefix, nextKey(prefix))
-	if err != nil {
-		panic(err)
-	}
-	defer it.Close()
-	for it.Valid() {
-		handler(it.Key(), it.Value())
-		if err = it.Next(); err != nil {
-			panic(err)
-		}
-	}
-}
-
 func (tx *tikvTxn) scanKeys(prefix []byte) [][]byte {
 	it, err := tx.Iter(prefix, nextKey(prefix))
 	if err != nil {
@@ -247,6 +234,29 @@ func (c *tikvClient) txn(f func(kvTxn) error) (err error) {
 	return err
 }
 
+func (c *tikvClient) scan(prefix []byte, handler func(key, value []byte)) error {
+	ts, err := c.client.CurrentTimestamp("global")
+	if err != nil {
+		return err
+	}
+	snap := c.client.GetSnapshot(ts)
+	snap.SetScanBatchSize(10240)
+	snap.SetNotFillCache(true)
+	snap.SetPriority(txnutil.PriorityLow)
+	it, err := snap.Iter(prefix, nextKey(prefix))
+	if err != nil {
+		return err
+	}
+	defer it.Close()
+	for it.Valid() {
+		handler(it.Key(), it.Value())
+		if err = it.Next(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (c *tikvClient) reset(prefix []byte) error {
 	_, err := c.client.DeleteRange(context.Background(), prefix, nextKey(prefix), 1)
 	return err


Test patch:
diff --git a/pkg/meta/tkv_test.go b/pkg/meta/tkv_test.go
index ecbbbc63..6d18a844 100644
--- a/pkg/meta/tkv_test.go
+++ b/pkg/meta/tkv_test.go
@@ -98,10 +98,8 @@ func testTKV(t *testing.T, c tkvClient) {
 	}
 
 	var keys [][]byte
-	txn(func(kt kvTxn) {
-		kt.scan([]byte("k"), func(key, value []byte) {
-			keys = append(keys, key)
-		})
+	c.scan([]byte("k"), func(key, value []byte) {
+		keys = append(keys, key)
 	})
 	if len(keys) != 2 || string(keys[0]) != "k" || string(keys[1]) != "k2" {
 		t.Fatalf("keys: %+v", keys)