opencode 界面卡住不动排查

七月 15, 2026 #linux #debug #troubleshooting #strace #ssh #git #futex #lock-contention #ipv6

现象

opencode 交互界面一直显示运行中,不输出任何新内容,像是卡死了一样。

第一反应:strace

strace -p <pid> 看一下卡在哪个系统调用上:

strace -p 4188775

结果有大量输出epoll_waitreadwriteepoll_wait 循环不断,并没有阻塞在某个系统调用上。

这说明进程本身在运行,事件循环在持续工作。

换个思路:进程状态 + 子进程树

strace 输出太多看不出问题,那就从最常规的进程状态入手。

看进程在等什么

ps -p 4188775 -o pid,stat,wchan,cmd
# → 4188775  Sl+  do_epoll_wait  opencode --log-level DEBUG

do_epoll_wait 只是某个采样时刻的快照。opencode 是事件循环模型,绝大多数时间都短暂地阻塞在 epoll_wait 上等待 I/O 事件,ps 拍到的 wchan 几乎永远是 do_epoll_wait,不代表一直卡在同一个调用上。

但这个信息告诉我们一件事:进程是 S (sleeping) 状态,不是 R (running),也不是 D (uninterruptible sleep)。没有死循环,没有内核态阻塞。

看子进程树

pstree -p 4188775

输出:

opencode(4188775)───git(2867)───ssh(2868)

一目了然:opencode 下面挂着 git push,git 下面挂着 ssh。阻塞链条是 opencode → git → ssh

再看 git 进程的状态:

ps -p 2867 -o pid,stat,wchan,etime,cmd
# → 2867  Ssl  do_wait  04:13  git push -u myself dev

git push 已经跑了 4 分多钟,在 do_wait 等待子进程 ssh 结束。

定位叶子进程:SSH 连接出了什么问题

看网络连接

ss -tnp | grep 2868
# → ESTAB  0  293076  192.168.5.99:60362 → 20.205.243.166:22  users:(("ssh",pid=2868,fd=3))

关键信息:

字段含义
StateESTABTCP 连接已建立
Recv-Q0没有待读数据
Send-Q293076 (~286KB)发送缓冲区积压了 286KB
目的地址20.205.243.166:22GitHub SSH 端口

TCP 三次握手已完成,但 Send-Q 积压 286KB,说明本地发出了数据而 GitHub 端没有确认 TCP 报文。这是典型的网络不通畅:连接能建起来,但数据传不过去。

看环境变量

cat /proc/2868/environ | tr '\0' '\n' | grep -i proxy
HTTPS_PROXY=http://192.168.5.244:10808
HTTP_PROXY=http://192.168.5.244:10808
all_proxy=socks5://192.168.5.244:10808

注意:SSH 不使用 HTTP/SOCKS 环境变量,它会直连目标主机的 22 端口。HTTP/SOCKS 代理只对 HTTP 客户端和部分应用生效,SSH 不受影响。

根因

git push 使用 SSH 协议推送到 GitHub(git@github.com:...),SSH 直连 github.com:22。本地网络到 GitHub 22 端口的 TCP 路径不畅通(可能被防火墙阻断或被 QoS 限速)。

排查思路总结

进程状态 (ps)                    ← 宏观:是 sleep / run / 内核阻塞?
    │
    ▼
子进程树 (pstree)                ← 谁在等谁?
    │
    ▼
叶子进程网络状态 (ss -tnp)        ← 网络有没有问题?
    │
    ▼
环境变量 (/proc/pid/environ)      ← 代理/环境因素

strace 看到大量事件循环输出时,不要被干扰。事件循环模型下 ps 看到的 epoll_wait 几乎永远不变,关键是它等的那件事有没有进展——用 pstree 一层层追到最底层,看叶子进程的状态。


第二次排查:锁争抢(Lock Contention)

同一天稍晚,另一个 opencode 进程(PID 138479)又卡住了。这次表现不同:CPU 占用 18%,看起来"活着"但响应极慢。

初步观察

ps -p 138479 -o pid,stat,wchan,pcpu,cmd
# → 138479  Sl+  do_epoll_wait  18.0  opencode --log-level DEBUG

cat /proc/138479/status | grep -E 'State|VmRSS|Threads'
# → State: R (running)
# → VmRSS: 790120 kB
# → Threads: 40

注意 ps 显示 Sl+(sleep),但 /proc/pid/status 显示 R(running)。790MB RSS,40 个线程,18% CPU —— 进程在干活,但效率极低。

strace 采样

strace -p 138479 -e trace=all -c -S time 2>&1 &
sleep 5; kill %1 2>/dev/null; wait 2>/dev/null

5 秒内的系统调用分布:

% time     seconds  calls    errors  syscall
 79.64    0.097539  20345      252  futex
 16.14    0.019772   2649            sched_yield
  2.56    0.003137    389            epoll_pwait2
  0.67    0.000821   1616            clock_gettime
  0.29    0.000359    277            pread64
  0.18    0.000215    281            read

关键数据:

线程分布

ps -Lp 138479 -o lwp,psr,pcpu,stat,wchan:32 --no-headers | sort -k3 -rn | head -10
  lwp  psr  pcpu  stat  wchan
138479   0   7.3  Rl+   -                  # 主线程 running
142051   3   3.4  Sl+   do_epoll_wait
138494   2   2.2  Sl+   do_epoll_wait
138487  14   0.9  Sl+   futex_wait_queue    # 7个线程等锁
138484   0   0.9  Rl+   futex_wait_queue     # 1个线程在 futex 中 running
...

~20 个线程futex_wait_queue,1 个主线程在用户态跑,其余在 do_epoll_wait

根因分析

进一步跟踪 futex 操作:

strace -p 138479 -e trace=futex 2>&1 | head -20
futex(0x7f601c0b4154, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x7f601c0c0154, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x7f601c0e0150, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x7f601c0b4154, FUTEX_WAKE_PRIVATE, 1) = 1
...
futex(0x7f601c01f154, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME,
      125123, NULL, FUTEX_BITSET_MATCH_ANY) = -1 EAGAIN

模式:大量 FUTEX_WAKE_PRIVATE 唤醒 + 偶尔 FUTEX_WAIT_BITSET_PRIVATE 超时(EAGAIN)。多个 futex 地址集中在同一内存区(0x7f601c0bxxx-0x7f601c0fxxx),像是一个线程池任务队列上的锁。

结论:惊群效应 + 严重锁争抢。

可能诱因:

  1. 线程池过大(40 线程 vs 实际 CPU 核数),过多线程争抢同一把锁
  2. big-pickle 本地 LLM 推理慢,IO 线程在等待结果期间造成锁堆积
  3. 39 个 LSP server 同时启动,大量并发初始化请求冲击线程池

第三次排查:IPv6 连接卡住 SYN-SENT

解决第一个进程后,又启动了一个新的 opencode(PID 183287)。这次 CPU 很低(2.8%),但也不响应。

基础信息

ps -p 183287 -o pid,stat,wchan,pcpu,cmd
# → 183287  Sl+  do_epoll_wait  2.8  opencode --log-level DEBUG

cat /proc/183287/status | grep -E 'State|VmRSS|Threads'
# → State:  S (sleeping)
# → VmRSS: 511812 kB
# → Threads: 16

16 线程,512MB,2.8% CPU,状态 S(sleeping)。和上一个完全不同。

线程分布

  lwp    pcpu  wchan
183348   0.8   do_epoll_wait
183287   0.6   do_epoll_wait     # 主线程
183295   0.1   futex_wait_queue
...其他线程都在 sleep...

所有线程 ≤ 0.8% CPU,大部分在 futex 上 sleep。进程完全空闲,不是在忙,是真的在等什么。

看文件描述符

ls -la /proc/183287/fd/ | grep socket
35 -> socket:[56963599]
36 -> socket:[56963601]
37 -> socket:[56963600]
38 -> socket:[56962309]
39 -> socket:[56963602]
40 -> socket:[56963603]

6 个 socket。用 ss 看它们的连接状态:

ss -npep | grep 183287
fdTCP 状态本地远端说明
38ESTABLISHED192.168.5.99:53102172.67.69.147:443Cloudflare IPv4 ✅
40ESTABLISHED192.168.5.99:5955820.205.243.165:443Microsoft/GitHub ✅
35SYN-SENT[3000::5054:ff:fe87:f354]:38682[2606:4700::6810:a22]:443Cloudflare IPv6 ❌
36SYN-SENT[3000::5054:ff:fe87:f354]:60690[2606:4700::6810:422]:443Cloudflare IPv6 ❌
37SYN-SENT[3000::5054:ff:fe87:f354]:46750[2606:4700::6810:222]:443Cloudflare IPv6 ❌
39SYN-SENT[3000::5054:ff:fe87:f354]:49118[2606:4700::6810:322]:443Cloudflare IPv6 ❌

4 个连接卡在 SYN-SENT,目标全是 Cloudflare 的 IPv6 地址段 2606:4700::/32

验证 IPv6 不通

ip -6 route show default
# → default via fe80::c0b1:65ff:fe3e:68c3 dev enp1s0

ping6 -c 2 -W 2 2606:4700::6810:a22
# → 2 packets transmitted, 0 received, 100% packet loss

IPv6 路由存在,但实际不通 —— ping Cloudflare IPv6 地址 100% 丢包。

根因分析

big-pickle 模型 API 通过 Cloudflare 托管,DNS 同时返回了 A 和 AAAA 记录。Node.js HTTP 客户端默认执行 Happy Eyeballs(RFC 8305):优先尝试 IPv6,如果连接建立缓慢,才回退到 IPv4。

问题在于:

  1. DNS 返回了多个 Cloudflare IPv6 地址
  2. Node.js 尝试并发连接所有 IPv6 地址
  3. 内核发出 SYN 后收不到 SYN-ACK,连接进入 SYN-SENT 状态
  4. 内核每 20 秒重传一次 SYN,总超时可达 120 秒以上
  5. 在此之前,IPv4 的请求即使已成功,部分客户端逻辑可能仍在等待 IPv6 尝试完成

这解释了为什么进程看起来"卡住了":它不是在死循环,而是事件循环被大量 pending 的 TCP 连接超时拖着,无法及时处理已就绪的请求。

临时缓解

# 方式 1:Node.js 层优先 IPv4
NODE_OPTIONS="--dns-result-order=ipv4first" opencode

# 方式 2:系统层禁用 IPv6
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1

根本修复

排查 IPv6 路由/网关问题,或确保 big-pickle API 域名在本地只解析到 IPv4。


排查思路总结(更新版)

加上今天的排查,归纳出 opencode 卡住的三种模式:

卡住了?
  │
  ├─ CPU 高 (>10%) + 大量 futex/sched_yield
  │     → 锁争抢 / 惊群效应
  │     → 检查线程池大小、LSP server 数量
  │
  ├─ CPU 低 (<5%) + 进程 idle (S/sleeping)
  │     → 看 fd 中的 socket
  │       ├─ SYN-SENT → IPv6 不通 / 防火墙
  │       └─ ESTABLISH + Send-Q 积压 → 网络拥塞
  │
  └─ CPU 低 + 有子进程 (pstree)
        → 看 pstree 找到叶子进程
          → git? ssh? → 网络问题 / 代理配置

解决方案(git push SSH)

两种方式让 git push 走代理:

方案一:SSH 配代理

~/.ssh/config 中添加:

Host github.com
    ProxyCommand nc -X 5 -x 192.168.5.244:10808 %h %p

方案二:改用 HTTPS 远程

git remote set-url myself https://github.com/moxuetianya/learn-opencode.git

HTTPS 协议会使用 HTTPS_PROXY 环境变量,自动走代理。