背景介绍
让我们首先思考一下你正在做的项目。当你改变了一些代码并准备提交一个PR时,你愿意如何:
- 在本地运行你项目的完整单元测试集?
- 启动一个集群并在本地运行一个完整的集成测试集?
- 对你的改动进行性能测试(例如,基准测试)?
在我们开始RisingWave项目时,开发人员总是抱怨测试速度慢,测试体验差。对开发者来说,它们的确是很痛苦的!
单元测试的速度超级慢。 我们有一个10多个crates的工作空间和700个单元测试用例。用cargo test运行一个完整的测试集需要很长的时间,更不用说花在编译代码覆盖率上的时间。
集成测试是非常困难的。 RisingWave从第一天起就是一个云原生流数据库,一个最小的RisingWave集群需要一个计算节点、一个元节点和一个前端节点,需要用许多参数和配置来启动它们。因此,我们有一个很长的bash脚本来启动一个集群,我们必须等待30秒才能运行集成测试。有时,如果开发人员忘记停止他们之前启动的组件,就需要花几分钟时间来弄清楚为什么e2e测试没有产出。随着越来越多的组件(如多个计算节点、MinIO、Kafka)被添加到集成测试集中,甚至很少有开发者愿意自己启动集群。我们会简单地提交一个PR,双手祈祷,希望这个变更能通过CI的e2e测试。此外,每当我们对命令行界面进行修改时,我们都必须hack启动脚本,并让其他开发者知道这些修改,以便每个人都能在他们的本地机器上正确设置RisingWave。当时,CI的失败率很高。当时没有办法检测一个组件是否正确设置(例如通过gRPC健康检查)--我们只是使用sleep 5s。有时e2e测试失败是因为TCP端口被关闭或服务没有完全初始化。
性能测试是很痛苦的。 开发人员不得不手动完成所有工作 —— 编译RisingWave,向EC2发送二进制文件,自己编写命令行参数,检查每个服务是否启动,配置Grafana仪表盘,运行benchmark,最后,清理一切。总之,没有人知道他们的PR是否会影响性能,因为(几乎)没有人会做性能测试。
RiseDev
我厌倦了这些问题,所以我在2021年12月底启动了RiseDev,即RisingWave的开发者工具。我最初想过将其命名为RiseLAB,因为开发者会在 "RisingWave "的 "实验室 "里做大量的 "实验"。尽管如此,为了避免与一些知名品牌的名称冲突,我们后来在开源之前将其更名为RiseDev。
RiseDev推出两周后,我们的所有开发人员都在使用它进行日常开发。它超级容易使用:只需一行命令./risedev d,它就能在4秒内启动一个RisingWave集群。它还提供了一些其他的选项来调整,但我们今天不会深入了解RiseDev的内部情况。
现在让我们来看看RiseDev如何简化开发过程,改善开发者的体验。
快速的单元测试
Rust开发者通常使用cargo test来调用单元测试。让我们回顾一下单元测试是如何通过cargo test编译和运行的。
- 工作区中的所有crate都会被编译成一个特殊的
test配置,这样#[cfg(test)]里面的代码就会被编译。 - 每个 crate(库)都会产生一个与 libtest 链接的二进制文件,它可以执行单元测试。
cargo test将逐一调用每个单元测试二进制文件来完成单元测试过程。
问题出在最后一步 —— 事实上,这些单元测试二进制文件是可以并行运行的!这就是 cargo-nexttest 的作用。它并行地运行来自不同模块的单元测试,以提高测试速度。此外,它还能很好地与 libtest-mimic 集成。我们有一些基于文件的测试,测试工具会扫描测试文件,并使用libtest-mimic动态生成单元测试。Nextest很好地支持这种用例。迁移到nextest后,我们可以在10秒内运行所有的测试案例。
当时nextest的一个问题是缺乏覆盖率工具。幸运的是,我发现了 cargo-llvm-cov,并发送了一个PR来增加与nextest的集成。现在,只需一个命令cargo llvm-cov nextest,我们就可以使用nextest运行带有覆盖率报告的单元测试。与我们之前使用的覆盖率工具cargo tarpaulin相比,这个解决方案也提高了构建速度。
简单的集群设置
RiseDev的核心功能是组装和启动一个测试集群。RiseDev实现了一个灵活的yaml配置引擎,使开发人员能够按照自己的意愿组装集群。让我们看一下下面的使用例子。
默认情况下,RiseDev只在内存模式下启动RisingWave:一个元节点在内存中存储元数据,一个计算节点在内存中存储状态,以及一个前端节点。集群配置存储在GitHub仓库根目录下的risedev.yml中。默认的配置是这样的。
risedev:
default:
- use: meta-node
- use: compute-node
- use: frontend
只需一个命令./risedev d,所有这些组件就都会逐一启动。RiseDev通过做健康检查来确保每个组件在启动下一个组件之前被完全初始化,以避免启动一个错误配置的集群。
✅ tmux: session risedev
✅ prepare: all previous services have been stopped
✅ meta-node-5690: api grpc://127.0.0.1:5690/, dashboard <http://127.0.0.1:5691/>
✅ compute-node-5688: api grpc://127.0.0.1:5688/
✅ frontend-4566: api postgres://127.0.0.1:4566/
✅ playground: done bootstrapping with config default
---- summary of startup time ----
meta-node-5690: 0.83s
compute-node-5688: 1.37s
frontend-4566: 0.83s
-------------------------------
RiseDev调用tmux命令在后台生成进程,这为开发者提供了对每个组件的最大控制。开发人员可以附加到tmux控制台,管理集群内的每个运行进程。
现在,当开发者想在磁盘上持久化状态存储数据时,他们可以这样修改risedev.yml。
risedev:
default:
- use: minio
- use: meta-node
- use: compute-node
- use: frontend
- use: compactor
这就是神奇的地方 —— RiseDev自动提供可用的服务。现在我们在集群配置文件中加入了minio,RiseDev在启动计算节点的时候会自动将其选中。
✅ tmux: session risedev
✅ prepare: all previous services have been stopped
✅ minio: api <http://127.0.0.1:9301/>, console <http://127.0.0.1:9400/>
✅ meta-node-5690: api grpc://127.0.0.1:5690/, dashboard <http://127.0.0.1:5691/>
✅ compute-node-5688: api grpc://127.0.0.1:5688/
✅ frontend-4566: api postgres://127.0.0.1:4566/
✅ compactor-6660: compactor 127.0.0.1:6660
✅ playground: done bootstrapping with config default
---- summary of startup time ----
minio: 0.38s
meta-node-5690: 0.82s
compute-node-5688: 1.41s
frontend-4566: 0.83s
compactor-6660: 0.69s
-------------------------------
在启动服务时,RiseDev会自动为MinIO创建桶和配置策略。在启动计算节点时,RiseDev添加了一个命令行参数--state-store hummock+minio://...,以便计算节点使用MinIO来存储数据。最终,两行的改动就能实现RisingWave的数据持久化!
常见的情况是,开发人员可能想测试一个3节点的集群,并确保他们的变更在分布式环境下工作。RiseDev也通过这个灵活的yaml配置引擎支持这一点。
risedev:
default:
- use: minio
- use: meta-node
- use: compute-node
port: 5687
- use: compute-node
port: 5688
- use: compute-node
port: 5689
- use: frontend
- use: compactor
而当开发者想用Kafka等外部数据源测试RisingWave,并在Grafana中查看指标时,可以简单地运行./risedev d full。RiseDev会自动配置一切!只要导航到localhost:3001,我们就可以找到我们想要的指标。
听起来很酷,对吗?启动集群后,我们现在可以调用sqllogictest,它是一个SQL测试框架,能来运行源自SQLite项目的集成测试。我们使用的是 sqllogictest-rs,它是Rust下对sqllogictest的重新实现,由RisingLightDB社区维护。我们贡献了很多功能,使其使用起来更简单、更方便。
总的来说,RiseDev提供了一个灵活的配置框架来启动一个RisingWave集群。它运行健康检查,配置服务,并为所有组件生成命令行参数。只需稍作改动,我们就可以根据自己的需要组装RisingWave集群,并运行集成测试!
如丝般顺滑的性能测试
在开发RisingWave时,我们非常关心性能问题 —— 如何更快地摄取数据,如何更有效地从S3读取和写入数据,等等。但在很长一段时间里,我们不知道每次提交会对性能产生什么影响。经过一些调查,我们在EC2上设置了一些标准的RisingWave AMIs,并安装了所有需要的工具。开发人员只需要根据模板创建一个EC2,他们就可以立即开始用RiseDev对他们的修改进行基准测试 —— 不需要自己安装Rust编译器或其他依赖性。
但单节点设置只适用于快速验证 —— 所有组件共享相同的磁盘和CPU,而且数据集的大小一般都很小。我们最终需要的是一个完整的分布式RisingWave配置,并有大量的数据来进行基准测试。
我们开发了两个独立的工具来设置测试环境和部署RisingWave。
用一个命令设置基准环境
感谢Terraform让我们可以使用HCL代码描述AWS上的基准基础设施。在RisingWave Labs(曾用名 Singularity Data)内部,我们有一个私有的Terraform模块,为每个RisingWave组件启动EC2,在一个VPC中连接它们,并设置一个S3桶和ECR(elastic container registry)。每个EC2都将获得一个IAM角色,该角色具有访问每个实验的S3桶和ECR的必要权限,无需任何手动配置。
部署RisingWave
RiseDev已经可以在本地集群中启动,但我们可以在远程集群中部署它吗?答案是YES!RiseDev支持生成docker-compose配置并使用docker-compose部署RisingWave。它将从Terraform读取部署信息,并在每个EC2上生成一个docker-compose配置。例如,我们可以指示RiseDev将一个计算节点部署到名为rw-compute-0的EC2上。
- use: compute-node
id: compute-node-0
listen-address: "0.0.0.0"
address: ${dns-host:rw-compute-0}
而用./risedev compos-deploy <profile>,RiseDev将为该节点生成一个docker-compose配置。然后,我们可以执行 ./risedev apply-compos-deploy 来启动部署过程。在一分钟内,开发者就可以让他们的集群启动并运行了
总结
RiseDev是RisingWave的开发者工具,它彻底改变了RisingWave的开发过程。开发人员可以轻松地使用RiseDev来运行本地测试和性能测试。它使启动和部署一个完整的RisingWave集群的过程自动化。它使RisingWave的开发过程更加顺畅和高效。
在RisingWave和RiseDev中还有很多值得探索的地方,比如jaeger tracing,自动生成Grafana仪表盘,使用确定性测试来加速基于时间的测试,等等。我们不可能在一篇文章中全部涵盖。我们一直在不断改进开发体验,所以开发者会发现在RisingWave项目上工作很容易。
- 注意1:RiseDev部署仅用于测试,不应使用于生产环境。请查看我们的文档,了解如何将RisingWave用于测试目的,如何在Kubernetes上部署RisingWave,并继续关注我们路线图中的RisingWave云。
- 注2:我们没有公布RisingWave的任何官方基准数据--在不同的使用情况下,其性能各不相同,您应该自己尝试一下
关于作者
Chi是RisingWave Labs的一名工程实习生。他对数据库内核的开发很感兴趣。同时,他也很关心开发体验,并试图让RisingWave的每一个开发者都能在编码中获得快乐。Chi目前是卡内基梅隆大学的一名硕士生。他在上海交通大学获得计算机科学学士学位。
- 注 1:RiseDev部署仅用于测试,不应在生产环境中使用。请查看我们的文档,了解如何将RisingWave用于测试,如何在Kubernetes上部署RisingWave,并继续关注我们路线图中的RisingWave云。
- 注 2:我们没有公布 RisingWave 的任何官方基准数据 —— 在不同的使用情况下,性能各不相同,您应该自己尝试一下!