[
](shanmukhsista.medium.com/?source=pos…)
6月8日
-
7分钟阅读
[
拯救
在Rust中使用REST APIs扩展Kafka摄取功能
高吞吐量的数据摄取系统

这篇文章是关于Kafka和Rust的工作。作为一个想法,我试图建立一个数据摄取服务,接受半结构化的Json Payloads并将其发布到Kafka。更确切地说,我想达到一个以Kafka为缓冲器的摄取服务的理论最大值,并尝试调整它来衡量性能。
我将分享如何使用Rust编写一个REST API,并运行一个简单的HTTP基准来进行性能调优。我还将展示不同的生产者类型与rdkafka,,可以用来获得良好的持久性和极高的吞吐量,用于高级用例。所有的东西都已经用Rust实现了。我们会看一下Kafka调优前后的数字来测试性能。剧透一下--我可以在一台8C32M的服务器上实现大约96K事件/秒的发布率。我将在这篇文章中分享细节。
所有的代码都可以在我的资源库中找到。https://github.com/shanmukhsista/rust-kafka-demo
让我们深入了解一下
应用程序的设置
这个应用程序的设置很简单。它通过REST API(POST调用)接受JSON数据的有效载荷(事件),并将这些events到Kafka主题。发布到Kafka是最有趣的部分。我们将介绍两种不同情况下的发布。
- 发布以获得某种程度的耐久性。在这种情况下,我们确保消息被送到Kafka时有一定的保证。
- 以优化的方式发布,以获得更高的性能。在这种情况下,我们将研究使用一个生产者,在发布前缓冲一些事件,并在不阻塞任何响应的情况下优化最佳性能。
由于性能很关键,我们将写一个基准测试,并使用一个简单的HTTP测试工具(plow )进行测量。
顺便提一下,在rust中编写API时使用Kafka Producer的目的也是为了强调在RUST中向处理程序传递一些状态/服务的能力。在编写真实世界的应用程序时,这将是很常见的。它甚至可能是一个数据库池服务或自定义资源管理器。了解这一点很重要,因为Rust中的编程范式与Java、C#等应用编程语言有很大不同。
对于这个演示,我们将使用Actix框架(Web)来编写API和发布事件。让我们开始吧。
创建一个新的二进制应用程序
让我们从创建一个新的应用程序开始。
cargo new rust-rdkafka-demo
在你的Cargo.toml 中添加以下依赖项。
我们添加serde 依赖关系来解析请求的JSON,并在发布数据之前将其反序列化为Rust Structures。
这个应用程序的另一个先决条件是在你的本地机器上安装kafka,或者准备好远程代理的urls来编写服务。我们将从我们的应用程序中连接到这个kafka集群。
阻塞生产者(等待响应)
在第一种情况下,我们将与一个Kafka生产者合作,确保消息在返回响应之前被发送到Kafka。这意味着会有延迟和性能上的影响,因为我们在等待一些网络操作,并等待Kafka确认消息的到来。
初始化生产者
我们通过设置一些默认的配置变量来初始化一个生产者,并在这个函数中返回生产者的所有权。
这个生产者是一个FutureProducer 的实例,可以跨线程共享。由于克隆对这个生产者来说很便宜,我们可以在每个请求中发布一个事件,并为高吞吐量进行扩展。这个生产者也为每个被发布的事件产生一个未来。因此,我们必须执行await ,以便从Kafka发布和获取结果。
在这种情况下,我们要用最小的所需配置来创建生产者。
定义payload结构
现在,让我们把我们的payload数据定义为一个ruststruct 。就本演示而言,一个payload是一个事件对象,有一个与之相关的名字,由一个data 部分组成,可以容纳任意的键-值对发送到服务器。这只是以键值对形式发送的半结构化JSON数据。
确保你添加Derive属性来序列化和反序列化JSON的有效载荷。
现在我们有了基本的应用设置,还有最后一个部分需要编写,用于处理请求。
定义API处理程序
用于执行我们的API逻辑的API处理程序看起来像这样。
collect_events 每次对根 端点进行POST调用时,都会调用这个方法。在这个方法中,我们也得到了我们的Kafka Producer作为数据依赖。我们可以用它来直接发布事件并返回响应。/
请注意,Kafka生产者默认是一个FutureProducer 。除非我们在生产者上await (第17行),否则没有消息会被发布到Kafka。对于最初的版本,让我们执行一个await 操作并测量一些请求延迟。我们很快就会看到另一种方法。
在这个函数中,我们为这个事件生成一个新的event_id。并把这个事件发回给Kafka。
还要注意的是,我们正在反序列化和再次序列化,中间没有应用任何逻辑。在现实世界的应用中,可能也会有一些丰富的内容发生。因此,我们需要这个步骤。一个更好的选择是使用Protocol Buffers ,而不是JSON序列化/反序列化,以减少序列化成本并更好地标准化有效载荷。
一旦事件被发布,我们会返回一个带有事件ID的文本响应。
主要功能
完成我们的应用程序的最后一步是整合所有东西并更新我们的主函数。请参考下面的代码来查看。
在main中,我们首先初始化我们的kafka生产者。这个生产者将在actix-web 内创建的所有线程中共享。 主函数获得了这个数据的所有权,然后负责这个变量的生命周期。
然后,我们使用Actix-Web的服务器工厂来定义服务器和它的路由。这里需要注意的一个关键问题是,Actix在这里只定义了布局和初始化。在我们收到客户端的请求之前,我们真的没有初始化并开始使用Kafka生产者。输入到HttpServer.new 的lambda只是一个工厂。我们的服务器每收到一个请求,就会创建一个新的实例。
在处理路由时,我们在处理程序中获得有效载荷的所有权。这个处理程序也得到了共享的kafka_producer ,以及有效载荷的引用。这就是运行我们的应用程序所需的全部内容。
运行和测试应用程序
要运行应用程序,在项目的根目录下执行以下命令。
cargo run
发射一个POST请求,测试向我们的服务发布事件。
如果一切正常,你应该在响应中看到一个新的事件ID,作为纯文本输出。
9502ea0e-9856-4626-b621-2c6fd0d35cfe
非阻塞式方法
上面显示的方法是在向Kafka发布事件后等待异步期货。尽管这是一个异步操作,但无论网络有多好,都可能会有一些等待。
我们将研究另一种方法来进一步优化它。一个不需要等待期货的非阻塞式Producer。
我们将利用低级别的ThreadedProducer 类来发送事件。在内部,这个类有自己的专用线程来缓冲和发布事件。在这个实现中,我们将用这个类创建一个生产者来发送事件。这应该会给我们带来更高的吞吐量,而不需要对代码做太大的改动。下面是代码的变化。
初始化生产者
API处理程序
我们在这里可以看到,我们并没有等待Kafka发送操作的完成。我们只是发布并立即返回一个响应。有突变器来保证线程安全,以分享生产者而不产生任何副作用。
主要功能
现在我们有了这两种方法,让我们看看一些性能数字。
性能测试
为了测量性能,我们将使用GCP内的一组远程机器来执行测试。这是一个简单的HTTP测试,从另一个远程机器上执行。一个全新的虚拟机被创建,在一个 单一的实例 上托管我们的应用程序 。 我们将比较两个生产者,看看延迟和带宽的分布。
设置
API服务器机器:8核32GB内存 - GCP实例
Kafka - 同一本地网络中的通用Kafka集群。
负载测试从同一网络中的一个 在同一网络内的独立机器上运行。
期货生产者 ( 等待操作 )
对于这两个生产者,我们将从 [Plow](https://github.com/six-ddc/plow)来发射50个并发请求,持续时间为1分钟。下面是一个屏幕截图。
对于这个设置,我注意到在事件以高吞吐量被摄取之前有一个初始延迟。吞吐量在几秒钟内从4000/秒扩展到45000。这就是最初的延迟在6ms左右。考虑到我们将每条记录发布到kafka并等待响应,这一点都不差。现在我们来看看另一种方法。
非阻塞式方法(无等待)
现在让我们来看看一种方法,它将极大地提高我们的服务性能。这可能不是最准确的方法,但它确实能显著提高性能。我还在研究一些方法,了解底层库的最佳方法。
请注意,我们使用这种方法的目标是为我们的摄取服务提供一个高吞吐量。在这种情况下,我们使用了一个不需要我们等待未来的生产者。

该服务被摄取到Kafka中的事件数达到了95k/秒。延迟也没有太多的变化。尽管这只是一个基本的设置,但我真的对这个性能印象深刻。
总结
综上所述,这是一个相当简单的实现。我对我看到的数字印象深刻。很快,我将尝试对数据有效载荷进行一些昂贵的操作,看看这些数字会受到什么影响。
但就我个人而言,如果我正在设计一个高吞吐量的分析数据收集服务,我会考虑将Rust作为其他考虑因素和因素中的一个选择。不仅仅是因为性能,也是为了能源效率 :)