本章内容包括
- 提高性能的主题(topic)设置
- 确定主题最佳分区数
- 影响性能的 Kafka broker 设置
- 生产者和消费者的性能调优
在上一章中,我们学习了如何用 Kafka 可靠地生产消息。本章将处理优化 Kafka 这一尚未完成的任务:我们如何实现 Kafka 所承诺的性能?
与可靠性类似,当我们谈论性能时,首先需要弄清对我们而言性能意味着什么。我们可以从不同角度定义性能。我们关心的是带宽吗?系统提供的带宽通常至关重要。带宽以字节每秒来衡量:我们的系统能达到多少 KB/MB/GB 每秒的数据吞吐量?如果只看带宽,常常会忽视延迟,但实际上延迟通常对我们更重要。
我们绝不应低估满载 microSD 卡货车的带宽。这样的带宽是巨大的。一张重量不到 1 克的 microSD 卡,现在即可存储超过 1 TB 的数据。这意味着每公斤约可存 1,000 TB(1 PB),每吨可达惊人的 1,000,000 TB(1 EB),彰显了现代技术在数据存储密度方面的非凡成就。但大多数情况下,这并不是我们真正关心的。最终,我们更在意的是系统的端到端延迟而非理论上的最大带宽。尤其是如今,用户期望我们提供即时响应。亚马逊在 2008 年估计,每增加 100 毫秒的加载时间,可能会导致多达 1% 的收入损失(mng.bz/QD16)。
虽然带宽与延迟对 IT 系统至关重要,但还有另一个不应忽视的方面:系统的资源友好性。通常我们并非拥有无限资源。我们希望谨慎使用资源以避免过度开支,并尽量降低 IT 系统对环境的影响。
在配置项如此丰富的 Kafka 中,理解如何优化系统尤为重要。在我们的 Kafka 培训课程中,学员们常常乐在其中地测试 Kafka 的极限:他们会把系统的吞吐量从每秒 0.2 MB 优化到在一台中等规模机器上超过每秒 200 MB。
那么,我们在哪里为 Kafka 配置性能?简短的回答是:Kafka 在许多方面天生就为性能做了调优(见图 6.1),但我们仍需关注各个组件的配置项。
Kafka 的性能优化始于它的基本假设。Kafka 是 21 世纪的产物。我们假设硬盘相对便宜,主内存也可以配置较大容量。但更重要的另一个假设是:我们知道单个 IT 系统并非特别可靠。与其不惜一切代价避免硬件故障,Kafka 采取了不同的做法------子系统发生故障也没关系,Kafka 仍然可以继续运行。Kafka 还尽量避免一刀切的方案,在同一个 Kafka 集群中我们也可以对性能与可靠性做出不同的权衡。
在架构上,Kafka 从底层就为性能进行了优化。一切从日志这一核心数据结构开始。正如我们在前几章学到的,日志是最简单的数据结构之一。日志的访问模式非常可预测,这意味着即便在传统旋转硬盘上,Kafka 也能保持高性能。这是因为尽管 SSD 在几乎所有指标上都优于传统硬盘,但后者的价格仍然要低得多。

回想一下,Kafka 能传输任意数据格式。这不仅给我们更大的灵活性,也迫使 Kafka 放弃其他消息系统常见的某些功能。由于 Kafka 完全不试图去解释消息本身,它不会因此损失性能。Kafka 整体策略是让消息系统尽量少做事,把实际工作外包给客户端------这同样有助于提升系统的总体性能。
6.1 为性能配置主题
在我们开始调整 broker 和客户端的配置之前,首先要确保主题配置正确。在前几章我们已经学到,分区主要用于水平扩展(horizontal scaling),它通过缓解消费者成为瓶颈来实现系统内的并行处理。此外,这种做法使得能处理单台服务器难以承载的更大数据量,但需要注意的是它会影响消息的全局顺序性。
6.1.1 扩展与负载均衡
分区数不仅影响生产者如何发送消息、负载如何在 broker 间分布,更重要的是影响我们如何扩展消费者。原则上,多个独立消费者可以消费同一主题的消息。回想第 4 章 4.2.2 节的示例:一个消费者用于分析价格对销量的影响(消费 products.prices.changelog
),另一个用于在商城中更新价格。
一方面,我们不希望这些独立的消费者相互干扰;这很好保证,因为消费者会从某个 offset 开始显式向 Kafka 查询所有新价格。如果每个独立消费者自己管理 offset,它们就不会互相干扰。
现在的情形是:分区很多,但只有一个消费者在消费全部消息。为了解决这个瓶颈并在 Kafka 中管理 offset,我们使用消费者组(consumer groups)。通过启动多个相同消费者的实例,Kafka 会帮助这些消费者把工作分配开来。但注意:我们不能任意把消息分配给消费者------为保证消息顺序,Kafka 是以整个分区为单位把分区分配给消费者的(见图 6.2)。
分区不仅帮助我们在不同 broker 间分配负载,还允许我们把消费者组扩展以并行处理数据。但分区也引入了限制。Kafka 确保消息在同一分区内按到达顺序存储,但不保证跨分区顺序。这意味着,即便多个生产者可以写入同一主题,不同生产者产生的消息之间并不保证顺序。要保证消息顺序,消息必须来自同一生产者并写入同一分区。
此外,只有在设置 enable.idempotence=true
(见第 5 章)时,顺序保证才更可靠。该设置通过生产者 ID + 序列 ID 的组合确保消息唯一性,防止在重试或失败时导致顺序混淆。若不启用,单个生产者在发生重试或故障时也可能出现顺序问题。
在分区上下文中,始终要考虑针对具体用例应有多少分区。很多人会觉得分区越多越好,因为能更好扩展并行化工作。但"合适的分区数"并非简单问题,通常没有唯一完美答案。
6.1.2 如何确定需要多少分区
首先要识别瓶颈出在哪里:是消费者还是 Kafka 本身?在许多情况下,消费者限制了性能上限。我们根据处理消息所需的消费者数量来确定分区数(除非是大数据场景)。例如,如果单个消费者处理一条消息大约需 100 ms(例如写入一个慢数据存储),则它能处理 10 条/秒。若峰值需要处理 100 条/秒,则至少需要 10 个消费者,因此需要至少 10 个分区。
注意:有时瓶颈来自 Kafka 之外,例如消费者受慢速数据存储限制,这种情况下增加消费者或分区可能无法显著提升性能。必须分析并确定瓶颈是否出在 Kafka 内部或外部依赖。
出于灵活性,建议分区数比消费者实际需求略多一些,且选择易于整除的数字以便平滑扩容。理想情况下,这个可整除性也适用于 broker 数量,尽管总体分区数通常会多于 broker 数。根据经验,起始使用 12 个分区通常能满足大多数场景。
为此我们要回答两个关键问题:第一,当前分区数是否太少,是否需要在同一消费者组内并行处理超过 12 个消费者?如果需要,就将分区数翻倍,直到满足需求(例如 24 个并行消费者)。第二,分区过多有什么影响?更多分区虽能提高吞吐量,但也带来复杂性。每个分区会占用客户端资源(如内存),特别是客户端要同时处理多个主题的多个分区时。历史上,在 ZooKeeper 环境下大量分区会导致故障期间长时间不可用------leader 重新分配每次操作耗时显著,数百或数千分区会累计成几分钟的恢复时间,从而短暂变得不可访问。
此外,过多分区会加重 Kafka 集群负担,消耗 CPU、内存和文件句柄,增加系统复杂性,可能导致性能下降并延长 broker 故障恢复时间;运维(监控、排查)也会更困难。因此必须在满足性能需求和避免不必要开销之间找到平衡。
接下来要考虑是为每个用例优化分区数还是采用统一默认值。第 12 章我们会看到,为了方便进行 join 操作,两个主题通常需要相同的分区数。
提示:综合以上因素,我们通常建议采用统一的默认分区数(例如 12),即便初期超出需求也没关系。对于高吞吐主题,视情况再增加分区,但要注意成本------一些云厂商(如 Confluent)按分区计费,因此精确估算对成本优化很重要。
在配置 Kafka 集群时,理解分区限制与最佳实践对实现性能与资源利用最优化至关重要。虽然 Kafka 本身没有严格的分区上限,但在 ZooKeeper 管理的环境下建议每个 broker 最多约 4000 个分区,整个集群最多可达约 200,000 个分区。KRaft 由于消除了对 ZooKeeper 的依赖并更高效地管理元数据,支持更大的集群规模。
评估是否需要额外分区也很重要------即便只有少数分区满负载运行,也可能超过下游系统(如关系型数据库或 MongoDB)的处理能力。因此,尽管 Kafka 的分区灵活性是优势,但实际效能优化必须考虑 Kafka 之外的整体系统能力。
当前(撰写时)正在开发一项新特性 "Queues for Kafka",引入共享组(shared groups),允许消费者协作式地从主题中消费记录。在共享组模型下,消费者数量可能超过分区数,这样或许可以减少为获得高并行度而过度分区的需求。该模型对消费较慢的场景尤其有利,更接近传统队列的行为。
但也会引出一个关键问题:共享分区内如何保证顺序?在这种模型下,传统的消费者组仍然保证分区内顺序,但共享组可能不会严格保持顺序,因为多个消费者可以从同一分区拉取消息。这与传统队列系统类似,在队列中通常对严格顺序的要求较低。
警告:尽管共享组在某些用例下提供了更大灵活性,但它们并不打算替代需要严格顺序保证的高吞吐场景的传统分区方案。对于需要严格顺序保证的场景,仍然需要使用传统的消费者组和适当的分区设计。
6.1.3 更改分区数量
我们在这里详细讨论分区数量,因为一旦要依赖消息的正确顺序,分区数并不是可以随意更改的。在 Kafka 中,分区数量只能增加。不能减少主题的分区数,因为那样 Kafka 将面临一个无法解决的问题:被删除分区中的消息该如何处理。简单地把消息移动到其他分区会破坏 Kafka 对消息顺序的保证。
当我们增加分区数时,新建的分区最初是空的,这会导致数据分布立即出现不均衡。如果我们的保留时间很短(例如仅几天),这种不均衡通常不是问题;随着保留期届满、旧消息被删除,各分区的数据量会自然趋于均衡。
更大的问题是:增加分区会破坏消息的顺序。这是因为 Kafka 使用消息键的哈希值对分区数取模来决定目标分区(partition_number = hash(key) % number_of_partitions
)。如果分区数发生变化,取模运算的结果就会不同,导致具有相同键的消息落到不同的分区中。消息顺序的扰动会一直持续,直到遵循旧分区方案的历史数据在保留期后被删除为止。图 6.3 展示了这种重分区如何影响消息分布。
当然,也存在不受此问题影响的用例,但在事后改变分区数量时仍应非常谨慎。如果我们不使用键(因为总体上并不关心主题内数据的顺序),增加分区通常没有问题,唯一需顾虑的是消息在分区间分布的不均衡。

如果我们不能允许消息顺序丢失,但又想增加分区数(或者之前算错想减少分区数),那我们别无选择------只能新建一个主题并删除旧主题。采用何种最佳做法取决于具体用例。最简单的情况是:我们的系统在消息写入后会立即处理消息,并且我们可以安排维护窗口。在这种情况下,可以在维护窗口开始时停止生产者,等待所有消息被处理完毕,然后删除并重新创建相关主题。
如果我们无法承受明显停机,但不会无限期地在主题中保存数据,则可以创建一个具有目标分区数的新主题,并让生产者将消息写入新主题。消费者需先消费完旧主题的消息,然后迁移到新主题。一旦所有消费者完成迁移,就可以删除旧主题。
如果我们希望长期保留数据,则这种迁移代价更高。通常的做法是先创建一个具有所需配置的新主题。然后使用例如 Kafka Streams 的工具将旧主题的数据拷贝到新主题,从而在使用键(key)时保证消息顺序不被破坏。创建并复制数据后,还需要将消费者从旧主题切换到新主题。这一步并不简单,因为我们必须考虑如何转换 offset------新分区的 offset 会不同,如图 6.4 所示。否则,消费者可能会漏读消息或重复消费消息,具体后果取决于应用场景。最后,将所有生产者迁移到新主题并删除旧主题。
提示:通过包含事件 ID 可以实现幂等消费。这样系统就能判断某个事件是否已被处理,从而避免重复处理。

警告:如上所示,事后更改分区数量既耗时又容易出错。因此,建议我们在事前仔细考虑主题的需求。不过,创建新主题并迁移数据是可行的。此过程也是一次审查并改进配置、命名约定和负载格式的机会,通常也会借此将消息格式从 JSON 迁移为 Avro 或 Protobuf。
6.2 生产者性能
既然我们已经知道如何为主题优化性能,接下来可以更细致地审视从生产者到 broker 再到消费者的整条路径。由于生产者是把消息写入 Kafka 集群的入口,对整体系统的性能和资源利用率会产生重要影响。
6.2.1 生产者配置
生产者通常使用 Java 的 Kafka 客户端库,或者在使用其他编程语言时采用基于 librdkafka
的库。应用代码通常只传入消息的 value(和值可选的 key),由客户端库决定将该消息写入哪个分区。理论上也可以实现自定义分区算法,但一般应尽量避免。让生产者决定消息落哪个分区看上去有些反直觉,但这能减少 broker 端的协调开销,从而提高性能。对于少数确有需要的场景,这种方式仍允许生产者实现自定义分区策略。
Kafka 通过批处理(batching) 来提升吞吐量,即在发送到 broker 之前把多条消息打包成一个批次。更大的批次意味着更少的网络请求和更高的效率。批处理可以在每个生产者端独立配置,主要由两个参数控制:batch.size
和 linger.ms
。batch.size
指定批次的最大字节数,默认值为 16 KB(16,384 字节)。linger.ms
指生产者在发送批次前愿意等待额外消息的最长时间,默认值为 0(表示尽快发送)。
即使 linger.ms
为默认的 0,实际上也常会发生批处理,因为消息产生速度并不总是和发送速度完全同步------当多条消息能填满一个批次时,我们自然会从批处理中受益。对于那些对即时传递要求不那么严格的场景,可以通过增大 batch.size
和提高 linger.ms
来增加吞吐量。注意:batch.size
的单位是字节,linger.ms
的单位是毫秒。图 6.5 说明了批处理过程。
我们认为 Kafka 的默认 batch.size
(16 KB)对于大多数用例来说偏小。增大批次大小通常能显著提升数据处理和存储效率,因为更大的批次能减少单条消息处理带来的开销。batch.size
的上限受 max.message.bytes
限制,默认允许调到 1 MB。类似地,默认的 linger.ms
(0 ms)通常也过低。将 linger.ms
设为大约 10 ms 通常能同时改善吞吐量和延迟,尤其适用于每秒发送超过 100 条消息的生产者。
对于更低的消息率,可以把 linger.ms
再调高,但这可能会降低实时性(增加延迟)。总体而言,除非面对高吞吐场景,否则对 linger.ms
的微调并非关键;但增大 batch.size
往往能极大提升性能并通过减少需要处理的网络消息数来减轻集群负载。

提示 :对于大多数用例,建议将 batch.size
设置到最大 1MB (或接近此值),并将 linger.ms
设为约 10ms。调整这些设置后务必监控系统的性能指标,以确保针对你的具体工作负载达到最佳配置。
我们在第 5 章已讨论过确认(ACK),并看到可以通过 ACK 来控制从生产者到 Kafka 的传递在可靠性与性能之间的权衡。确实可以通过将 ACK 设为 1
或 0
来换取更高性能,但 ACK 的选择主要应基于可靠性需求,而不是性能需求。
另外一个重要的"调节项"是压缩(compression),它不仅能显著提升 Kafka 环境的性能,还能减少资源消耗。可通过 compression.type
配置压缩算法。默认情况下压缩是关闭的,但 Kafka 支持 none
、gzip
、snappy
、lz4
与 zstd
等算法。
不同算法在压缩率、压缩耗时与解压耗时上有所差异。通常 zstd
或 lz4
在吞吐量与 CPU 开销之间提供最好的平衡。Kafka 并不是对单条消息逐一压缩,而是沿着从生产者到消费者的整条路径对整个批次进行压缩。这之所以可行,是因为生产者为每个分区使用独立的批次:同一分区的所有消息被归为一个批次,另一个分区的消息则在另一个批次中。
更频繁且更活跃的批处理通常还能提升压缩效果。生产者对整个批次压缩一次并将压缩后的批次发送给 leader;leader 将该压缩批次原封不动地写入本地磁盘,并将同样的压缩批次无改动地传给 followers。只有消费者在接收到批次时才解压并拆分为单条消息(如图 6.6 所示)。这种做法不仅在一次性减小了数据体积,还在多个环节减少了网络负载并最小化了持久化所需的磁盘空间。

值得注意的是,Kafka 如何在压缩批次内处理 offset。当消费者需要从一个压缩批次内的某个 offset 开始读取时,它会接收整个批次、将其解压,然后从该批次中正确的消息开始处理。这种设计与 Kafka 将数据视为不可变日志的理念一致,避免了 broker 需要解析或修改消息内容的必要。
此外,如果同一主题中有些生产者写入压缩消息而有些不写,Kafka 也能很好地处理。每个生产者都有自己的压缩设置,Kafka 并不强制要求所有生产者使用相同的压缩方式。因此,一个生产者可以发送压缩批次,而另一个发送未压缩消息,它们可以共存于同一分区中。不过要注意,混合的压缩配置可能导致吞吐量和存储效率的不一致,因为压缩批次相比未压缩批次会占用更少的空间并降低网络开销。为获得最佳性能,通常建议对给定主题统一生产者的压缩设置。
6.2.2 生产者性能测试
测试生产者性能的最佳方式是使用 Kafka 自带的工具 kafka-producer-perf-test.sh
。我们可以用此工具测试不同配置和消息类型对性能的影响。但在使用该工具做测试时,请始终记住:它不能替代使用真实生产者的端到端性能测试。对于真实的端到端性能测试,像 Apache JMeter(jmeter.apache.org/)这样的工具更合适。
警告 :kafka-producer-perf-test.sh
工具会根据其配置产生大量消息,因此会在磁盘上生成多个 GB 的数据。
启动 kafka-producer-perf-test.sh
最简单的命令如下。假设我们已经创建了主题 performance-test
:
css
$ kafka-producer-perf-test.sh \
--topic performance-test \
--num-records 1000000 \
--record-size 10000 \
--throughput -1 \
--producer-props bootstrap.servers=localhost:9092
这里我们生产 1,000,000 条消息(--num-records
),每条 10,000 字节(--record-size
),不限制吞吐量(--throughput -1
)------即尽可能快地发送。数据写入主题 performance-test
(--topic
),并通过 --producer-props
指定 bootstrap server。运行该命令后,会得到类似如下的评估输出:
matlab
39481 records sent, 7896.2 records/sec (75.30 MB/sec),
238.1 ms avg latency, 399.0 ms max latency.
62616 records sent, 12523.2 records/sec (119.43 MB/sec),
164.1 ms avg latency, 253.0 ms max latency.
[...]
1000000 records sent, 12317.546345 records/sec (117.47 MB/sec),
165.43 ms avg latency, 399.00 ms max latency, 157 ms 50th,
211 ms 95th, 287 ms 99th, 353 ms 99.9th.
我们来细看输出。该命令每隔几秒输出当前统计信息。kafka-producer-perf-test.sh
开始时速度慢一些,然后经过几次输出后吞吐量和延迟往往会改善,这常常归因于 JVM 的"热身"------JVM 会逐渐优化代码执行,并在开始时需要一些时间来分配缓冲区和其他对象。
在本例中,发送完目标数量的消息后我们看到,Kafka 大约以 12,317 条/秒的速度发送消息,总吞吐量为 117.47 MB/s,平均延迟为 165 ms。
如果只运行一次性能测试,应谨慎解读结果。正确的性能测试需要多次重复运行并进行评估。但这能给我们初步线索,说明可以如何进一步改进性能。
例如,我们知道同时增大批次大小并适当增加 linger 时间会提高吞吐量。可以通过 --producer-props
来设置这些参数。试试下面这个命令:
lua
$ kafka-producer-perf-test.sh \
--topic performance-test \
--num-records 1000000 \
--record-size 10000 \
--throughput -1 \
--producer-props bootstrap.servers=localhost:9092 \
batch.size=100000 linger.ms=100
...
1000000 records sent, 45879.977978 records/sec (437.55 MB/sec),
64.61 ms avg latency, 279.00 ms max latency, 58 ms 50th,
101 ms 95th, 126 ms 99th, 178 ms 99.9th.
可以看到,吞吐量从最初约 117 MB/s 提升到了 437.55 MB/s。更有意思的是平均延迟从原来的 165 ms 降到约 65 ms。延迟改善的原因可能是之前的批次大小对该用例来说太小。记住默认 batch.size
为 16 KB,因此如果每条记录约 10 KB,就无法利用批处理,消息会堆积,导致延迟恶化。
这个例子再次表明:在你自己的集群和用例下进行性能测试至关重要。表 6.1 总结了在调优生产者性能时应考虑的配置,但它不能替代在接近生产环境条件下、使用生产类数据进行的真实性能测试。每个用例不同,设置必须谨慎选择。
表 6.1 生产者设置(要点)
配置项 | 说明 |
---|---|
acks | 0 : 吞吐量更高,但有数据丢失风险;1 : 吞吐量略低,数据丢失可能性降低;-1 /all : 吞吐量再降低但可靠性更高。 |
enable.idempotence | false : 不推荐;true : 保证消息顺序与唯一性,仅与 acks=-1 兼容。 |
compression.type | none : 不压缩;gzip : 压缩率好但 CPU 占用高;zstd , snappy , lz4 : 在很多情况下比 gzip 更高效。 |
batch.size | 批次大小(字节):通常越大,延迟和吞吐表现越好。不要害怕把 batch.size 设大(上限由 max.message.bytes 决定,默认 1 MB)。 |
linger.ms | 等待填充批次的时间:通常越大,延迟越高但吞吐量越大。 |
6.3 Broker 配置与优化
Brokers 是 Kafka 的核心实体。所有数据都存放在 broker 上,客户端与 broker 通信,broker 通过协调集群协调管理自身。Broker 尽量减少自身要做的工作,把尽可能多的性能关键操作外包给客户端。归根结底,Kafka broker 的工作就是把字节从网络 socket 推到磁盘(写入时),以及把字节从磁盘推到网络 socket(消费时)。
许多数据库系统会设法绕过操作系统以获得更快的数据访问速度。而 Kafka 则与操作系统协同工作,而不是与之对抗。Kafka 尽可能方便操作系统在内存中保留 Kafka 需要的数据。核心思想是顺序读写。正如前面所学,日志的访问模式非常可预测:要么写入文件末尾,要么顺序读取文件。即便在传统的旋转硬盘上,这些访问模式也能实现较高性能。
Kafka 的优化还不止于此。我们之前讨论过 Kafka 并不会强制把写入的数据立即提交到文件系统。这意味着 Kafka 只是把数据写入主内存的页面缓存(page cache),而不会指示操作系统立即将这些数据写入磁盘。Kafka 依赖操作系统在后台延迟完成实际写盘。尽管如此,Kafka 在把数据写入内存后就会向生产者返回 ACK,即便在断电时这些数据可能会丢失。乍看之下这似乎很糟糕,但 Kafka 的可靠性并非仅靠立即写盘,而是靠把数据复制到多个 broker 上来保证的。这种做法假设所有 broker 不会同时崩溃且丢失数据。如果各 broker 部署在不同机器或数据中心的不同位置,这个假设通常是合理的。
鉴于 Kafka 数据格式在各处一致(生产者发送的数据在网络上传输、写入磁盘并被消费者读取的方式一致),broker 可以利用 Linux 内核的一个特性 ------ 零拷贝(zero-copy transfer)。理论上,生产者发送的数据应该从网络 socket 写到内存中的页面缓存,然后再写到磁盘。但既然数据在各处都是一致的,Linux 内核实际上只需修改少量指针,就能把数据"放入"页面缓存而无需实际移动数据。需要注意的是,这种优化仅在未使用传输层加密(TLS)的情况下适用,因为加密会引入额外处理步骤,从而无法享受零拷贝的优势。
6.3.1 优化 broker
那么我们如何影响 broker 的性能呢?Kafka 在 broker 端的默认设置已经经过性能调优。启动 Kafka 之前,建议调整一些操作系统设置。特别重要的是提高最大打开文件描述符数量,因为 broker 会将每个分区的每个日志段作为一个打开的文件保持着,很快就会累积很多文件句柄。
我们还应对虚拟内存做一些设置,以便操作系统能尽早在后台把数据写入磁盘,并尽可能不要阻塞我们的 broker 进程。我们还希望禁用或尽量减少操作系统的交换(swappiness),并在网络层面做一些设置。
例如,如果需要用 TLS 对数据流量进行加密,建议将网络线程数(num.network.threads
)从 3 翻倍到 6。如果使用多块磁盘,则 I/O 线程数(num.io.threads
)应与磁盘数匹配以优化吞吐量。对于大量生产者和消费者,增加最大排队请求数(queued.max.requests
)到超过默认的 500,且与客户端连接数相匹配,有助于维持性能。一般来说,这些调整主要针对处理大量数据的生产环境。配置复杂时建议寻求专业指导,因为覆盖所有用例超出本文范围。
提示:撰写本文时,Cloudera 提供了一个推荐参数的良好概述,见 mng.bz/XxQ9。
6.3.2 确定 broker 数量与容量规划
注意,并非在所有场景下我们都能影响这些参数;例如,在 Kubernetes 环境运行 Kafka 时,通常无法控制这些值。此时我们只能通过广泛监控来至少在某些限制被触及前察觉,并争取时间找出如何处理。
任何优化的基本规则是:先理解、再测量、最后优化。对 Kafka 尤其如此,因为 broker 的默认配置已经相当注重性能。然而,broker 的数量应尽早考虑。大多数小型环境从三个 broker 开始较为常见。这允许我们对分区使用副本因子 3,并且在做维护时还能容忍另一个 broker 出问题而不至于停机。我们需要基于可用机器类型来估算 broker 数量:需要多少内存和硬盘空间?预期的数据吞吐量是多少?网络带宽如何?我们希望数据保留多长时间?
基本上,需要在"broker 太大"和"broker 太小"之间取得平衡。broker 越多,单个 broker 故障的影响越小,但管理开销越大。此外,不应把机器选得过小,因为 Kafka 本身也会消耗一定的 CPU 和内存,而且随着机器变大这部分开销的相对影响会降低。
例如,如果我们使用带 2 TB 硬盘的机器,每天产生约 1 TB 的数据并希望保存 7 天,那么至少需要 4 台 broker 来存放 7 TB 数据。但我们还没做副本。若副本因子为 3,总存储需求变为 21 TB,需要约 11 台 broker。面对这样的数据量,我们还要考虑预期的数据速率及网络接口能力。
提示:对较小安装而言,通常先从 3 台 broker 起步,若因数据量过载再增加 broker。
6.4 消费者性能
与许多传统消息系统不同,Kafka 中的消费者采用拉取(fetch)模型。也就是说,Kafka broker 不会主动向消费者推送数据;消费者自行从 Kafka 拉取数据。消费者可以自行决定何时拉取、拉取哪些数据以及一次拉取多少数据。这样做的优点是:只要程序代码没有 bug,消费者不会被轻易压垮。
6.4.1 消费者配置
与生产者类似,我们可以通过配置来改善消费者的带宽或延迟。在消费者端有两个主要配置可调。首先 fetch.min.bytes
告诉 broker 在响应消费者之前应至少等待多少字节的数据。默认 fetch.min.bytes=1
,即 broker 一旦有至少 1 字节的新消息就会响应消费者。broker 也不会把单条消息逐条发送,而是总以完整批次发送,甚至可能在一次响应中包含多个批次。
当然,broker 不会无限等待,所以消费者可以用 fetch.max.wait.ms
配置 broker 在数据少于 fetch.min.bytes
时最多能等待多少毫秒。默认值为 500 ms。这意味着即便没有新消息,consumer 的请求在 500 ms 后也会收到响应。消费者收到响应并处理消息后,再请求下一次消息(当然 offset 会相应地向后移动)。
警告:切勿将 fetch.min.bytes
或 fetch.max.wait.ms
设为 0,因为这可能导致 broker 被请求淹没。每次 fetch 请求都会被立即回答且消费者紧接着立刻请求新的数据,这种配置会导致过度负载。考虑到每个分区并行地会发送一个请求,且客户端与 broker 的延迟可能低至 1 ms,即使只有一个消费者和一个主题,这种设置也可能导致每秒多达 10,000 个请求。
6.4.2 消费者性能测试
与生产者一样,我们也可以对消费者做性能测试。最好的办法是针对之前用 kafka-producer-perf-test.sh
生产的那个主题进行测试。用于消费者性能测试的工具是 kafka-consumer-perf-test.sh
:
css
$ kafka-consumer-perf-test.sh \
--topic performance-test \
--messages 1000000 \
--bootstrap-server localhost:9092
该命令的行为与 kafka-producer-perf-test.sh
略有不同。我们不能直接在命令行上改变消费者配置,而必须传入一个配置文件。例如,如果我们想把 fetch.min.bytes
设高到 1 MB,并显式把 fetch.max.wait.ms
设为 500 ms,就创建一个 consumer.properties
,内容如下:
python
fetch.min.bytes: 1000000
fetch.max.wait.ms: 500
然后再用该配置文件运行 kafka-consumer-perf-test.sh
:
css
$ kafka-consumer-perf-test.sh \
--topic performance-test \
--consumer.config ./consumer.properties \
--messages 1000000 \
--bootstrap-server localhost:9092
输出比较啰嗦,例如第一行会是:
lua
start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg,
nMsg.sec, rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec
2025-02-01 10:59:37:208, [...]
我们在文中省略了完整输出,便于对比时查看两次消费者测试结果(见表 6.2)。
即便以表格形式呈现,也需仔细看才能理解性能测试的含义。start.time
与 end.time
告诉我们命令执行耗时。使用自定义 consumer.properties
时命令耗时仅 13 秒,而默认配置时为 17 秒。两次命令处理的消息数量(data.consumed.in.nMsg=1000000
)和数据量(data.consumed.in.MB=9536.7432
,约 9.5 GB)相同。因此,我们用自定义配置达到约 734 MB/s 的读取速度,而默认值时为 547 MB/s。其他字段在使用消费者组时才重要(本例未用到),因此可忽略。
表 6.2 消费者性能输出(示例)
名称 | 使用默认值的结果 | 使用自定义 consumer.properties 的结果 |
---|---|---|
start.time | 2025-02-01 10:44:18:30 | 2025-02-01 10:59:37:208 |
end.time | 2025-02-01 10:44:35:742 | 2025-02-01 10:59:50:200 |
data.consumed.in.MB | 9536.7432 | 9536.7432 |
MB.sec | 547.0512 | 734.0473 |
data.consumed.in.nMsg | 1,000,000 | 1,000,000 |
nMsg.sec | 57,362.4735 | 76,970.4433 |
我们通过调整几项配置观察到消费者性能有显著提升。不过要记住,消费者性能通常受限于消费端处理数据的速度,而不是 Kafka 本身。此外,我们的测试显示消费者速度明显超过了生产者。在我们的例子中,生产者达到过最高 437 MB/s,而消费者则可达 734 MB/s。
注意:实际数值与性能差异强烈依赖硬件配置。特别是在本地 Kafka 环境中,两者差距可能较小。
这里的测试仅为示例,结果并不具代表性,应谨慎解读。真实性能测试应使用更接近生产的数据并在类似生产的环境中运行。
我们在此仅针对单个主题、单分区且无副本进行测试。这样可以避免复制开销,但也无法并行处理(若要并行需用更多分区)。
我们发现单个消费者通常能无缝接收数据;瓶颈通常不是 Kafka,而是网络带宽或自身代码中的后续处理。要真正提高消费者吞吐,单个消费者往往不够------需要用消费者组来横向扩展并并行处理数据。此外,消费者组也用于在 Kafka 中存储 offsets,通常即便只有一个消费者也会使用消费者组来管理 offsets,从而避免额外顾虑。
最后删除占用大量磁盘空间的 performance-test
主题:
css
$ kafka-topics.sh \
--delete \
--topic performance-test \
--bootstrap-server localhost:9092
总结
- 高吞吐不等于低延迟,但两者同样重要。
- 分区可以分散负载,从而提升性能。
- 分区策略应识别消费者或 Kafka 的性能瓶颈并相应调整分区数。
- 考虑平衡分区数量以管理客户端内存使用和运维复杂度。
- 可从默认 12 个分区开始,根据需要为高吞吐扩展,同时权衡运维和成本影响。
- 分区数不能减少。
- 增加分区数可能导致消息被错误地消费(顺序被打乱)。
- 消费者组会在成员之间分配负载。
- 批量(batching)可以提高带宽,但也可能增加延迟。
- 批量相关配置通过
batch.size
和linger.ms
控制。 - 生产者可以压缩批次以减少带宽需求,但这可能会增加延迟。
- 使用
acks=all
会略微降低生产者性能;启用幂等性(idempotence)亦然。 - Broker 不会解压批次,解压由消费者负责。
- 大多数情况下,Broker 无需额外微调。
- Broker 会为每个分区打开文件描述符。
- Kafka 在很大程度上依赖操作系统,因此需要进行操作系统层面的优化以发挥最佳性能。
- 消费者性能主要取决于消费者组中的消费者数量,但也可通过设置
fetch.max.wait.ms
和fetch.min.bytes
进行调优。