Oracle Pro*C 程序卡死排查:strace 抓不到的「库内 spin」用 gdb 定位

一句话场景:生产环境一个 Oracle Pro*C 数据加载程序卡死不退出,CPU 持续 97%,strace 却抓不到主线程在干什么;新起的同程序同 SQL 又能正常完成。本文记录从 strace 到 gdb 的定位过程,以及一路踩过的误判坑。

背景

某 Oracle Pro*C 数据加载程序(sqlcxt 执行 SELECT 拉数据,产索引文件)出现进程卡死:CPU 持续 97%,90 分钟不退出。但新起的同程序同 SQL 能正常完成。需要定位根因。

一、strace 看不到主线程在干嘛

strace -p <PID> -f

只看到子线程 nanosleep(1秒) 心跳,主线程一个 syscall 都没有。但 ps 显示主线程 CPU 97%。

关键认知:进程卡死 + strace 看不到 syscall + CPU 高 = 主线程在库内 spin(某个 .so 内部忙循环,不调 Linux syscall)。strace 抓不到,因为 strace 只抓系统调用。

这一步差点把我带偏——一开始以为 strace 没输出 = 进程在等 IO(CPU 应该低),但 ps 的 97% CPU 矛盾。意识到是「库内 spin」后才转用 gdb。

二、gdb 调用栈定位

gdb -p <PID> -batch -ex "thread apply all bt"

主线程调用栈:

ttcclr / ttcfopr / ttcdrv    ← Oracle TTC (Two-Task Common) 层
nioqwa                        ← Oracle 网络 I/O 等待
upirtrc                       ← UPI retry(重试,spin 不 sleep → CPU 97%)
kpurcsc / kpufch0 / kpufch    ← Oracle fetch 行
sqlcucFetch / sqlall / sqlsel / sqlnst / sqlcxt   ← Pro*C 执行 SQL + fetch

定位:程序卡在 Oracle Pro*C fetch(sqlcxt → kpufch),Oracle 客户端库 libclntsh 在 upirtrc(UPI retry)spin 重试不放弃,CPU 97%。

三、根因逐步收窄

每条观察推翻一个推测,最终才逼近真相:

观察 推翻的推测
新起同程序同 SQL 能成功SQL 性能 / Oracle 端普遍问题
非并发(每次单独起)并发锁竞争
同时段有成功有卡时段环境问题
两 IP 无防火墙同网段NAT/防火墙 idle 超时断 TCP
TCP ESTABLISHED 超 2 小时连接死 / session 死 / OOM kill

TCP ESTABLISHED 2h+ 是决定性证据:如果连接死了 / session 被 kill / OOM,TCP 会断开(RST/FIN),不会保持 ESTABLISHED 2 小时。说明 Oracle 端 TCP 活着,问题在应用层。

最终收窄:Oracle 应用层 session 不回 fetch 响应(session ACTIVE 卡在某 wait event,或 INACTIVE 双方互等),TCP 层活着,但 19c 客户端 libclntsh 无读超时,spin 不放弃。

四、19c vs 11g 客户端行为差异

同程序在 11g 客户端不卡(没设 sqlnet.ora 超时也正常):

  • 11g libclntsh:遇连接异常较快报 ORA-03113/03114 退出
  • 19c libclntsh:改了 upirtrc 重试逻辑,spin 不放弃

这是 Oracle 客户端版本行为差异,不是配置问题。

五、根因与放大因素

直接原因:Oracle 应用层 session 不回 fetch 响应(hang 或互等)

三个放大因素叠加,导致 spin 长达 90 分钟:

  1. 19c libclntsh upirtrc spin 重试不放弃
  2. sqlnet.ora RECV_TIMEOUT 默认 0(无限读等待)
  3. OS TCP keepalive 默认 7200 秒(2 小时,期间客户端不知道 session 异常)

六、处置与预防

应急处置:kill -9 卡死进程(libclntsh spin 不自愈,救不了)

治本预防——sqlnet.ora 设超时:

SQLNET.RECV_TIMEOUT = 60       # fetch 读超时 60 秒报错退出(核心,打破 spin)
SQLNET.SEND_TIMEOUT = 60
SQLNET.EXPIRE_TIME = 10        # DCD 死连接探测

下次卡时立即查 v$session(别急着 kill),定位具体 hang:

SELECT s.sid, s.serial#, s.status, s.event, s.sql_id,
       s.blocking_session, s.seconds_in_wait, s.wait_class, s.state,
       s.machine, s.program, p.spid
FROM v$session s, v$process p
WHERE s.paddr = p.addr
  AND (s.username = '<USER>' OR s.program LIKE '%<prog>%')
ORDER BY s.seconds_in_wait DESC NULLS LAST;

按 status + event 判断 hang 类型:

statusevent含义
INACTIVESQL*Net message from client + seconds_in_wait 巨大双方互等
ACTIVElatch: cache buffers chains / library cache lock内部争用
ACTIVEenq: TX row lock + blocking_session 非空被锁

七、方法论总结

  1. 进程卡死 + strace 看不到 syscall + CPU 高 = 库内 spin,第一时间 gdb -p <PID> -batch -ex "thread apply all bt",别在 strace 上耗时间
  2. 每条观察逐步收窄,避免经验主义误判——本次误判过 busy-loop / 并发锁 / SQL 慢 / 时段环境 / 连接死
  3. TCP ESTABLISHED 是判断「连接死 vs 应用层 hang」的关键证据
  4. Oracle 客户端版本(11g vs 19c)对连接异常的处理差异是常见坑
posted @ 2026-09-28 10:29  兔子春  阅读(3)  评论(0)    收藏  举报