Aggregator
Vulnerability in Acrobat Reader could lead to remote code execution; Microsoft patches information disclosure issue in Windows API
How to Handle Secrets in Go
Short-term Memory Effects in the Phototactic Behavior of Microalgae: Results
Why Hellman & Friedman Wants to Unload Checkmarx for $2.5B
Hellman & Friedman has met with several investments banks in recent weeks and will choose one to run the sale process for Paramus, New Jersey-based Checkmarx, in which it hopes to get at least $2.5 billion, Calcalist reported. The private equity firm bought Checkmarx for $1.15 billion in April 2020.
Mental Health Records Database Found Exposed on Web
An AI-powered virtual care provider's unsecured database allegedly exposed thousands of sensitive mental health and substance abuse treatment records between patients and their counselors on the internet - where they were available to anyone, said the security researcher who discovered the trove.
French Cyber Agency Warns of APT28 Hacks Against Think Tanks
Russian state hackers are targeting think tanks studying strategic interests and the defense sector, warned the French cyber agency. A hacking group that officially is Unit 26165 of the Russian Main Intelligence Directorate appears to be Russia's most prolific targeter of think tanks.
NoName Apparently Allies With RansomHub Operation
Up-and-coming online criminal extortion group RansomHub appears to have a new affiliate - NoName, a midtier actor whose main claim to fame so far has been impersonating the LockBit ransomware-as-a-service operation. NoName is known for exploiting years-old vulnerabilities.
Election Experts Still Demanding More Federal Cyber Support
Election security experts told Information Security Media Group the United States continues to lack adequate federal funding and resources to support state and local election information technology security efforts amid heightened global tensions and an ever-expanding threat landscape.
KDD 2024 OAG-Challenge Cup赛道三项冠军技术方案解读
The Role of State-Sponsored Actors in Election Interference
JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)
Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.
- JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)
- JuiceFS 元数据引擎再探:开箱解读 TiKV 中的 JuiceFS 元数据(2024)
- JuiceFS 元数据引擎三探:从实践中学习 TiKV 的 MVCC 和 GC(2024)
- JuiceFS 元数据引擎四探:元数据大小评估、限流与限速的设计思考(2024)
- JuiceFS 元数据引擎五探:元数据备份与恢复(2024)
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
- 1 JuiceFS 高层架构与组件
- 2 JuiceFS 元数据存储引擎对比:tikv vs. etcd
- 3 JuiceFS + TiKV:集群启动和宏观读写流程
- 4 TiKV 内部数据初探
- 5 总结
- 参考资料
Fig. JuiceFS components and architecutre.
如图,最粗的粒度上可以分为三个组件。
1.1 JuiceFS client- juicefs format ... 可以创建一个 volume;
- juicefs config ... 可以修改一个 volume 的配置;
- juicefs mount ... 可以把一个 volume 挂载到机器上,然后用户就可以在里面读写文件了;
- 用于存储 JuiceFS 的元数据,例如每个文件的文件名、最后修改时间等等;
- 可选择 etcd、TiKV 等等;
实际的对象存储,例如 S3、Ceph、阿里云 OSS 等等,存放 JuiceFS volume 内的数据。
2 JuiceFS 元数据存储引擎对比:tikv vs. etcd 2.1 设计与优缺点对比 TiKV as metadata engine etcd as metadata engine 管理节点(e.g. leader election) PD (TiKV cluster manager) etcd server 数据节点(存储 juicefs metadata) TiKV server etcd server 数据节点对等 无要求 完全对等 数据一致性粒度 region-level (TiKV 的概念,region < node) node-level Raft 粒度 region-level (multi-raft,TiKV 的概念) node-level 缓存多少磁盘数据在内存中 一部分 所有 集群支持的最大数据量 PB 级别 几十 GB 级别 性能(JuiceFS 场景) 高(猜测是因为 raft 粒度更细,并发读写高) 低 维护和二次开发门槛 高(相比 etcd) 低 流行度 & 社区活跃度 低(相比 etcd) 高 适用场景 大和超大 JuiceFS 集群 中小 JuiceFS 集群 2.2 几点解释etcd 集群,
- 每个节点完全对等,既负责管理又负责存储数据;
- 所有数据全部缓存在内存中,每个节点的数据完全一致。 这一点限制了 etcd 集群支持的最大数据量和扩展性, 例如现在官网还是建议不要超过 8GB(实际上较新的版本在技术上已经没有这个限制了, 但仍受限于机器的内存)。
TiKV 方案可以可以理解成把管理和数据存储分开了,
- PD 可以理解为 TiKV cluster manager,负责 leader 选举、multi-raft、元数据到 region 的映射等等;
- 节点之间也不要求对等,PD 按照 region(比如 96MB)为单位,将 N(默认 3)个副本放到 N 个 TiKV node 上,而实际上 TiKV 的 node 数量是 M,M >= N;
- 数据放在 TiKV 节点的磁盘,内存中只缓存一部分(默认是用机器 45% 的内存,可控制)。
TiKV 作为存储引擎,总结成一句话就是:根据硬件配置干活,能者多劳 —— 内存大、磁盘大就多干活,反之就少干活。
下面的监控展示是 7 台 TiKV node 组成的一个集群,各 node 内存不完全一致: 3 台 256GB 的,2 台 128GB 的,2 台 64GB 的, 可以看到每个 TiKV server 确实只用了各自所在 node 一半左右的内存:
Fig. TiKV engine size and memory usage of a 7-node (with various RAMs) cluster.
3 JuiceFS + TiKV:集群启动和宏观读写流程 3.1 架构用 TiKV 作为元数据引擎,架构如下(先忽略其中的细节信息,稍后会介绍):
Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.
3.2 TiKV 集群启动 3.2.1 TiKV & PD 配置差异两个组件的几个核心配置项,
$ cat /etc/tikv/pd-config.toml name = "pd-node1" data-dir = "/var/data/pd" client-urls = "https://192.168.1.1:2379" # 客户端(例如 JuiceFS)访问 PD 时,连接这个地址 peer-urls = "https://192.168.1.1:2380" # 其他 PD 节点访问这个 PD 时,连接这个地址,也就是集群内互相通信的地址 # 创建集群时的首批 PD initial-cluster-token = "<anything you like>" initial-cluster = "pd-node1=https://192.168.1.3:2380,pd-node2=https://192.168.1.2:2380,pd-node3=https://192.168.1.1:2380"可以看到,PD 的配置和 etcd 就比较类似,需要指定其他 PD 节点地址,它们之间互相通信。
TiKV 节点(tikv-server)的配置就不一样了,
$ cat /etc/tikv/tikv-config.toml [pd] endpoints = ["https://192.168.1.1:2379", "https://192.168.1.2:2379", "https://192.168.1.3:2379"] [server] addr = "192.168.1.1:20160" # 服务地址,JuiceFS client 会直接访问这个地址读写数据 status-addr = "192.168.1.1:20180" # prometheus可以看到,
- TiKV 会配置所有 PD 节点的地址,以便自己注册到 PD 作为一个数据节点(存储JuiceFS 元数据);
- TiKV 还会配置一个地址的 server 地址,这个读写本节点所管理的 region 内的数据用的; 正常流程是 JuiceFS client 先访问 PD,拿到 region 和 tikv-server 信息, 然后再到 tikv-server 来读写数据(对应 JuiceFS 的元数据);
- TiKV 不会配置其他 TiKV 节点的地址,也就是说 TiKV 节点之间不会 peer-to-peer 互连。 属于同一个 raft group 的多个 region 通信,也是先通过 PD 协调的,最后 region leader 才发送数据给 region follower。 详见 [1]。
Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.
对应图中 step 1 & 2:
- step 1. PD 集群启动,选主;
- step 2. TiKV 节点启动,向 PD 注册;每个 TiKV 节点称为一个 store,也就是元数据仓库。
对应图中 step 3~5:
-
step 3. JuiceFS 客户端连接到 PD;发出读写文件请求;
- JuiceFS 客户端中会初始化一个 TiKV 的 transaction kv client,这里面又会初始化一个 PD client,
- 简单来说,此时 JuiceFS 客户端就有了 PD 集群的信息,例如哪个文件对应到哪个 region,这个 region 分布在哪个 TiKV 节点上,TiKV 服务端连接地址是多少等等;
- step 4. JuiceFS (内部的 TiKV 客户端)直接向 TiKV 节点(准确说是 region leader)发起读写请求;
- step 5. 元数据处理完成,JuiceFS 客户端开始往对象存储里读写文件。
TiKV 内部存储的都是 JuiceFS 的元数据。具体来说又分为两种:
- 用户文件的元数据:例如用户创建了一个 foo.txt,在 TiKV 里面就会对应一条或多条元数据来描述这个文件的信息;
- JuiceFS 系统元数据:例如每个 volume 的配置信息,这些对用户是不可见的。
TiKV 是扁平的 KV 存储,所以以上两类文件都放在同一个扁平空间,通过 key 访问。 本文先简单通过命令看看里面的元数据长什么样,下一篇再结合具体 JuiceFS 操作来深入解读这些元数据。
4.1 简单脚本 tikv-ctl.sh/pd-ctl.sh简单封装一下对应的命令行工具,使用更方便,
$ cat pd-ctl.sh tikv-ctl \ --ca-path /etc/tikv/pki/root.crt --cert-path /etc/tikv/pki/tikv.crt --key-path /etc/tikv/pki/tikv.key \ --host 192.168.1.1:20160 \ "$@" $ cat pd-ctl.sh pd-ctl \ --cacert /etc/tikv/pki/root.crt --cert /etc/tikv/pki/pd.crt --key /etc/tikv/pki/pd.key \ --pd https://192.168.1.1:2379 \ "$@" 4.2 tikv-ctl scan 扫描 key/valuetikv-ctl 不支持只列出所有 keys,所以只能 key 和 value 一起打印(扫描)。
扫描前缀是 foo 开头的所有 key:
$ ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' --limit 100 ... key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile3.\377txt\000\000\000\000\000\372 key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile4.\377txt\000\000\000\000\000\372 ... key: zfoo-dev\375\377setting\000\376 default cf value: start_ts: 452330324173520898 value: 7B0A22...扫描的时候一定要在 key 前面加一个 z 前缀,这是 TiKV 的一个设计,
The raw-scan command scans directly from the RocksDB. Note that to scan data keys you need to add a ‘z’ prefix to keys.
代码出处 components/keys/src/lib.rs。 但对用户来说不是太友好,暴露了太多内部细节,没有 etcdctl 方便直接。
4.3 tikv-ctl mvcc 查看给定 key 对应的 value $ ./tikv-ctl.sh mvcc -k 'zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile1.\377txt\000\000\000\000\000\372' --show-cf default,lock,write key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile1.\377txt\000\000\000\000\000\372 write cf value: start_ts: 452330816414416901 commit_ts: 452330816414416903 short_value: 010000000000000002CF 是 column family 的缩写,进一步了解,可参考 Google bigtable 中关于 CF 的定义 译 | Bigtable: A Distributed Storage System for Structured Data (OSDI, 2006)。
4.4 tikv-ctl --decode <key> 解除字符转义 # tikv escaped format -> raw format ./tikv-ctl.sh --decode 'foo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile4.\377txt\000\000\000\000\000\372' foo-dev\375A\001\000\000\000\000\000\000\000Dfile4.txt 4.5 tikv-ctl --to-hex:转义表示 -> 十六进制表示 $ ./tikv-ctl.sh --to-hex '\375' FD 4.6 tikv-ctl --to-escaped <value>:十六进制 value -> 带转义的字符串 ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' --limit 100 key: zfoo-dev\375\377setting\000\376 default cf value: start_ts: 452330324173520898 value: 7B0A22...其中的 value 是可以解码出来的,
# hex -> escaped string $ ./tikv-ctl.sh --to-escaped '7B0A22...' {\n\"Name\": \"...\",\n\"UUID\": \"8cd1ac73\",\n\"Storage\": \"S3\",\n\"Bucket\": \"http://xxx\",\n\"AccessKey\": \"...\",\n\"BlockSize\": 4096,\n\"Compression\": \"none\",\n\"KeyEncrypted\": true,\n\"MetaVersion\": 1,\n\"UploadLimit\": 0,\n\"DownloadLimit\": 0,\n\"\": \"\"\n} 5 总结本文介绍了一些 JuiceFS 元数据引擎相关的内容。
参考资料- A Deep Dive into TiKV, 2016, pincap.com
JuiceFS 元数据引擎再探:开箱解读 TiKV 中的 JuiceFS 元数据(2024)
Fig. JuiceFS upload/download data bandwidth control.
- JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)
- JuiceFS 元数据引擎再探:开箱解读 TiKV 中的 JuiceFS 元数据(2024)
- JuiceFS 元数据引擎三探:从实践中学习 TiKV 的 MVCC 和 GC(2024)
- JuiceFS 元数据引擎四探:元数据大小评估、限流与限速的设计思考(2024)
- JuiceFS 元数据引擎五探:元数据备份与恢复(2024)
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
有了第一篇的铺垫,本文直接进入正题。
- 首先创建一个 volume,然后在其中做一些文件操作,然后通过 tikv-ctl 等工具在 TiKV 中查看对应的元数据。
- 有了这些基础,我们再讨论 JuiceFS metadata key 和 TiKV 的编码格式。
之前有一篇类似的,开箱解读 etcd 中的 Cilium 元数据: What’s inside Cilium Etcd (kvstore)。
1 创建一个 volume创建一个名为 foo-dev 的 JuiceFS volume。
1.1 JuiceFS client 日志用 juicefs client 的 juicefs format 命令创建 volume,
$ juicefs format --storage oss --bucket <bucket> --access-key <key> --secret-key <secret key> \ tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev foo-dev <INFO>: Meta address: tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev <INFO>: Data use oss://xxx/foo-dev/ <INFO>: Volume is formatted as { "Name": "foo-dev", "UUID": "ec843b", "Storage": "oss", "BlockSize": 4096, "MetaVersion": 1, "UploadLimit": 0, "DownloadLimit": 0, ... }- 对象存储用的是阿里云 OSS;
- TiKV 地址指向的是 PD 集群地址,上一篇已经介绍过,2379 是 PD 接收客户端请求的端口;
下面我们进入 JuiceFS 代码,看看 JuiceFS client 初始化和连接到元数据引擎的调用栈:
mount |-metaCli = meta.NewClient |-txnkv.NewClient(url) // github.com/juicedata/juicefs: pkg/meta/tkv_tikv.go | |-NewClient // github.com/tikv/client-go: txnkv/client.go | |-pd.NewClient // github.com/tikv/client-go: tikv/kv.go | | |-NewClient // github.com/tikv/pd: client/client.go | | |-NewClientWithContext // github.com/tikv/pd: client/client.go | | |-createClientWithKeyspace // github.com/tikv/pd: client/client.go | | |-c.pdSvcDiscovery = newPDServiceDiscovery // github.com/tikv/pd: client/pd_xx.go | | |-c.setup() // github.com/tikv/pd: client/pd_xx.go | | |-c.pdSvcDiscovery.Init() | | |-c.pdSvcDiscovery.AddServingURLSwitchedCallback | | |-c.createTokenDispatcher() | |-spkv, err := tikv.NewEtcdSafePointKV | |-tikv.NewRPCClient | |-tikv.NewKVStore(uuid, pdClient, spkv, rpcClient) // github.com/tikv/client-go: tikv/kv.go | |-oracles.NewPdOracle | |-store := &KVStore{} | |-go store.runSafePointChecker() | | |-check key "/tidb/store/gcworker/saved_safe_point" from etcd every 10s | |-go store.safeTSUpdater() |-metaCli.NewSession |-doNewSession |-m.setValue(m.sessionKey(m.sid), m.expireTime()) // SE |-m.setValue(m.sessionInfoKey(m.sid), sinfo) // SI这里面连接到 TiKV/PD 的代码有点绕,
- 传给 juicefs client 的是 PD 集群地址,
- 但代码使用的是 tikv 的 client-go 包,创建的是一个 tikv transaction client,
- 这个 tikv transaction client 里面会去创建 pd client 连接到 PD 集群,
所以,架构上看 juicefs 是直连 PD,但实现上并没有直接创建 pd client, 也没有直接使用 pd 的库。
Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.
1.3 tikv-ctl 查看空 volume 的系统元数据现在再把目光转到 TiKV。看看这个空的 volume 在 TiKV 中对应哪些元数据:
$ ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000I\000\000\000\000\000\000\371 # attr? key: zfoo-dev\375\377ClastCle\377anupSess\377ions\000\000\000\000\373 # lastCleanupSessions key: zfoo-dev\375\377CnextChu\377nk\000\000\000\000\000\000\371 # nextChunk key: zfoo-dev\375\377CnextIno\377de\000\000\000\000\000\000\371 # nextInode key: zfoo-dev\375\377CnextSes\377sion\000\000\000\000\373 # nextSession key: zfoo-dev\375\377SE\000\000\000\000\000\000\377\000\001\000\000\000\000\000\000\371 # session key: zfoo-dev\375\377SI\000\000\000\000\000\000\377\000\001\000\000\000\000\000\000\371 # sessionInfo key: zfoo-dev\375\377setting\000\376 # setting以上就是我们新建的 volume foo-dev 的所有 entry 了。 也就是说一个 volume 创建出来之后,默认就有这些 JuiceFS 系统元数据。
TiKV 中的每个 key 都经过了两层编码(JuiceFS 和 TiKV),我们后面再介绍编码规则。 就目前来说,根据 key 中的字符还是依稀能看出每个 key 是干啥用的, 为方便起见直接注释在上面每行的最后了。比如,下面两个 session 相关的 entry 就是上面调用栈最后两个创建的:
- session
- sessionInfo
TiKV 中的每个 entry 都是 key/value。现在我们尝试解码最后一个 entry,key 是 zfoo-dev\375\377setting\000\376, 我们来看看它的 value —— 也就是它的内容 —— 是什么:
$ value_hex=$(./tikv-ctl.sh mvcc -k 'zfoo-dev\375\377setting\000\376' --show-cf=default | awk '/default cf value:/ {print $NF}') $ value_escaped=$(./tikv-ctl.sh --to-escaped $value_hex) $ echo -e $value_escaped | sed 's/\\"/"/g' | jq .输出:
{ "Name": "foo-dev", "UUID": "1ce2973b", "Storage": "S3", "Bucket": "http://xx/bucket", "AccessKey": "xx", "SecretKey": "xx", "BlockSize": 4096, "MetaVersion": 1, "UploadLimit": 0, "DownloadLimit": 0, ... }可以看到是个 JSON 结构体。这其实就是这个 volume 的配置信息。如果对 JuiceFS 代码有一定了解, 就会看出来它对应的其实就是 type Format 这个 struct。
1.4.1 对应 JuiceFS Format 结构体 // https://github.com/juicedata/juicefs/blob/v1.2.0/pkg/meta/config.go#L72 type Format struct { Name string UUID string Storage string StorageClass string `json:",omitempty"` Bucket string AccessKey string `json:",omitempty"` SecretKey string `json:",omitempty"` SessionToken string `json:",omitempty"` BlockSize int Compression string `json:",omitempty"` Shards int `json:",omitempty"` HashPrefix bool `json:",omitempty"` Capacity uint64 `json:",omitempty"` Inodes uint64 `json:",omitempty"` UploadLimit int64 `json:",omitempty"` // Mbps DownloadLimit int64 `json:",omitempty"` // Mbps ... } 2 将 volume 挂载(mount)到机器接下来我们找一台机器,把这个 volume 挂载上去,这样就能在这个 volume 里面读写文件了。
2.1 JuiceFS client 挂载日志 $ juicefs mount --verbose --backup-meta 0 tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev /tmp/foo-dev <INFO>: Meta address: tikv://192.168.1.1:2379,192.168.1.2:2379,192.168.1.3:2379/foo-dev [interface.go:406] <DEBUG>: Creating oss storage at endpoint http://<url> [object_storage.go:154] <INFO>: Data use oss://xx/foo-dev/ [mount.go:497] <INFO>: Disk cache (/var/jfsCache/ec843b85/): capacity (10240 MB), free ratio (10%), max pending pages (15) [disk_cache.go:94] <DEBUG>: Scan /var/jfsCache/ec843b85/raw to find cached blocks [disk_cache.go:487] <DEBUG>: Scan /var/jfsCache/ec843b85/rawstaging to find staging blocks [disk_cache.go:530] <DEBUG>: Found 8 cached blocks (32814 bytes) in /var/jfsCache/ec843b85/ with 269.265µs [disk_cache.go:515] <INFO>: Create session 4 OK with version: 1.2.0 [base.go:279] <INFO>: Prometheus metrics listening on 127.0.0.1:34849 [mount.go:165] <INFO>: Mounting volume foo-dev at /tmp/foo-dev ... [mount_unix.go:203] <INFO>: OK, foo-dev is ready at /tmp/foo-dev [mount_unix.go:46]可以看到成功挂载到了本机路径 /tmp/foo-dev/。
2.2 查看挂载信息 $ mount | grep juicefs JuiceFS:foo-dev on /tmp/foo-dev type fuse.juicefs (rw,relatime,user_id=0,group_id=0,default_permissions,allow_other) $ cd /tmp/foo-dev $ ls # 空目录 2.3 查看 JuiceFS 隐藏(系统)文件新建的 volume 里面其实有几个隐藏文件:
$ cd /tmp/foo-dev $ ll -r-------- 1 root root .accesslog -r-------- 1 root root .config -r--r--r-- 1 root root .stats dr-xr-xr-x 2 root root .trash/ 2.3.1 .accesslog可以通过 cat 这个文件看到一些 JuiceFS client 底层的操作日志,我们一会会用到。
2.3.2 .config包括 Format 在内的一些 volume 配置信息:
$ cat .config { "Meta": { "Strict": true, "Retries": 10, "CaseInsensi": false, "ReadOnly": false, "NoBGJob": false, "OpenCache": 0, "Heartbeat": 12000000000, "MountPoint": "/tmp/foo-dev", "Subdir": "", "CleanObjFileLever": 1 }, "Format": { "Name": "foo-dev", "UUID": "ec843b85", "Storage": "oss", "Bucket": "http://<url>", "UploadLimit": 0, "DownloadLimit": 0, ... }, "Chunk": { "CacheDir": "/var/jfsCache/ec843b85", "CacheMode": 384, "CacheSize": 10240, "FreeSpace": 0.1, "AutoCreate": true, "Compress": "none", "MaxUpload": 20, "MaxDeletes": 2, "MaxRetries": 10, "UploadLimit": 0, "DownloadLimit": 0, "Writeback": false, "UploadDelay": 0, "HashPrefix": false, "BlockSize": 4194304, "GetTimeout": 60000000000, "PutTimeout": 60000000000, "CacheFullBlock": true, "BufferSize": 314572800, "Readahead": 0, "Prefetch": 1, "UseMountUploadLimitConf": false, "UseMountDownloadLimitConf": false }, "Version": "1.2.0", "AttrTimeout": 1000000000, "DirEntryTimeout": 1000000000, "EntryTimeout": 1000000000, "BackupMeta": 0, "HideInternal": false } 2.3.3 .statscat 能输出一些 prometheus metrics:
$ cat .stats ... juicefs_uptime 374.021754516 juicefs_used_buffer_size_bytes 0 juicefs_used_inodes 7 juicefs_used_space 28672用 prometheus 采集器把这个数据收上去,就能在 grafana 上展示 volume 的各种内部状态。
2.3.4 .trash类似于 Windows 的垃圾箱。如果启用了,删掉的文件会在里面保存一段时间再真正从对象存储删掉。
3 创建、更新、删除文件接下来做一些文件操作,看看 TiKV 中对应元数据的变化。
3.1 创建文件 3.1.1 创建文件 $ cd /tmp/foo-dev $ echo test3 > file3.txt 3.1.2 JuiceFS .accesslog $ cat .accesslog [uid:0,gid:0,pid:169604] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585251,1725585251,4096]) <0.001561> [uid:0,gid:0,pid:169604] lookup (1,file3.txt): no such file or directory <0.000989> [uid:0,gid:0,pid:169604] create (1,file3.txt,-rw-r-----:0100640): OK (103,[-rw-r-----:0100640,1,0,0,1725585318,1725585318,1725585318,0]) [fh:27] <0.003850> [uid:0,gid:0,pid:169604] flush (103,27): OK <0.000005> [uid:0,gid:0,pid:169604] write (103,6,0,27): OK <0.000048> [uid:0,gid:0,pid:169604] flush (103,27): OK <0.026205> [uid:0,gid:0,pid:0 ] release (103): OK <0.000006> [uid:0,gid:0,pid:169749] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585318,1725585318,4096]) <0.000995> [uid:0,gid:0,pid:169750] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585318,1725585318,4096]) <0.001219> 3.1.3 TiKV 元数据 $ ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' --limit 100 ... key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile3.\377txt\000\000\000\000\000\372 ...可以看到 meta 中多了几条元数据,依稀可以分辨出对应的就是我们创建的文件,
- 这个 key 经过了 juicefs 和 tikv 两次编码,
- 简单来说,它是 volume + 0xFD(8 进制的 \375)+ 文件名 + tikv 编码,最终得到的就是上面看到的这个 key。
对应的 value 一般长这样:
$ ./tikv-ctl.sh mvcc -k 'zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile3.\377txt\000\000\000\000\000\372' --show-cf default,lock,write key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfile3.\377txt\000\000\000\000\000\372 write cf value: start_ts: 452330816414416901 commit_ts: 452330816414416903 short_value: 010000000000000002先粗略感受一下,后面再具体介绍 key/value 的编解码规则。
3.2 删除文件操作 3.2.1 删除文件 rm file4.txt 3.2.2 JuiceFS .accesslog $ cat .accesslog [uid:0,gid:0,pid:169604] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585532,1725585532,4096]) <0.001294> [uid:0,gid:0,pid:169902] lookup (1,file4.txt): OK (104,[-rw-r-----:0100640,1,0,0,1725585532,1725585532,1725585532,6]) <0.001631> [uid:0,gid:0,pid:169902] unlink (1,file4.txt): OK <0.004206> [uid:0,gid:0,pid:169904] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585623,1725585623,4096]) <0.000718> [uid:0,gid:0,pid:169905] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585623,1725585623,4096]) <0.000843> 3.2.3 TiKV 元数据对应的元数据就从 TiKV 删掉了。
3.3 更新(追加)文件 3.3.1 更新文件 $ echo test3 >> file3.txt 3.3.2 JuiceFS .accesslog $ cat .accesslog [uid:0,gid:0,pid:169604] getattr (1): OK (1,[drwxrwxrwx:0040777,3,0,0,1725503250,1725585623,1725585623,4096]) <0.001767> [uid:0,gid:0,pid:169604] lookup (1,file3.txt): OK (103,[-rw-r-----:0100640,1,0,0,1725585318,1725585318,1725585318,6]) <0.001893> [uid:0,gid:0,pid:169604] open (103): OK [fh:51] <0.000884> [uid:0,gid:0,pid:169604] flush (103,51): OK <0.000011> [uid:0,gid:0,pid:169604] write (103,6,6,51): OK <0.000068> [uid:0,gid:0,pid:169604] flush (103,51): OK <0.036778> [uid:0,gid:0,pid:0 ] release (103): OK <0.000024> 3.3.3 TiKV 元数据- 如果追加的内容不多,TiKV 中还是那条元数据,但 value 会被更新;
- 如果追加的内容太多(例如几百兆),文件就会被切分,这时候元数据就会有多条了。
上一节简单看了下创建、更新、删除 volume 中的文件,TiKV 中对应的元数据都有什么变化。 我们有意跳过了 key/value 是如何编码的,这一节就来看看这块的内容。
4.1 JuiceFS key 编码规则 4.1.1 每个 key 的公共前缀:<vol_name> + 0xFDTiKV 客户端初始化:每个 key 的 base 部分:<vol_name> + 0xFD
// pkg/meta/tkv_tikv.go func init() { Register("tikv", newKVMeta) drivers["tikv"] = newTikvClient } func newTikvClient(addr string) (tkvClient, error) { client := txnkv.NewClient(strings.Split(tUrl.Host, ",")) prefix := strings.TrimLeft(tUrl.Path, "/") return withPrefix(&tikvClient{client.KVStore, interval}, append([]byte(prefix), 0xFD)), nil } 4.1.2 每个 key 后面的部分根据对应的是文件、目录、文件属性、系统元数据等等,会有不同的编码规则:
// pkg/meta/tkv.go /** Ino iiiiiiii Length llllllll Indx nnnn name ... sliceId cccccccc session ssssssss aclId aaaa All keys: setting format C... counter AiiiiiiiiI inode attribute AiiiiiiiiD... dentry AiiiiiiiiPiiiiiiii parents // for hard links AiiiiiiiiCnnnn file chunks AiiiiiiiiS symlink target AiiiiiiiiX... extented attribute Diiiiiiiillllllll delete inodes Fiiiiiiii Flocks Piiiiiiii POSIX locks Kccccccccnnnn slice refs Lttttttttcccccccc delayed slices SEssssssss session expire time SHssssssss session heartbeat // for legacy client SIssssssss session info SSssssssssiiiiiiii sustained inode Uiiiiiiii data length, space and inodes usage in directory Niiiiiiii detached inde QDiiiiiiii directory quota Raaaa POSIX acl */具体可以再看看这个文件中的代码。
4.1.3 最终格式:字节序列 // pkg/meta/tkv.go func (m *kvMeta) fmtKey(args ...interface{}) []byte { b := utils.NewBuffer(uint32(m.keyLen(args...))) for _, a := range args { switch a := a.(type) { case byte: b.Put8(a) case uint32: b.Put32(a) case uint64: b.Put64(a) case Ino: m.encodeInode(a, b.Get(8)) case string: b.Put([]byte(a)) default: panic(fmt.Sprintf("invalid type %T, value %v", a, a)) } } return b.Bytes() } 4.2 TiKV 对 JuiceFS key 的进一步编码JuiceFS client 按照以上规则拼好一个 key 之后,接下来 TiKV 会再进行一次编码:
-
加一些 TiKV 的前缀,例如给文件 key 加个 z 前缀;
- TiKV 代码 components/keys/src/lib.rs
-
转义,例如 8 个字节插入一个 \377(对应 0xFF),不够 8 字节的补全等等;
- tikv rust encode 代码
- 借鉴的是 golang protobuf 的代码
最终得到的就是我们用 tikv-ctl scan 看到的那些 key。
4.3 例子:查看特殊元数据:volume 的 setting/format 信息JuiceFS 的 Format 配置保存在 tikv 中,原始 key 是 setting,经过以上两层编码就变成了下面的样子:
$ ./tikv-ctl.sh scan --from 'zfoo' --to 'zfop' --limit 100 key: zfoo-dev\375\377setting\000\376 default cf value: start_ts: 452330324173520898 value: 7B0A22...其中的 value 是可以解码出来的,
# hex -> escaped string $ ./tikv-ctl.sh --to-escaped '7B0A22...' {\n\"Name\": \"foo-dev\",\n\"UUID\": \"8cd1ac73\",\n\"Storage\": \"S3\",\n\"Bucket\": \"http://xxx\",\n\"AccessKey\": \"...\",\n\"BlockSize\": 4096,\n\"Compression\": \"none\",\n\"KeyEncrypted\": true,\n\"MetaVersion\": 1,\n\"UploadLimit\": 0,\n\"DownloadLimit\": 0,\n\"\": \"\"\n}对应的就是 pkg/meta/config.go 中的 Format 结构体。
5 总结本文结合一些具体 JuiceFS 操作,分析了 TiKV 内的元数据格式与内容。
参考资料