[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里的消息在被消费者处理完后默认并不会被删除(它们只会在达到保留时间或大小限制后才会被清理)。只要你还想读,总能找到办法读。你可以随时加入一个新的消费者组,或者把旧消费者组的“书签”拨回去,从头再读一遍。
问题:当你部署了 3 个实例(Consumer Group),其中一个挂了,剩下的实例如何接管它的 Partition?
这种高可用设计必须通过代码里的监听器和配置来观察和优化。
当一个消费者实例挂掉后,剩下的实例自动接管它的分区,这个过程主要依赖Kafka的消费者组协调和**重平衡(Rebalance)**机制,而不是你代码中的监听器。代码主要是用来感知和优化这个过程。
下面我详细拆解一下这个过程,以及你可以做的优化。
核心过程:重平衡 (Rebalance)
- 故障检测:当消费者实例挂了(比如进程崩溃、网络隔离),它就不再向Kafka的**组协调器(Group Coordinator)**发送心跳了。组协调器会有一个
session.timeout.ms(默认45秒)的阈值,超时后就认为该消费者已离开消费者组。 - 触发重平衡:组协调器检测到消费者离开后,会立即触发该消费者组的重平衡。
- 分区重新分配:在重平衡过程中,所有还“活着”的消费者实例会暂停消费,然后一起参与分区分配。Kafka内置的分区分配策略(如
RangeAssignor、RoundRobinAssignor、StickyAssignor)会重新计算,将挂掉的那个实例原先负责的分区,平均分配给剩下的2个实例。 - 恢复消费:重平衡完成后,剩下的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 ]
总结
- 核心机制:高可用由Kafka内置的组协调器和重平衡自动完成。
- 代码优化:使用
ConsumerRebalanceListener来安全地提交位移,避免重复消费。 - 配置优化:调优
session.timeout.ms和heartbeat.interval.ms以加快故障检测;使用StickyAssignor来减少重平衡开销。 - 监控:在生产环境中,务必监控
kafka.consumer:type=consumer-coordinator-metrics下的rebalance-latency-avg和rebalance-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 个 分区数可以设高些 |
四、几个关键公式
- 最大吞吐量 = 单分区吞吐量 × 分区数
- 消费者并发上限 = 分区数(同一个消费者组内)
- 分区数建议 ≥ 消费者组中最大消费者数量(否则会有消费者空闲)
五、分区分配的策略性考量
- Keyed消息的顺序性:如果业务要求同一个Key(如用户ID、订单ID)的消息严格有序,那么分区数一旦设定就不能再增加。因为增加分区会导致相同的Key被哈希到不同的分区,破坏顺序。这种情况要提前估算好未来3年的容量。
- 无顺序要求:分区数可以后续增加,但会导致Key分布不均匀。建议初始按未来1-2年的峰值预估。
六、实际配置示例
假设你有一个订单系统,峰值QPS 5000,每条消息1KB,单分区吞吐量经测试为 20 MB/s:
- 吞吐量需求:5000 * 1KB = 5 MB/s
- 最少分区数:5 / 20 = 0.25 → 至少 1 个(显然不够并发)
- 并发考量:你计划部署3个消费者实例,分区数至少需要3个才能充分利用并发。
- 最终分区数:为了应对未来增长,设置为6个(3的整数倍,方便分配)。
- 副本因子:3
min.insync.replicas:2
创建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 左右,极其稳定。这说明你触碰到了物理硬封印。
可能的凶手:
- 云服务器磁盘 IOPS 限制: 很多入门级云服务器(像你这种 4G 内存的规格)配套的磁盘每秒读写次数(IOPS)非常低。
- 双重 I/O 压力: Kafka 在写
speed-test主题。但因为你内存紧,操作系统还在拼命地把内存里的数据往 Swap(磁盘交换区) 挪。磁盘既要忙着写 Kafka 日志,又要忙着搬运 Swap 里的内存镜像,直接彻底瘫痪。 - 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)。如果你想找回之前那种“快感”,可以尝试缓慢加压,看看你的机器到底在哪个点会“崩盘”:
- 将
--throughput 500改为600。 - 再改为
700。 - 当你发现延迟从 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 秒延迟,是不是觉得它其实就是系统在向你发出的“救命”信号?