Published at 2024-09-12 | Last Update 2024-09-12
Fig. JuiceFS upload/download data bandwidth control.
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
有了第一篇的铺垫,本文直接进入正题。
创建一个名为 foo-dev
的 JuiceFS volume。
用 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,
...
}
2379
是 PD 接收客户端请求的端口;TiKV/PD client
初始化/调用栈下面我们进入 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()
|-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 transaction client
,pd client
连接到 PD 集群,所以,架构上看 juicefs 是直连 PD,但实现上并没有直接创建 pd client, 也没有直接使用 pd 的库。
Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.
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-ctl mvcc
解码 volume setting 元数据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。
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
...
}
接下来我们找一台机器,把这个 volume 挂载上去,这样就能在这个 volume 里面读写文件了。
$ 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/
。
$ 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 # 空目录
新建的 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/
.accesslog
可以通过 cat
这个文件看到一些 JuiceFS client 底层的操作日志,我们一会会用到。
.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
}
.stats
cat
能输出一些 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 的各种内部状态。
.trash
类似于 Windows 的垃圾箱。如果启用了,删掉的文件会在里面保存一段时间再真正从对象存储删掉。
接下来做一些文件操作,看看 TiKV 中对应元数据的变化。
$ cd /tmp/foo-dev
$ echo test3 > file3.txt
.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>
$ ./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 中多了几条元数据,依稀可以分辨出对应的就是我们创建的文件,
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 的编解码规则。
.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>
对应的元数据就从 TiKV 删掉了。
$ echo test3 >> file3.txt
.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>
上一节简单看了下创建、更新、删除 volume 中的文件,TiKV 中对应的元数据都有什么变化。 我们有意跳过了 key/value 是如何编码的,这一节就来看看这块的内容。
<vol_name> + 0xFD
TiKV 客户端初始化:每个 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
}
根据对应的是文件、目录、文件属性、系统元数据等等,会有不同的编码规则:
// 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
*/
具体可以再看看这个文件中的代码。
// 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()
}
JuiceFS client 按照以上规则拼好一个 key 之后,接下来 TiKV 会再进行一次编码:
加一些 TiKV 的前缀,例如给文件 key 加个 z
前缀;
转义,例如 8 个字节插入一个 \377
(对应 0xFF
),不够 8 字节的补全等等;
protobuf
的代码最终得到的就是我们用 tikv-ctl scan
看到的那些 key。
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: 7B0A224E616D65...
其中的 value 是可以解码出来的,
# hex -> escaped string
$ ./tikv-ctl.sh --to-escaped '7B0A224...'
{\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
结构体。
还有几点先简单列一下,待有时间三探再详细讨论。
pd-ctl region
列出所有 region 信息$ pd-ctl.sh region | jq .
{
"regions": [
{
"id": 11501,
"start_key": "6161616161616161FF2D61692D6661742DFF6261636B7570FD41FFCF68030000000000FF4900000000000000F8",
"end_key": "...",
"epoch": {
"conf_ver": 23,
"version": 300
},
"peers": [
{
"id": 19038,
"store_id": 19001,
"role_name": "Voter"
},
...
],
"leader": {
"id": 20070,
"store_id": 20001,
"role_name": "Voter"
},
"written_bytes": 0,
"read_bytes": 0,
"written_keys": 0,
"read_keys": 0,
"approximate_size": 104,
"approximate_keys": 994812
},
]
}
tikv-ctl.sh region-properties
查看 region 属性详情$ ./tikv-ctl.sh region-properties -r 23293
mvcc.min_ts: 438155461254971396
mvcc.max_ts: 452403302095650819
mvcc.num_rows: 1972540
mvcc.num_puts: 3697509
mvcc.num_deletes: 834889
mvcc.num_versions: 4532503
mvcc.max_row_versions: 54738
num_entries: 4549844
num_deletes: 17341
num_files: 6
sst_files: 001857.sst, 001856.sst, 002222.sst, 002201.sst, 002238.sst, 002233.sst
region.start_key: 6e6772...
region.end_key: 6e6772...
region.middle_key_by_approximate_size: 6e6772...
tikv-ctl --to-escaped
:从 region 的 start/end key 解码文件名范围如上,每个 region 都会有 start_key/end_key
两个属性,
这里面编码的就是这个 region 内存放是元数据的 key 范围。我们挑一个来解码看看:
$ tikv-ctl.sh --to-escaped '6161616161616161FF2D61692D6661742DFF6261636B7570FD41FFCF68030000000000FF4900000000000000F8'
aaaaaaaa\377-ai-fat-\377backup\375A\377\317h\003\000\000\000\000\000\377I\000\000\000\000\000\000\000\370
再 decode 一把会更清楚:
$ tikv-ctl.sh --decode 'aaaaaaaa\377-ai-fat-\377backup\375A\377\317h\003\000\000\000\000\000\377I\000\000\000\000\000\000\000\370'
aaaaaaaa-ai-fat-backup\375A\317h\003\000\000\000\000\000I
对应的是一个名为 aaaaaaa-ai-fat-backup
的 volume 内的一部分元数据。
PD 客户端代码,
// GetRegion gets a region and its leader Peer from PD by key.
// The region may expire after split. Caller is responsible for caching and
// taking care of region change.
// Also, it may return nil if PD finds no Region for the key temporarily,
// client should retry later.
GetRegion(ctx , key []byte, opts ...GetRegionOption) (*Region, error)
// GetRegion implements the RPCClient interface.
func (c *client) GetRegion(ctx , key []byte, opts ...GetRegionOption) (*Region, error) {
options := &GetRegionOp{}
for _, opt := range opts {
opt(options)
}
req := &pdpb.GetRegionRequest{
Header: c.requestHeader(),
RegionKey: key,
NeedBuckets: options.needBuckets,
}
serviceClient, cctx := c.getRegionAPIClientAndContext(ctx, options.allowFollowerHandle && c.option.getEnableFollowerHandle())
resp := pdpb.NewPDClient(serviceClient.GetClientConn()).GetRegion(cctx, req)
return handleRegionResponse(resp), nil
}
PD 服务端代码,
func (h *regionHandler) GetRegion(w http.ResponseWriter, r *http.Request) {
rc := getCluster(r)
vars := mux.Vars(r)
key := url.QueryUnescape(vars["key"])
// decode hex if query has params with hex format
paramsByte := [][]byte{[]byte(key)}
paramsByte = apiutil.ParseHexKeys(r.URL.Query().Get("format"), paramsByte)
regionInfo := rc.GetRegionByKey(paramsByte[0])
b := response.MarshalRegionInfoJSON(r.Context(), regionInfo)
h.rd.Data(w, http.StatusOK, b)
}
// GetRegionByKey searches RegionInfo from regionTree
func (r *RegionsInfo) GetRegionByKey(regionKey []byte) *RegionInfo {
region := r.tree.search(regionKey)
if region == nil {
return nil
}
return r.getRegionLocked(region.GetID())
}
返回的是 region info,
// RegionInfo records detail region info for api usage.
// NOTE: This type is exported by HTTP API. Please pay more attention when modifying it.
// easyjson:json
type RegionInfo struct {
ID uint64 `json:"id"`
StartKey string `json:"start_key"`
EndKey string `json:"end_key"`
RegionEpoch *metapb.RegionEpoch `json:"epoch,omitempty"`
Peers []MetaPeer `json:"peers,omitempty"` // https://github.com/pingcap/kvproto/blob/master/pkg/metapb/metapb.pb.go#L734
Leader MetaPeer `json:"leader,omitempty"`
DownPeers []PDPeerStats `json:"down_peers,omitempty"`
PendingPeers []MetaPeer `json:"pending_peers,omitempty"`
CPUUsage uint64 `json:"cpu_usage"`
WrittenBytes uint64 `json:"written_bytes"`
ReadBytes uint64 `json:"read_bytes"`
WrittenKeys uint64 `json:"written_keys"`
ReadKeys uint64 `json:"read_keys"`
ApproximateSize int64 `json:"approximate_size"`
ApproximateKeys int64 `json:"approximate_keys"`
ApproximateKvSize int64 `json:"approximate_kv_size"`
Buckets []string `json:"buckets,omitempty"`
ReplicationStatus *ReplicationStatus `json:"replication_status,omitempty"`
}
// GetRegionFromMember implements the RPCClient interface.
func (c *client) GetRegionFromMember(ctx , key []byte, memberURLs []string, _ ...GetRegionOption) (*Region, error) {
for _, url := range memberURLs {
conn := c.pdSvcDiscovery.GetOrCreateGRPCConn(url)
cc := pdpb.NewPDClient(conn)
resp = cc.GetRegion(ctx, &pdpb.GetRegionRequest{
Header: c.requestHeader(),
RegionKey: key,
})
if resp != nil {
break
}
}
return handleRegionResponse(resp), nil
}
一句话总结:并没有一个线性的关系。
TiKV engine size 的大小,和集群的文件数量和每个文件的大小都有关系。 例如,同样是一个文件,
Section 1.4 和 4.3 等小节中都看到了,TiKV 中保存了 volume 的 setting 信息, 其中就包括两个网络带宽限速的配置。
--upload-limit/--download-limit
--upload-limit
,单位 Mbps
--download-limit
,单位 Mbps
juicefs mount
挂载时指定了这两个参数,就会以指定的参数为准;如果 juicefs mount
挂载时没指定,就会以 TiKV 里面的配置为准,
refresh()
方法一直在监听 TiKV 里面的 Format 配置变化,juicefs config
来修改 TiKV 中的配置信息),client 就会把最新配置 reload 到本地(本进程),Fig. JuiceFS upload/download data bandwidth control.
juicefs mount
时注册一个 reload 方法,
mount
|-metaCli.OnReload
|-m.reloadCb = append(m.reloadCb, func() {
updateFormat(c)(fmt) // fmt 是从 TiKV 里面拉下来的最新配置
store.UpdateLimit(fmt.UploadLimit, fmt.DownloadLimit)
})
然后有个后台任务一直在监听 TiKV 里面的配置,一旦发现配置变了就会执行到上面注册的回调方法,
refresh()
for {
old := m.getFormat()
format := m.Load(false) // load from tikv
if !reflect.DeepEqual(format, old) {
cbs := m.reloadCb
for _, cb := range cbs {
cb(format)
}
}
本文结合一些具体 JuiceFS 操作,分析了 TiKV 内的元数据格式与内容。