Loading

Python asyncio.run() 卡死排查实录:一行 socketpair() 引发的血案

一段在另一台电脑上完美运行的 MCP Server 代码,拷到自己的 Windows 10 后执行直接挂起——Ctrl+C 杀不掉。重装 Python 3.12/3.13(exe 和 zip 方式都试了),换 FastMCP 版本,查了数小时 AI Chat,无一能解。最终通过系统性逐层拆解,找到了一个让人意想不到的根因。

1. 症状

PS D:\Projects> python time_server.py
# ← 没有任何输出,直接卡死
# ← Ctrl+C 无反应
# ← 进程不可终止,只能任务管理器强杀

代码极简——一个基于 FastMCP 的 MCP Server:

from fastmcp import FastMCP
app = FastMCP("time-server")

@app.tool()
def get_now(tz: str = "UTC", fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
    """获取当前时间"""
    ...

if __name__ == "__main__":
    app.run()

在另一台同版本 Win10 22H2 的电脑上,这段代码会正常打印 FastMCP banner 然后进入 stdio 等待——标准 MCP Server 行为。

2. 走过的弯路

排查过程中,以下假设全部被验证后排除:

假设 操作 结果
WindowsSelectorEventLoopPolicy 导致卡死 删除该代码 仍卡,排除
FastMCP 版本差异 降级到 3.2.4 仍卡,排除
trio 后端绕过 asyncio 安装 trio,强制 anyio 用 trio trio.run 也卡死,排除
ucrtbase.dll 版本过旧 检查版本 10.0.19041 vs 系统 build 19045 实际正常,排除
Python 版本/安装方式 重装 3.12 exe/zip、3.13 exe/zip 全卡,排除
TAP-Windows VPN 网卡劫持 loopback 禁用 + 卸载 仍卡,排除

6 条弯路,数小时,全部排除。 问题不在 Python、不在库版本、不在 UCRT——在更底层。

3. 逐层拆解:从 asyncio.run 到 socket.socketpair

关键转折点:把 asyncio.run() 的每一步拆开,每步写文件确认执行位置。

Step 0: script started          ✅
Step 1: import asyncio           ✅
Step 2: asyncio.new_event_loop() ❌ ← 卡死在这!

asyncio.new_event_loop() 就卡了!进一步拆解 ProactorEventLoop 构造函数:

IocpProactor()                     ✅ (IOCP 端口创建正常)
socket.socketpair()                ❌ ← 卡死!

卡死点定位到 socket.socketpair()——Python 最底层的网络 IPC 函数。

再拆 socket.socketpair() 的实现(它在 127.0.0.1 上创建临时 TCP server + client):

socket.bind('127.0.0.1', 0)    ✅
socket.listen()                 ✅
socket.connect('127.0.0.1', port) ❌ ← 永久阻塞!

TCP connect 到 127.0.0.1 死锁。 这已经不是 Python 的问题了。

4. 跨语言验证:PowerShell 也卡

$listener = [System.Net.Sockets.TcpListener]::new([IPAddress]::Loopback, 0)
$listener.Start()
$client = [System.Net.Sockets.TcpClient]::new()
$client.Connect("127.0.0.1", $port)  # ← 卡死!

C# 的 TcpClient 连 localhost 也卡——Windows 系统级的 TCP loopback connect 被破坏了

5. 关键对照实验

操作 结果 含义
ping 127.0.0.1 ✅ 正常 ICMP 不受影响
socket.bind('127.0.0.1', 0) ✅ 正常 bind 不需要入站连接
socket.connect('127.0.0.1', port) ❌ 永久阻塞 TCP 入站连接被拦截
关闭 Windows 防火墙 socket.socketpair() ✅ 立即返回 防火墙是拦截者
添加 loopback 规则 + 开启防火墙 ✅ 正常运行 规则生效,loopback 被放行

6. 根因揭晓

火绒安全软件拦截了 TCP loopback 入站连接。

完整链路:

火绒 WFP 驱动注入过滤规则
  → 拦截 127.0.0.1 TCP 入站连接
    → socket.socketpair() 卡死(connect 到 localhost 被阻断)
      → ProactorEventLoop.__init__() 卡死(_make_self_pipe() 调 socketpair)
        → asyncio.new_event_loop() 卡死
          → asyncio.run() 卡死
            → 所有 MCP Server / asyncio 程序卡死
              → Ctrl+C 无法穿透 WFP 层面的 IO 阻塞

火绒(进程名 HipsDaemon / HipsTray / wsctrlsvc)通过 Windows Filtering Platform (WFP) 驱动注入网络过滤规则,其优先级高于 Windows 自带防火墙。火绒的默认策略将 127.0.0.1 的 TCP 入站连接视为可疑并拦截——而 Windows 原生防火墙默认不拦截 loopback。

另一台电脑正常运行,很可能是未安装火绒,或火绒版本不同已有 loopback 白名单。

7. 修复

立即生效的修复(管理员 PowerShell)

# 添加 loopback TCP 放行规则
New-NetFirewallRule -DisplayName 'Allow Loopback TCP Inbound' `
    -Direction Inbound -Action Allow -Protocol TCP -LocalAddress 127.0.0.1 -Profile Any
New-NetFirewallRule -DisplayName 'Allow Loopback TCP Outbound' `
    -Direction Outbound -Action Allow -Protocol TCP -RemoteAddress 127.0.0.1 -Profile Any

# 重新启用防火墙(关闭仅用于测试,不建议长期关闭)
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True

火绒侧配置(防止重启后规则被覆盖)

  1. 打开火绒 → 安全设置网络防护自定义规则
  2. 添加:允许 127.0.0.1 ↔ 127.0.0.1 的 TCP 连接(入站 + 出站)
  3. 或直接关闭火绒的"网络防护"模块(个人电脑不需要拦截本地 loopback)

一行验证

python -c "import socket; a,b=socket.socketpair(); print('OK'); a.close(); b.close()"

输出 OK 即修好。卡死则说明火绒覆盖了 Windows 规则,需在火绒内加白名单。

8. 反思

这次排查的核心教训:

当所有"常见原因"都排除后,不要继续在应用层打转——往下拆一层,拆到操作系统调用级别。

asyncio.run() 卡死 → 看起来是 asyncio 的 bug → 实际上是 socket.socketpair() 卡死 → 看起来是 Python socket 的 bug → 实际上是 Windows TCP loopback connect 死锁 → 看起来是系统 bug → 实际上是安全软件的防火墙规则拦截了本该放行的流量

每一层都比上一层更难想到,但每一层的验证只需要几行代码。关键方法是逐步写文件追踪——在每一步执行后立即将结果写入文件,这样即使进程卡死被强杀,也能从文件中看到最后成功执行到哪一步。

另外,ping 127.0.0.1 能通不代表 TCP loopback 能通——ICMP 和 TCP 是完全不同的协议栈,防火墙可以分别拦截。这个陷阱会让很多人误判"网络没问题"。

9. 影响范围

不仅是 Python asyncio,以下场景都会被此问题影响:

  • Node.js:部分框架的 cluster 模式使用 localhost IPC
  • Docker Desktop:容器与宿主机通信依赖 loopback
  • 数据库本地连接:MySQL/PostgreSQL 的 localhost 模式
  • 任何使用 socket.socketpair() 的程序
  • 任何 TCP 连接到 127.0.0.1 的程序

如果你的 Windows 程序莫名卡死在"连接"阶段,先跑那一行验证脚本。


排查日期:2026-05-16 | 环境:Windows 10 Home China 22H2 (Build 19045) + 火绒 6.0

posted @ 2026-05-16 21:24  飞鸿影  阅读(84)  评论(0)    收藏  举报