作者:Tushar Malik(开发者兼面条爱好者)
简单来说,RisingWave 是一个分布式云原生流式数据库。它是从零开始构建的,可从流消息队列中获取并处理数据,并能横向扩展。我的一个朋友在参加 Kubecon EU 大会时遇到了 RisingWave 背后的团队,我就是通过他得知了 RisingWave Code Camp(下文简称“Code Camp”)。这是个专为提高初级开发人员的软件和数据库系统技能而开设的导师制项目。由于我并不熟悉 Rust 并且对数据库开发还有些心虚,所以一开始我还挺犹豫要不要申请加入 Code Camp。不过转念一想,我确实需要找个理由学习 Rust,并且一想到能够与构建 RisingWave 的大牛们合作,我就来了精神!
源连接器是 RisingWave 从流数据源获取数据的组件。截至本文撰稿时,RisingWave 已经内建了 Apache Kafka、Apache Pulsar 和 Amazon Kinesis 的连接器。PR#5476 合并之后,RisingWave 将会自带 Google Cloud Pub/Sub 连接器。在本文中,我将回顾我在 Code Camp 实现连接器的过程。
Code Camp 启动
学习 Rust
RisingWave 是用 Rust 语言编写的。作为一种现代的低级语言,Rust 拥有独特的数据所有权模型,因此非常注重安全性。由于其独特的设计,Rust 与我之前用过的其他语言非常不同。因此,作为我的第一项任务,我的 Mentor指导我通过 Rustlings 练习来学习 Rust。另外,我还通过 Rust Book 和 Rust by Example 找到了很多有帮助的内容。这样一来,我很快就开始熟悉这门语言,并在两周内到达了初步掌握的程度。当然,这个时候我对自己的语言水平还不是很自信,也不是特别理解其中的复杂之处。但我的想法是我能够在工作中学习。我在之前参与过的项目中经常边做边学,这次我也有十足的信心。
RisingWave 架构
虽然对于构建连接器来说不是必要的,但理解 RisingWave 的架构对我来说是至关重要的。幸运的是,RisingWave 有完善的文档,其中就包括有关架构设计的文档。我还跟 Bohan Zhang 和 Yanghao Wang 两位 Mentor 一起开展了架构评审环节。期间 Yanghao 向我介绍了与连接器有关的数据库部件。说实话,我对这些知识有些招架不住,不过好在我研究过架构文档,这次介绍演示也让我对理解整个系统和连接器在系统中的作用更加自信了。
总结来说的话,源在 RisingWave 中被划分成不同的分片。分片是数据源中代表一个逻辑分片/分区的单位,分片随后被转移至分片读取器。读取器负责从源分片中读取消息,然后将它们转化成标准的、与源无关的数据类型,即源消息。
选择一个 Pub/Sub Crate
Rust 拥有非常丰富的软件包生态系统,这些软件包被称作 crate。对于 Google Pub/Sub 连接器来说,我们可以从头开始编写一个 crate,或者找一个符合我们目的的现成的 crate。为此,我研究了 Rust 的 crate 登记网站 crates.io,分析了相关的功能支持状态、在相关库上的开发活动以及 issue 和讨论等使用指标。最终,我在 google-cloud-pubsub 发现了一个非常棒的 crate。它是用异步 Rust 编写的,支持连接器所需的大多数功能。选好了 crate 之后,我编写了一个 PoC 演示 来进行测试。
编程
连接器的实现
RisingWave 大量使用 Rust 宏来改善和精简开发者的开发体验。对于源连接器来说,它使用宏来在编译时生成具体的连接器实现。在实际操作中,这意味着连接器的构建会涉及到某些特征的实现(Rust 接口),然后将其插入到代码生成宏中以避免重复样板代码。
我实现的第一个特征是分片枚举器。该特征负责枚举分片(通常每个分片代表源中的逻辑分区)。我们遇到的第一个挑战是 Google Pub/Sub 的特殊设计。Google Pub/Sub 是一个高级系统,它将 GCP 可能使用的任何逻辑分片从客户端抽象出来。我曾有段时间不确定应该如何继续。不过,在与 Bohan 和 Yanghao 对具体实现细节等问题进行充分讨论之后,我们决定(用 Bohan 的话来说)对分片的抽象化进行自上而下的调整。通过将分片的数量作为 Pub/Sub 源的一项配置参数,我们就可以从单一 Pub/Sub topic 中读取数据时进行分布式处理。
通过这种方式,我们就可以建立 n 个分片来读取负责在活跃 consumer 之间分配消息负载的 Pub/Sub 流。这样做的目的是在多个分片之间分配流处理。用户应在创建源时考虑到工作负载。
在解决完分片枚举器之后,就到了实现分片读取器特征的时候了。读取器是连接器的一部分,负责在给定存储的源分片的情况下从源实际拉入消息,然后将其提供给计算节点进行处理和存储。要实现这些,第一步就是从源原生格式中提取消息,然后将其转化为 RisingWave 原生源消息。从技术角度来说,实现这一特征相对简单,因为这规避了因性能问题对上游进行变更的需求。我将会在下一节中详细讨论这一问题。
随着分片枚举器和读取器的完成,连接器需要支持回溯到所消耗的消息的恢复点——这是 RisingWave 在出错时用于回滚 consumer 状态的操作。由于特殊的 Pub/Sub 设计,我们很快就遇到了另一个问题:在大多数流处理系统中,所有的消息不管确认状态如何都会留存一段时间。然而在 Pub/Sub 中,除明确设定外,已确认的消息会默认丢弃,而留存消息会带来额外的存储费用。经过讨论之后,我们决定实施“确认后留存”的策略,因为回滚是一个非常重要且高频的操作。我们还决定只接受用户预制的订阅,以确保连接器需求的透明性。对我来说,这就像是在保证我们对 Pub/Sub 的需求与给予用户一定控制权之间取得了最佳的平衡。
上游工作
RisingWave 作为一个分布式、高容错、性能关键的系统,我们的最初选择的 Pub/Sub crate 并不能完全满足需求。这其中的第一个问题就是缺少批确认 API(batch-acknowledgment API)。Pub/Sub 要求消息经过 consumer 确认消息已被消耗并可以安全地摒弃。consumer 确认的缺失意味着 Pub/Sub 将会在消息提取过程中重新传递该消息。消息可以通过单个 API 调用单独或批量确认,但单独确认的网络开销对于 RisingWave 的高容量用例来说是不可接受的。这就是我创建的第一个上游 issue。据作者说已经有了支持这一功能的解决方案,且很快就会发布相关补丁。

测试
小结
Code Camp 是我最近参与过的最有获得感的体验。我非常幸运能够遇到一些行业大牛并与他们展开合作。在课程中,我从对 Rust 和流处理系统一无所知,到成为一名能够使用该语言的新手,再到为最牛的流式数据库贡献代码,收获良多。
与导师的会议也让我受益匪浅。我们在长达三个月的 Code Camp 获得了非常棒的体验,其中,Bohan 还顺带夸了我的代码编辑器。当他说我的编辑器看起来像是 Vim 时我还挺惊讶的,这其实就是 Visual Studio Code,只不过看起来稍微前卫了“亿”点点。在那次会议中,我们还探讨了编译速度过慢的问题,我起初以为是由于我编译机器不是M系列芯片的 Mac,后来我才发现我的 LLD 连接器设置也有问题。

或许,最重要的一点是我的导师们始终能够耐心地帮我解决遇到的困难和问题。由于我只能在学校里参加大部分的网络会议,不免有些吵,而且网络连接也是个大问题。有一次,我们在讨论学习进度,由于网络连接一直出问题,我只能提前退出会议,招呼也没有打。但 Bohan 特别贴心地告诉我可以等网络转好的时候再与他异步沟通。
这期间的美好体验还有很多很多,这次经历也让我意识到我对数据库和流处理系统还有非常多的东西要学习。经过这次学习和体验,我可以说我现在是一个更加优秀的开发者了。
[1]: Sqllogictest 是一个 Rust crate,它提供的测试框架能验证一个 SQL 数据库的正确性。这个 crate 在 Rust 中实现了一个 Sqllogictest 解析器和运行器。可以点这里查看源码。
[2]: RiseDev 是 RisingWave 的开发者工具,旨在通过自动化和封装常见流程和设置来改善开发人员的体验。要了解更多,可以阅读这篇博客文章。
RisingWave是一个云原生SQL流式数据库。其旨在降低构建实时应用的门槛以及成本。
✨ GitHub: risingwave.com/github
💻 官网: risingwave.com
👨💻 Slack: risingwave.com/slack





