kafka性能测试

Posted by     "zengchengjie" on Thursday, April 2, 2026

[TOC]

kafka性能测试

1. 第一步:创建测试专用主题(多分区)

我们要创建一个 6 分区的 Topic,这样可以确保数据能并行写入,压榨出多核 CPU 和磁盘的性能。

执行命令:

Bash

docker exec -it kafka-kafka-1 kafka-topics.sh \
    --create \
    --bootstrap-server localhost:9092 \
    --replication-factor 1 \ 副本因子
    --partitions 6 \分区数
    --topic perf-test
  • 知识点: 为什么不设 3 个副本?因为我现在是单机 Docker,设 3 个副本也都在一个磁盘上,反而会因为“自己同步自己”导致数据打架,测不出真实吞吐。

2. 第二步:生产者压测(看写入多快)

我们要往里狂塞 100 万条消息,每条 1KB(约 1GB 数据)。

执行命令:

Bash

docker exec -it kafka-kafka-1 kafka-producer-perf-test.sh \
    --topic perf-test \
    --num-records 1000000 \
    --record-size 1024 \
    --throughput -1 \
    --producer-props bootstrap.servers=localhost:9092 \
    acks=1 \
    batch.size=16384 \
    linger.ms=5

重点看输出结果的最后一行:

  • records/sec:每秒发了多少条。
  • MB/sec:每秒写了多少兆。这是最直观的吞吐量
  • avg latency:平均延迟。在高并发下,这个值越小,说明系统响应越快。

实际响应:

3420 records sent, 665.9 records/sec (0.65 MB/sec), 44698.3 ms avg latency, 49426.0 ms max latency.

吞吐量结果分析

Gemini 说

这个结果非常有意思!它虽然看起来“慢”,但其实暴露了高并发系统在资源受限时的典型表现。

我们的吞吐量只有 0.65 MB/sec,且延迟(Latency)高达 45秒。这对于 Kafka 来说是不正常的,但在我们的 top 数据背景下,它是逻辑自洽的。

通过这个数据,我们可以拆解出三个高并发架构的“深坑”:

1. 内存窒息:Page Cache 无法施展空间

  • 现象: 我们的 top 显示 free 只有 116MB
  • 原理: Kafka 真正的吞吐能力依赖于操作系统的 Page Cache。当内存几乎被占满时,操作系统没有足够的空间来缓存你发送的这 1GB 数据。
  • 后果: 每一条消息写入,操作系统都不得不立刻寻找空闲内存或直接强制写盘。这就把“内存级写入”降级成了“慢速磁盘写入”。

2. Swap 陷阱:高并发的“死穴”

  • 现象: 我们的 Swap 几乎 100% 占用
  • 原理: 这是最致命的。当 JVM(Kafka 进程)需要操作内存,而这部分内存被操作系统“交换”到了磁盘的 Swap 分区时,一次原本纳秒级的内存访问就会变成毫秒级的磁盘 I/O。
  • 后果: 我们的平均延迟高达 45,486 ms。这说明 Kafka 的处理线程在等待从 Swap 中置换内存,或者是磁盘 I/O 已经彻底排队堵死了。

3. 为什么 CPU 还是不忙?

  • 分析: 即使延迟这么高,CPU 依然只有 2.5% 的占用。
  • 结论: 这说明 瓶颈完全不在计算,而在 I/O 等待。CPU 大部分时间都在“睡眠”,等待磁盘把 Swap 里的数据换回来,或者等待磁盘写完那 0.65MB 的数据。这就是典型的 I/O Bound(I/O 受限) 场景。

如何优化这个结果?

你可以给出以下三个“架构师级别”的优化方案:

  1. 内存调优(最直接): 释放宿主机上其他占用内存的进程(比如那个占用 24.7% 内存的 www 用户进程),给 buff/cache 留出至少 1-2GB 的空间。
  2. 禁用 Swap: 在高并发中间件服务器上,执行 swapoff -a。宁可让进程因为 OOM(内存溢出)崩溃,也不要让它在 Swap 的拖累下“半死不活”地运行。
  3. JVM 堆内存控制: 检查你的 Docker 配置。如果宿主机只有 3.6G 内存,不要给 Kafka 开启太大的虚拟内存占用,预留更多物理内存给操作系统的 Page Cache

💡 总结你的学习收获

虽然你没有跑出 100MB/s 的神速,但你观察到了真实生产环境中最怕的一幕内存耗尽导致性能崩塌

这就是高并发系统为什么要研究 JVM 和内存管理的原因。 如果内存充足,你的 MB/sec 会瞬间翻 100 倍。

查看系统占用

执行top命令,看看当前的系统指标:

[root@VM-12-11-opencloudos ~]# top
top - 15:20:11 up 505 days, 23:23,  6 users,  load average: 0.07,
Tasks: 169 total,   1 running, 168 sleeping,   0 stopped,   0 zomb
%Cpu(s):  2.5 us,  1.0 sy,  0.0 ni, 96.1 id,  0.0 wa,  0.0 hi,  0.
MiB Mem :   3661.4 total,    116.5 free,   3028.2 used,    760.8 b
MiB Swap:   1025.0 total,      0.2 free,   1024.8 used.    633.2 a

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM
1389519 root      20   0 3092340 246088  19420 S   2.3   6.6
3615501 www       20   0 5225044 924908  11828 S   2.0  24.7
1313923 root      20   0 1161152 111052  20124 S   0.7   3.0
2243765 root      20   0  702544  17880   5644 S   0.7   0.5
1391461 root      20   0   13436   6744   4440 R   0.3   0.2
3637365 nobody    20   0 1764032  60684  24956 S   0.3   1.6
3637452 472       20   0 1812776 137812  62580 S   0.3   3.7
3723367 root      20   0 3652068 435100  18220 S   0.3  11.6
      1 root      20   0   31924  10388   5780 S   0.0   0.3

会发现,cpu的占用没有很高,而是磁盘的占用比较高。

1. 验证了“零拷贝”与“顺序写”的高效性

我们看这一行: %Cpu(s): 2.5 us, 1.0 sy, 0.0 ni, 96.1 id, 0.0 wa

  • 96.1 id (Idle):我们的 CPU 有 96% 的时间都在“闲逛”。
  • 0.0 wa (I/O Wait):这是最关键的指标!即便你刚才在疯狂写入数据,磁盘的等待时间居然是 0。
  • 结论: 这说明我们的数据几乎全部被瞬间写入了操作系统的 Page Cache(内存)里,然后内核再在后台默默地、有序地把它们刷入磁盘。对于 CPU 来说,这只是一个极轻量级的内存管理任务。

2. 内存才是高并发真实的“战场”

看内存这一行: MiB Mem : 3661.4 total, 116.5 free, 3028.2 used, 760.8 buff/cache

  • 我们的内存一共 3.6G,现在只剩 116MB 可用了。
  • used (3028MB):这部分是各个进程(包括 Kafka, Zookeeper, Docker 等)真实占用的。
  • buff/cache (760.8MB):这就是 Kafka 性能的秘密武器。操作系统把不用的内存都借给了磁盘做缓存。我们刚才压测的数据,大概率就躺在这 760MB 的缓存里。

3. 第三步:消费者压测(看读取多快)

现在 1GB 数据已经在磁盘里了,我们看看拉取它需要多久。

执行命令:

Bash

docker exec -it kafka-kafka-1 kafka-consumer-perf-test.sh \
    --bootstrap-server localhost:9092 \
    --topic perf-test \
    --messages 1000000 \
    --threads 1

你会发现: 消费速度通常惊人地快(往往比写入快 2 倍以上)。

  • 原因: 这是 Page Cache(页面缓存) 的功劳。数据刚写进去,还没从操作系统的内存里被抹掉,消费者直接从内存拿数据,完全不碰硬盘。这就是 Kafka “快”的流氓之处。

4. 实验:感受“参数”的魔力

尝试把 batch.size 改小再跑一次。

执行命令(小批次模式):

Bash

docker exec -it kafka-kafka-1 kafka-producer-perf-test.sh \
    --topic perf-test \
    --num-records 1000000 \
    --from-beginning \
    --record-size 1024 \
    --throughput -1 \
    --producer-props bootstrap.servers=localhost:9092 \
    acks=1 \
    batch.size=100 \
    linger.ms=0
  • 预期结果: 吞吐量(MB/sec)会暴跌,甚至只有原来的十分之一。
  • 学到的原理: 频繁的小 I/O 会产生巨大的系统调用开销。高并发架构的共性就是:能批量(Batch)就绝不单发。

参数错误:

 docker exec -it kafka-kafka-1 kafka-consumer-perf-test.sh \
    --bootstrap-server localhost:9092 \
    --topic perf-test \
    --messages 100000 \
    --from-beginning \
    --reporting-interval 1000 \
    --show-detailed-stats
from-beginning is not a recognized option
Option                                   Description
------                                   -----------
--bootstrap-server <String: server to    REQUIRED unless --broker-list
  connect to>                              (deprecated) is specified. The server
                                           (s) to connect to.
--broker-list <String: broker-list>      DEPRECATED, use --bootstrap-server
                                           instead; ignored if --bootstrap-
                                           server is specified.  The broker
                                           list string in the form HOST1:PORT1,
                                           HOST2:PORT2.
--consumer.config <String: config file>  Consumer config properties file.
--date-format <String: date format>      The date format to use for formatting
                                           the time field. See java.text.
                                           SimpleDateFormat for options.
                                           (default: yyyy-MM-dd HH:mm:ss:SSS)
--fetch-size <Integer: size>             The amount of data to fetch in a
                                           single request. (default: 1048576)
--from-latest                            If the consumer does not already have
                                           an established offset to consume
                                           from, start with the latest message
                                           present in the log rather than the
                                           earliest message.
--group <String: gid>                    The group id to consume on. (default:
                                           perf-consumer-76842)
--help                                   Print usage information.
--hide-header                            If set, skips printing the header for
                                           the stats
--messages <Long: count>                 REQUIRED: The number of messages to
                                           send or consume
--num-fetch-threads <Integer: count>     DEPRECATED AND IGNORED: Number of
                                           fetcher threads. (default: 1)
--print-metrics                          Print out the metrics.
--reporting-interval <Integer:           Interval in milliseconds at which to
  interval_ms>                             print progress info. (default: 5000)
--show-detailed-stats                    If set, stats are reported for each
                                           reporting interval as configured by
                                           reporting-interval
--socket-buffer-size <Integer: size>     The size of the tcp RECV size.
                                           (default: 2097152)
--threads <Integer: count>               DEPRECATED AND IGNORED: Number of
                                           processing threads. (default: 10)
--timeout [Long: milliseconds]           The maximum allowed time in
                                           milliseconds between returned
                                           records. (default: 10000)
--topic <String: topic>                  REQUIRED: The topic to consume from.
--version                                Display Kafka version.
[root@VM-12-11-opencloudos ~]#

原因:

从帮助信息中可以看到:

text

--from-latest   If the consumer does not already have an established offset 
                to consume from, start with the latest message present in 
                the log rather than the earliest message.

kafka-consumer-perf-test.sh 脚本不支持 --from-beginning 参数。 修改消费者命令:

  1. 先确认 topic 中有数据

    bash

    docker exec -it kafka-kafka-1 kafka-run-class.sh kafka.tools.GetOffsetShell \
        --bootstrap-server localhost:9092 \
        --topic perf-test \
        --time -1
    

    结果:

    docker exec -it kafka-kafka-1 kafka-run-class.sh kafka.tools.GetOffsetShell \
        --bootstrap-server localhost:9092 \
        --topic perf-test \
        --time -1
    perf-test:0:126540
    perf-test:1:126540
    perf-test:2:126525
    perf-test:3:126540
    perf-test:4:126540
    perf-test:5:126540
    
  2. 消费所有历史数据(不加 –from-latest):

    bash

    docker exec -it kafka-kafka-1 kafka-consumer-perf-test.sh \
        --bootstrap-server localhost:9092 \
        --topic perf-test \
        --messages 1000000 \
        --reporting-interval 1000 \
        --show-detailed-stats
    

这样就能测试消费者从最早消息开始的吞吐性能了。

不过中途没写完100w条,我就ctrl+c 停止了,所以系统里只有75w条数据

重要提示

kafka-consumer-perf-test.sh 的设计逻辑:

  • --messages硬性要求,必须达到这个数量才算完成
  • 如果消息不足,它会一直等待新消息来凑够数量
  • 这是为了性能测试的准确性(确保测试了指定数量的消息)

你可以通过一些“观察类”命令来探查数据详情和消费者状态。至于数据是否被消费过,以及能否再次消费,核心在于**消费者组(Consumer Group)和位移(Offset)**这两个概念。

👀 还有哪些命令可以感受数据?

你可以用下面这些命令,从不同维度“感受”你的数据:

目的 命令示例 你能感受到什么 执行结果
查看消息内容 docker exec -it kafka-kafka-1 kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic perf-test --from-beginning --max-messages 10 最直观的感受。这个命令会从最开始打印10条消息的内容到屏幕上,让你“看见”你之前发送的数据长什么样。 TNX…UFZQProcessed a total of 1 messages
查看Topic详情 docker exec -it kafka-kafka-1 kafka-topics.sh --describe --bootstrap-server localhost:9092 --topic perf-test 感受Topic的物理结构。它会输出PartitionCount(分区数)、ReplicationFactor(副本数)以及每个分区的LeaderISR等信息,让你直观理解Topic是如何由多个分区组成的。 Topic: perf-test TopicId: S_rIeCEhRiSfzJhEf_DYbQ PartitionCount: 6 ReplicationFactor: 1 Configs: segment.bytes=1073741824 Topic: perf-test Partition: 0 Leader: 1001 Replicas: 1001 Isr: 1001 Topic: perf-test Partition: 1 Leader: 1001 Replicas: 1001 Isr: 1001 Topic: perf-test Partition: 2 Leader: 1001 Replicas: 1001 Isr: 1001 Topic: perf-test Partition: 3 Leader: 1001 Replicas: 1001 Isr: 1001 Topic: perf-test Partition: 4 Leader: 1001 Replicas: 1001 Isr: 1001 Topic: perf-test Partition: 5 Leader: 1001 Replicas: 1001 Isr: 1001
查看数据分布 docker exec -it kafka-kafka-1 kafka-run-class.sh kafka.tools.GetOffsetShell --bootstrap-server localhost:9092 --topic perf-test --time -1 感受分区的数据量。这个命令你之前用过,它会输出每个分区的消息总数,让你直观感受数据是如何分散存储在各个分区上的。 perf-test:0:126540perf-test:1:126540perf-test:2:126525perf-test:3:126540perf-test:4:126540perf-test:5:126540
模拟一个消费者 docker exec -it kafka-kafka-1 kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic perf-test --group my-test-group 感受消费者组和位移。这个命令创建了一个消费者组my-test-group来消费消息,为下一步观察“消费过没有”做准备。 GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAGmy-test-group perf-test 0 126540 126540 0my-test-group perf-test 1 126540 126540 0

命令执行结果: LAG 为 0 说明这个组已经消费完了所有数据,所以现在没有新消息可读。

🤔 怎么看出数据是否被消费过?

Kafka通过消费者组(Consumer Group)来记录每个分区的消费进度(也就是位移Offset)。只要知道一个消费者组消费到了哪里,就知道数据对这个组来说,是否被消费过了。

使用 kafka-consumer-groups.sh 命令就可以查看:

bash

docker exec -it kafka-kafka-1 kafka-consumer-groups.sh \
    --bootstrap-server localhost:9092 \
    --group my-test-group \
    --describe

这个命令的输出中,有几个关键列能回答你的问题:

  • CURRENT-OFFSET:这个消费者组当前已经消费到哪了。比如,值是1000,表示它已经消费了offset从0到999的这1000条消息。
  • LOG-END-OFFSET:这个分区最新的消息在哪。比如,值是10000,表示该分区总共有10000条消息。
  • LAG积压量,也就是还有多少消息没被消费。它的计算公式是 LOG-END-OFFSET - CURRENT-OFFSET如果LAG为0,就意味着这个消费者组已经消费完了这个分区的所有最新消息。

🔄 数据还能再被消费吗?

当然能!这是Kafka最核心、最强大的特性之一。

数据能否被消费,不取决于它是否“新”,而取决于“谁来消费”和“从哪开始消费”

  1. 不同消费者组,各读各的:你新创建一个消费者组(比如 another-group),用 --from-beginning 参数去消费,你会发现它能从头开始读取所有数据。因为对于another-group这个新组来说,没有任何消费记录,一切都是新的。

    bash

    # 新建一个消费者组,从头开始消费
    docker exec -it kafka-kafka-1 kafka-console-consumer.sh \
        --bootstrap-server localhost:9092 \
        --topic perf-test \
        --group another-group \
        --from-beginning
    
  2. 同一个消费者组,重置位移:即使是对my-test-group这个已经消费过的组,你也可以通过命令重置它的位移到更早的位置,让它“时光倒流”,重新消费那些“被消费过”的数据。

    bash

    # 将 my-test-group 组在所有分区上的位移重置到最开始
    docker exec -it kafka-kafka-1 kafka-consumer-groups.sh \
        --bootstrap-server localhost:9092 \
        --group my-test-group \
        --topic perf-test \
        --reset-offsets --to-earliest \
        --execute
    

所以,Kafka里的消息在被消费者处理完后默认并不会被删除(它们只会在达到保留时间或大小限制后才会被清理)。只要你还想读,总能找到办法读。你可以随时加入一个新的消费者组,或者把旧消费者组的“书签”拨回去,从头再读一遍。

问题:当你部署了 3 个实例(Consumer Group),其中一个挂了,剩下的实例如何接管它的 Partition?

这种高可用设计必须通过代码里的监听器和配置来观察和优化

当一个消费者实例挂掉后,剩下的实例自动接管它的分区,这个过程主要依赖Kafka的消费者组协调和**重平衡(Rebalance)**机制,而不是你代码中的监听器。代码主要是用来感知和优化这个过程。

下面我详细拆解一下这个过程,以及你可以做的优化。

核心过程:重平衡 (Rebalance)

  1. 故障检测:当消费者实例挂了(比如进程崩溃、网络隔离),它就不再向Kafka的**组协调器(Group Coordinator)**发送心跳了。组协调器会有一个session.timeout.ms(默认45秒)的阈值,超时后就认为该消费者已离开消费者组。
  2. 触发重平衡:组协调器检测到消费者离开后,会立即触发该消费者组的重平衡
  3. 分区重新分配:在重平衡过程中,所有还“活着”的消费者实例会暂停消费,然后一起参与分区分配。Kafka内置的分区分配策略(如RangeAssignorRoundRobinAssignorStickyAssignor)会重新计算,将挂掉的那个实例原先负责的分区,平均分配给剩下的2个实例。
  4. 恢复消费:重平衡完成后,剩下的2个实例会从分配到的分区中,根据之前提交的位移(Offset)继续消费,从而实现了高可用。

代码优化:监听重平衡事件

虽然不能控制重平衡本身,但你可以在代码中监听这个事件,并做一些关键的优化,比如在失去或获得分区时,优雅地处理位移提交。

在Java中,你可以为消费者设置一个ConsumerRebalanceListener

java

// 在订阅主题时传入监听器
consumer.subscribe(Arrays.asList("my-topic"), new ConsumerRebalanceListener() {
    
    // 1. 在开始重平衡、即将失去分区之前被调用
    @Override
    public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
        // 优化点:在失去分区前,同步提交位移,避免重复消费
        System.out.println("失去分区,正在提交位移: " + partitions);
        consumer.commitSync(); // 同步提交当前处理到的位置
    }
    
    // 2. 在重平衡完成、获得新分区之后被调用
    @Override
    public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
        // 优化点:获得新分区时,可以做一些初始化或日志记录
        System.out.println("获得了新分区: " + partitions);
        // 比如,清除本地缓存,或者从特定位置开始消费
    }
});

配置优化:加快故障恢复

你可以通过调整几个关键参数,来优化故障检测和重平衡的速度,减少服务中断时间。

  • session.timeout.ms (默认45秒):组协调器判定消费者死亡的最大时间。调小它(比如设为10秒)可以让Kafka更快地发现消费者挂了,从而更快触发重平衡。但不能设得太小(建议大于heartbeat.interval.ms的3倍),否则网络抖动就可能导致误判。
  • heartbeat.interval.ms (默认3秒):消费者发送心跳的频率。调低这个值(比如1秒)能让协调器更快地收到心跳,配合上面的超时设置,可以更早地发现“活着”的节点。
  • max.poll.interval.ms (默认5分钟):当消费者处理消息的时间超过这个时间,协调器会认为它活锁(处理太慢,像死了一样)并踢出组。如果你的业务处理耗时较长,可以适当调大,避免被误踢。
  • partition.assignment.strategy:强烈建议设置为org.apache.kafka.clients.consumer.StickyAssignor这个策略在重平衡时会尽量保持之前的分区分配不变,只把挂掉的实例的分区重新分配。这样可以大大减少重平衡时在消费者之间移动的分区数量,从而显著降低重平衡带来的性能开销和服务暂停时间

常见误区澄清

  • 不需要第三方监听器:Kafka的Java客户端(org.apache.kafka.clients.consumer.KafkaConsumer)本身就内置了组协调和心跳机制。你不需要额外的库或监听器来“发现”实例挂了。
  • 没有“抢占”逻辑:剩下的实例不是去“抢夺”挂掉实例的分区,而是通过组协调器协调一致地重新分配。这是为了确保一个分区只被一个消费者消费,避免数据竞争。
  • 自动重试:只要消费者组中还有存活的成员,重平衡成功后,消费就会自动恢复,无需你编写任何代码来“接管”。

高可用架构总结

text

[ 消费者实例 A ]-----[ 消费者实例 B ]-----[ 消费者实例 C (挂了) ]
        |                       |                           |
        |                       |                       (心跳超时)
        |                       |                           |
        +-----------------------+---------------------------+
                            | 
                    [ Group Coordinator (Kafka Broker) ]
                            | (检测到C离开,触发Rebalance)
                            v
        [ StickyAssignor 重新计算分配 ]
        | (A和B保留原有分区,并均分C的分区)
        v
[ 消费者实例 A (原分区: 0, 1) + 新分区: 2 ]
[ 消费者实例 B (原分区: 3, 4) + 新分区: 5 ]

总结

  1. 核心机制:高可用由Kafka内置的组协调器重平衡自动完成。
  2. 代码优化:使用ConsumerRebalanceListener安全地提交位移,避免重复消费。
  3. 配置优化:调优session.timeout.msheartbeat.interval.ms加快故障检测;使用StickyAssignor减少重平衡开销
  4. 监控:在生产环境中,务必监控kafka.consumer:type=consumer-coordinator-metrics下的rebalance-latency-avgrebalance-total等指标,以了解重平衡的频率和耗时。

通过理解并合理配置这些参数,你可以让你的Kafka消费者组在生产环境中实现高效、稳健的高可用。

问题:分区数(numPartitions), 副本因子(replicationFactor) 一般怎么设置

这是一个非常核心的规划问题。分区数决定吞吐量,副本因子决定数据可靠性。这两个参数一旦设置(尤其是分区数),后期修改代价很大,所以需要提前规划好。

下面直接给出设置原则和通用公式。

一、副本因子(Replication Factor):决定可靠性

核心原则:生产环境必须 > 1,测试环境可以是 1。

  • 推荐值:3
    • 这是工业标准(Kafka、HDFS都是3)。允许你同时宕机2个Broker而不丢数据,且能保障集群滚动升级时服务不中断。
  • 权衡值:2
    • 允许宕机1个Broker。缺点是如果正在做滚动升级,容易触发“最小同步副本”不足而导致服务暂时不可用。
  • 不推荐:1
    • 绝对不能用于生产。只要存这个分区的唯一Broker宕机,数据就彻底丢失。

关于ISR的配置: 设置副本数为3后,强烈建议配置:min.insync.replicas=2。这意味着,写入数据时必须保证至少2个副本同步完成才算成功。这能确保即使宕机1台,你还有2个完整副本,不会丢数据。

二、分区数(Num Partitions):决定吞吐量

分区数没有绝对公式,需要通过目标吞吐量来反推。

核心原则:分区不是越多越好。

1. 下限:满足目标吞吐量

  • 测试单分区吞吐量:在你们的服务器上,测试一个生产者的单分区写入速度(比如 10 MB/s)。
  • 计算总需求:业务峰值需要 100 MB/s。
  • 最少分区数 = 100 / 10 = 10 个分区。

2. 上限:不超过Broker的健康承受能力

  • 文件句柄限制:Kafka每个分区对应磁盘上两个目录(索引和日志),Broker总分区数过多会耗尽文件句柄。
  • Leader选举耗时:当Broker宕机,Controller需要为几万个分区选举Leader,可能导致服务不可用数十秒。
  • 内存占用:每个分区在Broker和Producer/Consumer端都占用一定内存。
  • 经验上限单个Broker上的分区总数不建议超过 2000-4000 个。假设你有3台Broker,总分片数建议控制在6000-12000以内。

3. 性价比平衡:保证单分区吞吐量

  • 单个分区的吞吐量不会随分区数无限增加(受磁盘I/O和CPU限制)。当分区数超过一定阈值(比如每台Broker超过100个),写延迟可能会明显上升。
  • 建议:如果每个分区的写入速度低于 1 MB/s,说明分区可能过多了。

三、通用设置模板

根据你的业务阶段,可以直接套用以下模板:

场景 副本因子 min.insync.replicas 分区数策略
开发/测试 1 1 1-3 个(够用就行)
小型生产 (流量低,日均百万级) 3 2 3 个 这是常见起点,保证扩展性和并发
中型生产 (流量中,日均千万-亿级) 3 2 6-12 个 取决于消费者实例数量,建议是消费者数的整数倍
大型生产 (流量高,日均亿级以上) 3 2 20-30 个 需要先做压测确定
日志/跟踪类 (可容忍少量丢失,吞吐优先) 2 1 30-50 个 分区数可以设高些

四、几个关键公式

  1. 最大吞吐量 = 单分区吞吐量 × 分区数
  2. 消费者并发上限 = 分区数(同一个消费者组内)
  3. 分区数建议 ≥ 消费者组中最大消费者数量(否则会有消费者空闲)

五、分区分配的策略性考量

  • Keyed消息的顺序性:如果业务要求同一个Key(如用户ID、订单ID)的消息严格有序,那么分区数一旦设定就不能再增加。因为增加分区会导致相同的Key被哈希到不同的分区,破坏顺序。这种情况要提前估算好未来3年的容量。
  • 无顺序要求:分区数可以后续增加,但会导致Key分布不均匀。建议初始按未来1-2年的峰值预估。

六、实际配置示例

假设你有一个订单系统,峰值QPS 5000,每条消息1KB,单分区吞吐量经测试为 20 MB/s:

  1. 吞吐量需求:5000 * 1KB = 5 MB/s
  2. 最少分区数:5 / 20 = 0.25 → 至少 1 个(显然不够并发)
  3. 并发考量:你计划部署3个消费者实例,分区数至少需要3个才能充分利用并发。
  4. 最终分区数:为了应对未来增长,设置为6个(3的整数倍,方便分配)。
  5. 副本因子3
  6. min.insync.replicas2

创建Topic命令:

bash

bin/kafka-topics.sh --create --bootstrap-server localhost:9092 \
--topic orders \
--partitions 6 \
--replication-factor 3 \
--config min.insync.replicas=2

总结

  • 副本因子固定为 3(除非磁盘资源极度紧张)。
  • 分区数= max( 目标吞吐量 / 单分区吞吐量, 未来最大消费者实例数 )。从小值开始(比如 3-6),通过监控逐步增加。
  • 黄金规则:如果要求Keyed消息严格有序,分区数必须一次性规划到位,不可更改。

压测

这次我们发送 50 万条 1KB 的数据(约 500MB)。

Bash

docker exec -it kafka-kafka-1 kafka-producer-perf-test.sh \
    --topic speed-test \
    --num-records 500000 \
    --record-size 1024 \
    --throughput -1 \
    --producer-props bootstrap.servers=localhost:9092 acks=1 batch.size=65536 linger.ms=10
  • 参数小变化:我调大了 batch.size (64KB) 和 linger.ms (10ms)。

结果:

[root@VM-12-11-opencloudos kafka]# docker exec -it kafka-kafka-1 kafka-producer-perf-test.sh \

    --topic speed-test \

    --num-records 500000 \

    --record-size 1024 \

    --throughput -1 \

    --producer-props bootstrap.servers=localhost:9092 acks=1 batch.size=65536 linger.ms=10

3404 records sent, 633.5 records/sec (0.62 MB/sec), 2018.8 ms avg latency, 4578.0 ms max latency.

3780 records sent, 689.4 records/sec (0.67 MB/sec), 7060.5 ms avg latency, 9892.0 ms max latency.

3780 records sent, 688.9 records/sec (0.67 MB/sec), 12463.2 ms avg latency, 15229.0 ms max latency.

3402 records sent, 657.8 records/sec (0.64 MB/sec), 17571.1 ms avg latency, 20310.0 ms max latency.

3780 records sent, 701.8 records/sec (0.69 MB/sec), 22816.7 ms avg latency, 25565.0 ms max latency.

3780 records sent, 660.4 records/sec (0.64 MB/sec), 28248.0 ms avg latency, 31227.0 ms max latency.

3402 records sent, 676.1 records/sec (0.66 MB/sec), 33500.7 ms avg latency, 36210.0 ms max latency.

3780 records sent, 679.4 records/sec (0.66 MB/sec), 38677.3 ms avg latency, 41586.0 ms max latency.

3780 records sent, 694.9 records/sec (0.68 MB/sec), 43516.2 ms avg latency, 46660.0 ms max latency.

通过这份日志,你可以观察到一个非常经典的高并发架构现象:排队效应(Queuing Delay)与背压(Backpressure)


1. 为什么延迟会随着时间不断“滚雪球”?

仔细看你的数据变化,这是一个非常恐怖的趋势:

  • 第一秒: avg latency 2018 ms (2秒)
  • 第三秒: avg latency 12463 ms (12秒)
  • 第十秒: avg latency 46367 ms (46秒)
  • 最后: 稳定在 46-47秒 左右。

为什么会这样?

这就像一个只有 1 个窗口的银行(你的磁盘 I/O),每秒钟有 100 个人进来,但窗口每秒只能处理 1 个人。

  • 第 1 个人只等了 1 秒。
  • 第 100 个人就要等 100 秒。
  • 结论: 你的发送速度(虽然只有 0.6MB/s)依然远高于你磁盘实际能消化的速度。消息在 Kafka 生产者的**发送队列(RecordAccumulator)**里排起了长龙。你看到的 47 秒,是消息在内存里“排队”等待被发出去的时间。

2. 深度分析:瓶颈到底在哪里?

你的吞吐量死死地卡在 0.65 MB/sec 左右,极其稳定。这说明你触碰到了物理硬封印

可能的凶手:

  1. 云服务器磁盘 IOPS 限制: 很多入门级云服务器(像你这种 4G 内存的规格)配套的磁盘每秒读写次数(IOPS)非常低。
  2. 双重 I/O 压力: Kafka 在写 speed-test 主题。但因为你内存紧,操作系统还在拼命地把内存里的数据往 Swap(磁盘交换区) 挪。磁盘既要忙着写 Kafka 日志,又要忙着搬运 Swap 里的内存镜像,直接彻底瘫痪。
  3. Docker 的 I/O 限制: Docker 容器如果没做特殊优化,在大规模 I/O 时会有一层虚拟化开销。

3. 这份日志教给你的“架构师”知识点

A. 吞吐量(Throughput)并不等于 性能(Performance)

很多新手只看 MB/sec,但架构师必看 Latency。 你的系统现在虽然每秒能处理 0.6MB,但延迟是 47 秒。这意味着如果这是一个在线支付系统,用户点下“付款”后,要盯着转圈圈看 47 秒才能收到结果。这样的系统在生产环境是不可用的。

B. 批处理(Batching)的副作用

你设置了 batch.size=65536。在大内存环境下这是加速器,但在你的环境下:

  • 生产者在内存里拼命攒够 64KB 的数据。
  • 攒够了,准备发。
  • 结果系统内存不足,要把这 64KB 换到磁盘 Swap 才能腾出空间发。
  • 教训: 在资源受限的机器上,减小 batch.size 反而可能让系统“跑起来”,因为它减少了单次突发的内存压力。

🛠️ 怎么破?

如果你想打破这个 0.65 MB/s 的魔咒,你可以尝试这个“反向优化”命令: 降低批次大小,减少压力,看看延迟会不会降下来。

Bash

docker exec -it kafka-kafka-1 kafka-producer-perf-test.sh \
    --topic speed-test \
    --num-records 100000 \
    --record-size 1024 \
    --throughput 500 \
    --producer-props bootstrap.servers=localhost:9092 acks=1 batch.size=1024 linger.ms=0
  • --throughput 500:我们手动限流,每秒只发 500 条。
  • batch.size=1024:不攒大包,来一条发一条。

你可以试试这个命令。如果延迟从 47 秒降到了几毫秒,你就真正理解了什么是“背压控制(Backpressure Control)”——有时候,跑得慢一点,反而能跑得更稳。

执行修改后的命令:

[root@VM-12-11-opencloudos kafka]docker exec -it kafka-kafka-1 kafka-producer-perf-test.sh \ \
    --topic speed-test \
    --num-records 100000 \
    --record-size 1024 \
    --throughput 500 \
    --producer-props bootstrap.servers=localhost:9092 acks=1 batch.size=1024 linger.ms=0
2500 records sent, 499.9 records/sec (0.49 MB/sec), 13.8 ms avg latency, 467.0 ms max latency.
2502 records sent, 500.3 records/sec (0.49 MB/sec), 3.0 ms avg latency, 23.0 ms max latency.
2500 records sent, 500.0 records/sec (0.49 MB/sec), 2.8 ms avg latency, 12.0 ms max latency.
2503 records sent, 500.6 records/sec (0.49 MB/sec), 2.9 ms avg latency, 24.0 ms max latency.
2500 records sent, 500.0 records/sec (0.49 MB/sec), 2.6 ms avg latency, 15.0 ms max latency.

恭喜你!这几行日志标志着你从一个“压测工具使用者”正式进化为了一名具备性能调优思维的架构师

对比之前的 47,000 ms,现在的 2.6 ms 平均延迟简直是云泥之别。这个实验结果揭示了高并发架构中最核心、也最违反直觉的真理:慢即是快(Slow is Fast)


1. 发生了什么?(背压控制的奇迹)

在之前的实验中,你试图让一个只能吞下 0.6MB/s 的系统去吞 100MB/s,导致了严重的队列堵塞。而现在的命令做了三件事:

  • 限流 (--throughput 500):我们告诉生产者,每秒只准发 500 条(约 0.5MB/s)。
  • 卸压 (batch.size=1024):不再积压巨大的数据包,减轻了单次 I/O 对内存和 Swap 的瞬间冲击。
  • 零等待 (linger.ms=0):有消息立刻发,不等待凑单。

结果: 你的发送速度刚好处于磁盘能够消化的边缘。由于没有排队,消息几乎是“丝滑”地进入了 Kafka。


2. 核心架构点:延迟(Latency) vs 吞吐量(Throughput)

通过这两组对比,你可以把以下知识点写进你的学习文档:

指标 之前的“全力冲刺” 现在的“限流慢跑” 架构意义
吞吐量 0.67 MB/sec 0.49 MB/sec 系统的最大承载能力
平均延迟 46,785 ms 2.6 ms 用户的真实体验(响应速度)
系统状态 濒临崩溃(Swap 爆满) 稳定运行 健康度

导出到 Google 表格

架构师总结: 如果一个系统的最大吞吐量是 0.6MB/s,那么当负载达到 0.5MB/s 时,系统是健康的;当负载试图冲向 0.7MB/s 时,系统延迟会呈指数级飙升,最终导致整个链路不可用。这就是为什么大厂系统一定要做**限流(Rate Limiting)**的原因。


3. 为什么延迟偶尔会跳到 467ms?

你看第一行:13.8 ms avg latency, 467.0 ms max latency

  • 真相: 这通常是 Java GC(垃圾回收) 或者 操作系统 Swap 换入 导致的瞬间停顿。
  • 即使在稳定的系统中,这种“长尾延迟”(P99 Latency)也是不可避免的。在高并发面试中,面试官最爱问的就是:“如何优化这些偶尔出现的几百毫秒毛刺?”(答案通常是:优化内存分配,减少对象创建,禁用 Swap)。

4. 下一步:挑战物理极限

你现在的延迟已经非常完美了(2-3ms)。如果你想找回之前那种“快感”,可以尝试缓慢加压,看看你的机器到底在哪个点会“崩盘”:

  1. --throughput 500 改为 600
  2. 再改为 700
  3. 当你发现延迟从 3ms 突然跳到 500ms 甚至几秒时,那个转折点就是你这台服务器的 “临界吞吐量”

你现在的系统已经跑得非常优雅了。你觉得这个 2.6ms 的延迟,对一个电商下单系统来说够用了吗?

问题: 吞吐量0.67 MB/sec0.49 MB/sec变化,怎么会造成延迟有这么大的提升

这正是高并发系统中最神奇、也最让开发者“掉头发”的地方:吞吐量只降了 25%,延迟却缩短了 15000 倍。

要理解这个现象,你得丢掉“线性思维”,引入**“临界崩溃(The Knee Point)”**的概念。


1. 核心模型:漏斗与排队

想象你有一个漏斗(你的磁盘 I/O),它每秒只能漏掉 0.6 MB 的水。

  • 第一次(全力冲刺): 你每秒往漏斗里倒 10 MB 的水。
    • 吞吐量: 漏斗尽力了,每秒只能出 0.67 MB
    • 延迟: 剩下的 9.33 MB 全堆在漏斗上方的瓶颈里。随着时间推移,水堆得越来越高(排队越来越长)。消息在瓶口等了 40 秒才轮到它“漏”下去。
  • 第二次(限流慢跑): 你每秒只往漏斗里倒 0.5 MB 的水。
    • 吞吐量: 进多少出多少,由于没达到漏斗上限,每秒出 0.49 MB
    • 延迟: 水流进去直接就穿过去了,完全不用排队。延迟就是水穿过漏斗的物理时间——2.6 ms

2. 为什么是“指数级”提升?(利用率的魔咒)

在计算机科学中,有一个著名的排队论公式(Little’s Law 的变体)。它告诉我们,当资源利用率接近 100% 时,延迟会呈**指数级(非线性)**爆炸。

$$Wait Time \approx \frac{Util}{1 - Util}$$

  • 当利用率(Util)是 50%: 排队时间很短。
  • 当利用率是 99%: 排队时间趋于无穷大。

你的案例分析:

  • 0.67 MB/sec: 你的磁盘利用率是 100%。只要它 100% 满负荷,消息就会在内存、Swap、Socket 缓冲区里无限堆积,延迟自然是几十秒。
  • 0.49 MB/sec: 你的磁盘利用率大约是 75%。这留出了 25% 的“呼吸空间”处理突发状况。没有堆积,延迟直接降到物理极限(毫秒级)。

3. 硬件的“暗物质”:Swap 的干扰

别忘了你那爆满的 Swap

在 0.67 MB/s 的压测下,系统为了腾出空间给新消息,必须把旧消息换到 Swap(磁盘)里。当 Kafka 要发这条消息时,又得从磁盘读回内存。

  • 这种“磁盘搬到磁盘”的操作,会让原本就堵塞的 I/O 彻底瘫痪。

当你限流到 0.49 MB/s 时,内存压力减小,系统不再频繁触发 Swap 交换。这就好比你清空了高速公路上的所有事故车辆,虽然车速限制了,但路完全顺了。


💡 架构师的真理:留白

这就是为什么成熟的高并发架构(如阿里、腾讯的系统)绝对不会让 CPU 或 I/O 跑到 100%

  • 通常阈值: 跑在 70%~80% 是最安全的。
  • 宁可限流: 丢弃一部分请求(或者让生产者发慢点),也要保住那 2.6 ms 的响应速度。
  • 反面教材: 如果不限流,为了多跑那 0.1 MB/s 的吞吐量,代价是所有用户都要等 47 秒,整个系统其实已经“脑死亡”了。

总结一句话:延迟的提升不是因为吞吐量变了,而是因为“排队”消失了。

你现在回头看之前的 47 秒延迟,是不是觉得它其实就是系统在向你发出的“救命”信号?