opencode 界面卡住不动排查
七月 15, 2026 #linux #debug #troubleshooting #strace #ssh #git #futex #lock-contention #ipv6现象
opencode 交互界面一直显示运行中,不输出任何新内容,像是卡死了一样。
第一反应:strace
strace -p <pid> 看一下卡在哪个系统调用上:
strace -p 4188775
结果有大量输出,epoll_wait → read → write → epoll_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))
关键信息:
| 字段 | 值 | 含义 |
|---|---|---|
| State | ESTAB | TCP 连接已建立 |
| Recv-Q | 0 | 没有待读数据 |
| Send-Q | 293076 (~286KB) | 发送缓冲区积压了 286KB |
| 目的地址 | 20.205.243.166:22 | GitHub 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
关键数据:
- 79.6% 时间花在 futex — 20,345 次调用,线程在抢锁
- 16.1% 花在 sched_yield — 2,649 次,争不到锁主动让出 CPU
- 真正的 I/O(read/write)占比可以忽略
线程分布
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),像是一个线程池任务队列上的锁。
结论:惊群效应 + 严重锁争抢。
- 有大量线程同时被唤醒去竞争少数任务或资源
- 大部分线程发现没活可干又 sleep,如此循环
- CPU 大部分消耗在 futex 调度上而非实际工作
- 进程"活着"(能响应请求),但有效吞吐极低
可能诱因:
- 线程池过大(40 线程 vs 实际 CPU 核数),过多线程争抢同一把锁
big-pickle本地 LLM 推理慢,IO 线程在等待结果期间造成锁堆积- 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
| fd | TCP 状态 | 本地 | 远端 | 说明 |
|---|---|---|---|---|
| 38 | ESTABLISHED | 192.168.5.99:53102 | 172.67.69.147:443 | Cloudflare IPv4 ✅ |
| 40 | ESTABLISHED | 192.168.5.99:59558 | 20.205.243.165:443 | Microsoft/GitHub ✅ |
| 35 | SYN-SENT | [3000::5054:ff:fe87:f354]:38682 | [2606:4700::6810:a22]:443 | Cloudflare IPv6 ❌ |
| 36 | SYN-SENT | [3000::5054:ff:fe87:f354]:60690 | [2606:4700::6810:422]:443 | Cloudflare IPv6 ❌ |
| 37 | SYN-SENT | [3000::5054:ff:fe87:f354]:46750 | [2606:4700::6810:222]:443 | Cloudflare IPv6 ❌ |
| 39 | SYN-SENT | [3000::5054:ff:fe87:f354]:49118 | [2606:4700::6810:322]:443 | Cloudflare 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。
问题在于:
- DNS 返回了多个 Cloudflare IPv6 地址
- Node.js 尝试并发连接所有 IPv6 地址
- 内核发出 SYN 后收不到 SYN-ACK,连接进入 SYN-SENT 状态
- 内核每 20 秒重传一次 SYN,总超时可达 120 秒以上
- 在此之前,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 环境变量,自动走代理。