如何将EDA Kafka应用的云计算成本降低99%?

263 阅读7分钟

如何为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