自从有了 GPT4,我可以在数据库内核工程师、测试工程师、文档工程师之间轻松切换,做到了如🐯添🪽。同时,也开始影响身边的人如何更好地利用AI。非常希望 2024 年能够基于开源 LLM 做一些有意义的事情。
2023 年,继续为 Databend 贡献了大量代码,虽然与 2022 相比少了一些,但今年的代码更加实用。每次编码,都能感觉到无比的温暖和崇高的使命感,也使我越来越热爱“程序员”这个职业:一台电脑就可以改变世界。
2023,滑板成为我锻炼身体的主要方式。每次一个小时的高强度训练就会让我汗流浃背,感觉非常畅快,现在可以完成一些高级且具欣赏性的动作,虽然也发生过一些意外:手指骨折,小腿骨撞墙,不过都已经恢复,对滑板这个充满挑战和冒险的运动热爱不减。
2023 一个重大变化是,我把用了十几年的 Ubuntu 系统切到了 MacOS,使我的装备更加轻量,只需要一台 MacBook 就能随时随地完成所有工作,彻底告别了显示器和机械键键盘。
2023年,所有精力依然围绕 Databend 展开,致力于让 Databend 更加符合 “Bring Compute to Data” 的理念,为用户带来更高的价值,这也为 2024 年实现更加宏大的目标打下了坚实基础。
预测 2024 年大数据领域将会发生一些结构性的变革,并演化出更加丰富的应用场景,希望 Databend 在这一进程中发挥重要作用,继续走在行业前列,为世界更加美好贡献一份力量。
]]>在数据库行业,质量是核心要素。
Databend 的应用场景广泛,特别是在金融相关领域,其查询结果的准确性对用户至关重要。因此,在快速迭代的过程中,如何确保产品质量,成为我们面临的重大挑战。
随着 Databend 开源社区的快速发展,新功能的持续增加和现有功能的优化提出了新的测试挑战。我们致力于在每次代码更新中实施严格的测试,确保稳定性并防止任何潜在问题。
为了确保软件的稳定性和可靠性,Databend 的测试方法覆盖从代码级到系统级的各个方面。
单元测试作为测试的基石,着重验证代码的基本功能和逻辑。我们在每次代码提交前自动运行单元测试,确保及时捕捉任何潜在问题。
Databend 引入了大量的 DuckDB、CockroachDB 和 PostgreSQL 的 SQL 逻辑测试(感谢他们)。这些测试覆盖了广泛的 SQL 场景,帮助我们发现并修复潜在问题,保障 SQL 查询的精确性。
兼容性测试确保新版本与旧版本的向后兼容,帮助用户平稳过渡到 Databend 的更新版本,保障业务的连续性和稳定性。
Databend 使用 ClickBench hits 数据集和 TPCH-SF100 作为性能指标,通过这些测试来确保每个版本的性能都符合预期。
Longrun 测试专注于数据写入、更新和合并等操作的长期效果,通过监测 CPU 和内存的稳定性,确保 Databend 的长期运行稳定性和可靠性。
除 Longrun Tests 外,这些测试在每个 GitHub Pull Request 提交时都会执行,以保证任何更改都符合我们的质量标准。
尽管已经采用了多种测试方法,Databend 团队始终在寻求创新。近期,我们引入了 GPT-4 来进一步提升测试流程。
针对涉及核心路径的修改,我们采用双缝探测模型进行验证。这种方法通过比较当前 PR 版本与主分支(main)版本的结果集来进行验证。如果两者结果一致,则可视为无大碍。但这些验证的 SQL 语句的质量至关重要,这正是我们利用 GPT-4 生成的部分。
首先,我们指导 GPT-4 根据需求推理出随机数据生成方式,如 setup.sql 所示。然后,基于这些数据,GPT-4 进一步生成用于校验的 SQL 语句,例如 check.sql。这些验证 SQL 语句可以根据不同场景进行调整。
接下来,我们在这两个版本的 Databend 上运行这些 SQL 语句,以验证结果集的一致性。
为了确保 Databend 的结果集的正确性,我们选择了 Snowflake 作为参考。这一方法包括三个步骤:
这些 SQL 语句都是由 GPT-4 根据 setup.sql 的数据模式生成的,更加复杂和随机,以便更有效地探测潜在的问题。
Databend 团队通过引入 GPT-4,为测试流程带来了显著的进步。我们已在 Databend Wizard 项目中发布了更多测试集:
https://github.com/datafuselabs/wizard
借助这些 GPT-4 生成的测试模型,Databend 的质量和稳定性又前进了一大步,科技是第一生产力。
[1] datafuselabs/wizard
[2] Databend
[3] Snowflake
Go 语言里做各种 CPU 和 Memory profiling 非常方便,尤其是火焰图这种可视化,排查问题非常方便,但是在Rust语言里,稍微有些困难,这次就来分享下如何使用工具对 Rust 程序进行 CPU 和 Memory 的火焰图分析。
为了支持 CPU 和 Memory Profiling,我们需要增加一些 API,比如在 Databend 中,它们的位置在:cpu/pprof.rs 和 mem/jeprof.rs 。
我们只需在 Databend 服务器上执行:
1 | go tool pprof -http="0.0.0.0:8081" http://localhost:8080/debug/pprof/profile?seconds=30 |
localhost:8080
, Databend 的管理地址和端口0.0.0.0:8081
,go tool pprof server 地址seconds=30
,采集时间为 30 s这样就可以在浏览器中打开地址: <your-ip>:8081/ui/flamegraph
查看 CPU 的火焰图了,非常方便。
Memory 的火焰图要复杂些,需要做一些前置工作。
1 | cargo build --bin databend-query --release --features memory-profiling |
MALLOC_CONF
启动1 | MALLOC_CONF=prof:true,lg_prof_interval:30 ./target/release/databend-query |
1 | git clone https://github.com/gimli-rs/addr2line |
这样你的 jeprof 就会从 30 分钟飞速到 3 秒。
由于旧版 jeprof 不支持火焰图的一些参数,需要对 jeprof 进行升级,由于 jeporf 是一个 perl 脚本,升级就比较暴力。
首先找出本机的jeprof文件的路径:
1 | whereis jeprof |
然后打开jeprof 最新版,拷贝并覆盖你本机的 jeprof,注意不要覆盖旧版本的这两个参数,否则会执行失败:
1 | my $JEPROF_VERSION = "5.2.1-0-gea6b3e973b477b8061e0076bb257dbd7f3faa756"; |
1 | jeprof ./databend-query-main ./jeprof.206330.563.i563.heap --collapse | flamegraph.pl --reverse --invert --minwidth 3 > heap.svg |
flamegraph.pl
需要从 github下载
databend-query-main
,你的可执行文件路径
jeprof.206330.563.i563.heap
,选取一个heap 文件
[1] brendangregg/FlameGraph
[2] https://github.com/jemalloc/jemalloc/blob/dev/bin/jeprof.in
[3] Databend, Cloud Lakehouse: https://github.com/datafuselabs/databend
早上起来感觉嗓子有点不舒服,下午身体开始发冷,体温 37 度,于是在房间隔离,开始戴 N95。
调皮蛋们在房间门口拉上了警戒线,并贴上了”密接者“告示,玩起了cosplay。
晚饭时开始升到 38 度,不过食欲正常,就是头疼的厉害,好不容易搞到几个抗原,测试显示阴性。
半夜烧到 39 度,头疼的非常厉害,做了好多奇怪的梦,这个疼痛很像小时候感冒发烧时的感觉,瞬间想到了方便面或者罐头,上网搜了下黄桃罐头都要买不到了(药就更难买了!),赶紧淘了几袋方便面。
黄桃罐头和方便面都没到!
这天最严重,烧的头痛欲裂,早上测试抗原,已经显示强阳性。食欲正常。
晚上高烧到了巅峰,超过了 39 度,头疼的几乎睡不着,特别想吃水果(如果有黄桃罐头就好了)。
经过一晚上的高烧折腾,早上起来嗓子有点不舒服,但是没有大家说的那种”刀片过喉“感觉,吃过早饭嗓子几乎没有任何感觉。
测试了下体温,37度多,温度开始下降,食欲正常。
食欲下降,四肢有点无力,味觉和嗅觉下降。
小孩开始出现症状,发烧开始。
体温基本正常,开始咳痰,头昏昏沉沉,无味觉和嗅觉。
体温正常,但抗原显示阳性(较弱),不过已经无所谓了,因为家里其他人基本都开始出现症状,轮到我做后勤保障了。
至此,网上买的黄桃罐头和方便面都没到!
]]>马上要进入 2023 了,但是 Databend 社区仍然忙的热火朝天,我们正在让 Databend 成为真正的 Warehouse + Datalake,致力于解决大数据的低成本和易用性问题,让大家在 2023 和未来生活更加美好!
最近,一直在探索 OpenAI 的 ChatGPT,刚开始只是觉得好玩,写 PPT 的时候用它做了一些辅助,效果还不错,总算找到了一个对个人有用的点。
经过一番探索,发现 GPT3 的代码能力非常强大,于是琢磨着怎么让”他”跟 Databend 融合起来。
早上,给 Databend 写了一段代码(Rust),目的是限定 CSV 的换行分隔符在:单个 char 或者 \r\n
,代码如下:
1 | pub fn check_record_delimiter(option: &mut String) -> Result<()> { |
这段代码老感觉什么地方可以再被优化下,于是向 ChatGPT 这个大聪明征求意见:
me: please improve it
ChatGPT 首先对这段代码做了专业解读:
第一次给了一个答案,目测没有我写的好 :),于是再次征求,发现他的代码几乎跟我的一样,这下就放心了。
me: please write a test for it
ChatGPT 果然一步到位,单元测试代码也写的很专业,对代码逻辑的理解很到位,真是结对编程的好搭档,第一次体验到超级 AI 的威力。
他写的单测代码一行没改放到了我的PR里:这里,真感谢这个大聪明!
第二个探索是 Databend 的 logic test。
虽然 Databend 已经有大量的测试用例,是否可以利用 ChatGPT 生成更多的测试用例,进一步提升软件的质量呢?
还有很多用例是可以改进的,比如这个 issue#9184 ,基本是一些流程性的 prompt,怎么让 ChatGPT 帮助我们完成?
这块目前还没有探索出来,不过非常期待这块的进展。
[1] ChatGPT: https://chat.openai.com/
[2] Databend, Cloud Lakehouse: https://github.com/datafuselabs/databend
在大数据分析领域,很多优化都是围绕一句话来做:”reduce distance to the data.“
Bloom Filter Index 的作用是访问 Storage 之前使用布隆索引做下探测,然后决定是否需要从后端读取真正的数据块。
大家所熟悉的数据库,大部分都在使用 Bloom Filter 解决等值查询的问题,避免做一些无用的数据读取。
Databend 第一版( databend#6639) 使用的也是 Bloom Filter 经典算法,经过测试发现了一些问题:
Bloom Filter Index 的索引空间占用过大,Bloom Filter Index 大小甚至超过了原始数据大小(为了让用户简单易用,Databend 会自动为某些数据类型创建 Bloom 索引),这样做 Bloom 检测跟直接读取后端数据区别不大,所以并没有太大的性能提升。
原因是 Bloom Filter 生成时并不知道数据的基数,比如 Boolean 类型,它也会根据数目分配空间,并不会关注基数问题(基数为 2,True or False)。
于是 Databend 社区开始了新方案的探索之路,初步确定一个可行方案是通过 HyperLoglog 检测出数据唯一度,然后做空间分配。
9 月份某个周六的 TiDB 用户大会上,Databend 有个展台,跟 xp(@drmingdrmer) 见面(Databend 团队是 Remote 办公,大家线下见面不太容易 😭)重新讨论起这个问题,他想用 Trie 思想来解决,思路挺好,但复杂度较高。
xp 是 Trie 领域的高手,工程实现来对他来说不是问题,但隐约感觉一些现有技术可以很好的解决这个问题。
周日进行了一番探索,发现了 Daniel Lemire 团队在 2019 年提出的 Xor Filter 算法: Xor Filters: Faster and Smaller Than Bloom Filters,从介绍看效果非常不错。
抱着试试看的心理,基于 Rust 版(xorfilter)做了一个测试 (Xor Filter Bench,发现疗效非常不错:
1 | u64: |
于是,在 databend#7860 实现了 Bloom Filter 到 Xor Filter 的切换,让我们做一些测试来看看效果。
Databend: v0.8.122-nightly,单节点
VM: 32 vCPU, 32 GiB (Cloud VM)
Object Store: S3
数据集: 100 亿记录,350G Raw Data,Xor Filter Index 700MB,索引和数据全部持久化到对象存储
表结构:
1 | mysql> desc t10b; |
Step1:下载安装包
1 | wget https://github.com/datafuselabs/databend/releases/download/v0.8.122-nightly/databend-v0.8.122-nightly-x86_64-unknown-linux-musl.tar.gz . |
解压后目录结构:
1 | tree |
Step2:启动 Databend Meta
1 | ./bin/databend-meta -c configs/databend-meta.toml |
Step3:配置 Databend Query
1 | vim configs/databend-query.toml |
1 | ... ... |
详细部署文档请参考: https://databend.rs/doc/deploy/deploying-databend
Step4: 启动 Databend Query
1 | ./bin/databend-query -c configs/databend-query.toml |
Step5: 构造测试数据集
1 | mysql -uroot -h127.0.0.1 -P3307 |
构造 100 亿条测试数据(耗时 16 min 0.41 sec):
1 | create table t10b as select number as c1, cast(rand() as string) as c2 from numbers(10000000000) |
查询(无任何缓存,数据和索引全部在对象存储):
1 | mysql> select * from t10b where c2='0.6622377673133426'; |
单节点 Databend 利用 filter 下推,然后使用 Xor Filter 索引做过滤,在 100 亿规模的随机数据上做点式查询,可以在 20s 左右。
也可以利用 Databend 的分布式能力来加速点查,Databend 的设计理念是一份数据计算弹性扩展,从单节点扩展到集群模式也非常简单:Expanding a Standalone Databend。
[1] Arxiv: Xor Filters: Faster and Smaller Than Bloom and Cuckoo Filters
[2] Daniel Lemire’s blog: Xor Filters: Faster and Smaller Than Bloom Filters
[3] Databend, Cloud Lakehouse: https://github.com/datafuselabs/databend
TokuDB 又是个什么东东呢?
TokuDB 是一个“老古董“,在机械硬盘时代曾风靡一时,它有着不错的压缩,同时又兼具着不错的读、写能力,被各大厂用来解决容量+性能问题。
这个故事还得从 LevelDB 说起。
2011年,LevelDB 开放源代码,当时虎哥被这个清新的 kv 给吸引住了,很快就把它的源码翻了个底朝天,大概熟悉了这个 kv 的内部运行机制,从这个项目开始虎哥就正式开启了他的数据库之旅。
一边着手写原型,一边跟踪数据库技术的最新进展,当时虎哥把推上一些跟数据库相关的 tag 几乎都关注了,每天早上第一件事就是翻阅这些 tag 相关的推文,数据库这种高大上的技术太有吸引力了,每天都在琢磨着怎么能快速入门!
有趣的的事情是从 2012 年开始的,虎哥开始尝试实现 Skip List,然后琢磨怎么优化它,Skip List 玩腻了就开始玩 B-Tree,最后无聊到在饭馆门口等座的时候就可以写一个 B-Tree,当时想:数据库就这?于是决定再实现一个完整版的 kv ,项目代号 nessDB。
在研发 nessDB 过程中,偶然发现了来自 MIT CSAIL 的《Cache-Oblivious B-Trees》系列,他们通过对 B-Tree 做一个常数项优化从而让整体 IO 大幅降低,基于这个想法他们创立了 TokuTek 这家公司,全力研发 TokuDB 产品,干活的基本都是老板们最得意的学生,3 个学生做研发,外加贝尔实验室的老手 Rik 做架构(这位老哥是个神人,能力非常了得,贝尔实验室硬核精神的体现者,目前已经退休,前段时间还邀请他加入我们的初创公司,由于家庭原因,暂时无法全职工作)。
当时 TokuDB 还是闭源产品,很多资料都是来自 TokuTek 官网以及老板们的几篇 Paper,2012 年底终于跟 TokuDB 主程 Leif 取得了联系,每周我们都会通过gtalk 进行沟通,在 Leif 专业的指导下,虎哥在 2013 年实现一个非常简化的 TokuDB(Fractal-Tree Index),感觉到达了人生的巅峰,走路姿势都不一样了,成为了掌握数据库核心技术的人!
2013 年 4月 22 号,TokuDB 正式开源,虎哥开始研究其源码,最后发现跟之前推测的差不太多,但是 TokuDB 为了集成到 MySQL 作为一个 Storage Engine,做了大量的工作,比如 tokudb-engine 就是对接 MySQL Plugin 层的,而真正核心的是 ft-index,一个基于 Fractal-Tree 实现的 kv 存储。
随后,虎哥由于工作需要就全面投入 TokuDB 的研发工作,先是给 TokuDB 实现了一个 hot backup,让 Xtrabackup 也可以热备 TokuDB 的数据,这个过程需要对 TokuDB 内核做一些改动,虎哥把这个思路同步给了 TokuTek,结果他们当时的VP Tim 给发了一份邮件:Hey, send me your name/address, we owe you something as the "first TokuDB contributor"!
结果他们从美国给邮寄了一个 TokuMX 的T恤,TokuMX 是基于 TokuDB 的 MongoDB(当时的 MongoDB 还在使用 MMAP 苦苦挣扎,TokuMX 当时有不少用户),还有一个小小的 TokuDB 蓝色贴纸,上面简单的印着 “First TokuDB Contributor”。
虎哥内心是无比激动的,依然记得那个阳光明媚的下午,在繁华的 WFC 收到了一个被磨蜕皮的编织袋,由FedEx承运,这是来自大洋彼岸的关怀!
随后的一段时间内,开源 TokuDB 都是 TokuTek 在折腾,几乎没有社区可言,这种东西外人很难入手,而且他们的能力极强,重构一个大的模块几周就搞定,虎哥也孤独的维护着公司内部的 TokuDB 分支,修 bug,增加 feature。
2015 年 4 月份,虎哥盛情邀请 Leif 来中国,带他逛了一些知名景点,临走前一天,Leif 找了后海的一家酒吧,很豪情的说反正回到美国,换汇的人民币也用不着了,咱们不醉不归。
Leif 边点酒边介绍这个酒的味道以及在美国的欢迎度,每点一个新酒,自己先尝一口后再把口杯转 180 度,俩人整了半天,最后 Leif 透漏一个消息,TokuDB 已经被老板卖给了Percona,他们原班人马一个都不会过去,理由是 bla bla…,Google 给了他们团队发了 Offer,但他发誓这辈子再也不碰 Database 这个行业,说是太辛苦,看来 TokuDB 这个事情对他们伤害蛮大。
话说 TokuDB 被 Percona 收购后,很快成为了一个烂摊子,也慢慢停止维护,他们 CTO 也多次邀请虎哥加入,都被婉拒。
2016 年,虎哥和 Rik 成立了 XeLabs 组织,负责 TokuDB 版本的持续维护,这个版本被多家公司使用,稳定支撑着上万个 TokuDB 实例运行,最终由于精力有限,项目也逐渐停更,维护 MySQL 版本没那么容易,改一个小地方就需要做一堆测试,好歹 TokuDB 工程质量非常好,bug 已经很少,即使现在使用问题也不大,只是现在是 Cloud Database 的天下,很少有人再去部署 MySQL 了。
好了,这就是虎哥和 TokuDB 的故事。
虎哥还有和分布式数据库的故事,和数仓的故事,故事太多了,真惆怅。
]]>Databend 是一个使用 Rust 研发、开源的、完全面向云架构的新式数仓,致力于提供极速的弹性扩展能力,打造按需、按量的 Data Cloud 产品体验。
开源地址:https://github.com/datafuselabs/databend
随着移动互联网的发展,我们时刻都在生产着数据。
如果你做了一款 APP,3 月份新增用户 1000 人,你是不是想了解在未来的某些时间段内,这部分用户里有多少人持续使用了你的 APP?
如果你在经营一个电商,你可能更加关注用户在登录
,访问(某个商品)
,下单
,付款
流程里每个环节的转化率,了解用户行为轨迹变化,以精准优化产品设计。比如,如果 Andorid 用户在 下单
到 付款
这个环节转化率明显低于其他客户端,说明 Andorid 客户端在 付款
这个环节上存在一些问题。
这就是我们经常说的用户留存和漏斗转化率分析。
大部分数仓要满足这两个需求,基本都要写一堆 SQL 来进行复杂表达,且性能低下,因为这两个分析会重度依赖 GROUP BY,百万级数据可能就会在分钟级。
本篇就来聊聊 Databend 如何做到简洁、高效的满足这两个需求,使用一个简单的 SQL, 在千万级的数据集上也可以轻松搞定。
1 | CREATE TABLE events(`user_id` INT, `visit_date` DATE); |
user_id
- 用户 IDvisit_date
- 用户访问日期构造用户访问记录。
1 | # user_id 从 0 到 10000000 在 2022-05-15 访问数据 |
1 | SELECT |
这里使用 Databend retention 函数轻松搞定:
1 | +----------+---------+--------+ |
2022-05-15
有 10000000
人访问2022-05-16
有 5000000
个用户持续访问,用户留存率 5000000/10000000
= 50%
2022-05-17
有100000
个用户持续访问,用户留存率 100000/10000000
= 10%
1 | CREATE TABLE events(user_id BIGINT, event_name VARCHAR, event_timestamp TIMESTAMP); |
user_id
- 用户 IDevent_name
- 事件类型:登录
, 访问
,下单
,付款
event_timestamp
- 事件时间(Databend TIMESTAMP 类型精度是小数点后 6 位, 微秒(microsecond))1 | # 用户 100123 事件 |
1 | SELECT |
1 | +-------+-------+ |
这里使用 Databend window_funnel 函数对用户在 1 小时窗口内,进行事件链下钻分析。
一小时内:有多少用户登录(level-1) –> 有多少用户访问(level-2) –> 有多少用户下单(level-3) –> 有多少用户付款(level-4)
从结果来看:
登录 --> 访问
这条事件链上总共有 2 个用户,100126
和 100127
登录 --> 访问 --> 下单
这条事件链上有 1 位用户,100125
登录 --> 访问 --> 下单 --> 购买
这条事件链上总共有 1 位用户,100123
这样我们就可以轻松计算出每个阶段的转化率。
从上面示例可以看出,留存和漏斗分析都重度依赖 GROUP BY user_id
,如果 user_id
较多,对 GROUP BY
计算速度有比较高的要求,Databend 在 GROUP BY
上做了大量的优化,目前性能已经非常强悍,具体机制可以参考这篇文章 Databend 的 Group By 聚合查询为什么跑的这么快?
Databend 留存(RETENTION)函数和漏斗分析(WINDOW_FUNNEL)函数去年已经实现,把复杂的逻辑进行封装,让用户使用起来更加方便。
Databend 作为一个新一代云数仓,在设计上做了一个很大的转变:数据不再是重心,用户的体验才是。
对于一个数仓产品,相信大部分用户都希望:
随着云基础设施的发展,我们在 Databend Platform 里让这一切都变成了可能。
基于开源 Databend 内核、AWS EC2 计算资源、S3 的对象存储,加上自研的 Serverless Infrastructure,Databend 团队即将推出他们的第一个企业级产品:Databend Platform。
来,让我们一起看看在 Databend Platform 里如何做漏斗分析:
相信很多同学还在使用 RSA 算法用于生成 SSH 公钥,可能还会纠结选择多少位才足够安全,一般建议是 4096 bits:
1 | ssh-keygen -t rsa -b 4096 -C "your_email@example.com" |
这样我们的公钥(public key) 就会非常长:
1 | cat test_rsa_4096.pub |
其实,有一些更先进的算法,比 RSA 更安全,公钥更简短,随着区块链的普及,它们正慢慢被更多的人接受,比如 Ed25519,虽然它只有 256 bit,但安全性比 RSA 3072 还要高。
Ed25519 SSH Key 生成:
1 | ssh-keygen -t ed25519 -C "your_email@example.com" |
公钥:
1 | cat test_ed25519.pub |
Github 已经默认推荐大家使用 Ed25519: https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
Ed25519 是一个椭圆曲线,非常优美,安全性经过数学严格证明:
By Deirdre Connolly in [State of the Curve] (2016)这里有一份目前使用 Ed25519 的列表: https://ianix.com/pub/ed25519-deployment.html
]]>Databend 是一个使用 Rust 研发、开源的、完全面向云架构的新式数仓,致力于提供极速的弹性扩展能力,打造按需、按量的 Data Cloud 产品体验。
开源地址:https://github.com/datafuselabs/databend
这篇来介绍下 Databend 底座: Fuse Engine,一个动力澎湃的列式存储引擎,Databend Fuse Engine 在设计之初社区给它的定位是:动力要澎湃,架构要简单,可靠性要高。
在正式介绍之前,我们先看一组“挑战数据”,Databend Fuse Engine + AWS S3,一个事务在 ~1.5 小时写入了 22.89 TB 原始数据:
1 | mysql> INSERT INTO ontime_new SELECT * FROM ontime_new; |
同时,在功能上要满足:
从这些需求出发,你会发现 Fuse Engine 跟 Git “形似”(Git-inspired),在介绍 Fuse Engine 设计之前,我们先来看看 Git 底层是如何工作的。
Git 解决了分布式环境下的数据版本管理(data version control)问题,它有隔离(branch)、提交(commit)、回溯(checkout),以及合并(merge)功能,基于 Git 语义完全可以打造出一个分布式存储引擎。市面上也出现一些基于 Git-like 思想而构建的产品,比如 Nessie - Transactional Catalog for Data Lakes 和 lakeFS 。
为了更好的探索 Git 底层工作机制,我们选择从数据库角度出发,使用 Git 语义来完成一系列“数据”操作。
cloud.txt
,内容为:1 | 2022/05/06, Databend, Cloud |
cloud.txt
数据写到 Git 系统:1 | git commit -m "Add olap.txt" |
7d972c7ba9213c2a2b15422d4f31a8cbc9815f71
:1 | git log |
warehouse.txt
1 | 2022/05/07, Databend, Warehouse |
warehouse.txt
数据写到 Git 系统1 | git commit -m "Add warehouse.txt" |
15af34e4d16082034e1faeaddd0332b3836f1424
1 | commit 15af34e4d16082034e1faeaddd0332b3836f1424 (HEAD) |
到此为止,Git 已经为我们维护了 2 个版本的数据:
1 | ID 15af34e4d16082034e1faeaddd0332b3836f1424,版本2 |
我们可以根据 Commit ID 进行版本间的任意切换,也就是实现了 Time Travel 和 Table Zero-Copy 功能,那么 Git 底层是如何做到的呢? 方式也比较简单,它通过引入 3 类对象文件来进行关系描述:
首先,我们需要知道一个 HEAD 指针:
1 | cat .git/HEAD |
Commit 文件会记录跟 commit 相关的一些元数据信息,比如当前 tree 以及 parent,还有提交人等,文件路径:
1 | .git/objects/15/af34e4d16082034e1faeaddd0332b3836f1424 |
文件内容:
1 | git cat-file -p 15af34e4d16082034e1faeaddd0332b3836f1424 |
Tree 文件记录当前版本下所有的数据文件,文件路径:
1 | .git/objects/57/6c63e580846fa6df2337c1f074c8d840e0b70a |
文件内容:
1 | git cat-file -p 576c63e580846fa6df2337c1f074c8d840e0b70a |
Blob 文件是原始数据文件,同样可以通过 git cat-file
命令来查看文件内容(如果使用 Git 来管理代码,Blob 就是我们的代码文件)。
1 | git cat-file -p 688de5069f9e873c7e7bd15aa67c6c33e0594dde |
Databend Fuse Engine 在设计上,跟 Git 非常类似,它引入 3 个描述文件:
我们继续在 Fuse Engine 里进行一把刚才在 Git 进行的操作。
1 | CREATE TABLE git(file VARCHAR, content VARCHAR); |
把 cloud.txt
数据写到 Fuse Engine
1 | INSERT INTO git VALUES('cloud.txt', '2022/05/06, Databend, Cloud'); |
Fuse 为我们生成一个新的 Snapshot ID 6450690b09c449939a83268c49c12bb2
:
1 | CALL system$fuse_snapshot('default', 'git'); |
把 warehouse.txt
数据写到 Fuse Engine
1 | INSERT INTO git VALUES('warehouse.txt', '2022/05/07, Databend, Warehouse'); |
Fuse Engine 为我们生成一个新的 Snapshot ID efe2687fd1fc48f8b414b5df2cec1e19
,并指向前一个 Snapshot ID 6450690b09c449939a83268c49c12bb2
1 | CALL system$fuse_snapshot('default', 'git'); |
目前为止,Fuse Engine 为我们生成了 2 个版本的数据:
1 | ID efe2687fd1fc48f8b414b5df2cec1e19,版本2 |
是不是跟 Git 非常类似?
跟 Git 一样,Fuse Engine 也需要一个 HEAD 作为入口,查看 Fuse Engine 的 HEAD:
1 | SHOW CREATE TABLE git\G; |
SNAPSHOT_LOCATION
就是 HEAD,默认指向最新的快照 efe2687fd1fc48f8b414b5df2cec1e19
,那我们如何切到 ID 为 6450690b09c449939a83268c49c12bb2
的快照数据呢? 很简单,先查看当前表的 Snapshot 信息:
1 | CALL system$fuse_snapshot('default', 'git')\G; |
然后创建一个新表(git_v1)并把 SNAPSHOT_LOCATION
指向相应的 Snapshot 文件:
1 | CREATE TABLE git_v1(`file` VARCHAR, `content` VARCHAR) SNAPSHOT_LOCATION='53/133/_ss/6450690b09c449939a83268c49c12bb2_v1.json'; |
用于存储 Segment 信息,文件路径 :
1 | 53/133/_ss/efe2687fd1fc48f8b414b5df2cec1e19_v1.json |
文件内容:
1 | { |
用于存储 Block 相关信息,文件路径:
1 | 53/133/_sg/df56e911eb26446b9f8fac5acc65a580_v1.json |
文件内容:
1 | { |
Fuse Engine 底层数据使用 Parquet 格式,每个文件内部有多个 Block 组成。
Databend Fuse Engine 在早期设计(2021 年 10 月)时候,需求很明确,但方案选型还是经历过一段小曲折。当时,Databend 社区调研了市面上大量的 Table Format 方案(比如 Iceberg 等),当时面临的挑战是基于现有方案还是自己造一套?最终选择研发一套简洁的、适合自己的 Storage Engine,但数据存储格式选择 Parquet 标准。
在 Fuse Engine 里,我们把 Parquet Footer 单独存放,以减少不必要的 Seek 操作,另外增加了一套更加灵活的索引机制,比如 Aggregation,Join 等都可以有自己的索引来进行加速。
欢迎体验 Fuse Engine,挂上对象存储,让你体验不一样的大数据分析 https://databend.rs/doc/deploy
Databend 开源地址:https://github.com/datafuselabs/databend
Databend 是一个使用 Rust 研发、开源的、完全面向云架构的新式数仓,致力于提供极速的弹性扩展能力,打造按需、按量的 Data Cloud 产品体验。
开源地址:https://github.com/datafuselabs/databend
Databend 从第一天就是开源的,测试系统也是基于开源生态所构建,使用了大量的 github CI(免费),已经支撑了我们半年来的快速迭代。
对于一个开源数据库项目,做到可测试性是加速迭代的不二法宝。一个 Pull Request (通常说的 Patch) 从提交到合并到主干分支,作为一个 Review 人员会比较关注以下几个问题:
本篇就从一个 Pull Request 测试周期说起,看看它从创建再到合并入主干分支,Databend 经过了哪些测试,针对上面四个问题做到心中有数,让每个 Pull Request 都有质量保障。
单元测试是最小的测试单元。
我们每写一个函数都要做到可独立测试,如果这个函数有其他状态依赖,那状态也要是可以 Mock 的。
在 Databend 中,单元测试都放在一个独立的文件中,比如,x_test.rs:
1 |
|
目前 Databend 有 500+ 的单元测试,并对一些状态做了全局 Mock,让开发者更加容易的编写测试用例,以从代码层面确保函数执行都符合预期,尽早的发现和解决问题。
Databend 的单元测试会在 Ubuntu 和 MacOS 两个系统上运行 (Databend 研发主要使用 Mac 和 Ubuntu 两个主力系统)。
当单元测试通过后,并不一定保证功能是正确的,因为功能通常来说是由多个函数逻辑性的贯穿而成。
功能测试又分为 Stateless 和 Stateful 两种模型,其中 Stateless 测试模型不需要加载数据集, Stateful 测试模型则需要加载预值的数据集,接下来我们着重看下 Stateless 测试模型。
Databend 参考了 ClickHouse 的做法,使用表函数 numbers_mt
来便捷的做 Stateless 测试。
比如这个稍微“复杂”的SQL:
1 | SELECT number%3 as c1, number%2 as c2 FROM numbers_mt(10000) WHERE number > 2 GROUP BY number%3, number%2 ORDER BY c1, c2; |
它先根据条件过滤数据,然后再进行 GROUP BY 分组,最后做一个排序,这个 SQL 执行时,会涉及非常多的函数,所以我们必须有一套便捷的机制来保证多个函数组成的功能也是正确的。
Databend 是如何做的呢?
我们会先定义一个需要测试的 SQL 集,x.sql:
1 | SELECT number%3 as c1, number%2 as c2 FROM numbers_mt(10000) WHERE number > 2 GROUP BY number%3, number%2 ORDER BY c1, c2; |
然后再定义一个预期的结果集,x.result:
1 | 0 0 |
每次做功能测试的时候,Databend 会调用这个 x.sql 文件,然后把得到的结果集和 x.result 文件进行对比,如果有出入则报错并给出提示信息。
由于 Databend 具备分布式的 MPP 能力,所以功能测试会在单机(Standalone)和 集群(Cluster)两种模式下进行回归测试,以确保 Patch 对功能没有影响。
当单元测试和功能测试都通过后,我们还会关注一个重要的指标:这个 Patch 是否导致性能下降?或者是一个性能优化的 Patch 提升了多少性能?
针对这个问题,Databend 使用数字来做量化,我们只需在 Pull Request 里回复: /run-perf master
CI 会自动编译当前分支然后跑相应的性能测试,再跟 master 做对比并生成一份性能对比报告:
这样,Review 人员就可以根据这个报告清晰的知道当前 Patch 对性能的影响,以确保每个 Patch 在性能上都是可控的。
Databend 目标是打造一个跨平台的 Cloud Warehouse,所以要求每个 Patch 在以下几个平台都可以正常编译和工作:
1 | - {os: ubuntu-latest, toolchain: stable, target: x86_64-unknown-linux-gnu, cross: false} |
当这个 CI 跑完后,我们可以明确的知道当前 Patch 对跨版本编译是没有影响的。
以上所有的 CI 测试都通过后,我们的 Pull Request 才算合格,具备合并到主干分支的条件。
如果没有这些自动化测试 CI 做保障,每个问题都会消耗 Review 人员大量的精力去做验证,这种模式肯定不可持久,严重影响产品的迭代速度,拖慢社区的节奏。
Databend 从第一天开始就在努力打造一个可测试的系统,为此我们研发了 test-infra 以及社区协作的 fusebot 机器人,以加速 Databend 产品迭代,尽快提供一个可试用的 Alpha 版本。
Databend 是一个使用 Rust 研发、开源的、完全面向云架构的新式数仓,致力于提供极速的弹性扩展能力,打造按需、按量的 Data Cloud 产品体验。
开源地址:https://github.com/datafuselabs/databend
此篇不算一个技术文章,但是比较重要,因为我们项目改名了,做个说明。
Datafuse 和开源项目 DataFusion 名称在英语语境里比较接近,为了大家未来的发展,我们决定把项目更名为:Databend 。
由于文化差异导致的一些误解,大家聊开后也就化解了,感谢 Andy 的理解和支持,相信 DataFusion 和 Databend 都有着美好的前景。
相对论是 20 世纪最伟大的发现之一,由于物质的存在,时间和空间会发生弯曲(space-time bend),它让人类重新审视时间与空间。我们期望 Databend 的出现,让更多人重新审视数据,发掘出更大的价值。
「Datafuse Labs」成立于 2021 年 3 月,是开源项目 Databend 的背后团队,团队在云原生数据库领域有着丰富的工程经验,同时也是数据库开源社区活跃贡献者,目前在中国、美国、新加坡均设有研发中心,致力于成为业界领先的 Data Cloud 基础软件供应商。
数据库工程师:负责 Databend 内核设计与研发,有丰富的分布式系统经验,熟悉 Rust 或 C++ ,对数据库有着浓厚的兴趣,可 Worldwide Remote 办公。
云平台工程师:负责 Databend Cloud 产品设计与研发,有 Kubernetes 开发经验者优先,可 Worldwide Remote 办公。
如果对我们感兴趣,简历请投递:hr@datafuselabs.com
Databend 在设计上专注以下能力:
上图是 Databend 的整体架构图,整个系统主要由三大部分组成:Meta service Layer
、Compute Layer
和 Storage Layer
。
Meta Service 是一个多租户、高可用的分布式 key-value 存储服务,具备事务能力,主要用于存储:
Metadata
: 表的元信息、索引信息、集群信息、事务信息等。Administration
:用户系统、用户权限等信息。Security
:用户登录认证、数据加密等。计算层由多个集群(cluster)组成,不同集群可以承担不同的工作负载,每个集群又有多个计算节点(node)组成,你可以轻松的添加、删除节点或集群,做到资源的按需、按量管理。
计算节点是计算层的最小构成单元,其中每个计算节点包含以下几个组件:
根据用户输入的 SQL 生成执行计划,它只是个逻辑表达,并不能真正的执行,而是用于指导整个计算流水线(Pipeline)的编排与生成。
比如语句 SELECT number + 1 FROM numbers_mt(10) WHERE number > 8 LIMIT 2
执行计划:
1 | datafuse :) EXPLAIN SELECT number + 1 FROM numbers_mt(10) WHERE number > 8 LIMIT 2 |
这个执行计划自下而上分别是 :
ReadDataSource
:表示从哪些文件里读取数据Filter
: 表示要做 (number > 8) 表达式过滤Expression
: 表示要做 (number + 1) 表达式运算Projection
: 表示查询列是哪些Limit
: 表示取前 2 条数据
对执行计划做一些基于规则的优化(A Rule Based Optimizer), 比如做一些谓词下推或是去掉一些不必要的列等,以使整个执行计划更优。
处理器(Processor)是执行计算逻辑的核心组件。根据执行计划,处理器们被编排成一个流水线(Pipeline),用于执行计算任务。
整个 Pipeline 是一个有向无环图,每个点是一个处理器,每条边由处理器的 InPort 和 OutPort 相连构成,数据到达不同的处理器进行计算后,通过边流向下一个处理器,多个处理器可以并行计算,在集群模式下还可以跨节点分布式执行,这是 Databend 高性能的一个重要设计。
例如,我们可以通过 EXPLAIN PIPELINE 来查看:
1 | datafuse :) EXPLAIN PIPELINE SELECT number + 1 FROM numbers_mt(10000) WHERE number > 8 LIMIT 2 |
同样,理解这个 Pipeline 我们自下而上来看:SourceTransform
:读取数据文件,16 个物理 CPU 并行处理FilterTransform
:对数据进行 (number > 8) 表达式过滤,16 个物理 CPU 并行处理ExpressionTransform
:对数据进行 (number + 1) 表达式执行,16 个物理 CPU 并行处理ProjectionTransform
:对数据处理生成最终列LimitTransform
:对数据进行 Limit 2 处理,Pipeline 进行折叠,由一个物理 CPU 来执行
Databend 通过 Pipeline 并行模型,并结合向量计算最大限度的去压榨 CPU 资源,以加速计算。
计算节点使用本地 SSD 缓存数据和索引,以提高数据亲和性来加速计算。
缓存的预热方式有:
Databend 使用 Parquet 列式存储格式来储存数据,为了加快查找(Partition Pruning),Datafuse 为每个 Parquet 提供了自己的索引(根据 Primary Key 生成):
通过这些索引, 我们可以减少数据的交互,并使计算量大大减少。
假设有两个Parquet 文件:f1
, f2
。f1
的 min_max.idx: [3, 5]
;f2
的 min_max.idx: [4, 6]
。如果查询条件为:where x < 4
, 我们只需要 f1
文件就可以,再根据 sparse.idx
索引定位到 f1
文件中的某个数据页。
Databend 是一个完全面向云架构而设计的新式数仓,它把传统数据库进行解耦,再根据需求组装出一个 Cloud Warehouse,以追求高弹性、低成本为宗旨。
Databend 目前的聚合函数已经非常高效,基于这些高效的聚合函数(尤其是 Group By),我们实现了模型函数 windowFunnel,用于漏洞模型的高效计算。
Databend 正处于高速迭代期,欢迎关注我们: https://github.com/datafuselabs/databend/
]]>Cloud Warehouse 解决了什么问题?
Cloud Warehouse 架构应该是什么样?
带着问题,通过实战,向 Cloud Warehouse 出发。
首先,来看看传统式 Sharding Warehouse 架构,以及它在云上的局限性。
每个 shard 数据区间是固定的,很容易发生数据热点(data skew)问题,一般解决办法:
① 提升该 shard 硬件配置,如果热点很难预估,整个集群配置都需要提升,资源上粒度控制粗暴。
② 扩容,扩容过程(增加 shard-4)涉及数据迁移,如果数据量大,shard-4 可服务等待时间也会加长。
如果只是把 Sharding Warehouse 简单的搬到云上,资源控制粒度还是很粗糙,很难做到精细化控制,从而无法实现比较精确的按需、按量计费。
也就说,虽然我们可以随意扩展,但是成本依然高昂。
如果一个 Cloud Warehouse 满足:
那么它的架构应该是什么样子呢?
首先它是一个存储和计算分离的架构,其次是计算节点尽量无状态,这样我们可以根据需要添加/删除计算节点,算力随时增加和减少,是一个很平滑的过程,不涉及数据的迁移。
node-4 基本是 severless 的,可认为是一个进程,运行完毕自动消亡,在调度上可以做到更加精细化。
大家看到这个架构后或许有一个疑问:
Cloud Warehouse 架构比传统架构更简单啊 :)
Shared Storage 可以是 AWS S3,还可以是 Azure Blob Storage,都让云来做了,compute 使用类似 Presto 的计算引擎不就是完美的 Cloud Warehouse 了吗?
这里有一个现实问题挡住了通往理想的大道:
Shared Storage 通常不是为低延迟、高吞吐而设计,偶尔性的抖动也很难控制,如果靠计算引擎蛮力硬刚,这看起来并不是一个好的产品。
首先我们看下 Cloud Warehouse 里的数据有几种状态:
既然 Shared Storage 已经假设是不可靠的,那我们尽量减少从 Shared Storage 读取数据好了,增加 Cache 来解决。
新的问题又来了,这个 Cache 到底 Cache 什么数据呢,是原始的块数据还是索引?是一个全局 Cache 还是计算节点内的 Cache?
我们先看看 Snowflake 老大哥的设计:
Snowflake 在计算和存储之间加了一个共享的 Ephemeral Storage,主要用于 Intermediate data 存储,同时肩负着 Persistent data cache,好处是缓存可以充分利用,缺点是这个 Distributed Emphemeral Storage 做到 Elastic 同样面临一些挑战,比如多租户情况下资源隔离等问题。
Cloud Warehouse 强调状态分离,我们可以把 Persistent data 预先生成足够多的索引放到 Metadata Service,每个计算节点进行订阅,根据需要更新本地的 Cache,这个架构跟 FireBolt 比较相似。
这是目前比较简单可行的方式,增加计算节点,只要加热 Cache 即可,同样会面临一些挑战,比如海量的索引信息快速同步问题。
Databend 是一个开源的 Cloud Warehouse,重在计算和状态分离,专注云上的弹性扩展,让大家轻松打造出自己的 Data Cloud。
很高兴,又开了新系列来讲 Databend,一个把 Rust 和 Cloud 进行连接的 Warehouse 项目,充满乐趣和挑战。
首先要尊重客观事实,在什么场景下,x 比 y 快?
其次是为什么 x 会比 y 快?
如果以上两条都做到了,还有一点也比较重要: x 的优势可以支撑多久? 是架构等带来的长期优势,还是一袋烟的优化所得,是否能持续跟上自己的灵魂。
如果只是贴几个妖艳的数字,算不上是 benchmark,而是 benchmarket。
好了,回到 Group By 正题。
相信很多同学已经体验到 ClickHouse Group By 的出色性能,本篇就来分析下快的原因。
首先安慰一下,ClickHouse 的 Group By 并没有使用高大上的黑科技,只是摸索了一条相对较优的方案。
1 | SELECT sum(number) FROM numbers(10) GROUP BY number % 3 |
我们就以这条简单的 SQL 作为线索,看看 ClickHouse 怎么实现 Group By 聚合。
1 | EXPLAIN AST |
1 | EXPLAIN |
代码主要在 InterpreterSelectQuery::executeImpl@Interpreters/InterpreterSelectQuery.cpp
1 | EXPLAIN PIPELINE |
Pipeline 是从底部往上逐一执行。
首先从 ReadFromStorage 执行,生成一个 block1, 数据如下:
1 | ┌─number─┐ |
ExpressionTransform 包含了 2 个 action:
经过 ExpressionTransform 运行处理后生成一个新的 block2, 数据如下:
1 | ┌─number─┬─modulo(number, 3)─┐ |
代码主要在 ExpressionActions::execute@Interpreters/ExpressionActions.cpp
AggregatingTransform 是 Group By 高性能的核心所在。
本示例中的 modulo(number, 3) 类型为 UInt8,在做优化上,ClickHouse 会选择使用数组代替 hashtable作为分组,区分逻辑见 Interpreters/Aggregator.cpp
在计算 sum 的时候,首先会生成一个数组 [1024],然后做了一个编译展开(代码 addBatchLookupTable8@AggregateFunctions/IAggregateFunction.h):
1 | static constexpr size_t UNROLL_COUNT = 4; |
sum(number) … GROUP BY number % 3 计算方式:
1 | array[0] = 0 + 3 + 6 + 9 = 18 |
这里只是针对 UInt8 做的一个优化分支,那么对于其他类型怎么优化处理呢?
ClickHouse 针对不同的类型分别提供了不同的 hashtable,声势比较浩大(代码见 Aggregator.h):
1 | using AggregatedDataWithUInt8Key = FixedImplicitZeroHashMapWithCalculatedSize<UInt8, AggregateDataPtr>; |
如果我们改成 GROUP BY number*100000 后,它会选择 AggregatedDataWithUInt64Key 的 hashtable 作为分组。
而且 ClickHouse 提供了一种 Two Level 方式,用语应对有大量分组 key 的情况,Level1 先分大组,Level2 小组可以并行计算。
针对 String 类型,根据不同的长度,hashtable 也做了很多优化,代码见 HashTable/StringHashMap.h
ClickHouse 会根据 Group By 的最终类型,选择一个最优的 hashtable 或数组,作为分组基础数据结构,使内存和计算尽量最优。
这个”最优解“是怎么找到的?从 test 代码可以看出,是不停的尝试、测试验证出来的,浓厚的 bottom-up 哲学范。
hashtable 测试代码:Interpreters/tests
lookuptable 测试代码: tests/average.cpp
]]>最后更新: 2020-09-28
如果多个 ClickHouse server 可以挂载同一份数据(分布式存储等),并且每个 server 都可写,这样会有什么好处呢?
首先,我们可以把副本机制交给分布式存储来保障,上层架构变得简单朴素;
其次,clickhouse-server 可以在任意机器上增加、减少,使存储和计算能力得到充分发挥。
本文就来探讨一下 ClickHouse 的存储计算分离方案,实现上并不复杂。
ClickHouse 运行时数据由两部分组成:内存元数据和磁盘数据。
我们先看写流程:
1 | w1. 开始写入数据 |
再来看读流程:
1 | r1. 从part metadata定位需要读取的part |
这样,如果 server1 写了一条数据,只会更新自己内存的 part metadata,其他 server 是感知不到的,这样也就无法查询到刚写入的数据。
存储计算分离,首先要解决的就是内存状态数据的同步问题。
在 ClickHouse 里,我们需要解决的是内存中 part metadata 同步问题。
在上篇 <ReplicatedMergeTree表引擎及同步机制> 中,我们知道副本间的数据同步机制:
首先同步元数据,再通过元数据获取相应part数据。
这里,我们借用 ReplicatedMergeTree 同步通道,然后再做减法,同步完元数据后跳过 part 数据的同步,因为磁盘数据只需一个 server 做更新(需要 fsync 语义)即可。
核心代码:
MergeTreeData::renameTempPartAndReplace
1 | if (!share_storage) |
script:
<path>/home/bohu/work/cluster/d1/datas/</path>
原型
需要注意的是,这里只实现了写入数据同步,而且是非常 tricky 的方式。
由于 DDL 没有实现,所以在 zookeeper 上的注册方式也比较 tricky,demo 里的 replicas 都是手工注册的。
本文提供一个思路,算是抛砖引玉,同时也期待更加系统的工程实现。
ClickHouse 暂时还不支持 Distributed Query 功能,如果这个能力支持,ClickHouse 存储计算分离就是一个威力无比的小氢弹。
]]>最后更新: 2020-09-13
在 MySQL 里,为了保证高可用以及数据安全性会采取主从模式,数据通过 binlog 来进行同步。
在 ClickHouse 里,我们可以使用 ReplicatedMergeTree 引擎,数据同步通过 zookeeper 完成。
本文先从搭建一个多 replica 集群开始,然后一窥底层的机制,简单吃两口。
搭建一个 2 replica 测试集群,由于条件有限,这里在同一台物理机上起 clickhouse-server(2个 replica) + zookeeper(1个),为了避免端口冲突,两个 replica 端口会有所不同。
1 | docker run -p 2181:2181 --name some-zookeeper --restart always -d zookeeper |
replica-1 config.xml:
1 | <zookeeper> |
replica-2 config.xml:
1 | <zookeeper> |
1 | CREATE TABLE default.rtest1 ON CLUSTER 'mycluster_1' |
1 | docker exec -it some-zookeeper bash |
两个 replica 都已经注册到 zookeeper。
如果在 replica-1 上执行了一条写入:
1 | replica-1> INSERT INTO rtest VALUES(33,33); |
数据是如何同步到 replica-2 的呢?
1 | s1. replica-1> StorageReplicatedMergeTree::write --> ReplicatedMergeTreeBlockOutputStream::write(const Block & block) |
也可以进入 zookeeper docker 内部直接查看某个 LogEntry:
1 | [zk: localhost:2181(CONNECTED) 85] get /clickhouse/tables/replicated/test/log/log-0000000022 |
本文以写入为例,从底层分析了 ClickHouse ReplicatedMergeTree 的工作原理,逻辑并不复杂。
不同 replica 的数据同步需要 zookeeper(目前社区有人在做etcd的集成 pr#10376)做元数据协调,是一个订阅/消费模型,涉及具体数据目录还需要去相应的 replica 通过 interserver_http_port 端口进行下载。
replica 的同步都是以文件目录为单位,这样就带来一个好处:我们可以轻松实现 ClickHouse 的存储计算分离,多个 clickhouse-server 可以同时挂载同一份数据进行计算,而且这些 server 每个节点都是可写,虎哥已经实现了一个可以 work 的原型,详情请参考下篇 <存储计算分离方案与实现>。
[1] StorageReplicatedMergeTree.cpp
[2] ReplicatedMergeTreeBlockOutputStream.cpp
[3] ReplicatedMergeTreeLogEntry.cpp
[4] ReplicatedMergeTreeQueue.cpp
最后更新: 2020-08-31
在 ClickHouse 里,物化视图(Materialized View)可以说是一个神奇且强大的东西,用途别具一格。
本文从底层机制进行分析,看看 ClickHouse 的 Materalized View 是怎么工作的,以方便更好的使用它。
对大部分人来说,物化视图这个概念会比较抽象,物化?视图?。。。
为了更好的理解它,我们先看一个场景。
假设你是 *hub 一个“幸福”的小程序员,某天产品经理有个需求:实时统计每小时视频下载量。
用户下载明细表:
1 | clickhouse> SELECT * FROM download LIMIT 10; |
计算每小时下载量:
1 | clickhouse> SELECT toStartOfHour(when) AS hour, userid, count() as downloads, sum(bytes) AS bytes FROM download GROUP BY userid, hour ORDER BY userid, hour; |
很容易嘛,不过有个问题:
每次都要以 download
表为基础数据进行计算,*hub 数据量太大,无法忍受。
想到一个办法:如果对 download
进行预聚合,把结果保存到一个新表 download_hour_mv
,并随着 download
增量实时更新,每次去查询download_hour_mv
不就可以了。
这个新表可以看做是一个物化视图,它在 ClickHouse 是一个普通表。
1 | clickhouse> CREATE MATERIALIZED VIEW download_hour_mv |
这个语句主要做了:
SummingMergeTree
的物化视图 download_hour_mv
download
表,并根据 select
语句中的表达式进行相应“物化”操作2020-08-31 18:00:00
)作为开始点 WHERE when >= toDateTime('2020-09-01 04:00:00')
,表示在2020-09-01 04:00:00
之后的数据才会被同步到 download_hour_mv
这样,目前 download_hour_mv
是一个空表:
1 | clickhouse> SELECT * FROM download_hour_mv ORDER BY userid, hour; |
注意:官方有 POPULATE 关键字,但是不建议使用,因为视图创建期间 download
如果有写入数据会丢失,这也是我们加一个 WHERE
作为数据同步点的原因。
那么,我们如何让源表数据可以一致性的同步到 download_hour_mv
呢?
在2020-09-01 04:00:00
之后,我们可以通过一个带 WHERE
快照的INSERT INTO SELECT...
对 download
历史数据进行物化:
1 | clickhouse> INSERT INTO download_hour_mv |
查询物化视图:
1 | clickhouse> SELECT * FROM download_hour_mv ORDER BY hour, userid, downloads DESC; |
可以看到数据已经“物化”到 download_hour_mv
。
写一些数据到 download
表:
1 | clickhouse> INSERT INTO download |
查询物化视图 download_hour_mv
:
1 | clickhouse> SELECT * FROM download_hour_mv ORDER BY hour, userid, downloads; |
可以看到最后一条数据就是我们增量的一个物化聚合,已经实时同步,这是如何做到的呢?
ClickHouse 的物化视图原理并不复杂,在 download
表有新的数据写入时,如果检测到有物化视图跟它关联,会针对这批写入的数据进行物化操作。
比如上面新增数据是通过以下 SQL 生成的:
1 | clickhouse> SELECT |
物化视图执行的语句类似:
1 | INSERT INTO download_hour_mv |
代码导航:
添加视图 OutputStream, InterpreterInsertQuery.cpp
1 | if (table->noPushingToViews() && !no_destination) |
构造 Insert , PushingToViewsBlockOutputStream.cpp
1 | ASTPtr insert_query_ptr(insert.release()); |
1 | Context local_context = *select_context; |
物化视图的用途较多。
比如可以解决表索引问题,我们可以用物化视图创建另外一种物理序,来满足某些条件下的查询问题。
还有就是通过物化视图的实时同步数据能力,我们可以做到更加灵活的表结构变更。
更强大的地方是它可以借助 MergeTree 家族引擎(SummingMergeTree、Aggregatingmergetree等),得到一个实时的预聚合,满足快速查询。
原理是把增量的数据根据 AS SELECT ...
对其进行处理并写入到物化视图表,物化视图是一种普通表,可以直接读取和写入。
最后更新: 2020-09-03
几天前 ClickHouse 官方发布了 v20.8.1.4447-testing,这个版本已经包含了 MaterializeMySQL 引擎,实现了 ClickHouse 实时复制 MySQL 数据的能力,感兴趣的朋友可以通过官方安装包来做体验,安装方式参考: https://clickhouse.tech/#quick-start,需要注意的是要选择 testing 分支。
MaterializeMySQL 在 v20.8.1.4447-testing 版本是基于 binlog 位点模式进行同步的。
每次消费完一批 binlog event,就会记录 event 的位点信息到 .metadata 文件:
1 | Version:1 |
这样当 ClickHouse 再次启动时,它会把 {‘mysql-bin.000002’, 328} 二元组通过协议告知 MySQL Server,MySQL 从这个位点开始发送数据:
1 | s1> ClickHouse 发送 {'mysql-bin.000002', 328} 位点信息给 MySQL |
看起来不错哦,但是有个问题:
如果 MySQL Server 是一个集群(比如1主2从),通过 VIP 对外服务,MaterializeMySQL 的 host 指向的是这个 vip。
当集群主从发生切换后,{binlog-name, binlog-position} 二元组其实是不准确的,因为集群里主从 binlog 不一定是完全一致的(binlog 可以做 reset 操作)。
1 | s1> ClickHouse 发送 {'mysql-bin.000002', 328} 给集群新主 MySQL |
为了解决这个问题,我们开发了 GTID 同步模式,废弃了不安全的位点同步模式,目前已被 upstream merged #PR13820,下一个 testing 版本即可体验。
着急的话可以自己编译或通过 ClickHouse Build Check for master-20.9.1 下载安装。
GTID 是 MySQL 复制增强版,从 MySQL 5.6 版本开始支持,目前已经是 MySQL 主流复制模式。
它为每个 event 分配一个全局唯一ID和序号,我们可以不用关心 MySQL 集群主从拓扑结构,直接告知 MySQL 这个 GTID 即可,.metadata变为:
1 | Version:2 |
f4aee41e-e36f-11ea-8b37-0242ac110002
是生成 event的主机UUID,1-5
是已经同步的event区间。
这样流程就变为:
1 | s1> ClickHouse 发送 GTID:f4aee41e-e36f-11ea-8b37-0242ac110002:1-5 给 MySQL |
那么,MySQL 侧怎么开启 GTID 呢?增加以下两个参数即可:
--gtid-mode=ON --enforce-gtid-consistency
比如启动一个启用 GTID 的 MySQL docker:
1 | docker run -d -e MYSQL_ROOT_PASSWORD=123 mysql:5.7 mysqld --datadir=/var/lib/mysql --server-id=1 --log-bin=/var/lib/mysql/mysql-bin.log --gtid-mode=ON --enforce-gtid-consistency |
启用 GTID 复制模式后,metadata Version 会变为 2,也就是老版本启动时会直接报错,database 需要重建。
MaterializeMySQL 引擎还处于不停迭代中,对于它我们有一个初步的规划:
稳定性保证
这块需要更多测试,更多试用反馈
索引优化
OLTP 索引一般不是为 OLAP 设计,目前索引转换还是依赖 MySQL 表结构,需要更加智能化
可观测性
在 ClickHouse 侧可以方便的查看当前同步信息,类似 MySQL show slave status
数据一致性校验
需要提供方式可以校验 MySQL 和 ClickHouse 数据一致性
MaterializeMySQL 已经是社区功能,仍然有不少的工作要做。期待更多的力量加入,我们的征途不止星辰大海。
]]>数据库系统为了提高写入性能,会把数据先写到内存,等“攒”到一定程度后再回写到磁盘,比如 MySQL 的 buffer pool 机制。
因为数据先写到内存,为了数据的安全性,我们需要一个 Write-Ahead Log (WAL) 来保证内存数据的安全性。
今天我们来看看 ClickHouse 新增的 MergeTreeWriteAheadLog 模块,它到底解决了什么问题。
对于 ClickHouse MergeTree 引擎,每次写入(即使1条数据)都会在磁盘生成一个分区目录(part),等着 merge 线程合并。
如果有多个客户端,每个客户端写入的数据量较少、次数较频繁的情况下,就会引发 DB::Exception: Too many parts
错误。
这样就对客户端有一定的要求,比如需要做 batch 写入。
或者,写入到 Buffer 引擎,定时的刷回 MergeTree,缺点是在宕机时可能会丢失数据。
我们先看看在没有 WAL 情况下,MergeTree 是如何写入的:
每次写入 MergeTree 都会直接在磁盘上创建分区目录,并生成分区数据,这种模式其实就是 WAL + 数据的融合。
很显然,这种模式不适合频繁写操作的情况,否则会生成非常多的分区目录和文件,引发 Too many parts
错误。
设置SETTINGS: min_rows_for_compact_part=2
,分别执行2条写 SQL,数据会先写到 wal.bin 文件:
当满足 min_rows_for_compact_part=2
后,merger 线程触发合并操作,生成 1_1_2_1
分区,也就是完成了 wal.bin 里的 1_1_1_0
和 1_2_2_0
两个分区的合并操作。当我们执行第三条 SQL 写入:
1 | insert into default.mt(a,b,c) values(1,3,3) |
数据块(分区)会继续追加到 wal.bin 尾部:
此时,3 条数据分布在两个地方:分区 1_1_2_1
, wal.bin 里的 1_3_3_0
。
这样就有一个问题:当我们执行查询的时候,数据是怎么合并的呢?
MergeTree 使用全局结构 data_parts_indexes
维护分区信息,当服务启动的时候, MergeTreeData::loadDataParts
方法:
1 | 1. data_parts_indexes.insert(1_1_2_1) |
这样,它总是能维护全局的分区信息。
WAL 功能在 PR#8290 实现,master 分支已经默认开启。
MergeTree 通过 WAL 来保护客户端的高频、少量写机制,减少服务端目录和文件数量,让客户端操作尽可能简单、高效。
]]>