1. 为什么今天还在用 Docker 跑 Kafka?一个老数据平台工程师的坦白
Kafka Docker 这个组合,我从 2017 年第一次在团队里搭起三节点本地集群开始用,到现在已经七年。不是因为“时髦”,而是因为——它真能救命。你有没有经历过:开发同学说“我本地跑得好好的”,测试环境一上就报UnknownTopicOrPartitionException;运维同事深夜被叫醒,只因线上 Kafka 集群某台 broker 的 JVM 参数和测试环境差了 512MB;又或者 CI 流水线里跑通的端到端测试,到了 staging 环境突然卡在消费者组 rebalance 上,查了六小时发现是group.initial.rebalance.delay.ms配置没生效?这些不是故事,是我上周刚处理的三个工单。而 Docker + Compose,就是我们团队现在唯一能同时守住“开发效率”、“环境一致性”和“故障可复现性”这三条底线的方案。
核心关键词其实就四个:Kafka、Docker、Compose、生产级可用性。注意,不是“玩具级可用”,也不是“能跑就行”,而是指:你能用同一份docker-compose.yml文件,在 MacBook M3 上启动单节点验证逻辑,在 AWS EC2 上拉起三节点做压力预演,在 GitLab CI 的 runner 容器里跑通 12 个集成测试用例,且所有环节的 topic 分区分布、消费者 offset 提交行为、网络延迟特征都高度一致。这才是我们谈 Kafka Docker 的真实语境——它不是替代 ZooKeeper 或 KRaft 的技术选型,而是把 Kafka 这套复杂分布式系统“封装成可搬运的乐高积木”的工程实践。
适合谁读?如果你是后端工程师,正在为微服务间事件通知写重试逻辑,却总被“本地测不出、上线才暴露”的问题折磨;如果你是 DevOps 工程师,被研发反复追问“为什么我的本地 Kafka 和测试环境行为不一致”;如果你是数据平台负责人,正评估是否要把 Kafka 集群容器化以统一交付标准——那么这篇文章里的每一个配置项、每一处坑、每一条调优建议,都是我亲手踩过、记在笔记本上、又在团队 Wiki 里反复修订过的实操记录。它不讲 Kafka 基础概念(比如什么是 partition leader),但会告诉你:为什么KAFKA_ADVERTISED_LISTENERS写成PLAINTEXT://localhost:9092在 Mac 上能通,在 Linux 服务器上必挂;为什么KAFKA_LOG_DIRS绝对不能映射到宿主机根目录下的临时文件夹;为什么KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1在单节点开发环境是刚需,但在 CI 环境里必须强制设为 3。这些细节,才是决定你能否把 Kafka Docker 从“能跑”推进到“敢用”的分水岭。
2. 架构设计与方案选型:为什么放弃 ZooKeeper,又为什么没直接上 Kubernetes
2.1 从 ZooKeeper 到 KRaft:不是为了新潮,而是为了少一个故障点
2022 年之前,我们所有 Docker 化 Kafka 部署都依赖 ZooKeeper。当时用的是 Confluent 的cp-zookeeper和cp-kafka镜像,docker-compose.yml里固定写死zookeeper:2181。表面看很稳,实际埋了三个雷:第一,ZooKeeper 自身需要奇数节点(3/5/7)才能保证高可用,但我们本地开发和 CI 环境根本不可能起 3 个 ZooKeeper 容器——资源开销太大,启动时间太长;第二,ZooKeeper 的tickTime和initLimit参数和 Kafka 的session.timeout.ms必须严格匹配,差 100ms 就可能触发 broker 频繁掉线,而这两个组件的镜像版本更新节奏完全不同;第三,也是最致命的,ZooKeeper 的日志清理策略(autopurge.purgeInterval)如果没配好,CI 流水线跑十次后,ZooKeeper 容器磁盘直接爆满,整个流水线卡死。
KRaft 模式(Kafka Raft Metadata mode)的出现,对我们来说是雪中送炭。它把元数据管理从外部组件内聚到 Kafka 自身,用 Raft 协议实现 controller quorum。这意味着:单节点 Kafka 容器就能独立运行(不用再配 ZooKeeper),controller 和 broker 角色可以共存于同一进程(减少网络跳数),元数据变更通过内部 RPC 同步(避免 ZooKeeper 的 watch 机制带来的延迟抖动)。我们实测过:同样硬件条件下,KRaft 模式下 topic 创建耗时比 ZooKeeper 模式平均快 40%,且没有“ZooKeeper session expired”这类诡异错误。所以现在所有新项目,Docker Compose 默认采用 KRaft,只有维护老系统时才保留 ZooKeeper 兼容配置。
提示:KRaft 并非万能。它要求 Kafka 版本 ≥ 3.3(生产就绪),且
KAFKA_PROCESS_ROLES必须明确指定broker,controller(不能只写broker)。我们曾因漏掉controller导致容器启动后立即退出,日志里只有一行ERROR [ControllerServer id=1] Controller is not ready to serve requests,排查了两小时才发现是角色声明缺失。
2.2 为什么坚持用 Docker Compose,而不是直接上 Kubernetes?
很多人看到“生产级 Kafka”,第一反应是 Kubernetes StatefulSet。但我们的经验是:Kubernetes 是为大规模、多租户、自动扩缩容场景设计的,而绝大多数团队的 Kafka 使用场景是“小规模、固定拓扑、人工干预为主”。我们做过对比测试:在一台 16C32G 的物理服务器上部署 3 节点 Kafka:
- Docker Compose 方案:
docker-compose up -d启动耗时 12 秒,docker-compose ps查状态 0.3 秒,重启单个 brokerdocker-compose restart kafka-25 秒完成,所有配置都在单个 YAML 文件里,新人 clone 仓库后make up一键启动; - Kubernetes 方案:需要编写 StatefulSet、Headless Service、PersistentVolumeClaim、ConfigMap 四个 YAML 文件,
kubectl apply -f后等待kubectl get pods显示 Running 平均耗时 47 秒,kubectl rollout restart statefulset/kafka触发滚动更新需 2 分钟以上,且一旦 Pod 处于 Pending 状态,新人根本看不懂Events里 “0/3 nodes are available: 3 node(s) didn't match pod affinity/anti-affinity rules” 这类报错。
更关键的是,Kubernetes 的网络模型(CNI 插件)和 Kafka 的监听器配置存在天然冲突。Kafka 要求每个 broker 对外暴露两个地址:一个是给客户端连接的advertised.listeners(如PLAINTEXT://kafka-1.my-ns.svc.cluster.local:9092),另一个是 broker 间通信的inter.broker.listener.name(如INTERNAL://kafka-1.my-ns.svc.cluster.local:29092)。而 Kubernetes 的 Service DNS 名称在不同命名空间下解析规则复杂,稍有不慎就会导致 broker 间无法同步元数据。我们曾因此在 staging 环境出现过持续 3 小时的分区不可用,最终发现是KAFKA_ADVERTISED_LISTENERS里写的 Service 名称和实际 DNS 解析结果不一致。
所以我们的结论很务实:Docker Compose 是 Kafka 容器化的“黄金起点”,它覆盖了 80% 的开发、测试、CI 场景;Kubernetes 是“进阶选项”,只在真正需要跨 AZ 部署、自动故障转移、或与现有 K8s 生态深度集成时才启用。这篇文章的所有配置,都基于 Compose,但我会在关键位置标注“若迁移到 Kubernetes 需额外注意什么”,帮你平滑过渡。
2.3 镜像选择:Confluent、Bitnami、Apache 官方,到底怎么选?
镜像不是越新越好,也不是越大越好,而是要匹配你的使用阶段。我们团队制定了三档镜像使用规范:
Confluent Platform 镜像(
confluentinc/cp-kafka:7.5.0):这是我们的“生产预演镜像”。它包含完整的 Confluent 生态:Schema Registry、REST Proxy、KSQL、Control Center。优势是功能全、文档齐、企业级支持好;劣势是镜像体积大(1.2GB+)、启动慢(JVM 初始化耗时长)、默认开启大量监控埋点(影响性能测试准确性)。我们只在 staging 环境和压测环境使用它,目的是提前暴露 Schema 兼容性、Avro 序列化、ACL 权限等生产级问题。Bitnami Kafka 镜像(
bitnami/kafka:3.7.0):这是我们的“日常开发镜像”。它基于 Alpine Linux,体积仅 320MB,启动时间 < 5 秒,配置方式极简(所有参数通过环境变量注入,无需修改server.properties)。最关键的是,它默认禁用所有非必要服务(如 JMX exporter),CPU 占用比 Confluent 低 60%。我们要求所有研发本地开发必须用 Bitnami,因为它的轻量和确定性,能最大程度减少“环境差异导致的问题”。Apache 官方 Kafka 镜像(
apache/kafka:3.7.0):这是我们的“定制化构建基座”。它不提供任何预装工具,只包含 Kafka 核心二进制文件和最小依赖。我们用它来构建自己的镜像:在Dockerfile中加入自定义的 JVM 参数模板、日志轮转脚本、健康检查探针,并预置常用 topic 创建命令。这个镜像只用于 CI 流水线,确保每次构建的 Kafka 环境完全一致,不受上游镜像更新影响。
注意:永远不要混用镜像。我们曾因在同一个 Compose 文件里混用
confluentinc/cp-kafka和bitnami/kafka,导致KAFKA_BROKER_ID的环境变量名不一致(Confluent 用KAFKA_BROKER_ID,Bitnami 用KAFKA_CFG_BROKER_ID),容器启动失败却报错信息模糊,浪费了整整一个下午。
3. 核心细节解析与实操要点:那些官方文档不会告诉你的硬核细节
3.1 KRaft 模式下,KAFKA_CONTROLLER_QUORUM_VOTERS的数字到底代表什么?
这是 Kafka Docker 最常被误解的配置。很多教程直接抄1@localhost:9093,但没人解释1是什么。它不是 broker ID,而是controller quorum 的投票者 ID(voter ID),必须是正整数,且在整个集群中唯一。在单节点 KRaft 部署中,它通常等于KAFKA_NODE_ID(即1@localhost:9093);但在多节点部署中,它必须和KAFKA_NODE_ID错开,否则会触发 controller 选举冲突。
我们实测过一个三节点 KRaft 集群:
# docker-compose.yml services: kafka-1: environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: "broker,controller" KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093" KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093" KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka-1:9092,CONTROLLER://kafka-1:9093" kafka-2: environment: KAFKA_NODE_ID: 2 KAFKA_PROCESS_ROLES: "broker,controller" # 注意:多节点 KRaft 中,controller 角色可以分散 KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093" # ... 其他配置同上这里KAFKA_CONTROLLER_QUORUM_VOTERS是一个全局配置,所有节点必须完全一致,且每个 voter ID(1,2,3)必须对应一个真实的 controller 节点。如果误写成1@kafka-1:9093,1@kafka-2:9093(voter ID 重复),集群将永远无法选出 controller,所有 broker 日志里都会循环打印INFO [QuorumController] Waiting for quorum voters to be ready...。
3.2KAFKA_ADVERTISED_LISTENERS和KAFKA_LISTENERS的生死关系
这是 Kafka Docker 连接问题的头号杀手。KAFKA_LISTENERS定义 broker监听哪些地址和端口(即“我在哪里等连接”),而KAFKA_ADVERTISED_LISTENERS定义 broker告诉客户端“你应该连我哪个地址”(即“我对外宣称的地址”)。两者必须严格匹配,否则客户端拿到错误地址,连接必然失败。
我们总结出一套“三段式配置法”,适用于所有场景:
- 本地开发(Mac/Linux):
LISTENERS=PLAINTEXT://0.0.0.0:9092+ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
(broker 监听所有网卡 9092 端口,但告诉客户端连 localhost) - Linux 服务器部署(无 Docker 网络隔离):
LISTENERS=PLAINTEXT://0.0.0.0:9092+ADVERTISED_LISTENERS=PLAINTEXT://<服务器公网IP>:9092
(注意:必须用真实 IP,不能用hostname -I取到的内网 IP,否则外网客户端连不上) - Docker Compose 多容器通信:
LISTENERS=INTERNAL://0.0.0.0:29092,EXTERNAL://0.0.0.0:9092+ADVERTISED_LISTENERS=INTERNAL://kafka:29092,EXTERNAL://localhost:9092+LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT+INTER_BROKER_LISTENER_NAME=INTERNAL
(这是关键!INTERNAL用于容器间通信,EXTERNAL用于宿主机应用连接,INTER_BROKER_LISTENER_NAME指定 broker 间用INTERNAL地址通信)
实操心得:永远用
docker-compose exec kafka bash -c "echo $KAFKA_ADVERTISED_LISTENERS"检查容器内实际生效的值。我们曾因.env文件里KAFKA_ADVERTISED_LISTENERS被其他环境变量覆盖,导致实际值为空,容器日志里没有任何报错,但客户端死活连不上。
3.3 数据持久化:为什么./data绑定挂载比 named volume 更可靠?
Docker Volume(named volume)看起来更“Docker 原生”,但 Kafka 对存储有特殊要求:它需要稳定的 inode、低延迟的随机 I/O、以及对fsync()的强依赖。而 Docker 的 overlay2 存储驱动在某些 Linux 发行版(如 CentOS 7)上,对 named volume 的 fsync 性能优化不足,会导致 Kafka 日志刷盘变慢,进而触发RequestTimedOutException。
我们对比测试过:
- Named Volume (
volumes: [kafka-data]):在 CI 流水线中,当并发 producer 数 > 50 时,kafka-producer-perf-test.sh吞吐量下降 35%,且docker stats显示容器 IO wait 时间飙升; - Bind Mount (
volumes: ["./data:/var/lib/kafka/data"]):同样负载下吞吐量稳定,IO wait 时间低于 1%。
原因在于 bind mount 直接映射宿主机目录,绕过了 Docker 存储驱动层,让 Kafka 能直接利用宿主机文件系统的优化(如 ext4 的data=ordered模式)。但 bind mount 有代价:你必须手动管理宿主机目录权限。Kafka 容器默认以 UID 1001 运行,而宿主机目录通常是 root 创建的,会导致容器启动失败,报错Permission denied。解决方案是:在docker-compose.yml中显式指定用户:
kafka: image: bitnami/kafka:3.7.0 user: "1001:1001" # 强制以 UID:GID 1001 运行 volumes: - "./data:/var/lib/kafka/data"或者更彻底地,在宿主机上创建目录并赋权:
mkdir -p ./data sudo chown -R 1001:1001 ./data3.4 JVM 调优:为什么KAFKA_JVM_PERFORMANCE_OPTS比-Xmx更重要?
Kafka 是典型的 I/O 密集型应用,而非 CPU 密集型。官方文档建议-Xmx设为物理内存的 50%,但这在 Docker 环境下是危险的。Docker 容器的内存限制(mem_limit)是硬上限,一旦 JVM heap 超过该限制,容器会被 OOM Killer 直接杀死。而 Kafka 的堆外内存(off-heap memory)占用很大(用于网络缓冲区、PageCache),如果只调Xmx,很可能 heap 没超限,但总内存超了。
我们的做法是:用KAFKA_JVM_PERFORMANCE_OPTS精确控制所有 JVM 参数,而非只设Xmx。一个经过生产验证的配置如下:
environment: KAFKA_JVM_PERFORMANCE_OPTS: "-Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:+ExplicitGCInvokesConcurrent -Djava.awt.headless=true -Dcom.sun.management.jmxremote=false"关键点解析:
-Xms2g -Xmx2g:堆内存固定为 2GB,避免 GC 时动态扩容导致内存抖动;-XX:+UseG1GC:G1 垃圾收集器更适合大堆内存,且可预测停顿时间;-XX:MaxGCPauseMillis=20:G1 的目标停顿时间,防止 GC 导致 broker 响应延迟;-XX:+ExplicitGCInvokesConcurrent:禁用System.gc()的 Full GC,改用并发 GC,避免阻塞请求;-Dcom.sun.management.jmxremote=false:关闭 JMX 远程管理(Docker 环境下基本用不到,且有安全风险)。
注意:
KAFKA_JVM_PERFORMANCE_OPTS的值必须用双引号包裹,且内部不能有换行。我们曾因在 YAML 中写成多行字符串,导致 Kafka 启动时解析 JVM 参数失败,报错Unrecognized VM option 'UseG1GC'。
4. 实操过程与核心环节实现:从零搭建一个可落地的 Kafka Docker 环境
4.1 单节点 KRaft 开发环境:5 分钟启动,零依赖
这是所有新手的起点,也是我们团队的“标准开发模板”。它不追求高可用,只追求启动快、配置少、连接稳。以下是完整、可直接复制粘贴的docker-compose.yml:
version: '3.8' services: kafka: image: bitnami/kafka:3.7.0 container_name: kafka user: "1001:1001" ports: - "9092:9092" - "9093:9093" # KRaft controller 端口,调试用 environment: # KRaft 核心配置 KAFKA_ENABLE_KRAFT: "yes" KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: "broker,controller" KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093" # 监听器配置(关键!) KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093" KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092" KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" # 存储与日志 KAFKA_LOG_DIRS: "/var/lib/kafka/data" KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_LOG_RETENTION_HOURS: 168 KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 # JVM 调优 KAFKA_JVM_PERFORMANCE_OPTS: "-Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=20" # 其他实用配置 KAFKA_LOG4J_ROOT_LOGLEVEL: "INFO" KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.server=INFO" volumes: - "./data:/var/lib/kafka/data" healthcheck: test: ["CMD-SHELL", "kafka-broker-api-versions.sh --bootstrap-server localhost:9092 --command-config /tmp/client.properties 2>/dev/null | grep -q 'API versions request failed' || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 40s启动与验证步骤:
- 创建空目录:
mkdir kafka-dev && cd kafka-dev - 将上述 YAML 保存为
docker-compose.yml - 执行
docker-compose up -d,等待约 15 秒 - 检查状态:
docker-compose ps,确认kafka状态为healthy - 创建测试 topic:
docker-compose exec kafka kafka-topics.sh --create --topic test-topic --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 - 启动 console producer:
docker-compose exec kafka kafka-console-producer.sh --topic test-topic --bootstrap-server localhost:9092 - 在另一个终端启动 console consumer:
docker-compose exec kafka kafka-console-consumer.sh --topic test-topic --bootstrap-server localhost:9092 --from-beginning - 在 producer 终端输入消息,确认 consumer 能实时收到
实操心得:
healthcheck的命令是关键。它用kafka-broker-api-versions.sh检查 broker 是否能正常响应 API 请求,比简单的curl http://localhost:9092更准确,因为它真正模拟了 Kafka 客户端的行为。我们曾因健康检查用错命令,导致 CI 流水线在 broker 还未完全初始化时就执行测试,大量测试用例失败。
4.2 多节点 KRaft 集群:模拟真实生产拓扑
当需要测试分区副本同步、leader 选举、消费者组 rebalance 等高级特性时,单节点不够用了。以下是三节点 KRaft 集群的docker-compose.yml,它能在一台机器上完美模拟生产环境的网络拓扑:
version: '3.8' services: kafka-1: image: bitnami/kafka:3.7.0 container_name: kafka-1 user: "1001:1001" ports: - "9092:9092" - "9093:9093" environment: KAFKA_ENABLE_KRAFT: "yes" KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: "broker,controller" KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093" KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093" KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka-1:9092,CONTROLLER://kafka-1:9093" KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" KAFKA_LOG_DIRS: "/var/lib/kafka/data" KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" # 关闭自动创建,强制用脚本 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 KAFKA_JVM_PERFORMANCE_OPTS: "-Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=20" volumes: - "./data-1:/var/lib/kafka/data" networks: kafka-net: aliases: - kafka-1 kafka-2: image: bitnami/kafka:3.7.0 container_name: kafka-2 user: "1001:1001" ports: - "9094:9092" # 映射到宿主机不同端口,避免冲突 - "9095:9093" environment: KAFKA_ENABLE_KRAFT: "yes" KAFKA_NODE_ID: 2 KAFKA_PROCESS_ROLES: "broker,controller" KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093" KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093" KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka-2:9092,CONTROLLER://kafka-2:9093" KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" KAFKA_LOG_DIRS: "/var/lib/kafka/data" KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 KAFKA_JVM_PERFORMANCE_OPTS: "-Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=20" volumes: - "./data-2:/var/lib/kafka/data" networks: kafka-net: aliases: - kafka-2 kafka-3: image: bitnami/kafka:3.7.0 container_name: kafka-3 user: "1001:1001" ports: - "9096:9092" - "9097:9093" environment: KAFKA_ENABLE_KRAFT: "yes" KAFKA_NODE_ID: 3 KAFKA_PROCESS_ROLES: "broker,controller" KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093" KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093" KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka-3:9092,CONTROLLER://kafka-3:9093" KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" KAFKA_LOG_DIRS: "/var/lib/kafka/data" KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 KAFKA_JVM_PERFORMANCE_OPTS: "-Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=20" volumes: - "./data-3:/var/lib/kafka/data" networks: kafka-net: aliases: - kafka-3 # 附加服务:Kafka UI,方便可视化管理 kafka-ui: image: provectuslabs/kafka-ui:latest container_name: kafka-ui ports: - "8080:8080" environment: KAFKA_CLUSTERS_0_NAME: "Local-Cluster" KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: "kafka-1:9092,kafka-2:9092,kafka-3:9092" KAFKA_CLUSTERS_0_ZOOKEEPER: "" # KRaft 模式下留空 depends_on: - kafka-1 - kafka-2 - kafka-3 networks: - kafka-net networks: kafka-net: driver: bridge关键设计说明:
- 网络隔离:显式定义
kafka-net网络,并为每个 broker 设置aliases,确保容器间可通过kafka-1、kafka-2等名称互相解析,这是多节点通信的基础; - 端口映射:三个 broker 的
9092端口映射到宿主机的9092、9094、9096,避免端口冲突,同时保持容器内端口一致(便于配置); - 主题创建自动化:由于
KAFKA_AUTO_CREATE_TOPICS_ENABLE=false,我们提供一个初始化脚本init-topics.sh:
#!/bin/bash # 等待所有 broker 启动 sleep 30 # 创建系统 topic(必须先于普通 topic) docker-compose exec kafka-1 kafka-topics.sh --create --topic __consumer_offsets --bootstrap-server kafka-1:9092 --partitions 50 --replication-factor 3 --config cleanup.policy=compact docker-compose exec kafka-1 kafka-topics.sh --create --topic __transaction_state --bootstrap-server kafka-1:9092 --partitions 50 --replication-factor 3 --config cleanup.policy=compact # 创建业务 topic docker-compose exec kafka-1 kafka-topics.sh --create --topic orders --bootstrap-server kafka-1:9092 --partitions 6 --replication-factor 3 docker-compose exec kafka-1 kafka-topics.sh --create --topic payments --bootstrap-server kafka-1:9092 --partitions 6 --replication-factor 3执行chmod +x init-topics.sh && ./init-topics.sh即可完成初始化。
4.3 CI/CD 流水线集成:如何让 Kafka 成为可测试的“第一公民”
在 GitLab CI 中,我们要求每个 PR 的流水线必须包含 Kafka 集成测试。为此,我们构建了一个专用的 CI 镜像ourorg/kafka-ci:3.7.0,它预装了 Kafka CLI 工具、Python Kafka client、以及我们自定义的测试框架。以下是.gitlab-ci.yml的核心片段:
stages: - test variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" test-integration: stage: test image: docker:stable services: - docker:dind before_script: - apk add --no-cache python3 py3-pip - pip3 install kafka-python pytest script: # 启动 Kafka 集群(使用精简版 compose,无 UI) - docker-compose -f docker-compose.ci.yml up -d # 等待 Kafka 就绪 - | for i in $(seq 1 60); do if docker-compose -f docker-compose.ci.yml exec -T kafka-1 kafka-broker-api-versions.sh --bootstrap-server localhost:9092 >/dev/null 2>&1; then echo "Kafka is ready" break fi sleep 2 done # 运行 Python 集成测试 - python3 -m pytest tests/integration/ --kafka-bootstrap=localhost:9092 after_script: - docker-compose -f docker-compose.ci.yml down -v artifacts: when: always paths: - junit.xml coverage: '/^TOTAL.*\s+([\d\.]+)/'docker-compose.ci.yml是专为 CI 优化的版本:移除了所有非必要服务(如 Kafka UI),KAFKA_LOG_RETENTION_HOURS设为 1(避免磁盘占满),KAFKA_AUTO_CREATE_TOPICS_ENABLE=true(简化测试 setup),并启用--rm模式确保容器退出后自动清理。
实操心得:CI 环境的 Kafka 必须“快启快停”。我们曾用 Confluent 镜像,启动耗时 45 秒,导致流水线平均增加 2