Skip to main content

One post tagged with "engineering"

View All Tags

背景

时至今日,Databend 已经成长为一个大型、复杂、完备的数据库系统。团队维护着数十万行代码,每次发布十几个编译产物,并且还提供基于 Docker 的一些构建工具以改善开发者 / CI 的体验。

之前的文章介绍过 PGO ,用户可以根据自己的工作负载去调优 Databend 的编译。再早些时候,还有一篇介绍 Databend 开发环境和编译的文章。 对于 Databend 这样的中大型 Rust 程序而言,编译实在算不上是一件轻松的事情:

  • 一方面,在复杂的项目依赖和样板代码堆积之下,Rust 的编译时间显得不那么理想,前两年 Brian Anderson 的文章中也提到“Rust 糟糕的编译时间”这样的描述。
  • 另一方面,为了维护构建结果,不得不引入一些技巧来维护编译流水线的稳定,这并不是一件“一劳永逸”的事情,随着 Workflow 复杂性的提高,就不得不陷入循环之中。

为了优化编译体验,Databend 陆陆续续做过很多优化工作,今天的文章将会和大家一同回顾 Databend 中改善编译时间的一些优化。

可观测性

可观测性并不是直接作用于编译优化的手段,但可以帮助我们识别当前编译的瓶颈在什么地方,从而对症下药。

cargo build --timings

这一命令有助于可视化程序的编译过程。

在 1.59或更早版本时可以使用 cargo +nightly build -Ztimings

在浏览器中打开结果 HTML 可以看到一个甘特图,其中展示了程序中各个 crate 之间的依赖关系,以及程序的编译并行程度和代码生成量级。 通过观察图表,我们可以决定是否要提高某一模块的代码生成单元数目,或者要不要进一步拆解以优化整个编译流程。

cargo-depgraph

这个工具其实不太常用,但可以拿来分析依赖关系。有助于找到一些潜在的优化点,特别是需要替换某些同类依赖或者优化 crates 组织层次的时候。

无痛优化,从调整配置开始

改善编译体验的第一步其实并不是直接对代码动手术,很多时候,只需要变更少数几项配置,就能够获得很大程度上的改善。

Bump, Bump, Booooooooom

前面提到过 Rust 团队的成员们也很早就意识到,编译时间目前还并不理想。所以编译器团队同样会有计划去不断进行针对性的优化。经常可以看到在版本更新说明中有列出对编译的一些改进工作。

[toolchain]
channel = "nightly-2023-03-10"
components = ["rustfmt", "clippy", "rust-src", "miri"]

另外,上游项目同样可能会随着时间的推移去改善过去不合理的设计,很多时候这些改进也最终会反映在对编译的影响上。

一个改善编译时间的最简单的优化方式就是始终跟进上游的变更,并且秉着“上游优先”的理念去参与到生态建设之中。Databend 团队从一开始就是 Rust nightly 的忠实簇拥,并且为更新工具链和依赖关系提供了简明的指导。

缓存,转角遇到 sccache

缓存是一种常见的编译优化手段,思路也很简单,只需要把预先构建好的产物存储起来,在下次构建的时候继续拿过来用。

早期 Databend 使用 rust-cache 这个 action 在 CI 中加速缓存,获得了不错的效果。但是很遗憾,我们不得不经常手动更新 key 来清理缓存,以避免构建时的误判。而且,Rust 早期对增量构建的支持也很差劲,有那么一段时间可能会考虑如何配置流水线来进行一些权衡。

随着时间的推移,一切变得不同了起来。

首先是 Sccache 恢复了活力,而 OpenDAL 也成功打入其内部,成为支撑 Rust 编译缓存生态的重要组件,尽管在本地构建时使用它常常无法展现出真正的威力,但是放在 CI 中,还是能够带来很大惊喜的。

另一个重要的改变是,Rust 社区意识到增量编译对于 CI 来讲并不能很好 Work 。

CI builds often are closer to from-scratch builds, as changes are typically much bigger than from a local edit-compile cycle. For from-scratch builds, incremental adds an extra dependency-tracking overhead. It also significantly increases the amount of IO and the size of ./target, which make caching less effective.

轻装上阵,将冷气传递给每一个依赖

Rust 生态里面有一个很有意思的项目是 https://github.com/mTvare6/hello-world.rs ,它尽可能给你展现了如何让一个 Rust 项目变得尽可能糟糕。

特别是:

in a few lines of code with few(1092) dependencies

Rust 自身是不太能很好自动处理这一点的,它需要把所有依赖一股脑下载下来编译一通。所以避免无用依赖的引入就成为一件必要的事情了。

最开始的时候,Databend 引入 cargo-udeps 来检查无用的依赖,大多数时候都工作很良好,但最大的缺点在于,每次使用它检查依赖就相当于要编译一遍,在 CI 中无疑是不划算的。

sundy-li 发现了另外一个快速好用的工具,叫做 cargo-machete 。

一个显著的优点是它很快,因为一切只需要简单的正则表达式来处理。而且也支持了自动修复,这意味着我们不再需要挨个检索文件再去编辑。

不过 machete 并不是完美的工具,由于只是进行简单的正则处理,有一些情况无法准确识别,不过 ignore 就好了,总体上性价比还是很高的。

稀疏索引

为了确定 crates.io 上存在哪些 crates,Cargo 需要下载并读取 crates.io-index ,该索引位于托管在 GitHub 上的 git 存储库中,其中列出了所有 crates 的所有版本。

然而,随着时间推移,由于索引已经大幅增长,初始获取和更新变得很慢。RFC 2789 引入了稀疏索引来改进 Cargo 访问索引的方式,并使用 https://index.crates.io/ 进行托管。

[registries.crates-io]
protocol = "sparse"

linker

如果项目比较大,而且依赖繁多,那么可能在链接时间上会比较浪费。特别是在你只改了几行代码,但编译却花了很久的时候。

最简单的办法就是选择比默认链接器更快的链接器。

lld 或者 mold 都可以改善链接时间,Databend 最后选择使用 mold 。其实在 Databend 这个量级的程序上,两个链接器的差距并不明显,但是,使用 mold 的一个潜在好处是能够节约一部分编译时候消耗的内存。

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=/path/to/mold"]

编译相关配置

先看一个常见的 split-debuginfo,在 MacOS 上,rustc 会运行一个名为 dsymutil 的工具,该工具会分析二进制文件,然后构建调试信息目录。配置 split-debuginfo,可以跳过 dsymutil ,从而加快构建速度。

split-debuginfo = "unpacked"

另外的一个例子是 codegen-units,Databend 在编译时使用 codegen-units = 1 来增强优化,并且克制二进制体积大小。但是考虑到部分依赖在编译时会有特别长的代码生成时间(因为重度依赖宏),所以需要针对性放开一些限制。

[profile.release.package]
arrow2 = { codegen-units = 4 }
common-functions = { codegen-units = 16 }
databend-query = { codegen-units = 4 }
databend-binaries = { codegen-units = 4 }

重新思考,更合理的代码组织

前面是一些配置上的调整,接下来将会探讨重构对代码编译时间的一些影响。

拆分到更合理的 crates 规模

对于一个大型的 All in One 式的 Crate 而言,拆分 crates 算是一个比较有收益的重构。一方面可以显著改善并行度。另一方面,通过解耦交叉依赖/循环依赖,可以帮助 Rust 更快地处理代码编译。

同时,还有一个潜在的好处,就是拆分以后,由于代码的边界更为清晰,维护起来也会省力一些。

单元式测试与集成式测试的界限

单元测试的常见组织形式包括在 src 中维护 tests mod ,和在 tests 目录下维护对应的测试代码。

根据 Delete Cargo Integration Tests 的建议,Databend 很早就从代码中剥离了所有的单元测试,并组织成类似这样的形式

tests/
it/
main.rs
foo.rs
bar.rs

这种形式避免将 tests/ 下面的每个文件都编译成一个单独的二进制文件,从而减轻对编译时间的影响。

另外,Rust 编译时处理 tests mod 和 docs tests 也需要花费大量时间,特别是 docs tests 还需要另外构建目标,在采用上面的组织形式之后,就可以在配置中关掉。

但是,这种形式并不十分优雅,不得不为所有需要测试的内容设置成 public ,容易破坏代码之间的模块化组织,在使用前建议进行深入评估。

更优雅的测试方法

对应到编译时间上,可以简单认为,单元测试里需要编译的代码越多,编译时间自然就会越慢。

另外,对于 Databend 而言,有相当一部分测试都是对输入输出的端到端测试,如果硬编码在单元测试中需要增加更多额外的格式相关的工作,维护也会比较费力。

Databend 巧妙运用 golden files 测试和 SQL logic 测试,替换了大量内嵌在单元测试中的 SQL 查询测试和输出结果检查,从而进一步改善了编译时间。

遗珠之憾

cargo nextest

cargo nextest 让测试也可以快如闪电,并且提供更精细的统计和优雅的视图。Rust 社区中有不少项目通过引入 cargo nextest 大幅改善测试流水线的时间。

但 Databend 目前还无法迁移到这个工具上。一方面,配置相关的测试暂时还不被支持,如果再针对去单独跑 cargo test 还要重新编译。另一方面,有一部分与超时相关的测试设定了执行时间,必须等待执行完成。

cargo hakari

改善依赖项的编译,典型的例子其实是 workspace-hack ,将重要的公共依赖放在一个目录下,这样这些依赖就不需要反复编译了。Rust 社区中的 cargo-hakari,可以用来自动化管理 workspace-hack 。

Databend 这边则是由于有大量的 common 组件,主要二进制程序都建立在 common 组件上,暗中符合这一优化思路。另外,随着 workspace 支持依赖继承之后,维护压力也得到减轻。

总结

这篇文章介绍了 Databend 团队在改善 Rust 项目编译时间上做的一些探索和努力,从配置优化和代码重构这两个角度,提供了一些能够优化编译时间的一些建议。

参考资料

尊敬的 Databenders,在 Databend Labs 成立两周年之际,我们非常高兴地宣布 Databend v1.0 正式发布。

Databend 社区一直在致力于解决大数据分析的成本和复杂度问题,并正在被顶级场景和顶级需求所推动。 根据可统计信息,每天约 700TB 数据在使用 Databend 写入到云对象存储并进行分析,用户来自欧洲、北美、东南亚、非洲、中国等地,每月为他们节省数百万美元成本。 Databend v1.0 是一个具有里程碑意义的版本,我们相信它将进一步加速云端海量数据分析的发展。

今天,我将首先介绍 Databend v1.0 相比 v0.9 版本所做的改进,然后探讨我们团队的愿景和未来展望。现在就让我们开始吧!

v1.0 改进

Databend 在版本 v1.0 中实现了惊人的性能提升,在 ClickBench 测试中获得:数据加载第一名,在查询环节,c6a.4xlarge 第一名,c5a.4xlarge 第二名,c6a.metal 第三名

此外,Databend 社区还在版本 v1.0 中推出了多项新功能:

UPDATE

现在,用户可以使用 UPDATE 语句来更新 Databend 中的数据。

更新语句的格式如下:

-- Update a book (Id: 103)
UPDATE bookstore SET book_name='The long answer (2nd)' WHERE book_id=103;

通过支持 UPDATE 功能,Databend 实现了对 CRUD 操作的完整支持。

ALTER TABLE

在 v1.0 中,用户可以使用 ALTER TABLE 来修改 Databend 中的表结构:

-- Add a column
ALTER TABLE t ADD COLUMN c Int DEFAULT 10;

DECIMAL

在完成了 Databend 类型系统的大型重构之后,社区在一个坚实的基础上实现了 DECIMAL 数据类型的支持!

-- Create a table with decimal data type.
create table tb_decimal(c1 decimal(36, 18));

-- Insert two values.
insert into tb_decimal values(0.152587668674722117), (0.017820781941443176);

select * from tb_decimal;
+----------------------+
| c1 |
+----------------------+
| 0.152587668674722117 |
| 0.017820781941443176 |
+----------------------+

Native Format

在 v0.9 版本中引入的 Native Format strawboat 得到了进一步的完善!社区为 strawboat 增加了半结构化数据的支持,并引入了多项性能优化,帮助 Databend 在 HITS 数据集的性能取得了巨大提升。

CBO

引入了直方图框架,可以利用统计信息更为精确地进行代价估算。进一步完善和强化 join reorder 算法,从而大大的提高多表 join 的性能,帮助 Databend 在 TPCH 数据集上的性能取得显著提升。

SELECT FROM STAGE

STAGE 是 Databend 数据流转的核心。我们之前已经支持从 STAGE 中加载数据和向 STAGE 中导出数据,现在我们更进一步,支持了直接在 STAGE 中进行数据查询!

用户只需要为 Databend 创建一个包含数据文件的 STAGE,就可以轻松进行数据查询,无需编写复杂的建表语句或繁琐的数据导入流程。

select min(number), max(number) from @lake (pattern => '.*parquet');
+-------------+-------------+
| min(number) | max(number) |
+-------------+-------------+
| 0 | 9 |
+-------------+-------------+

如果用户只需要进行一次性的查询,还可以直接使用更简短的 URI 形式:

select count(*), author
from 'https://datafuse-1253727613.cos.ap-hongkong.myqcloud.com/data/books.parquet' (file_format => 'parquet')
group by author;
+----------+---------------------+
| count(*) | author |
+----------+---------------------+
| 1 | Jim Gray |
| 1 | Michael Stonebraker |
+----------+---------------------+

Query Result Cache

在 v1.0 版本中,Databend 社区借鉴了 ClickHouse 社区的设计,并增加了 Query Result Cache 功能。当底层数据没有发生变化时,执行相同的查询会命中缓存,避免了重复执行查询的过程。

MySQL [(none)]> SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) 
FROM hits
GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10;
+--------------------+-------------+-----+----------------+----------------------+
| watchid | clientip | c | sum(isrefresh) | avg(resolutionwidth) |
+--------------------+-------------+-----+----------------+----------------------+
| 6655575552203051303| 1611957945 | 2 | 0 | 1638.0 |
| 8566928176839891583| -1402644643 | 2 | 0 | 1368.0 |
| 7904046282518428963| 1509330109 | 2 | 0 | 1368.0 |
| 7224410078130478461| -776509581 | 2 | 0 | 1368.0 |
| 5957995970499767542| 1311505962 | 1 | 0 | 1368.0 |
| 5295730445754781367| 1398621605 | 1 | 0 | 1917.0 |
| 8635802783983293129| 900266514 | 1 | 1 | 1638.0 |
| 5650467702003458413| 1358200733 | 1 | 0 | 1368.0 |
| 6470882100682188891| -1911689457 | 1 | 0 | 1996.0 |
| 6475474889432602205| 1501294204 | 1 | 0 | 1368.0 |
+--------------------+-------------+-----+----------------+---------------------+
10 rows in set (3.255 sec)

MySQL [(none)]> SELECT
WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth)
FROM hits
GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10;

+---------------------+-------------+------+----------------+--------------+
| watchid | clientip | c | sum(isrefresh)| avg(resolutionwidth) |
+---------------------+-------------+------+----------------+-------------+
| 6655575552203051303 | 1611957945 | 2 | 0| 1638.0 |
| 8566928176839891583 | -1402644643 | 2 | 0| 1368.0 |
| 7904046282518428963 | 1509330109 | 2 | 0| 1368.0 |
| 7224410078130478461 | -776509581 | 2 | 0| 1368.0 |
| 5957995970499767542 | 1311505962 | 1 | 0| 1368.0 |
| 5295730445754781367 | 1398621605 | 1 | 0| 1917.0 |
| 8635802783983293129 | 900266514 | 1 | 1| 1638.0 |
| 5650467702003458413 | 1358200733 | 1 | 0| 1368.0 |
| 6470882100682188891 | -1911689457 | 1 | 0| 1996.0 |
| 6475474889432602205 | 1501294204 | 1 | 0| 1368.0 |
+---------------------+-------------+---+---------------+-----------------+
10 rows in set (0.066 sec)

Table Data Cache

缓存是存算分离架构中的重要组成部分。在 v1.0 版本中,Databend 社区为我们带来了 Table Data Cache!当 Databend 执行查询时,会根据访问数据的热度情况决定是否将该数据块保存到缓存中,以加速下一次访问。

Aggregate Spill

在 v1.0 版本中,Databend 引入了 Aggregate spill, 当在 Databend 中执行聚合查询时,会根据 Databend 当前的内存使用情况动态决定将内存中的聚合数据临时保存并持久化到对象存储中,防止查询过程中使用过高的内存。

未来展望

经过这些版本的打磨,Databend 终于有了一个雏形。现在,让我们重新认识一下 Databend:

  • 一个使用 Rust 开发的云原生数据仓库:存算分离,面向对象存储设计,极致弹性

  • 支持完整的 CRUD 特性,提供了 MySQL/Clickhouse/HTTP RESTful 等协议支持

  • 提供原生的 ARRAY、MAP、JSON 等复杂类型和 DECIMAL 高精度类型支持

  • 构建了类似于 Git 的 MVCC 列式存储引擎,支持 Data Time Travel 和 Data Share 能力

  • 不受存储供应商的限制,可以在任何存储服务上运行,并直接查询任何存储服务上的数据

  • 目前已全面支持 HDFS/Cloud-Based Object Storage 协议,包括:阿里云 OSS,腾讯云 COS,华为云 OBS,以及 S3,Azure Blob, Google Cloud Storage

Databend 的征程远远不止于此,在未来我们希望 Databend 能拥有:

更强大的功能

在紧随其后的 v1.1 版本中,我们希望实现如下功能:

  • JSON 索引:提高半结构化数据检索能力

  • 分布式 Ingest 能力:提高数据写入速度

  • MERGE INTO 功能:实现数据源增、删、改的实时 CDC 能力

  • Windows Function

我们希望这些功能能进一步满足用户的需求,并且实现 Databend 在 CDC 场景下的突破。

更开放的社区

Databend Labs 由一群开源爱好者组成,Databend 项目从创建之初就是采用 Apache 2.0 协议授权的开源项目。在借鉴和吸收 ClickHouse,CockroachDB 等开源项目优秀思想的同时,我们也在以自己的方式回馈社区:

  • 开源了 Databend 元数据服务集群的共识引擎 Openraft

  • 向 Apache 软件基金会捐赠了底层的数据访问引擎 OpenDAL 并成功进入孵化器开始孵化

  • 成为向量计算基础库 arrow2 等多个依赖项目的贡献者

  • 跟进并采用 Rust Nightly,帮助 Rust 社区复现并验证问题

没有开源社区就没有今天的 Databend,感谢 144 个参与 Databend 的贡献者!接下来,我们将更开放地与其他开源社区合作,支持读写 IcebergDelta Lake 等格式,打破数据间的壁垒,使数据能够更自由灵活地流转。


感谢大家!

关于 Databend

Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。

Databend Cloud: https://databend.cn

Databend 文档:https://databend.rs/

Wechat:Databend

GitHub: https://github.com/datafuselabs/databend

grafana-display-log-on-databend

本篇文章以 Grafana 展示 Databend 中 Nginx Log。

在 databend 新建 grafana 用户

  1. 先连接到 databend

    ❯ bendsql connect
    Connected to Databend on Host: localhost
    Version: DatabendQuery v0.9.46-nightly-67b0fe6(rust-1.68.0-nightly-2023-02-22T03:47:09.491571Z)
    ❯ bendsql query
    Connected with driver databend (DatabendQuery v0.9.46-nightly-67b0fe6(rust-1.68.0-nightly-2023-02-22T03:47:09.491571Z))Type "help" for help.
    dd:root@localhost/default=>
  2. 创建用户并赋予权限

    CREATE USER grafana IDENTIFIED BY 'grafana_password';
    GRANT SELECT ON *.* TO grafana;

安装 grafana 并配置数据源

  1. 打开 grafana 插件页面,并搜索Altinity plugin for ClickHouse 安装

  1. 用刚刚安装的插件新建数据源,并配置接口和用户名密码

  1. 保存并测试数据源

使用数据源

我们使用一个已经有的 nginx log 表来进行可视化

CREATE TABLE `access_logs` (  
`timestamp` TIMESTAMP,
`client` VARCHAR,
`method` VARCHAR,
`path` VARCHAR,
`protocol` VARCHAR,
`status` INT,
`size` INT,
`referer` VARCHAR,
`agent` VARCHAR,
`request` VARCHAR
);
  1. 新建 dashboard 和 panel,选择刚刚创建的数据源,选择 database 和 table,点击 Go to Query

  1. 输入可视化查询
SELECT 
(to_int64(timestamp) div 1000000 div $interval * $interval) * 1000 as t,
status,
count() as qps
FROM $table
WHERE timestamp >= to_datetime($from)
AND timestamp <= to_datetime($to)GROUP BY t, statusORDER BY t

常用宏参考:

  • $interval 在 panel 配置 Query Options 里选择的 interval

  • $table 新建 panel 时选择的 database 和 table

  • $from 在 grafana UI 上选择的时间范围 (单位为 ms)

  • $to 在 grafana UI 上选择的时间范围 (单位为 ms)

  1. 查看效果

按上述步骤多添加几个 panel 之后查看整体效果:

Connect With Us

Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。

databend-on-top-clickbench-benchmark

经历近两年的打磨,Databend 终于要发 1.0 版本了!我们深知从零开始构建数据库是一段非常艰辛的历程。从两年前的第一行代码到现在的 30 万行 Rust 代码,记录了 Databenders 小伙伴们的不懈付出。

许多人都非常关注我们是如何从零开始构建数据库的。相比于其他基于成熟底层技术栈的数据库产品,我们的性能表现如何?今天我们来基于最新版本来测试体验下 Databend 的优秀性能。

考虑到市场上已经有很多优秀的 OLAP 数据库,我们希望使用一个公开的标准来衡量 Databend 的性能,而不是自己建立测试标准。因此,我们选择了 ClickBench[1],这是由 ClickHouse 发起的分析型数据库性能测试排行榜,测试数据集为 hits,来自生产环境,使用 clickhouse-obfuscator 工具进行了混淆处理,覆盖了常见的多维报表查询,能够真实地反映各类数据库在生产环境下的性能比较。这个排行榜收录了 Snowflake、Doris、ClickHouse、MySQL、Greenplum、DuckDB 等 50 个主流数据库的测试结果。

评测方式是在特定的机型下,将近 70G 的原始数据导入到数据库中,并对比数据的导入时间。接着,对 43 个查询 SQL 进行测试,在每次执行 SQL 之前清空内存缓存,然后重复执行三次,并记录三次执行中耗时最短的一次。ClickBench 考察的是数据库在所有测试场景下的总体表现,因此会将所有数据库在所有场景的评分结果进行对比计算,得出总排名。

为了展现 Databend 的真实性能,我们没有针对测试场景进行特别优化:

  • 全部使用默认配置,不进行任何参数调优

  • 不对数据进行 特定字段分区导入,使用默认建表语句

  • 不对原始数据进行 Cache,也不对查询结果进行 Cache,只 Cache 元数据和索引

我们提交了最常见的三种机型的测试结果:

  • c6a.metal, 500gb gp2 (192core)

  • c6a.4xlarge, 500gb gp2 (16core)

  • c5.4xlarge, 500gb gp2 (16core)

1 导入性能, 三个机型下均排行第一

特别是在 c6a.metal 机型上,我们的导入性能仅需要 70 秒。与其他数据库的性能表现相比,可以看到 Databend 的导入性能有巨大的优势。这主要得益于 Databend 在向量化并行导入下的极致优化,以及 pipeline 在计算和 io 之间的优秀调度能力。

此外,我们的底层 Storage Access 模块都基于 OpenDAL[2] ,它具有简洁的 API 设计,同时不失原生 API 的性能,覆盖了 s3、fs、memory、ftp、azblob、ipfs 等十多种后端存储。许多优秀的 Rust 项目,如 RisingWave、Greptimedb、Sscache 等,都采用了 OpenDAL 作为底层 IO 模块。目前,OpenDAL 即将进入 Apache 孵化器,旨在打造云原生的统一存储底座。

2 查询性能,三个机型下各居一二三

在 hot run 查询下,Databend 在 c6a.4xlarge 上表现出色,排名第一;在 c5.4xlarge 微弱劣势居第二,c6a.metal 居第三。

得益于 Databend 新的表达式系统设计[3],所有的算子都已经实现了向量化,并且我们所有的算子都有基于 Domain 值域推导能力。基于此,我们可以应用强大的常量折叠框架来做数据多级裁剪,尽可能略过不必要的数据块。此外,pipeline 的调度能力以及聚合算子的功能也再次加强,使得 CPU 和 IO 能够高效调度,从而发挥极致性能。

Databend 是面向对象存储而设计的新一代云原生数仓, 并没有对本地 fs 场景做太多优化,在上面榜单中,前三的差距其实并不是很大。因此,在不太擅长的场景中,结合高性能的计算能力,Databend 也可以取得不错的优势。

由于 Databend 使用了默认配置,禁用了 DataCache,因此,在 cold run 场景下,对比意义不大。我们也可以对建表语句做一些针对性的优化(例如,按 UserID 分区导入,优化 Q17 等按 UserId 聚合场景),或者加入一些参数调优,这样性能估计还能再提升一个档次,但面向 Benchmark 调优并不是这次测试的主要意义,以用户为中心,在通用场景带给用户极致的性能、易用的产品体验才是我们的终极目标。

做 Benchmark 主要为了让我们有个衡量性能的方式,从而提升产品质量,ClickBench 测试非常具有代表意义,所以我们将 ClickBench 集成到了各个版本和 PR 的性能测试 CI 中[4],方便开发者观测性能回退和提升,优化产品开发。

3 更多

登顶 ClickBench 证明了 Databend 的导入查询性能已经取得非常优异的成绩, 从零用 Rust 实现的数据库不一定比 C++ 慢,两年时间我们也可以做出别人十年的成就。 但这只是我们前进的一小步。性能并不能完全体现数据库的全部优点,但它始终是我们数据库内核开发者心中的一个目标。

在 v1.0 即将发布之际,希望有更多的开源爱好者加入 Databend 社区,和优秀的开发者一起协作,打造世界一流的数据库产品!让我们一起携手努力,为数据库技术的进步和发展贡献力量!

4 引用

[1] ClickBench: https://benchmark.clickhouse.com/

[2] OpenDAL: https://github.com/datafuselabs/opendal

[3] Databend 新的表达式系统设计: https://github.com/datafuselabs/databend/pull/9411

[4] PR 的性能测试 CI 中: https://repo.databend.rs/benchmark/clickbench/release/index.html

Connect With Us

Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。

mysql-databend-diff

Databend 提供了 MySQL 协议的兼容,但实质上和 MySQL 还有一定的区别。Databend 定位在基于对象存储实现一个真正的存算分离的弹性数仓。现在也在有很多朋友利用 Databend 做 MySQL 和 RDS MySQL 的归档存储分析。

这里给大家介绍一下 Databend 数据类型,索引,DDL,协议 几方面和 MySQL 的区别,其中文章最后一条可能减少你使用很多麻烦,以下详细内容供大家参考:

1. 数据类型

TypeDatabendMySQL
tinyintYesYes
smallintYesYes
mediumintNoYes
intYesYes
bigintYesYes

2. 浮点类型

TypeDatabendMySQL
decimalYesYes
floatYesYes
doubleYesYes

3. 日期类型

TypeDatabendMySQL
datetimeYesYes
timestampYesYes
dateNoYes
timeNoYes
yearNoYes

在 Databend 中 datetime 实质上是 timstamp 的同义词,现在支持 6 位精度:YYYY-MM-DD hh:mm:ss[.fraction]

4. 字符类型

TypeDatabendMySQL
varcharYesYes
stringYesNo
binaryNoYes
varbinaryNoYes
blogNoYes
enumNoYes
setNoYes

在 Databend 中 string 是 Varchar 的同义词,另外使用需要注意在 Databend 声明 Varchar 不需要声明长度,存储按实际长度存储

5. JSON 类型

TypeDatabendMySQL
jsonYesYes
variantYesNo

Databend 中 json 基于 jsonb 实现,函数上和 MySQL 不一样,参考:https://databend.rs/doc/reference/functions/semi-structured-functions

json 格式建议只是使用在数据清洗过程

6. 嵌套类型

TypeDatabendMySQL
arrayYesNo
tupleYesNo

Databend 主要定位在大数据解环境,对于数据格式支持更加利于使用一点。后续马上会加一个 map 类型。

7. 其它数据类型

TypeDatabendMySQL
bitNoYes
booleanYesNo

如你用 MySQL 的习惯使用 Databend 需要别小心 Databend 的 Boolean 类型,MySQL 没有 Boolean 类型,一般是使用 tinyint 中的 0 和 1 表示。

8. 索引上区别

在 Databend 中不用定义索引,默认情况下每一列都自带 min/max, bloom index 索引,在 Databend 中也没有唯一约束,外键等。这里使用上也需注意一下。

9. DDL 支持

目前 Databend 已经支持无 Block 实现 alter table 操作。

参考:https://databend.rs/doc/sql-commands/ddl/table/alter-table-column

10. 协议和一些细节

Databend 支持 MySQL 协议 和 Clickhouse HTTP 协议,同时也支持 HTTP Restful API 设计。

参考:https://databend.rs/doc/reference/api

Databend 在双引号和单引号这块参考了 PostgreSQL 明确约束,如果你是 MySQL 使用的风格的用户可以通过:

set global sql_dialect='MySQL';

把 SQL 会话习惯更改成:MySQL 风格。

Databend 默认时区是:timezone= UTC,如果你在国内使用可以通过:

 set global timezone='Asia/Shanghai';

小结

现在 Databend 和 MySQL 结构的场景

  • 使用 Databend 使用对象存储的成本优势担任 MySQL 的数据归档和分析

  • 使用 Databend 担任 MySQL 的离线 AP 库

  • 使用 Databend 把线上的分库分表的库合并到一起

Connect With Us

Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。

Alt text

在介绍 给 Databend 添加 Scalar 函数 | 函数开发系例一 后,我们来看 Aggregate Function。

Aggregate Function 用于对列的值进行操作并返回单个值。常见的 Agg Function 有 sum, count, avg 等。

函数注册

Agg Function 的 register 函数以小写的函数名和 AggregateFunctionDescription 类型为参数,每个被注册的函数都存入 case_insensitive_desc ( HashMap 结构) 中。

case_insensitive_combinator_desc 是为了存储一些组合函数,比如与 _if 组合的 count_if, sum_if 这类函数。

pub struct AggregateFunctionFactory {
    case_insensitive_desc: HashMap<String, AggregateFunctionDescription>,
    case_insensitive_combinator_desc: Vec<(String, CombinatorDescription)>,
}
impl AggregateFunctionFactory {
  ...
pub fn register(&mut self, name: &str, desc: AggregateFunctionDescription) {
        let case_insensitive_desc = &mut self.case_insensitive_desc;
        case_insensitive_desc.insert(name.to_lowercase(), desc);
    }
  ...
}

每个被注册的函数都要实现 trait AggregateFunctionAggregateFunctionFeatures。其中 AggregateFunctionFeaturesScalar 中的 FunctionProperty 比较类似,都是存储函数的一些特质。

pub type AggregateFunctionRef = Arc<dyn AggregateFunction>;
pub type AggregateFunctionCreator =
    Box<dyn Fn(&str, Vec<Scalar>, Vec<DataType>) -> Result<AggregateFunctionRef> + Sync + Send>;
pub struct AggregateFunctionDescription {
    pub(crate) aggregate_function_creator: AggregateFunctionCreator,
    pub(crate) features: AggregateFunctionFeatures,
}

主要来看 trait AggregateFunction,这里面是 Agg Function 的构成。

函数构成

可以看到与 Scalar 直接使用一个 Struct 不同,AggregateFunction 是一个 trait。因为聚合函数是按 block 累加列中的数据,再累加过程中会产生一些中间结果。

因此 Aggregate Function 必须有初始状态,而且聚合过程中生成的结果也要是 mergeable (可合并) 和 serializable (可序列化) 的。

主要函数有:

  • name 表示被注册的函数的名字,比如 avg, sum 等等。
  • return_type 表示被注册的函数返回值的类型,同样的函数返回值可能会由于参数类型的不同而产生变化。比如 sum(int8) 参数为 i8 类型,但是返回返回值可能是 int64。
  • init_state 用来初始化聚合函数状态。
  • state_layout 用来表示 state 在内存中的大小和内存块的排列方式。
  • accumulate 用于 SingleStateAggregator。也就是着整个块可以在单个状态下聚合,没有任何 keys。比如 select count(*) from t 此时查询中没有任何分组列的聚合,这时会调度 accumulate 函数。
  • accumulate_keys 则是用于 PartialAggregator。这里需要考虑 key 和 offset,每个 key 代表一个唯一的内存地址,记为函数参数 place。
  • serialize 将聚合过程中的 state 序列化为二进制。
  • deserialize 从二进制反序列化为 state
  • merge 用于合并其他 state 到当前 state
  • merge_result 可以将 Aggregate Function state 合并成单个值。

示例

以 avg 为例

具体实现在 aggregate_avg.rs 中。

因为我们需要累加每个值,并除以非 null 总行数。因此 avg function 被定义为 struct AggregateAvgFunction<T, SumT>。其中 T 和 SumT 是实现 Number 的逻辑类型。

在聚合过程中 avg 会产生的中间状态值是 已经累加的值的总和 以及 已经扫描过的非 null 的行。因此 AggregateAvgState 可以被定义为如下结构。

#[derive(Serialize, Deserialize)]
struct AggregateAvgState<T: Number> {
    #[serde(bound(deserialize = "T: DeserializeOwned"))]
    pub value: T,
    pub count: u64,
}
  • return_type 设置为 Float64Type。比如 value = 3, count = 2, avg = value/count。
  • init_state 初始状态设置 value 为 T 的 default 值,count 为 0。
  • accumulate AggregateAvgState 的 count, value 分别对 block 中非 NULL 的行数和值进行累加。
  • accumulate_keys 通过 place.get::<AggregateAvgState<SumT>>() 获取对应的状态值,并进行更新。
fn accumulate_keys(
    &self,
    places: &[StateAddr],
    offset: usize,
    columns: &[Column],
    _input_rows: usize,
) -> Result<()> {
    let darray = NumberType::<T>::try_downcast_column(&columns[0]).unwrap();
    darray.iter().zip(places.iter()).for_each(|(c, place)| {
        let place = place.next(offset);
        let state = place.get::<AggregateAvgState<SumT>>();
        state.add(c.as_(), 1);
    });
    Ok(())
}

类似的聚合函数示例也可以参考 sum 和 count 的实现:

函数测试

Unit Test

聚合函数相关单元测试在 agg.rs 中。

Logic Test

Functions 相关的 logic 测试在 tests/logictest/suites/base/02_function/ 中。

关于 Databend

Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。

Alt text

在 Databend 中按函数实现分为了:scalars 函数和 aggregates 函数。

Scalar 函数: 基于输入值,返回单个值。常见的 Scalar function 有 now, round 等。

Aggregate 函数: 用于对列的值进行操作并返回单个值。常见的 Agg function 有 sum, count, avg 等。

https://github.com/datafuselabs/databend/tree/main/src/query/functions/src

该系列共两篇,本文主要介绍 Scalar Function 从注册到执行是如何在 Databend 运行起来的。

函数注册

由 FunctionRegistry 接管函数注册。

#[derive(Default)]
pub struct FunctionRegistry {
    pub funcs: HashMap<&'static str, Vec<Arc<Function>>>,
    #[allow(clippy::type_complexity)]
    pub factories: HashMap<
        &'static str,
        Vec<Box<dyn Fn(&[usize], &[DataType]) -> Option<Arc<Function>> + 'static>>,
    >,
    pub aliases: HashMap<&'static str, &'static str>,
}

三个 item 都是 Hashmap。

其中,funcs 和 factories 都用来存储被注册的函数。不同之处在于 funcs 注册的都是固定参数个数的函数(目前支持最少参数个数为 0,最多参数个数为 5),分为 register_0_arg, register_1_arg 等等。而 factories 注册的都是参数不定长的函数(如 concat),调用 register_function_factory 函数。

由于一个函数可能有多个别名(如 minus 的别名有 subtract 和 neg),因此有了 alias,它的 key 是某个函数的别名,v 是当前的存在的函数名,调用 register_aliases 函数。

另外,根据不同的功能需求,我们提供了不同级别的 register api。

640.png

函数构成

已知 funcs 的 value 是函数主体,我们来看一下 Function 在 Databend 中是怎么构建的。

pub struct Function {
    pub signature: FunctionSignature,
    #[allow(clippy::type_complexity)]
    pub calc_domain: Box<dyn Fn(&[Domain]) -> Option<Domain>>,
    #[allow(clippy::type_complexity)]
    pub eval: Box<dyn Fn(&[ValueRef<AnyType>], FunctionContext) -> Result<Value<AnyType>, String>>,
}

其中,signature 包括 函数名,参数类型,返回类型以及函数特性(目前暂未有函数使用特性,仅作为保留位)。要特别注意的是,在注册时函数名需要是小写。而一些 token 会经过 src/query/ast/src/parser/token.rs 转换。

#[allow(non_camel_case_types)]
#[derive(Logos, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TokenKind {
    ...
    #[token("+")]
    Plus,
    ...
}

以实现 `select 1+2` 的加法函数为例子,`+` 被转换为 Plus,而函数名需要小写,因此我们在注册时函数名使用 `plus`

with_number_mapped_type!(|NUM_TYPE| match left {
    NumberDataType::NUM_TYPE => {
        registry.register_1_arg::<NumberType<NUM_TYPE>, NumberType<NUM_TYPE>, _, _>(
            "plus",
            FunctionProperty::default(),
            |lhs| Some(lhs.clone()),
            |a, _| a,
        );
    }
});

calc_domain 用来计算输出值的输入值的集合。用数学公式描述的话比如 `y = f(x)` 其中域就是 x 值的集合,可以作为 f 的参数生成 y 值。这可以使我们在索引数据时轻松过滤掉不在域内的值,极大提升响应效率。

eval 可以理解成函数的具体实现内容。本质是接受一些字符或者数字,将他们解析成表达式,再转换成另外一组值。

示例

目前在 function-v2 中实现的函数有这几类:arithmetric, array, boolean, control, comparison, datetime, math, string, string_mult_args, variant

以 length 的实现为例:

length 接受一个 String 类型的值为参数,返回一个 Number 类型。名字为 length,domain 不做限制(因为任何 string 都有长度)最后一个参数是一个闭包函数,作为 length 的 eval 实现部分。

registry.register_1_arg::<StringType, NumberType<u64>, _, _>(
    "length",
    FunctionProperty::default(),
    |_| None,
    |val, _| val.len() as u64,
);

在 register_1_arg 的实现中,我们看到调用的函数是 register_passthrough_nullable_1_arg,函数名包含一个 nullable。而 eval 被 vectorize_1_arg 调用。

注意:请不要手动修改 register_1_arg 所在的文件 src/query/expression/src/register.rs 。因为它是被 src/query/codegen/src/writes/register.rs 生成的。

pub fn register_1_arg<I1: ArgType, O: ArgType, F, G>(
    &mut self,
    name: &'static str,
    property: FunctionProperty,
    calc_domain: F,
    func: G,
) where
    F: Fn(&I1::Domain) -> Option<O::Domain> + 'static + Clone + Copy,
    G: Fn(I1::ScalarRef<'_>, FunctionContext) -> O::Scalar + 'static + Clone + Copy,
{
    self.register_passthrough_nullable_1_arg::<I1, O, _, _>(
        name,
        property,
        calc_domain,
        vectorize_1_arg(func),
    )
}

这是因为 eval 在实际应用场景中接受的不只是字符或者数字,还可能是 null 或者其他各种类型。而 null 无疑是最特殊的一种。而我们接收的参数也可能是一个列或者一个值。比如

select length(null);
+--------------+
| length(null) |
+--------------+
|         NULL |
+--------------+
select length(id) from t;
+------------+
| length(id) |
+------------+
|          2 |
|          3 |
+------------+

基于此,如果我们在函数中无需对 null 类型的值做特殊处理,直接使用 register_x_arg 即可。如果需要对 null 类型做特殊处理,参考 try_to_timestamp

而对于需要在 vectorize 中进行特化的函数则需要调用 register_passthrough_nullable_x_arg,对要实现的函数进行特定的向量化优化。

例如 comparison 函数 regexp 的实现:regexp 接收两个 String 类型的值,返回 Bool 值。在向量化执行中,为了进一步优化减少重复正则表达式的解析,引入了 HashMap 结构。因此单独实现了 `vectorize_regexp`

registry.register_passthrough_nullable_2_arg::<StringType, StringType, BooleanType, _, _>(
    "regexp",
    FunctionProperty::default(),
    |_, _| None,
    vectorize_regexp(|str, pat, map, _| {
        let pattern = if let Some(pattern) = map.get(pat) {
            pattern
        } else {
            let re = regexp::build_regexp_from_pattern("regexp", pat, None)?;
            map.insert(pat.to_vec(), re);
            map.get(pat).unwrap()
        };
        Ok(pattern.is_match(str))
    }),
);

函数测试

Unit Test

函数相关单元测试在 scalars 目录中。

Logic Test

Functions 相关的 logic 测试在 02_function 目录中。

关于 Databend

Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。