1. 这个报错到底在说什么
1.1 先看一段典型日志
如果你在 Windows 上跑 Kafka,大概率迟早会碰到这个报错。它不是一条冷门边界问题,而是一个几乎每个 Windows Kafka 用户都绕不开的经典坑。
我第一次遇到java.nio.file.FileSystemException的时候,是在一台 Windows 开发机上准备重启一个本地 Kafka 单节点。启动脚本跑起来之后,还没来得及看到 broker started,控制台就直接抛了这样一段异常:
Exception in thread "main" java.nio.file.FileSystemException: D:\kafka\data\test-topic-0\00000000000000000000.log -> 另一个程序正在使用此文件,进程无法访问。 at java.base/sun.nio.fs.WindowsFileSystemProvider.delete(WindowsFileSystemProvider.java) at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java) at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java) at java.base/sun.nio.fs.Files.delete(Files.java)运行过程中也会出现。比较常见的是日志清理线程报错,比如:
ERROR kafka.log.LogCleaner: Failed to delete segment ... java.nio.file.FileSystemException: D:\kafka\data\topic-a-0\00000000000001234567.log -> 另一个程序正在使用此文件,进程无法访问。不管是启动阶段还是运行阶段,这个异常背后其实只有一个问题:Kafka 想去删除或者替换一个文件,但 Windows 文件系统不允许。
1.2 这句话翻译成人话
另一个程序正在使用此文件,进程无法访问是 Windows 自己的原生提示。Kafka 在底层调用文件删除操作时,Windows 检查目标文件的句柄状态,发现它正处于被占用状态,于是直接把请求挡了回去。
这里要特别提醒一句:报错里的“另一个程序”不一定真的是别的软件,很多时候正是 Kafka 自己。
Kafka 会长期打开日志分段文件句柄,正好 Windows 对“删除一个正在被打开的日志文件”这件事又特别严格。于是 Kafka 自己的清理线程想删除旧日志,Windows 拿着 Kafka 自己的句柄回怼:“不行,你正在用它。” 这种自己卡自己的现象,在 Windows 上非常常见。
如果启动阶段报的是.lock文件,那原因可能更简单:上一个 Kafka 进程还没完全退出,或者同一个数据目录被另一个 broker 实例占用了。
所以,看到这个报错别急着怀疑 Kafka 本身有问题,先冷静下来,把目标聚焦在“哪个进程握着这个文件”上。
2. 为什么偏偏是 Kafka 容易撞上 Windows 文件锁
2.1 Kafka 日志分段的文件设计
要知道 Kafka 为什么容易踩这个坑,先得看它怎么存日志。
Kafka 的每个 topic 分区都是一个独立目录,目录里不是一个大文件,而是按大小切分成多个日志分段,也就是 log segment。每个 segment 至少包含三个文件:
.log:真正的消息数据文件。.index:偏移量索引文件。.timeindex:时间戳索引文件。
在有事务、有过快照的情况下,目录里还会有.snapshot、.txnindex之类的文件。
消息写入时,Kafka 会往当前活跃的.log文件末尾追加。当文件大小超过log.segment.bytes限制后,就滚动生成一个新的 segment。随着时间推移,过期 segment 会被清理线程标记为可删除,然后从磁盘上删掉。
也就是说,Kafka 天生就是一个高频“创建文件、打开文件、删除文件”的系统。在 Linux 上这套流程毫无障碍,但在 Windows 上,只要任何一个句柄没释放,删除就会失败。
2.2 Windows 和 Linux 删除文件的语义完全不同
Kafka 在 Linux 上跑得顺,并不是因为它对文件系统做了什么特殊处理,而是 Linux 本身的删除语义太宽松了。
在 Linux 里,一个文件即使正在被进程打开,你也可以直接rm。删除之后,文件的目录项消失了,但被打开的句柄仍然有效,进程还能继续读写这个文件,直到它关闭句柄为止。这个特性叫“延迟删除”,很多服务都在依赖它。
Windows 不一样。Windows 在删除文件时,会检查所有已打开的句柄。如果一个句柄没有声明FILE_SHARE_DELETE权限,哪怕只有一毫秒的短暂占用,删除操作也会失败。Windows 的报错就是这句熟悉的“另一个程序正在使用此文件”。
Java 的 NIO 在 Windows 上最终会调用CreateFileW这套底层 API。Kafka 用FileChannel.open打开日志文件时,具体共享模式取决于 JDK 版本和调用路径。多数情况下,Kafka 持有的句柄并没有完全开放删除共享。于是,当一个日志文件还处于打开状态时,清理线程尝试删除它,Windows 会直接抛出FileSystemException。
这个问题不只是 Kafka 有。Elasticsearch 依赖的 Lucene 在 Windows 上也有类似行为,Exception in thread "main" java.nio.file.FileSystemException在它的数据目录里同样经常出现。根因都是同一套 Windows 文件锁语义。
2.3 除了 Kafka 自己,还有三双看不见的手
如果你已经排除了 Kafka 自锁,就要考虑 Windows 环境里那些“爱摸文件”的进程。
第一是杀毒软件和 Windows Defender 的实时防护。它们会在新文件生成后的瞬间打开文件做扫描。Kafka 日志目录每秒钟可能滚动出很多小文件,扫描器一旦和清理线程抢同一个文件,就会有概率触发删除失败。
第二是 Windows Search 索引服务。如果你把 Kafka 数据目录放在 C 盘用户目录附近,或者放在一个被默认索引的目录下,Windows Search 会读取日志文件内容建立索引。搜索索引服务和 Kafka 的清理线程并发操作时,同样可能产生冲突。
第三是输入法、编辑器、文件资源管理器预览,甚至网盘同步工具。很多人会把log.dirs配置到D:\kafka\data,但这个目录如果被 OneDrive、网盘客户端同步了,问题会变得更加频繁。因为这些工具会长期监听目录变化,一旦发现新文件就打开读取。在 Windows 上,这种第三方打开行为足以让 Kafka 的删除操作失败。
注意:并不是说你装了杀毒软件就一定会出问题,而是这些进程的“打开-关闭”窗口刚好撞上 Kafka 删除文件的瞬间。频率很高时,异常就会明显增多。
3. 排查思路:先定位文件,再定位进程
3.1 看日志:异常里的路径是第一线索
遇到FileSystemException,不要一上来就重启,先看报错里出现的文件路径。
如果路径最后是.lock,优先怀疑“上一个 Kafka 实例没退干净”或者“同一个数据目录被启动了两次”。
如果路径是.log或者.index,说明 Kafka 在删除过期 segment 时被 Windows 挡了一下。此时要结合日志上下文看两件事:
- 是不是刚好在 “log retention” 或者 “log cleaner” 附近报错。
- 是不是刚触发滚动 segment 之后的一两秒内报错。
Kafka 的日志文件默认在logs/server.log里。Windows 下如果用kafka-server-start.bat启动,默认会把日志写到 Kafka 安装目录下的logs文件夹里。搜索FileSystemException关键词,找到第一次报错的时间点,再往上看几行,就能知道当时 Kafka 在做什么。
3.2 用 Handle 工具找到占用文件的进程
Windows 自带的资源监视器也能看到句柄,但筛选能力一般。真正好用的是 Sysinternals 的 Handle 工具,命令行形式,效率非常高。
把handle.exe下载下来之后,用管理员权限打开 PowerShell 或 CMD,按文件名过滤:
handle64.exe -accepteula -nobanner "D:\kafka\data\test-topic-0\00000000000000000000.log"如果 Kafka 正在运行且报错刚刚发生,这个命令会输出所有打开了该文件的进程名和进程 ID。系统返回结果里如果看到java.exe,多半就是 Kafka 自己的句柄;如果看到SearchIndexer.exe、MsMpEng.exe,那就是系统组件在“帮倒忙”。
如果没有 Handle 工具,也可以用 Process Explorer。打开菜单 Find,输入句柄或 DLL 名字,输入文件名即可搜索哪个进程打开了这个文件。一般经过这两步,元凶很快就能现形。
在定位进程之前,不建议直接执行taskkill /F。尤其是 Kafka 运行中的阻塞,强行杀进程可能带来未刷盘数据丢失和恢复开销,属于最后手段。
3.3 三个被忽略的高频原因
第一个是开发机上同时跑了两套 Kafka。比如有些人为了测试,在 9092 端口启动了一个 Kafka,又用 Docker 在 9093 端口偷偷卷了一个实例。两个实例的log.dirs如果指向同一个目录,启动时抢.lock是必然的。
第二个是 Kafka 进程虽然显示已经关闭,但 JVM 还挂在后台。Windows 的控制台窗口如果被直接关闭,不一定意味着 JVM 进程立即退出。用jps -l或者下面的命令确认:
Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*kafka.Kafka*" } | Select ProcessId, CommandLine第三个是日志目录被映射成了网络驱动器或者挂在了同步盘里。如果log.dirs指向Z:\kafka\data,而这台机器上又安装了同步客户端,删除操作要经过同步软件的文件监控,比较容易出现奇怪的文件锁。
4. 从应急到预防:Windows 下 Kafka 的整改建议
4.1 紧急恢复步骤
如果 Kafka 已经因为.lock文件报错起不来,第一步是把所有相关进程确认一遍。确认没有其他 broker 在跑之后,再删掉残留的.lock文件。
具体操作顺序:
- 用
jps -l或Get-CimInstance确认没有残留kafka.Kafka进程。 - 如果存在,先用正常方式停止 broker;实在无法停止,再在开发环境里考虑
taskkill /F /PID。 - 进入
log.dirs配置的目录,删掉.lock文件。 - 重新启动 Kafka。
如果 Kafka 运行中反复报.log文件删除失败,但生产消费没有明显异常,更稳妥的做法是不要立刻重启。先通过 Handle 工具确定占用者,再决定是否加目录排除项。因为 Kafka 的清理线程通常会在下一个周期重试,某些场景下等几秒钟就会自行恢复。
4.2 配置层面的缓冲做法
从配置上并不能彻底解决 Windows 文件锁问题,但可以降低碰撞概率。
开发环境下常见的调整是增大单个日志分段大小,减少 segment 滚动频率。文件数少了,被各种扫描进程盯上的概率也会降低。可以在config/server.properties里做如下设置:
log.segment.bytes=1073741824 log.retention.hours=72 log.retention.check.interval.ms=600000 log.segment.delete.delay.ms=120000log.segment.delete.delay.ms的默认值是 60000,也就是清理任务在真正删除 segment 前会预留 60 秒的缓冲。在 Windows 上把它调大到 120000 并不会丢数据,只是旧文件多占用一会儿磁盘空间,但能给那些“打开-关闭”很快的扫描器留足退出时间。
如果你的 Kafka 版本比较老,找不到log.segment.delete.delay.ms这个参数,就不用强行加。优先把数据目录从系统盘挪走,往往立竿见影。
4.3 给 Windows 写一套专属“护身符”
第二件事是把 Kafka 数据目录从 Windows 的各种扫描范围里排除出去。
如果你用的是 Windows Defender,可以把log.dirs对应的目录加入排除项。操作路径是:Windows 安全中心,病毒和威胁防护,管理设置,排除项。加入之后,Defender 不会实时扫描这个目录下的新文件,清理线程删除日志时少了一个对手。
Windows Search 的索引也可以关掉。右键点击 Kafka 数据目录,选择不将该文件夹加入索引。如果目录在C:\kafka这类自定义路径下,通常默认不会被索引,但也要确认一次。
同步盘是另一个隐藏雷区。不要把 Kafka 的log.dirs放到 OneDrive、坚果云、百度网盘等同步目录里。Kafka 对文件操作频率极高,同步工具本身的文件锁行为会放大FileSystemException的概率。
提示:排除目录不是偷懒,而是基于 Windows 文件锁特性做的合理规避。日志目录本来就不需要杀毒软件逐字节扫描,这类目录用排除项处理完全合理。
4.4 生产环境尽量往容器或 Linux 靠
如果你的目标只是本地学习或者临时演示,Windows 原生 Kafka 够用,配合上面这些规避措施能解决大多数问题。
但如果这是正式环境,或者你要搭一个 Kafka 集群,我还是建议你认真考虑把 Kafka 放到 WSL2 或者 Linux 机器里跑。这里不是说 Windows 不能跑 Kafka,而是 Kafka 的社区工具链、运维脚本、监控方案几乎都是以 Linux 为默认环境的。Windows 文件锁系统纵然可以通过各种办法绕开,但投入在这些绕行上的时间,往往比部署一个干净 Linux 环境更多。
如果你选择用 Docker Desktop 在 Windows 上跑 Kafka 容器,并且把 Windows 本机目录挂载进容器,也要注意:文件锁问题不会因为你用了 Docker 就自动消失。挂在容器里的数据卷如果落在 Windows NTFS 文件系统上,底层还是要经过 Windows 的文件打开和删除语义,遇到报错的概率和原生跑 Kafka 差不多。
5. 常见问题速查表与我的避坑心得
5.1 问题速查表
| 报错场景 | 优先怀疑对象 | 直接处理动作 |
|---|---|---|
启动时报.lock文件被占用 | 上一个 Kafka 进程没退出,或者多个实例共用目录 | 确认无进程后删除.lock,再启动 |
运行时日志清理报.log删除失败 | Kafka 自己的清理线程撞上 Windows 文件锁 | 用 Handle 定位占用者,排除杀毒和索引 |
| 报错集中在某个 topic 分区 | 该分区 segment 文件比较多,滚动频繁 | 适当调大log.segment.bytes,减少文件数 |
| 报错在网盘同步目录频繁出现 | 同步工具持有文件句柄 | 把log.dirs移出同步目录 |
| 重启后短暂恢复正常,随后复现 | 某个外部进程周期性扫描日志目录 | 给数据目录加 Defender 排除项和索引排除项 |
5.2 我踩过几次坑之后的体会
第一次遇到这个报错时,我差点把 Kafka 数据目录删了重来,后来才发现罪魁祸首其实是上一轮启动时没有等 JVM 完全退出。Windows 上点掉控制台窗口不代表 Kafka 停了,尤其是用bin\windows\kafka-server-start.bat启动时,JVM 可能还挂在后台。现在我每次重启 Kafka,都会先执行一次进程检查,确认没有残留再动手,再也没有被.lock卡住过。
还有一次是运行中的 Kafka 高频报错,我查了很久,最后发现是日志目录放在了一个同步盘文件夹里。看起来只是普通的D:\sync\kafka-data,但同步客户端对文件变化的监听会让清理任务十次有八次失败。把目录移到本地裸盘之后,问题彻底消失。
所以如果你想在 Windows 上稳定跑 Kafka,最核心的心法其实是三句话:目录别放系统盘,目录别让杀毒软件盯上,目录别让同步工具碰。把这三点做到位,再记住“先看路径、再找进程、最后决定动不动”的排查顺序,java.nio.file.FileSystemException基本就不会再来折磨你了。