说实话,看到“基于 docker window10 wsl2 搭建 kafka 服务”这个标题,我第一反应就是:又一个在 Windows 上被环境折腾到怀疑人生的兄弟。Kafka 本身不复杂,但它的运行环境在 Windows 上一直是个老大难。以前要在 Windows 上跑 Kafka,你得先装 Java、下压缩包、配环境变量,还得伺候 ZooKeeper(老版本),稍有不慎就是端口冲突、版本不兼容、路径带空格之类的幺蛾子。
后来 Docker 普及了,很多人直接装 Docker Desktop,然后发现还得配一个 Linux 后端,这就是 WSL2。WSL2 加 Docker 的组合,既能用接近 Linux 原生的方式跑容器,又不用抛弃 Windows 的日常生态。这篇文章我会完整拆解 Windows 10 + WSL2 + Docker 搭建 Kafka 的完整过程:为什么选这套方案、compose 文件每个参数是什么意思、以及我实测中踩过的几个坑。适合想在本地快速起一套 Kafka 做开发验证、研究消息队列机制、或者搭数据管道的开发者。
1. 方案选型:为什么偏偏是 WSL2 + Docker
1.1 这套组合的底层逻辑
先简单说说三个东西之间的关系。WSL2 是 Windows 内置的轻量级虚拟机,底层用的是真正的 Linux 内核,所以它和原生 Linux 环境几乎零差异。Docker Desktop 在 Windows 上有两种后端:一种是老式的 Hyper-V 虚拟机,另一种就是 WSL2。当你在 Docker Desktop 的设置里勾选“Use the WSL 2 based engine”之后,Docker 的所有容器都会跑在 WSL2 的发行版里,但端口、文件系统、命令行都通过 Windows 这边的体验来操作。
这意味着什么?意味着你在 Windows 的浏览器里访问 localhost:9092,实际上 Docker 已经把容器的 9092 端口映射到了 WSL2 再映射到 Windows,整个过程链路是通的,而且延迟极低。同时,你不需要单独去维护一台完整的虚拟机,WSL2 随开随用,资源占用也小得多。
1.2 为什么不选别的方案
其实其他方案我都试过一遍。第一种是直接在 Windows 原生跑 Kafka,装 JDK、解压 Kafka 2.x 包、启动 ZooKeeper 和 Kafka 两个进程。能用,但非常痛苦:Windows 下的脚本是 .bat,路径斜杠问题频繁出现,新版本 Kafka 对 Windows 的原生支持也一般,而且卸载不干净会残留一堆 Java 进程。第二种是装 VMware 或 VirtualBox 跑完整 Linux 虚拟机,资源开销大,文件共享和端口转发也没那么顺滑。第三种是直接在 WSL2 里安装 Docker Engine(不装 Docker Desktop),可行但需要自己手动配置 systemd、网络和开机自启,对新手不太友好。
对比下来,Docker Desktop + WSL2 后端的操作成本最低,稳定性也够,我最终就固定在这套方案上。它既保留了“环境隔离”的核心理念,又不牺牲 Windows 的日常使用体验。
1.3 这套方案解决的核心痛点
归纳起来,这套方案解决几个核心痛点。
第一,环境隔离。Kafka 依赖 JVM,不同项目可能需要不同版本的 Kafka。用容器跑,每个项目一份 compose 文件,互不干扰,想换版本改个镜像 tag 就行。
第二,复制成本低。整个搭建过程就是一份 docker-compose.yml,给别人分享或换电脑部署,复制过去改两个参数就能跑起来。配置即代码,这才是现代化开发流程该有的样子。
第三,清理干净。docker compose down 一下,容器、网络、数据卷都可以一键移除,不会在系统里留下乱七八糟的残留进程和垃圾文件。我记得以前卸载原生 Kafka 时检查任务管理器里还有一堆 java.exe,得手动 kill,非常麻烦。
第四,和 WSL2 的高兼容性。因为 Kafka 本身在 Linux 环境下表现最好,WSL2 提供的 Linux 内核让 Kafka 的落盘、网络、文件系统行为都非常接近生产环境,你在本地测出的行为和线上差异很小。这点做数据管道的同学体会最深,本地跑得好好的,上线就出问题,多半就是环境差异导致的。
2. 环境准备:给 Windows 10 装上 WSL2 和 Docker
2.1 确认系统版本并开启 WSL2
Windows 10 要跑 WSL2,系统版本必须能在 2004 以上(我实测在 22H2 上最顺滑,老版本容易遇到内核组件缺失的问题)。你可以在“设置 - 系统 - 关于”里看 Windows 版本号。
确认版本没问题后,以管理员身份打开 PowerShell,执行:
wsl --install这一步如果系统较新,会自动安装 WSL2 内核并下载默认的 Ubuntu 发行版。如果执行完提示需要重启,重启即可。装完后再执行:
wsl --set-default-version 2把默认版本设置为 WSL2。如果你之前装过 WSL1 的发行版,可以通过wsl --set-version <发行版名> 2单独转换。有一种常见的坑是执行完wsl --install之后没设默认版本,结果容器里跑的还是 WSL1,性能和网络都会有问题。建议每次装完都确认一下。
确认是否启用成功,可以执行:
wsl --status看到“默认版本:2”就说明没问题。
2.2 安装 Docker Desktop 并切换到 WSL2 后端
Docker Desktop 的安装包(现在叫 Docker Desktop for Windows)体积不小,安装过程中一路 Next 就行。装完打开设置,找到 General 选项卡,确保勾选 Use the WSL 2 based engine。如果安装时 Docker Desktop 没自动检测到 WSL2,它会提示你安装内核更新包,去微软官方页面下载对应更新包装一下就行。
然后在 Settings - Resources - WSL Integration 里,确保你的 Ubuntu 发行版对应的开关是打开的。这一步容易被忽略,不打开的话你在 WSL2 终端里执行 docker 命令会提示找不到命令。配置完成后重启 Docker Desktop,在终端里执行:
docker version docker compose version能正常输出版本信息,就算准备完毕。
我建议顺手把 Docker Desktop 的“Start Docker Desktop when you sign in”打开,这样开机后 Docker 服务自动就绪,省得每次手动启动。另外如果公司网络有代理,记得在 Docker Desktop 的 Proxy 设置里填好代理地址,否则镜像拉取会一直卡在连接阶段。
2.3 限制 WSL2 的内存占用
这是很多人不装不知道的坑。WSL2 默认会占用宿主机最多 50% 的内存,如果机器只有 8GB 内存,Kafka 加上 Docker Desktop 很容易把内存吃满,电脑卡到怀疑人生。解决办法是在用户目录下创建.wslconfig文件:
[wsl2] memory=4GB processors=2 swap=2GB我自己的机器是 16GB 内存,给 WSL2 分配 4GB,跑 Kafka 加几个容器绰绰有余。改完这个文件之后,需要在 PowerShell 里执行wsl --shutdown再重新进入 WSL2,配置才会生效。
还有一个细节:如果你发现 WSL2 的虚拟磁盘文件越来越大,可以在.wslconfig里加上sparse=true,让虚拟磁盘按需增长而不是一下子占满整个文件。这个对后面要长期使用的人来说很有用。
3. 核心搭建:docker-compose 一键拉起 Kafka
3.1 镜像选型:官方 apache/kafka vs 老牌 bitnami 方案
以前搜“docker kafka”,搜出来的几乎都是 wurstmeister/kafka 或者 bitnami/kafka,这俩都要搭配一个 zookeeper 容器,配置项又多又绕。从 Kafka 3.3 开始,官方引入了 KRaft 模式,不再需要 ZooKeeper,直接由 Kafka 内置的 controller 管理元数据。3.5 之后 KRaft 已经进入生产可用阶段,所以我强烈推荐直接用官方镜像 apache/kafka。
这个镜像的好处在于:配置直观、和官方文档一致、不需要额外管 ZooKeeper。唯一的代价是如果你需要和 ZooKeeper 兼容的老集群对接,那还是得用老方案,但本地开发完全没必要。
| 镜像 | 是否仍需 ZooKeeper | 配置复杂度 | 维护状态 |
|---|---|---|---|
| wurstmeister/kafka | 是 | 较高 | 已停更多年 |
| bitnami/kafka | 可切换 | 较高 | 活跃但配置差异大 |
| apache/kafka(官方) | 不需要(KRaft) | 低 | 官方持续发布 |
3.2 docker-compose.yml 详解
我现在的开发环境固定用这个 compose 文件,注释写得很详细,这里直接贴出来:
version: "3.8" services: kafka: image: apache/kafka:3.7.1 container_name: kafka ports: - "9092:9092" environment: # KRaft 模式必备,每个节点唯一 ID KAFKA_NODE_ID: 1 # 当前节点既当 broker 又当 controller,单节点部署最省事 KAFKA_PROCESS_ROLES: broker,controller # 监听地址:PLAINTEXT 供客户端连接,CONTROLLER 供集群通信 KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 # 对外公布的地址,这里是最容易出错的点 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER # controller 集群的投票地址,格式:nodeId@host:port KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 # 监听器的安全协议映射 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT # 允许自动创建 topic,省去手动建 topic 的步骤 KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" # 单节点部署,副本数设为 1,避免 ISR 报错 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 # JVM 堆内存限制,防止在容器里把物理内存吃满 KAFKA_HEAP_OPTS: "-Xmx512m -Xms512m" volumes: - kafka-data:/var/lib/kafka/data volumes: kafka-data:3.3 关键参数逐一拆解
有几个参数第一次用的人特别容易搞混。
KAFKA_ADVERTISED_LISTENERS 是最重要也最容易背锅的配置。它告诉客户端“你要连接我时,应该用哪个地址”。很多人在本机能通、换台机器就连不上的原因都出在这里。在 Docker Desktop + WSL2 环境下,容器端口映射到了 Windows localhost,所以本机客户端(比如在 Windows 上的 Java 程序)访问 localhost:9092 就能连到容器,因此这里填 PLAINTEXT://localhost:9092 是正确的。
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 里的 kafka 是 compose 服务名,在同一个 Docker 网络内,容器之间可以通过服务名互相访问。如果你改成 localhost 反而会出错,因为这是给容器内部通信用的。
KAFKA_HEAP_OPTS 限制 JVM 堆大小。这类参数对本地开发挺重要——Kafka 默认配置会用系统可用内存的一半作为堆,如果 WSL2 分配了 4GB 内存,Kafka 就可能吃掉 2GB,再加上操作系统和 Docker 本身,内存压力会很大。设成 512m 对本地开发完全够用。
3.4 启动与验证
在放 compose 文件的目录下执行:
docker compose up -d第一次会拉取镜像,等待完成后执行:
docker compose ps看到状态为 running 就成功了。接着验证端口是否正常。在 Windows 终端里执行:
netstat -ano | findstr 9092能看到端口处于 LISTENING 状态就说明映射成功。
真正让人放心的验证方式是实际生产消费一次消息。进入容器,用 Kafka 自带的命令行工具发一条消息试试:
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test-topic如果没有 topic,Kafka 会自动创建(因为 KAFKA_AUTO_CREATE_TOPICS_ENABLE 设置了 true)。输入几条消息后按 Ctrl+C 退出。再开另一个终端,进入消费者端:
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test-topic --from-beginning能看到刚才发的消息在消费者端刷出来,说明整条链路已经通了。这里有个小细节,--bootstrap-server localhost:9092 在容器内部用 localhost 是合理的,因为 Kafka 进程本身就监听在容器内,不需要走客户端的广告地址。
3.5 用 Python 客户端做端到端验证
命令行工具验证通了固然好,但很多项目最终是用编程语言客户端接入的。这里用 Python 快速验证一下整个链路。先安装客户端库:
pip install kafka-python然后在 Windows 终端里写一个简单的生产者脚本:
from kafka import KafkaProducer producer = KafkaProducer(bootstrap_servers="localhost:9092") producer.send("test-topic", b"hello from python") producer.flush() print("send ok")再写一个消费者:
from kafka import KafkaConsumer consumer = KafkaConsumer("test-topic", bootstrap_servers="localhost:9092", auto_offset_reset="earliest") for msg in consumer: print(msg.value.decode())先跑消费者脚本,再跑生产者脚本,能正常打印出消息说明 Windows 上的客户端通过 localhost:9092 访问容器内 Kafka 的整条链路完全没有问题。这一步的意义在于:命令行工具是在容器内执行的,而 Python 脚本是 Windows 进程,两者网络路径不同,后者更能代表实际项目接入时的场景。
如果你项目里用的是 Spring Boot,Kafka 的地址也同样填 localhost:9092,原理一模一样,配置上的底层逻辑是共通的。
4. 实战踩坑与问题排查手册
4.1 端口冲突:9092 已被占用怎么办
这是最常见的启动失败原因之一。Windows 上如果已经装了别的中间件占用 9092,docker compose up 会直接在端口映射这一步报错。解决方法是改宿主机侧的端口号,比如映射到 19092:
ports: - "19092:9092"然后 KAFKA_ADVERTISED_LISTENERS 改成 PLAINTEXT://localhost:19092。为什么容器内还是 9092?因为 Kafka 在容器里监听的端口没变,变的是宿主机对外映射的入口。很多组件(比如 Flink、Spring Cloud Stream)配置 Kafka 地址时,填的是宿主机的入口地址,所以这时候它们填 localhost:19092 就对。改完端口不要忘记重启:
docker compose down docker compose up -d不要用 docker compose restart,因为有些环境变量不会在 restart 时重新读取,我踩过一次这个坑,改了 advertised 地址没生效,排查了半天才发现是 restart 不会重新加载环境变量。
4.2 WSL2 内存占用异常、容器频繁重启
如果跑着跑着发现 Kafka 容器动不动就 OOM,或者整个 WSL2 非常卡,先检查 .wslconfig 是否生效。执行:
free -h看 total 和 used 是否在预期范围内。如果 total 还是默认值,说明 .wslconfig 没生效,检查文件位置。它必须位于 Windows 用户主目录下(C:\Users<用户名>.wslconfig),不是 Linux 发行版内部。改完执行 wsl --shutdown 重新进。
另外还有一个容易被忽略的点:KAFKA_HEAP_OPTS 只限制 JVM 堆内存,Kafka 本身还有堆外内存和文件缓存。如果 WSL2 总内存给太少,比如 2GB,Kafka 加上容器操作系统就会很紧张。我实测 4GB 是比较舒适的阈值,16GB 内存的机器没有压力。如果你的机器是 8GB,建议给 WSL2 分配 3GB 左右,同时把 Kafka 堆内存压到 256m,也能跑。
4.3 客户端连不上:ADVERTISED_LISTENERS 的原理与排查
这是本地开发时遇到最多的问题,值得单独讲。Kafka 的客户端连接流程是这样的:客户端先连接到 bootstrap.servers 里的地址,拿到集群元数据后,Kafka 返回给客户端的是 advertised.listeners 里的地址,然后客户端用这个地址去建立真正连接。
所以如果你在本机测试,Kafka 容器内部把自己“广告”成 localhost:9092,客户端也通过 localhost:9092 连,两边一致,没问题。但如果你想让局域网内另一台电脑连过来,把 advertised 填成 localhost 就不行了,因为对那台电脑来说 localhost 指的是它自己。这种情况应该把 ADVERTISED_LISTENERS 改成宿主机的局域网 IP:
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.1.100:9092同时 ports 映射不变,还是把容器 9092 映射到宿主机 9092。
如果你遇到“能连上、但生产和消费时偶发超时”这类玄学问题,也可以从这个参数入手检查。用 Kafka 自带的工具看客户端视角的广告地址:
docker exec -it kafka /opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092能正常列出 broker 支持的所有 ApiVersion,说明连接链路没有大问题。如果这里卡住或报错,大概率就是地址不对。
4.4 数据持久化问题
不配置 volume 的情况下,docker compose down 之后容器删除,Kafka 的 topic 数据和 offset 记录会全部丢失。本地开发虽然不要求数据不丢,但如果你在调试消费者逻辑,丢了 offset 会让消费从头开始,可能重复消费大量数据,干扰判断。
解决方案就是我在 compose 里配置的卷:
volumes: - kafka-data:/var/lib/kafka/data这里的 kafka-data 是命名的 Docker 卷,不是宿主机目录映射。用命名卷的好处是数据由 Docker 管理,不用关心具体的路径,docker compose down 后数据还在。如果你希望数据映射到宿主机某个常见目录,可以改成:
volumes: - ./kafka-data:/var/lib/kafka/data但 Windows 和 WSL2 之间的文件系统转换有性能损耗,Kafka 又是 IO 密集型应用,所以我不建议在本地开发时把数据目录放到 Windows 文件系统上,命名卷的性能和稳定性都更好。
4.5 容器重启后的状态保持
这里再补充一个实际运维中很容易遇到的问题。docker compose restart kafka 之后,有时你会发现消费者连接报错,或者生产端出现 leader 选举错误。原因通常是重启后容器的网络 IP 发生变化,而一些客户端缓存了旧的 broker 元数据。解决办法:客户端侧配置里连接地址不要用动态 IP,用域名或固定端口;服务端侧如果一定要固定容器 IP,可以给 compose 服务分配静态地址:
services: kafka: networks: default: ipv4_address: 172.18.0.10 networks: default: driver: bridge ipam: config: - subnet: 172.18.0.0/24不过本地开发一般不需要这么较真,客户端连 localhost 时靠的是端口映射,和容器 IP 关系不大。只有当你把其他容器(比如 Flink、Spark)也塞进同一个 compose 网络并且通过容器名互访时,才需要注意保持网络稳定。
4.6 常用运维命令速查
整理一份我自己常用的命令,贴在这里方便直接抄:
# 查看日志,排查启动问题 docker compose logs -f kafka # 进入容器 docker exec -it kafka bash # 查看所有 topic docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list # 创建 topic,设置分区数和副本数 docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic my-topic --partitions 3 --replication-factor 1 # 查看 topic 的详细信息 docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic my-topic # 修改 topic 分区数(注意:只能增加不能减少) docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic my-topic --partitions 5 # 删除 topic(同时需要开启 delete.topic.enable) docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic my-topic # 停止并移除容器、网络(数据卷保留) docker compose down # 停止并移除容器、网络、数据卷(数据全部清空) docker compose down -v删除 topic 这个操作,我提醒一下:默认配置下删除 topic 不会真正删除,只是打了删除标记。如果确实需要频繁删除 topic,可以在 compose 里加上这个环境变量:
KAFKA_DELETE_TOPIC_ENABLE: "true"4.7 常见问题速查表
最后把高频问题汇总成一张表,方便你后续快速定位:
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| compose up 报端口错误 | 9092 被占用 | netstat -ano | findstr 9092,改映射端口 |
| 生产者报 Connection to node -1 could not be established | advertised.listeners 填错 | 检查 KAFKA_ADVERTISED_LISTENERS 是否是客户端可达地址 |
| 容器频繁重启或 OOM | WSL2 内存不足或 JVM 堆太大 | free -h 检查,调整 KAFKA_HEAP_OPTS 和 .wslconfig |
| 重启后消费者重复消费 | offset 存储在容器内未持久化 | 用命名卷挂载 /var/lib/kafka/data |
| 局域网其他机器连不上 | advertised.listeners 填了 localhost | 改成宿主机局域网 IP |
| docker 命令在 WSL2 里找不到 | WSL Integration 未开启 | Docker Desktop Settings - Resources - WSL Integration 打开对应开关 |
说实话,这套方案我用了大半年,从最初在 Windows 上裸跑 Kafka 被各种 .bat 脚本和 ZooKeeper 折磨,到现在一份 compose 文件走天下,体验是质的飞跃。如果你只是本地开发、学习 Kafka 原理、或者调试消息流,这套组合基本是效率最高的方案。后续如果觉得单节点不够玩,在这个基础上加 KRaft controller 节点、或者接外部的 schema registry、Kafka UI(比如 Kafdrop),都是顺着这个思路往长尾扩展,往上加服务就行——反正环境都在容器里,改坏了 docker compose down -v 重来,不用心疼。
最后一提,写这篇的时候我特意复盘了所有配置项的来龙去脉,发现真正把时间花在“环境”上的次数已经很少了,大头反而在理解 Kafka 本身的机制上。这恰好是容器化想达到的效果:把环境变简单,让精力回归到业务本身。如果你在照着这篇文章搭建的过程中还有新的坑,欢迎沿用我说的排查思路自己顺一遍,大部分问题都卡在那几个环境变量上,排查清楚一次,以后就通透了。