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