如何为EDA Kafka应用减少99%的云计算成本
了解如何通过从Kafka切换到另一个开源的Java队列实现来节省你的云费用。
虽然云计算提供了极大的便利性和灵活性,但部署在云计算中的应用程序的运营成本有时会很高。本文展示了一种方法,通过从Kafka迁移到Chronicle Queue开源系统,可以大幅降低对延迟敏感的事件驱动架构(EDA)Java应用的运营成本,这是一种资源效率更高和延迟更低的队列实现。
什么是EDA?
EDA应用是一个分布式的应用,其中事件(以消息或DTO的形式)被生产、检测、消费和反应。分布式意味着它可能运行在不同的机器上,或在同一台机器上,但在不同的进程或线程中。后者的概念在本文中被使用,即消息被持久化在队列中。
设置场景
假设我们有一个由五个服务组成的EDA应用程序,我们有一个要求,即从第一个生产者到最后一个消费者的99.9%的消息的延迟应该小于100毫秒,消息速率为每秒1,000条. chronicle.software/wp-content/…
图1,五个服务和基准是由六个主题/队列相互连接的。
换句话说,从基准线程发送消息(即使用主题0)到基准线程再次收到消息(即通过主题5)所需的时间,只允许平均每秒发送1000条消息中的一条消息高于100ms。
本文中使用的消息很简单。它们包含一个长纳秒级的时间戳,当消息第一次通过主题0发布时,这个时间戳就是初始时间戳,还有一个int值,每次消息从一个服务传播到下一个服务时,这个int值就会增加1(这个值实际上并没有被使用,而是说明了一个基本的服务逻辑)。当一个消息回到Benchmark线程时,当前的纳米时间与最初在主题0上发送的消息中的原始纳米时间进行比较,以便计算出整个服务链的总延迟时间。随后,延迟样本被送入直方图,供以后分析。
从上面的图1可以看出,主题/队列的数量等于服务的数量加1。因此,有六个主题/队列,因为有五个服务。
问题
本文的问题是。在一个给定的硬件上,我们可以设置多少个这些链的实例,并且仍然满足延迟要求?或者,换个说法,我们可以运行多少个这样的应用程序,并且仍然为所使用的硬件支付相同的价格?
默认设置
在这篇文章中,我选择使用Apache Kafka,因为它是市场上最常用的队列类型之一。我还选择了Chronicle Queue,因为它能够提供低延迟和资源效率。
Kafka和Chronicle Queue都有几个可配置的选项,包括在几个服务器上复制数据。在这篇文章中,将使用一个非复制的队列。出于性能方面的考虑,Kafka代理将与服务在同一台机器上运行,允许使用本地回环网络接口。
Kafka Producer实例被配置为优化的低延迟(例如设置 "acks=1"),KafkaConsumer实例也是如此。
Chronicle Queue实例是使用默认设置创建的,没有明确的优化。因此,Chronicle Queue中更高级的性能特征,如CPU核心钉住和繁忙的旋转等待,并没有被使用。
Kafka
Apache Kafka是一个开源的分布式事件流平台,用于高性能数据管道、流分析、数据集成和关键任务应用,广泛用于各种EDA应用,特别是当驻扎在不同地点的几个信息源需要被聚合和消费时。
在这个基准中,每个测试实例将创建六个不同的Kafka主题,它们被命名为topicXXXX0, topicXXXX1, ..., topicXXXX5,其中XXXX是一个随机数字。
Chronicle Queue
开源的Chronicle Queue是一个持久的低延迟的消息传递框架,适用于高性能和关键应用。有趣的是,Chronicle Queue使用离堆内存和内存映射来减少内存压力和垃圾收集的影响,这使得该产品在金融科技领域很受欢迎,因为确定性的低延迟消息传递是至关重要的。
在这个基准测试中,每个测试实例将创建六个Chronicle Queue实例,命名为topicXXXX0, topicXXXX1, ..., topicXXXX5,其中XXXX是一个随机数。
代码
两个不同的服务线程实现的内循环如下所示。它们都轮询其输入队列,直到被命令关闭,如果没有消息,它们将等待八分之一的预期消息间时间,然后再进行新的尝试。
以下是代码。
Kafka
while (!shutDown.get()) {
ConsumerRecords<Integer, Long> records =
inQ.poll(Duration.ofNanos(INTER_MESSAGE_TIME_NS / 8));
for (ConsumerRecord<Integer, Long> record : records) {
long beginTimeNs = record.value();
int value = record.key();
outQ.send(new ProducerRecord<>(topic, value + 1, beginTimeNs));
}
}
使用记录key() ,携带一个int值,可能有点不合常理,但可以让我们提高性能,简化代码。
Chronicle Queue
while (!shutDown.get()) {
try (final DocumentContext rdc = tailer.readingDocument()) {
if (rdc.isPresent()) {
ValueIn valueIn = rdc.wire().getValueIn();
long beginTime = valueIn.readLong();
int value = valueIn.readInt();
try (final DocumentContext wdc =
appender.writingDocument()) {
final ValueOut valueOut = wdc.wire().getValueOut();
valueOut.writeLong(beginTime);
valueOut.writeInt(value + 1);
}
} else {
LockSupport.parkNanos(INTER_MESSAGE_TIME_NS / 8);
}
}
}
基准测试
基准测试有一个最初的热身阶段,在此期间,JVM的C2编译器对代码进行分析和编译,以获得更好的性能。热身阶段的抽样结果被丢弃了。
越来越多的测试实例被手动启动(每个实例都有自己的五个服务),直到不能再满足延迟要求。在运行基准的同时,还使用 "top "命令观察所有实例的CPU利用率,并在几秒钟内取平均值。
基准测试没有考虑协调的遗漏,在Ubuntu Linux(5.11.0-49-generic)上运行,AMD Ryzen 9 5950X 16核处理器的频率为3.4 GHz,内存为64 GB,应用程序在隔离的核心2-8(共7个CPU核心)上运行,队列被持久化到1 TB NVMe闪存设备上。使用了OpenJDK 11(11.0.14.1)。
所有的延迟数字都是以毫秒为单位,99%意味着99分位数,99.9%意味着99.9分位数。
Kafka
Kafka代理和基准都是使用前缀 "taskset -c 2-8 "和相应的命令(例如taskset -c 2-8 mvn exec:java@Kafka)运行。Kafka的结果如下。
实例 | 延迟中位数 | 99% | 99.9% | CPU利用率 |
1 | 0.9 | 19 | 30 | 670% |
2 | 16 | 72 | 106 (*) | 700% (已饱和) |
表1,显示Kafka实例与延迟和CPU利用率。
在99.9分位数上超过100毫秒。
可以看出,只有一个EDA系统的实例可以同时运行。运行两个实例增加了99.9分位数,所以它超过了100毫秒的限制。这些实例和Kafka代理很快就使可用的CPU资源饱和了。
下面是运行两个实例和一个代理(PID 3132946)时 "top "命令的输出快照。
纯文本
3134979 per.min+ 20 0 20.5g 1.6g 20508 S 319.6 2.6 60:27.40 java
3142126 per.min+ 20 0 20.5g 1.6g 20300 S 296.3 2.5 19:36.17 java
3132946 per.min+ 20 0 11.3g 1.0g 22056 S 73.8 1.6 9:22.42 java
纪事队列
使用 "taskset -c 2-8 mvn exec:java@ChronicleQueue "命令运行基准,得到了以下结果。
实例 | 延迟中位数 | 99% | 99.9% | CPU利用率 |
1 | 0.5 | 0.8 | 0.9 | 5.2% |
10 | 0.5 | 0.9 | 0.9 | 79% |
25 | 0.5 | 0.9 | 3.6 | 180% |
50 | 0.5 | 0.9 | 5.0 | 425% |
100 | 1.0 | 5 | 20 | 700% (饱和的) |
150 | 2.0 | 7 | 53 | 700%(饱和) |
200 | 3.1 | 9 | 59 | 700%(饱和) |
250 | 4.8 | 12 | 62 | 700%(饱和) |
375 | 8.7 | 23 | 75 | 700%(饱和) |
500 | 11 | 36 | 96 | 700% (已饱和) |
表2显示了Chronicle Queue实例与延迟和CPU利用率。
当500个实例可以同时运行时,Chronicle Queue的纯粹效率在这些基准中变得很明显,这意味着我们在7个核心上同时处理3,000个队列和3,000,000条消息,在99.9分位数时延迟不到100ms。
比较
下面是一张图表,显示了两种不同队列类型的实例数量与99.9分位数的对比(越少越好)。
图1,显示实例数与99.9分位数的延迟(ms)。
可以看出,Kafka的曲线从30毫秒到106毫秒只有一步之遥,所以Kafka的延迟增长在这个规模上看起来像一堵墙。
结论
如果为特定的对延迟敏感的EDA应用从Kafka切换到Chronicle Queue,那么在相同的硬件上可以运行大约四百倍的应用。
图2,显示了归一化成本与队列类型的关系(越少越好)。
如上图2所示,大约四百倍的应用对应于减少约99.8%的云或硬件成本的潜力(少即是好)。事实上,在所使用的规模上,几乎看不到成本。
应用 云计算 kafka