Aggregator
CVE-2007-3312 | Efstratios Geroulis Jasmine CMS 1.0 admin/plugin_manager.php u path traversal (EDB-4081 / BID-24546)
coraza: OWASP Coraza Web Application Firewall
OWASP Coraza Web Application Firewall Welcome to OWASP Coraza WAF, Coraza is a golang enterprise-grade Web Application Firewall framework that supports Modsecurity’s seclang language and is 100% compatible with OWASP Core Ruleset. Coraza...
The post coraza: OWASP Coraza Web Application Firewall appeared first on Penetration Testing Tools.
SecretScanner: Find secrets and passwords in container images and file systems
SecretScanner Deepfence SecretScanner can find unprotected secrets in container images or file systems. SecretScanner is a standalone tool that retrieves and searches container and host filesystems, matching the contents against a database of approximately...
The post SecretScanner: Find secrets and passwords in container images and file systems appeared first on Penetration Testing Tools.
kdigger: context discovery tool for Kubernetes penetration testing
kdigger kdigger, short for “Kubernetes digger”, is a context discovery tool for Kubernetes penetration testing. This tool is a compilation of various plugins called buckets to facilitate pentesting Kubernetes from inside a pod. Please...
The post kdigger: context discovery tool for Kubernetes penetration testing appeared first on Penetration Testing Tools.
CVE-2007-3294 | PHP 5.2.3 Tidy Extension tidy_parse_string memory corruption (EDB-4080 / Nessus ID 25971)
Google 呼吁停止将 WHOIS 用于 TLS 域名验证
CVE-2016-7621 | Apple macOS up to 10.12.1 Kernel use after free (HT207423 / EDB-40956)
.NET 一款通过管道模拟传递哈希的工具
.NET 安全攻防知识交流社区
.NET内网实战:白名单文件反序列化执行命令
CVE-2007-3306 | Ultrize MiniBill 1.2.5 crontab crontab/run_billing.php config[include_dir] file inclusion (EDB-4079 / XFDB-34919)
The Noonification: How to Excel in Your Career: 5 Important Skills to Have (9/21/2024)
CVE-2016-7621 | Apple tvOS up to 10.0 Kernel use after free (HT207425 / EDB-40956)
JuiceFS 元数据引擎三探:从实践中学习 TiKV 的 MVCC 和 GC(2024)
Fig. TiKV MVCC GC mechanisms.
- JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)
- JuiceFS 元数据引擎再探:开箱解读 TiKV 中的 JuiceFS 元数据(2024)
- JuiceFS 元数据引擎三探:从实践中学习 TiKV 的 MVCC 和 GC(2024)
- JuiceFS 元数据引擎四探:元数据大小评估、限流与限速的设计思考(2024)
- JuiceFS 元数据引擎五探:元数据备份与恢复(2024)
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
- 1 概念与实测
- 2 TiKV MVCC GC
- 3 GC 不及时导致的问题一例
- 4 问题讨论
- 参考资料
来自 wikipedia 的定义,
Multiversion concurrency control (MCC or MVCC), is a non-locking concurrency control method commonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory.
TiKV 支持 MVCC,当更新数据时,旧的数据不会被立即删掉,而是新老同时保留,以时间戳来区分版本。 官方有几篇很不错的博客 [1,3]。
下面进行一个简单测试来对 MVCC 有一个初步的直观认识。
1.1.2 TiKV MVCC 测试参考上一篇,新创建一个新 volume,里面什么文件都没有,有 8 条记录,
$ tikv-ctl.sh scan --from 'zfoo' --to 'zfop' | grep "key:" | wc -l 8然后进入这个 volume 的挂载目录,在里面创建一个文件,
$ cd <mount dir> $ echo 1 > foo.txt再次扫描这个 volume 对应的所有 keys,
$ tikv-ctl.sh scan --from 'zfoo' --to 'zfop' | grep "key:" | wc -l 16可以看到变成 16 条记录,比之前多了 8 条。内容如下,依稀能看出大部分条目的用途 (行末的注释是本文加的),
key: zfoo-dev\375\377A\001\000\000\000\000\000\000\377\000Dfoo.tx\377t\000\000\000\000\000\000\000\370 # foo.txt key: zfoo-dev\375\377A\002\000\000\000\000\000\000\377\000C\000\000\000\000\000\000\375 key: zfoo-dev\375\377A\002\000\000\000\000\000\000\377\000I\000\000\000\000\000\000\371 key: zfoo-dev\375\377ClastCle\377anupFile\377s\000\000\000\000\000\000\000\370 # lastCleanupFile key: zfoo-dev\375\377ClastCle\377anupSess\377ions\000\000\000\000\373 # lastCleanupSessions key: zfoo-dev\375\377CtotalIn\377odes\000\000\000\000\373 # totalInodes key: zfoo-dev\375\377CusedSpa\377ce\000\000\000\000\000\000\371 # UsedSpace key: zfoo-dev\375\377U\001\000\000\000\000\000\000\377\000\000\000\000\000\000\000\000\370接下来继续更新这个文件 1000 次(每次都是一个整数,由于文件内容极小,不会导致 TiKV 的 region split 等行为),
$ for n in {1..1000}; do echo $n > bar.txt; done再次查看元数据条目数量:
$ tikv-ctl.sh scan --from 'zfoo' --to 'zfop' | grep key | wc -l 59又多了 43 条。多的条目大致长这样:
key: zfoo-dev\375\377L\000\000\000\000f\356\221\377\231\000\000\000\000\000\000\000\3777\000\000\000\000\000\000\000\370 key: zfoo-dev\375\377L\000\000\000\000f\356\221\377\233\000\000\000\000\000\000\000\377j\000\000\000\000\000\000\000\370 key: zfoo-dev\375\377L\000\000\000\000f\356\221\377\234\000\000\000\000\000\000\000\377\235\000\000\000\000\000\000\000\370 ... key: zfoo-dev\375\377L\000\000\000\000f\356\221\377\271\000\000\000\000\000\000\003\377\362\000\000\000\000\000\000\000\370TiKV supports MVCC, which means that there can be multiple versions for the same row stored in RocksDB. All versions of the same row share the same prefix (the row key) but have different timestamps as a suffix.
https://tikv.org/deep-dive/key-value-engine/rocksdb/
下面我们再看看执行以上文件更新操作期间,juicefs 客户端的日志。
1.1.2 JuiceFS client 日志在执行以上 for 循环期间,JuiceFS client 的日志,
$ juicefs mount ... ... <DEBUG>: PUT chunks/0/0/170_0_4 (req_id: "xx", err: <nil>, cost: 32.002516ms) [cached_store.go:669] <DEBUG>: PUT chunks/0/0/171_0_4 (req_id: "xx", err: <nil>, cost: 32.002516ms) [cached_store.go:669] <DEBUG>: PUT chunks/0/0/172_0_4 (req_id: "xx", err: <nil>, cost: 32.002516ms) [cached_store.go:669] ...这个似乎对应的就是以上多出来的条目。
1.1.3 小结本节的例子让我们看到,虽然 volume 里面从头到尾只有一个文件, 但随着我们不断覆盖这个文件内的值,元数据引擎 TiKV 内的条目数量就会持续增加。 多出来的这些东西,对应的就是这份数据的多个版本,也就是 MVCC 里面 multi-version 的表现。
显然,没有冲突的话,只保留最后一个版本就行了,其他版本都可以删掉 —— 这就是垃圾回收(GC)的作用。
1.2 GC(垃圾回收)垃圾回收 (GC) 的功能是清理 MVCC 留下的旧版本。比如同一份数据保存了 1000 个版本,那原则上前面大部分版本都可以清掉了,只保留最新的一个或几个。
那如何判断哪些版本可以安全地清掉呢?TiKV 引入了一个时间戳概念: safepoint。
GC is a process to clean up garbage versions (versions older than the configured lifetime) of each row.
https://tikv.org/deep-dive/key-value-engine/rocksdb/
1.3 Safepoint(可安全删除这个时间戳之前的版本)In order to ensure the correctness of all read and write transactions, and make sure the GC mechanism works, TiKV/TiDB introduced the concept of safe-point. There is a guarantee that all active transactions and future transactions’ timestamp is greater than or equal to the safe-point. It means old versions whose commit-ts is less than the safe-point can be safely deleted by GC. [3]
2 TiKV MVCC GC以上看到,TiKV 有 GC 功能,但由于其“历史出身”,也存在一些限制。
2.1 历史:从 TiDB 里面拆分出来,功能不完整TiKV 是从 TiDB 里面拆出来的一个产品,并不是从一开始就作为独立产品设计和开发的。 这导致的一个问题是:MVCC GC 功能在使用上有点蹩脚:
- 默认情况下,靠底层 RocksDB 的 compaction 触发 GC,这周触发周期不确定且一般比较长;
- TiKV+PD 也内置了另一种 GC 方式,但并不会自己主动去做,而是将 GC 接口暴露出来,靠 TiDB 等在使用 TiKV 的更上层组件来触发(见下节的图);
- tikv-ctl/pd-ctl 等等命令行工具也都没有提供 GC 功能,这导致 TiKV 的运维很不方便,比如有问题想快速手动触发时用不了。
下面具体看看 TiKV 中的 GC 设计。
2.2 TiKV GC 设计和配置项Fig. TiKV MVCC GC mechanisms.
2.2.1 设计:两种 GC 触发方式- 被动 GC:TiKV 底层的 RocksDB compact 时进行垃圾回收。
- 通过 tikv-server 的 enable-compaction-filter 配置项控制;
- 默认启用;
- 触发 RocksDB compaction 时才能进行 GC。
- tikv-ctl compact/compact-cluster 可以手动触发这种 compact,进而 GC。
- 半主动 GC:内置了 GC worker,
- 定期获取 PD 里面的 gc safepoint,然后进行 GC;会占用一些 CPU/IO 资源;
- PD 不会主动更新这个 gc safepoint,一般是由在使用 TiKV 的更外围组件来更新的,例如 TiDB、JuiceFS 等等;
- 所以本文把这种方式称为“半主动”。
tikv-server.log,
[INFO] [server.rs:274] ["using config"] [config="{..., "enable-compaction-filter":true, ...}"] [INFO] [compaction_filter.rs:138] ["initialize GC context for compaction filter"] [INFO] [gc_worker.rs:786] ["initialize compaction filter to perform GC when necessary"] 2.2.3 tikv-ctl compact/compact-cluster 触发被动 GC 例子 # compact-cluster 必须要指定 --pd 参数,因为针对是整个集群。指定 --host 会失败,但没有提示错在哪,TiKV 的命令行工具经常这样 $ tikv-ctl.sh compact-cluster --from 'zfoo' --to 'zfop' $ tikv-ctl.sh compact --from 'zfoo' --to 'zfop' store:"192.168.1.1:20160" compact db:Kv cf:default range:[[122, 122, 121, 110], [122, 122, 121, 111]) success! $ tikv-ctl.sh compact --from 'zfoo' --to 'zfop' -c default # 很快 $ tikv-ctl.sh compact --from 'zfoo' --to 'zfop' -c lock # 很快 store:"192.168.1.1:20160" compact db:Kv cf:lock range:[[122, 122, 121, 110], [122, 122, 121, 111]) success! $ tikv-ctl.sh compact --from 'zfoo' --to 'zfop' -c write # 非常慢 store:"192.168.1.1:20160" compact db:Kv cf:write range:[[122, 122, 121, 110], [122, 122, 121, 111]) success! # 还可以指定本地 TiKV 数据路径直接 compact # -d: specify the RocksDB that performs compaction. default: kv. Valid values: {kv, raft} $ tikv-ctl --data-dir /path/to/tikv compact -d kv 2.2.4 小结“半主动方式”需要外围组件去更新 PD 中的 gc safepoint 信息,这样下面的 TiKV 才会去执行 GC 操作。作为两个具体例子,我们接下来看看 TiDB 和 JuiceFS 在使用 TiKV 时,分别是怎么去更新这个信息的。
2.3 TiDB 中触发 TiKV GC 的方式TiDB 有 GC 相关的配置和 worker,会按照配置去触发底层的 TiKV GC,
Fig. TiDB SQL layer overview. GC worker is outside of TiKV. Image Source: pingcap.com
更多信息可以参考 [3,4]。
2.4 JuiceFS 触发 TiKV GC 的方式TiKV 作为元数据引擎时,JuiceFS 并没有使用 TiDB,而是直接使用的 TiKV(和 PD), 所以就需要 JuiceFS client 来触发这个 GC (因为不考虑 CSI 部署方式的话,JuiceFS 就一个客户端组件,也没有其他 long running 服务来做这个事情了)。
Fig. Typical JuiceFS cluster.
2.4.1 定期更新 gc safepoint 的代码JuiceFS v1.0.4+ 客户端会周期性地设置 PD 中的 gc safepoint,默认是 now-3h,也就是可以删除 3 小时之前的旧版本数据,
// pkg/meta/tkv_tikv.go func (c *tikvClient) gc() { if c.gcInterval == 0 { return } safePoint := c.client.GC(context.Background(), oracle.GoTimeToTS(time.Now().Add(-c.gcInterval))) }接下来的调用栈:
gc // github.com/juicedata/juicefs pkg/meta/tkv_tikv.go |-c.client.GC // github.com/tikv/client-go tikv/gc.go |-s.pdClient.UpdateGCSafePoint // github.com/tikv/pd client/client.go |-ctx = grpcutil.BuildForwardContext(ctx, c.GetLeaderAddr()) |-c.getClient().UpdateGCSafePoint(ctx, req) / gRPC / /----<--<----/ / UpdateGCSafePoint // github.com/tikv/pd server/grpc_service.go |-rc := s.GetRaftCluster() |-oldSafePoint := s.storage.LoadGCSafePoint() |-s.storage.SaveGCSafePoint(newSafePoint) |-key := path.Join(gcPath, "safe_point") // gcPath = "gc" |-value := strconv.FormatUint(safePoint, 16) |-return s.Save(key, value) 2.4.2 配置:META URL \?gc-interval=1h这个 gc-interval 可在 juicefs 挂载卷时加到 TiKV URL 中,
- 默认值:3h
- 最小值:1h,设置的值小于这个值会打印一条 warning,然后强制设置为 1h。
juicefs client 挂载时显式设置 gc-interval,
$ juicefs mount tikv://localhost:2379\?gc-interval=1h ~/mnt/jfs <INFO>: Meta address: tikv://localhost:2379?gc-interval=1h [interface.go:491] <INFO>: TiKV gc interval is set to 1h0m0s [tkv_tikv.go:84] ... 2.4.3 juicefs gc 手动触发 TiKV GC还可以通过 juicefs gc 子命令来主动触发 TiKV GC。这个例子中设置的时间太短,可以看到被强制改成了允许的最小值 1h,
$ juicefs gc tikv://<ip>:2379/foo-dev\?gc-interval=1m --delete ... <WARNING>: TiKV gc-interval (1m0s) is too short, and is reset to 1h [tkv_tikv.go:133] <INFO>: TiKV gc interval is set to 1h0m0s [tkv_tikv.go:138] Cleaned pending slices: 0 0.0/s Pending deleted files: 0 0.0/s Pending deleted data: 0.0 b (0 Bytes) 0.0 b/s Cleaned pending files: 0 0.0/s Cleaned pending data: 0.0 b (0 Bytes) 0.0 b/s Cleaned trash: 0 0.0/s Cleaned detached nodes: 0 0.0/s Listed slices: 2047 4930.4/s Trash slices: 2026 55423.8/s Trash data: 7.7 KiB (7883 Bytes) 211.8 KiB/s Cleaned trash slices: 0 0.0/s Cleaned trash data: 0.0 b (0 Bytes) 0.0 b/s Scanned objects: 2047/2047 [===========================================] 18138.6/s used: 113.115519ms Valid objects: 21 187.2/s Valid data: 85.0 b (85 Bytes) 758.0 b/s Compacted objects: 2026 18064.2/s Compacted data: 7.7 KiB (7883 Bytes) 68.6 KiB/s Leaked objects: 0 0.0/s Leaked data: 0.0 b (0 Bytes) 0.0 b/s Skipped objects: 0 0.0/s Skipped data: 0.0 b (0 Bytes) 0.0 b/s <INFO>: scanned 2047 objects, 21 valid, 2026 compacted (7883 bytes), 0 leaked (0 bytes), 0 delslices (0 bytes), 0 delfiles (0 bytes), 0 skipped (0 bytes) [gc.go:379] 2.5 外挂组件 github.com/tikv/migration/gc-worker代码仓库,是个在 TiKV 之上的组件, 从 PD 获取 service safepoint 信息,然后计算 gc safepoint 并更新到 PD,从而触发 TiKV GC。
3 GC 不及时导致的问题一例这里挑一个典型的问题讨论下。
3.1 问题现象 3.1.1 监控:TiKV db size 暴增,磁盘空间不断减小如下面监控所示,
Fig. TiKV DB size soaring in a JuiceFS cluster, caused by TiKV GC lagging.
- TiKV DB size 暴增;
- TiKV region 分布出现显著变量,总数量也有一定程度上升;
- TiKV node 可用磁盘空间不断下降。
查看 tikv-server 日志,看到一直在刷下面这样的 warning/error:
[WARN] [split_observer.rs:73] ["invalid key, skip"] [err="\"key 6E677... should be in (6E677..., 6E677...)\""] [index=0] [region_id=39179938] [ERROR] [split_observer.rs:136] ["failed to handle split req"] [err="\"no valid key found for split.\""] [region_id=39179938] [WARN] [peer.rs:2971] ["skip proposal"] [error_code=KV:Raftstore:Coprocessor] [err="Coprocessor(Other(\"[components/raftstore/src/coprocessor/split_observer.rs:141]: no valid key found for split.\"))"] [peer_id=39179939] [region_id=39179938]也就是 region split 失败。
3.2 问题排查- 根据日志报错,网上搜到一些帖子,初步了解问题背景(JuiceFS/TiKV 新人,接触没多久);
-
对报错日志进行分析,发现:
- 报错集中在几十个 region(grep "failed to handle split req" tikv.log | awk '{print $NF}' | sort | uniq -c | sort -n -k1,1),相对总 region 数量很少;
- pd-ctl region-properties -r <region> 看,发现 start/end key 都来自同一个 volume(命令行操作见下一篇);
- 根据 volume 监控看,只有一个客户端 set 请求非常高,每秒 400 次请求,而这个 volume 只有几个 GB,可以说非常小;
- tikv-ctl mvcc -k <key> 查看有问题的 key,发现超时了,报错说文件(元数据)太大;
结合以上三点,判断是某个或少数几个文件的 MVCC 版本太多,导致 TiKV split region 失败,进而不断累积垃圾数据。
3.3 问题根因以上,猜测直接原因是这个用户 非正常使用 JuiceFS,疯狂更新文件,也就是我们 1.1 中例子的极端版。 这导致部分文件的历史版本极其多,TiKV 在 auto split region 时失败。网上也有一些类似的 case(大部分是 TiDB 用户)。
但本质上,还是因为 TiKV 的 GC 太滞后,
- 被动 GC(RocksDB compact 方式)的频率不可控,跟集群所有客户端的总 write/update/delete 行为有关;
-
JuiceFS 的主动 GC 频率太慢,跟不上某些文件的版本增长速度。
- JuiceFS 默认 now-3h,最小 now-1h,也就是至少会保留一个小时内的所有版本(实际上我们是有个外部服务在定期更新 PD 的 gc safepoint,但也是设置的 now-1h);
- 根据监控看,异常的 juicefs client 每秒有 400+ set 请求,一个小时就是 144w 次的更新(这些请求更新的文件很集中)。
- 写了个程序,允许以非常小的粒度去更新 PD 的 gc safepoint,例如 now-5m, 也就是最多保留最近 5 分钟内的版本,其他的都删掉;这一步下去就有效果了,先稳住了,DB 不再增长,开始缓慢下降;
- 通知用户去处理那个看起来异常的客户端(我们没权限登录用户的机器,客户端不可控,这是另一个问题了)。
1+2,DB 开始稳步下降,最终完全恢复正常。
3.5 问题小结对于 TiKV 这种 MVCC 的元数据引擎来说,JuiceFS 的一条元数据可能会保留多个版本,老版本什么时候删掉很大程度上依赖外部 GC 触发。 如果 GC 间隔太长 + 文件更新太频繁,单条元数据极端情况下就可以占几个 GB,这时候不仅 DB size 暴大,还会导致 TiKV split region 工作不正常。
4 问题讨论前面看到,JuiceFS 支持配置 TiKV 的 GC 间隔,但从管理和运维层面,这里面也有几个问题可以探讨。
4.1 允许的最小 GC 间隔太大目前最小是 now-1h,极端情况会导致第 3 节中的问题,TiKV DB size 暴增,集群被打爆。
4.2 GC 配置放在客户端,增加了用户的认知负担和学习成本-
用户必需感知 TiKV gc 这个东西,增加认知成本和使用负担;
用户只是用 JuiceFS volume 读写文件,原则上没有必要去知道 JuiceFS 集群用什么元数据引擎, 甚至还必现了解这种元数据引擎的 GC 知识,后者都是 JuiceFS 集群管理员需要关心和解决的;
-
用户如果没有配置,就只完全依赖 RocksDB compaction 来 GC,更容易触发版本太多导致的问题。
用户一旦没有显式配置 gc-interval(使用很大的默认值),TiKV 可能就被打爆, 这种情况下用户不知道,管理员知道但可能没短平快的解决办法(不一定有权限管理用户的机器)。
4.4 小结对集群管理员来说,更好的方式可能是,
- 有个(内部或外部)服务,可以按管理员的需求随时和/或定时去 GC;
- 用户侧完全不用感知这个事情;
- 有 Meta 操作的限流能力(可以隔离有问题的 volume 或 client),下一篇讨论。
- MVCC in TiKV, pingcap.com, 2016
- JuiceFS 元数据引擎最佳实践:TiKV, juicefs.com
- Deep Dive into Distributed Transactions in TiKV and TiDB, medium.com, 2024
- MVCC garbage collection, TiDB doc, 2024
JuiceFS 元数据引擎四探:元数据大小评估、限流与限速的设计思考(2024)
Fig. JuiceFS upload/download data bandwidth control.
- JuiceFS 元数据引擎初探:高层架构、引擎选型、读写工作流(2024)
- JuiceFS 元数据引擎再探:开箱解读 TiKV 中的 JuiceFS 元数据(2024)
- JuiceFS 元数据引擎三探:从实践中学习 TiKV 的 MVCC 和 GC(2024)
- JuiceFS 元数据引擎四探:元数据大小评估、限流与限速的设计思考(2024)
- JuiceFS 元数据引擎五探:元数据备份与恢复(2024)
水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处。
- 1 元数据存储在哪儿?文件名到 TiKV regions 的映射
- 2 JuiceFS 集群规模与元数据大小(engine size)
- 3 限速(上传/下载数据带宽)设计
- 4 限流(metadata 请求)设计
- 参考资料
如上,每个 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 内的一部分元数据。
1.4 filename -> region:相关代码这里看一下从文件名映射到 TiKV region 的代码。
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 } 2 JuiceFS 集群规模与元数据大小(engine size) 2.1 二者的关系一句话总结:并没有一个线性的关系。
2.1.1 文件数量 & 平均文件大小TiKV engine size 的大小,和集群的文件数量和每个文件的大小都有关系。 例如,同样是一个文件,
- 小文件可能对应一条 TiKV 记录;
- 大文件会被拆分,对应多条 TiKV 记录。
GC 的勤快与否也会显著影响 DB size 的大小。第三篇中有过详细讨论和验证了,这里不再赘述,
Fig. TiKV DB size soaring in a JuiceFS cluster, caused by TiKV GC lagging.
2.2 两个集群对比- 集群 1:~1PB 数据,以小文件为主,~30K regions,~140GB TiKV engine size (3 replicas);
- 集群 2:~7PB 数据,以大文件为主,~800 regions,~3GB TiKV engine size (3 replicas);
如下面监控所示,虽然集群 2 的数据量是前者的 7 倍,但元数据只有前者的 1/47,
Fig. TiKV DB sizes and region counts of 2 JuiceFS clusters: cluster-1 with ~1PB data composed of mainly small files, cluster-2 with ~7PB data composed of mainly large files.
3 限速(上传/下载数据带宽)设计限速(upload/download bandwidth)本身是属于数据平面(data)的事情,也就是与 S3、Ceph、OSS 等等对象存储关系更密切。
但第二篇中已经看到,这个限速的配置信息是保存在元数据平面(metadata)TiKV 中 —— 具体来说就是 volume 的 setting 信息; 此外,后面讨论元数据请求限流(rate limiting)时还需要参考限速的设计。所以,这里我们稍微展开讲讲。
3.1 带宽限制:--upload-limit/--download-limit- --upload-limit,单位 Mbps
- --download-limit,单位 Mbps
- 如果 juicefs mount 挂载时指定了这两个参数,就会以指定的参数为准;
-
如果 juicefs mount 挂载时没指定,就会以 TiKV 里面的配置为准,
- juicefs client 里面有一个 refresh() 方法一直在监听 TiKV 里面的 Format 配置变化,
- 当这俩配置发生变化时(可以通过 juicefs config 来修改 TiKV 中的配置信息),client 就会把最新配置 reload 到本地(本进程),
- 这种情况下,可以看做是中心式配置的客户端限速,工作流如下图所示,
Fig. JuiceFS upload/download data bandwidth control.
3.3 JuiceFS client reload 配置的调用栈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) } } 4 限流(metadata 请求)设计 4.1 为什么需要限流?如下图所示,
Fig. JuiceFS cluster initialization, and how POSIX file operations are handled by JuiceFS.
- 限速保护的是 5;
- 限流保护的是 3 & 4;
下面我们通过实际例子看看可能会打爆 3 & 4 的几种场景。
4.2 打爆 TiKV API 的几种场景 4.2.1 mlocate (updatedb) 等扫盘工具 一次故障复盘下面的监控,左边是 TiKV 集群的请求数量,右边是 node CPU 利用率(主要是 PD leader 在用 CPU),
Fig. PD CPU soaring caused by too much requests.
大致时间线,
- 14:30 开始,kv_get 请求突然飙升,导致 PD leader 节点的 CPU 利用率大幅飙升;
- 14:40 介入调查,确定暴增的请求来自同一个 volume,但这个 volume 被几十个用户的 pod 挂载, 能联系到的用户均表示 14:30 没有特殊操作;
- 14:30~16:30 继续联系其他用户咨询使用情况 + 主动排查;期间删掉了几个用户暂时不用的 pod,减少挂载这个 volume 的 juicefs client 数量,请求量有一定下降;
- 16:30 定位到请求来源
- 确定暴增的请求不是用户程序读写导致的,
- 客户端大部分都 ubuntu 容器(AI 训练),
- 使用的是同一个容器镜像,里面自带了一个 daily 的定时 mlocate 任务去扫盘磁盘,
这个扫盘定时任务的时间是每天 14:30,因此把挂载到容器里的 JuiceFS volume 也顺带扫了。 确定这个原因之后,
- 16:40 开始,逐步强制停掉(pkill -f updatedb.mlocate) 并禁用(mv /etc/cron.daily/mlocate /tmp/)这些扫盘任务, 看到请求就下来了,PD CPU 利用率也跟着降下来了;
- 第二天早上 6:00 又发生了一次(凌晨 00:00 其实也有一次),后来排查发生是还有几个基础镜像也有这个任务,只是 daily 时间不同。
其实官方已经注意到了 mlocate,所以 juicefs mount 的入口代码就专门有检测,开了之后就自动关闭,
// cmd/mount_unix.go func mountMain(v *vfs.VFS, c *cli.Context) { if os.Getuid() == 0 { disableUpdatedb() |-path := "/etc/updatedb.conf" |-file := os.Open(path) |-newdata := ... |-os.WriteFile(path, newdata, 0644) } ...但是,在 K8s CSI 部署方式中,这个代码是部分失效的:
Fig. JuiceFS K8s CSI deployment
JuiceFS per-node daemon 在创建 mount pod 时,会把宿主机的 /etc/updatedb.conf 挂载到 mount pod 里面, 所以它能禁掉宿主机上的 mlocate,
volumes: - hostPath: path: /etc/updatedb.conf type: FileOrCreate name: updatedb但正如上一小结的例子看到的,业务 pod 里如果开了 updatedb,它就管不到了。 而且业务容器很可能是同一个镜像启动大量 pod,挂载同一个 volume,所以扫描压力直线上升。
4.2.2 版本控制工具类似的工具可能还有版本控制工具(git、svn)、编程 IDE(vscode)等等,威力可能没这么大,但排查时需要留意。
4.3 需求:对元数据引擎的保护能力以上 case,包括上一篇看到的用户疯狂 update 文件的 case,都暴露出同一个问题: JuiceFS 缺少对元数据引擎的保护能力。
4.3.1 现状:JuiceFS 目前还没有社区版目前(2024.09)是没有的,企业版不知道有没有。
下面讨论下如果基于社区版,如何加上这种限流能力。
4.4 客户端限流方案设计Fig. JuiceFS upload/download data bandwidth control.
基于 JuiceFS 已有的设计,再参考其限速实现,其实加上一个限流能力并不难,代码也不多:
- 扩展 Format 结构体,增加限流配置;
- juicefs format|config 增加配置项,允许配置具体限流值;这会将配置写到元数据引擎里面的 volume setting;
- juicefs mount 里面解析 setting 里面的限流配置,传给 client 里面的 metadata 模块;
- metadata 模块做客户端限流,例如针对 txnkv 里面的不到 10 个方法,在函数最开始的地方增加一个限流检查,allow 再继续,否则就等待。
这是一种(中心式配置的)客户端限流方案。
4.5 服务端限流方案设计在 TiKV 集群前面挡一层代理,在代理上做限流,属于服务端限流。
参考资料