10多年前的游戏后端消息队列从 Kafka 0.x 用到 3.x,之前都是老老实实上 ZooKeeper 集群模式。这次测试 Kafka 3.8.0 的 KRaft 模式,本以为能省点心,结果还是踩了几个坑,单独开一篇记录一下,方便后续查阅。
一、为什么选择 KRaft 模式
以前 Kafka 强依赖 ZooKeeper 来做元数据管理,部署的时候不仅要搞定 Broker,还得额外维护一套 ZK 集群,链路长、出问题的概率也高。KRaft 模式下,Kafka 自己通过内部选举搞定 Controller 选主,彻底摆脱了对 ZooKeeper 的依赖。想用 Docker 快速拉起一个测试环境?一把梭就行。
当然了,这次用单节点纯粹是为了测试环境验证。生产环境的游戏服日均消息量是千万级的,真要上线还是得老老实实上多节点集群。
二、Docker Compose 部署配置
完整 docker-compose.yml 如下:
version: '3.8'
services:
kafka:
image: apache/kafka:3.8.0
container_name: kafka
network_mode: host
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://<服务器IP>:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@0.0.0.0:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_LOG_DIRS: /opt/kafka/logs
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false"
volumes:
- kafka-data:/opt/kafka/logs
volumes:
kafka-data:
关键参数说明:
| 参数 |
值 |
作用 |
KAFKA_PROCESS_ROLES |
broker,controller |
单节点同时充当 Broker 和 Controller |
KAFKA_ADVERTISED_LISTENERS |
<服务器IP>:9092 |
这里踩坑了,详见第四节 |
KAFKA_AUTO_CREATE_TOPICS_ENABLE |
false |
禁止自动创建 Topic,防止乱序问题 |
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR |
1 |
单节点必须设为 1 |
启动及就绪检查命令:
docker-compose up -d
# 等待 60 秒让 KRaft 选主完成(首次启动需要 generate UUID)
docker exec kafka bash -c "sleep 60 && echo 'KRaft ready'"
三、初始化 Topic
先把这次测试的业务需求交代清楚:
business:业务日志,12 分区,7 天保留
system:系统日志,6 分区,7 天保留
- 日均消息量:千万级
初始化脚本 init-topics.sh 内容如下:
cat > init-topics.sh << 'EOF'
#!/bin/bash
TOPIC_BUSINESS="business"
TOPIC_SYSTEM="system"
PARTITIONS_BUSINESS=12
PARTITIONS_SYSTEM=6
RETENTION_MS=604800000 # 7天
KAFKA_BIN="/opt/kafka/bin"
CONTAINER_NAME="kafka"
# 手动创建 __consumer_offsets(KRaft 单节点不自动创建)
docker exec ${CONTAINER_NAME} ${KAFKA_BIN}/kafka-topics.sh \
--create \
--topic __consumer_offsets \
--partitions 50 \
--config cleanup.policy=compact \
--if-not-exists
# 创建业务 topic
docker exec ${CONTAINER_NAME} ${KAFKA_BIN}/kafka-topics.sh \
--create \
--topic ${TOPIC_BUSINESS} \
--partitions ${PARTITIONS_BUSINESS} \
--replication-factor 1 \
--config retention.ms=${RETENTION_MS}
# 创建系统 topic
docker exec ${CONTAINER_NAME} ${KAFKA_BIN}/kafka-topics.sh \
--create \
--topic ${TOPIC_SYSTEM} \
--partitions ${PARTITIONS_SYSTEM} \
--replication-factor 1 \
--config retention.ms=${RETENTION_MS}
echo "Topic 初始化完成"
docker exec ${CONTAINER_NAME} ${KAFKA_BIN}/kafka-topics.sh --list
EOF
chmod +x init-topics.sh
./init-topics.sh
特别注意:__consumer_offsets 这个内部 Topic 必须手动创建,KRaft 单节点模式可不会像以前 ZooKeeper 模式那样自动帮你生成。这是后续导致消费者组无法工作的最常见原因,没有之一。
四、踩坑记录
坑 1:advertised.listeners 配置错误
症状:本地用命令行测试,生产者发消息看起来一切正常,但消费者组死活注册不上。去查 kafka consumer 的日志,发现连 poll 操作的影子都没有。
原因:测试服 SSH 进去后,习惯性用 localhost:9092 发消息,而 advertised.listeners 配的却是容器内部地址。外部客户端拿到的引导地址是错的,后续所有请求都走不通了。
排查过程:
# 查看消费者组状态——无任何注册信息
docker exec kafka /opt/kafka/bin/kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--list
# 对比测试服发消息时用的配置
# acks=0, retries=0,通过 localhost:9092 发送 —— 消息静默丢失了
解决:把 advertised.listeners 老老实实改成测试服的真实 IP,重建容器,重启 GameServer 用正确的 bootstrap.servers 地址验证链路,问题解决。
坑 2:__consumer_offsets 不存在
症状:消费者组无法启动,日志里没有任何 kafkaInit、subscribe、poll 的痕迹,像是根本没连上。
原因:KRaft 单节点下,__consumer_offsets 这个消费者组元数据存储的内部 Topic 不会自动创建,导致协调者(Group Coordinator)没地方写元数据。
解决:手动执行创建命令即可:
docker exec kafka /opt/kafka/bin/kafka-topics.sh \
--create \
--topic __consumer_offsets \
--partitions 50 \
--config cleanup.policy=compact
创建完成后,消费者组立刻注册成功,开始正常消费 business(24条)和 system(4条)消息。
坑 3:TOPIC_AUTHORIZATION_FAILED
症状:clog 服务疯狂报 errorCode=17,消费者拿不到 system 和 business 这两个 Topic 的访问权限。
原因:消费者实例的 ACL 权限配置不完整,光有 Broker 连接权限没用,还缺对业务 Topic 的 Read 授权。
解决:检查并补全消费者对相关 Kafka Topic 的 Read 权限配置。
五、验证集群
编写一个验证脚本 verify.sh,一键检查集群状态:
cat > verify.sh << 'EOF'
#!/bin/bash
CONTAINER_NAME="kafka"
KAFKA_BIN="/opt/kafka/bin"
echo "=== 检查 broker 是否就绪 ==="
docker exec ${CONTAINER_NAME} ${KAFKA_BIN}/kafka-broker-api-versions.sh \
--bootstrap-server localhost:9092
echo "=== 列出所有 topic ==="
docker exec ${CONTAINER_NAME} ${KAFKA_BIN}/kafka-topics.sh \
--list --bootstrap-server localhost:9092
echo "=== 测试生产者 ==="
docker exec ${CONTAINER_NAME} ${KAFKA_BIN}/kafka-console-producer.sh \
--bootstrap-server localhost:9092 \
--topic business \
--property "parse.key=true" \
--property "key.separator=:"
echo "=== 测试消费者 ==="
docker exec -d ${CONTAINER_NAME} ${KAFKA_BIN}/kafka-console-consumer.sh \
--bootstrap-server localhost:9092 \
--topic business \
--from-beginning \
--property "print.key=true"
EOF
chmod +x verify.sh
./verify.sh
六、参数自动优化依据
针对日均千万级消息量的场景,以下是自动计算出的调优参数:
| 参数 |
值 |
计算逻辑 |
num.network.threads |
8 |
CPU 核数 × 2 |
num.io.threads |
16 |
CPU 核数 × 4 |
socket.send.buffer.bytes |
2097152 |
2MB |
socket.receive.buffer.bytes |
2097152 |
2MB |
log.retention.hours |
168 |
7天 |
log.retention.bytes |
-1 |
不设上限 |
七、总结
- KRaft 模式下
__consumer_offsets 必须手动创建,这是和传统 ZooKeeper 模式部署体验上最大的区别。
advertised.listeners 一定要填外部可访问的真实地址,千万别图省事用 localhost 或容器内部 IP,否则消费者根本找不到回来的路。
- 消费者权限要单独配全,别以为配了 Broker 连接权限就万事大吉,Topic 级别的 Read 权限也得跟上。
如果在技术选型时想了解更多关于 Kafka 或其他消息中间件的实践经验,可以在云栈社区与大家交流探讨。