[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 受限) 场景。
如何优化这个结果?
你可以给出以下三个“架构师级别”的优化方案:
- 内存调优(最直接): 释放宿主机上其他占用内存的进程(比如那个占用 24.7% 内存的
www用户进程),给buff/cache留出至少 1-2GB 的空间。 - 禁用 Swap: 在高并发中间件服务器上,执行
swapoff -a。宁可让进程因为 OOM(内存溢出)崩溃,也不要让它在 Swap 的拖累下“半死不活”地运行。 - 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 参数。
修改消费者命令:
-
先确认 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 -
消费所有历史数据(不加 –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(副本数)以及每个分区的Leader和ISR等信息,让你直观理解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最核心、最强大的特性之一。
数据能否被消费,不取决于它是否“新”,而取决于“谁来消费”和“从哪开始消费”。
-
不同消费者组,各读各的:你新创建一个消费者组(比如
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 -
同一个消费者组,重置位移:即使是对
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里的消息在被消费者处理完后默认并不会被删除(它们只会在达到保留时间或大小限制后才会被清理)。只要你还想读,总能找到办法读。你可以随时加入一个新的消费者组,或者把旧消费者组的“书签”拨回去,从头再读一遍。