大家好,我是大圣。
Fluss 提供了可靠的底层存储设计与灵活的查询更新机制。然而,这一切听起来似乎很复杂,里面有太多看似晦涩的技术名词——比如日志表(LogTablet)、键值表(KvTablet)、Tablet、TabletServer 等等。
那么,Fluss 的存储到底是怎么运作的?本文将从一个具体的数据例子出发,带你逐步了解 Fluss 的底层存储逻辑,以及查询和更新数据时,系统背后的变化过程。
假设我们在 Fluss 中创建了一个表,用于记录用户的行为数据。这张表的结构如下:
CREATE TABLE user_activity (
user_id BIGINT,
activity STRING,
event_date DATE,
score INT,
PRIMARY KEY (user_id, event_date) NOT ENFORCED
)
PARTITIONED BY (event_date)
WITH ('bucket.num' = '2');
user_id
:用户的唯一标识。activity
:用户的行为类型(如登录、浏览)。event_date
:行为发生的日期。score
:行为得分。user_id
和 event_date
,这意味着每个用户在每一天的行为记录是唯一的。event_date
进行分区,每个分区包含 2 个桶(bucket.num=2
)。接下来,我们向表中插入以下三条记录,模拟一些用户行为:
user_id | activity | event_date | score |
---|---|---|---|
1 | login | 2023-01-01 | 10 |
2 | browse | 2023-01-01 | 15 |
1 | purchase | 2023-01-02 | 50 |
Fluss 是如何存储这些数据的?下面我们从 分区存储、分桶存储、日志表存储 和 键值表存储 四个角度详细说明。
Fluss 按照 event_date
字段对数据进行分区,将数据分为不同的分区。我们的数据中有两个不同的 event_date
:
分区 event_date=2023-01-01
:
user_id=1, activity=login, score=10
user_id=2, activity=browse, score=15
分区 event_date=2023-01-02
:
user_id=1, activity=purchase, score=50
分区存储的好处在于,可以按照数据的某些维度(如时间、地域等)将数据进行逻辑划分,提高查询效率和管理方便性。
每个分区中的数据会进一步按照 哈希算法 分配到不同的 桶 中。假设我们将 user_id
作为分桶的依据,并设置每个分区有 2 个桶(bucket.num=2
)。
user_id=1
,哈希值是 1,1 % 2 = 1
,所以它被分配到 桶 1。user_id=2
,哈希值是 2,2 % 2 = 0
,所以它被分配到 桶 0。这样,分区和分桶的最终数据分布如下:
event_date=2023-01-01
:
user_id=2, activity=browse, score=15
user_id=1, activity=login, score=10
event_date=2023-01-02
:
user_id=1, activity=purchase, score=50
通过 分桶,Fluss 实现了数据的并行处理,使得每个桶的数据可以单独进行处理,极大地提升了并发性能。
Fluss 使用日志表(LogTablet)记录所有的 数据变更(插入、更新、删除)。每条数据变更都会按顺序写入 .log
文件,并生成对应的 .index
文件,便于快速定位。
日志表(.log 文件) 存储的内容:
[Offset=0] user_id=1, event_date=2023-01-01, activity=login, score=10
[Offset=64] user_id=2, event_date=2023-01-01, activity=browse, score=15
[Offset=128] user_id=1, event_date=2023-01-02, activity=purchase, score=50
日志表的索引(.index 文件) 内容:
Offset=0 -> user_id=1, event_date=2023-01-01
Offset=64 -> user_id=2, event_date=2023-01-01
Offset=128 -> user_id=1, event_date=2023-01-02
日志表保证了每一条数据变更都被记录下来,并且通过索引文件可以高效定位到变更记录。
键值表(KvTablet)用于存储 每个主键的最新状态,即每个用户在每一天的最新行为记录。在 Fluss 中,键值表的底层存储使用了 RocksDB,它基于 LSM 树 的设计,支持高效的查询和更新操作。
键值表中存储的数据如下:
Key | Value |
---|---|
1-2023-01-01 |
{"activity":"login", "score":10} |
2-2023-01-01 |
{"activity":"browse", "score":15} |
1-2023-01-02 |
{"activity":"purchase", "score":50} |
通过键值表,我们可以快速查询某个主键的最新数据,例如查询 user_id=1
在 2023-01-02
的行为记录,直接从键值表中获取 {"activity":"purchase", "score":50}
。
在 Fluss 中,Tablet 和 TabletServer 是关键组件,负责管理和存储数据。接下来我们将详细讲解这两个概念如何与数据存储和查询流程结合。
event_date
) 和 分桶(如 user_id
) 进行划分。event_date
)和 桶字段(例如 user_id
)进行分配。例如:
event_date=2023-01-01
的数据,并按 user_id
哈希值分配到桶中:
user_id=2, activity=browse, score=15
user_id=1, activity=login, score=10
event_date=2023-01-02
的数据,并将所有数据分配到桶 1:
user_id=1, activity=purchase, score=50
每个 Tablet 可以包含多个桶的数据,它们是数据存储的 逻辑单元,存储在物理硬盘上,支持高效的并行存储和查询。
TabletServer 是 Fluss 中的服务端组件,负责管理和处理多个 Tablet。每个 TabletServer 管理一定数量的 Tablet,确保数据的存储和查询能够顺利进行。
event_date
)和桶字段(如 user_id
),快速定位到目标 Tablet,并从中检索数据。user_id=1
和 event_date=2023-01-01
的数据。TabletServer 会首先定位到 Tablet 1,然后根据哈希值找到桶 1,获取数据 user_id=1, activity=login, score=10
。假设我们查询 user_id=1
在 2023-01-01
的行为数据。
查询键值表:
user_id=1
和 event_date=2023-01-01
直接查找对应的键值。{"activity":"login", "score":10}
。查询日志表:
如果需要查看历史变更,Fluss 会查询 日志表。日志表通过 .index
文件快速定位到 .log
文件中的变更记录。
查找到 Offset=0,读取日志记录:
[Offset=0] user_id=1, event_date=2023-01-01, activity=login, score=10
Tablet 和 TabletServer 的作用:
event_date
)定位到对应的 Tablet,并从该 Tablet 中返回数据。user_id=1, event_date=2023-01-01
时,TabletServer 定位到 Tablet 1,从中返回数据。假设我们更新 user_id=1
在 2023-01-01
的 score
为 20
。
写入日志表:
追加一条更新记录到 .log文件:
[Offset=192] UPDATE user_id=1, event_date=2023-01-01, activity=login, score=20
更新键值表:
在键值表中更新对应的键值对:
Key: `1-2023-01-01`
Value: `{"activity":"login", "score":20}`
Tablet 和 TabletServer 的作用:
Fluss 的底层存储和查询更新流程通过 分区、分桶、日志表 和 键值表 的协同工作,保证了高效的数据存储、查询与更新:
event_date
进行分区,方便管理和查询。user_id
划分到不同的桶,支持并行查询和处理。上面的内容通过一个具体数据的例子,简单梳理了 Fluss 的底层存储和数据查询、更新的过程。后面的文章会陆续更新 Fluss 的文章,让大家对 Fluss 这个后起之秀有更直观的认识,最后欢迎大家关注"大圣数据星球"微信公众号,一起来讨论大数据技术。
本文由博客一文多发平台 OpenWrite 发布!