odps链接表并预测出现程序阻塞导致任务未完成问题排查
问题背景
- 在使用pyodps链接odps表, 进行读取数据, 并利用模型进行预测, 然后将预测结果写入到数据表中的过程, 在程序启动之后, 数据预测到一半, 变阻塞了, 导致最后任务流程超时报错.
问题排查
- 找到阻塞的进行
ps -axu | grep LLM_predict_pname_table_auto_process.*
搜索结果如下
deploy 530658 0.0 0.1 1448824 90704 ? Ssl 01:17 0:15 /home/deploy/research/workspace/pname_auto_predict/.venv/bin/python ./code/LLM_predict_pname_table_auto_process.py --source mid_algo_product_predict_xxx/ds=20250901 --sink mid_algo_product_predict_xxx/ds=20250901
- 针对python程序, 查看堆栈情况
# 安装
pip install py-spy
# 查看进程的实时堆栈(替换PID为程序进程ID)
py-spy top -p <PID>
# 或生成堆栈快照
py-spy dump -p <PID>
说明: 下面使用的 py-spy dump -p 530658
2.1 确认进程基本状态
ps -p <PID> -o %cpu,%mem,state,cmd
输出结果解读:
输出字段说明:
%cpu:CPU 使用率(接近 0 可能是 IO 阻塞,高则可能是计算 / 死循环)
%mem:内存使用率(过高可能导致 OOM 阻塞)
state:进程状态(关键!参考下方解释)
cmd:启动命令(确认程序身份)
状态(state)关键值:
S:休眠(等待 IO / 网络,如正常等待资源可能没问题)
D:不可中断休眠(通常是严重 IO 阻塞,如磁盘故障、NFS 挂了)
R:运行中(若 CPU 低但一直运行,可能是死循环)
Z:僵尸进程(已终止但未被回收,通常不直接阻塞但需清理父进程
2.2 查看堆栈快照
py-spy dump -p 530658
输出结果如下:
Process 530658: /home/deploy/research/workspace/pname_auto_predict/.venv/bin/python ./code/LLM_predict_pname_table_auto_process.py --source mid_algo_product_predict_xxx/ds=20250901 --sink mid_algo_product_predict_xxx/ds=20250901
Python v3.8.5 (/home/deploy/anaconda3/bin/python3.8)Thread 530658 (idle): "MainThread"_wait_for_tstate_lock (threading.py:1027)join (threading.py:1011)work (LLM_predict_pname_table_auto_process.py:206)<module> (LLM_predict_pname_table_auto_process.py:227)
Thread 531534 (idle): "Thread-2"getaddrinfo (socket.py:918)create_connection (urllib3/util/connection.py:60)_new_conn (urllib3/connection.py:199)connect (urllib3/connection.py:279)send (http/client.py:950)_send_output (http/client.py:1010)endheaders (http/client.py:1250)request (urllib3/connection.py:441)_make_request (urllib3/connectionpool.py:495)urlopen (urllib3/connectionpool.py:789)send (requests/adapters.py:667)send (requests/sessions.py:703)_request (odps/rest.py:273)request (odps/rest.py:200)get (odps/rest.py:293)_open_reader (odps/tunnel/tabletunnel.py:233)open_record_reader (odps/tunnel/tabletunnel.py:250)call_with_retry (odps/utils.py:930)_open_and_iter_reader (odps/models/readers.py:139)_retry_iter_reader (odps/models/readers.py:80)read (odps/models/readers.py:158)read_data_thread (utils.py:94)run (threading.py:870)_bootstrap_inner (threading.py:932)_bootstrap (threading.py:890)
Thread 531536 (idle): "Thread-3"getaddrinfo (socket.py:918)create_connection (urllib3/util/connection.py:60)_new_conn (urllib3/connection.py:199)connect (urllib3/connection.py:279)send (http/client.py:950)_send_output (http/client.py:1010)endheaders (http/client.py:1250)request (urllib3/connection.py:441)_make_request (urllib3/connectionpool.py:495)urlopen (urllib3/connectionpool.py:789)send (requests/adapters.py:667)send (requests/sessions.py:703)_request (odps/rest.py:273)request (odps/rest.py:200)post (odps/rest.py:297)_call_tunnel (odps/tunnel/tabletunnel.py:335)call_with_retry (odps/utils.py:930)_create_or_reload_session (odps/tunnel/tabletunnel.py:345)_init (odps/tunnel/tabletunnel.py:354)__init__ (odps/tunnel/tabletunnel.py:305)create_upload_session (odps/tunnel/tabletunnel.py:976)call_with_retry (odps/utils.py:930)open_writer (odps/models/table.py:810)write_table (odps/core.py:739)write_data_thread (utils.py:143)run (threading.py:870)_bootstrap_inner (threading.py:932)_bootstrap (threading.py:890)
Thread 531538 (idle): "Thread-4"getaddrinfo (socket.py:918)create_connection (urllib3/util/connection.py:60)_new_conn (urllib3/connection.py:199)connect (urllib3/connection.py:279)send (http/client.py:950)_send_output (http/client.py:1010)endheaders (http/client.py:1250)request (urllib3/connection.py:441)_make_request (urllib3/connectionpool.py:495)urlopen (urllib3/connectionpool.py:789)send (requests/adapters.py:667)send (requests/sessions.py:703)_request (odps/rest.py:273)request (odps/rest.py:200)post (odps/rest.py:297)_call_tunnel (odps/tunnel/tabletunnel.py:335)call_with_retry (odps/utils.py:930)_create_or_reload_session (odps/tunnel/tabletunnel.py:345)_init (odps/tunnel/tabletunnel.py:354)__init__ (odps/tunnel/tabletunnel.py:305)create_upload_session (odps/tunnel/tabletunnel.py:976)call_with_retry (odps/utils.py:930)open_writer (odps/models/table.py:810)write_table (odps/core.py:739)write_data_thread (utils.py:143)run (threading.py:870)_bootstrap_inner (threading.py:932)_bootstrap (threading.py:890)
Thread 531540 (idle): "Thread-5"getaddrinfo (socket.py:918)create_connection (urllib3/util/connection.py:60)_new_conn (urllib3/connection.py:199)connect (urllib3/connection.py:279)send (http/client.py:950)_send_output (http/client.py:1010)endheaders (http/client.py:1250)request (urllib3/connection.py:441)_make_request (urllib3/connectionpool.py:495)urlopen (urllib3/connectionpool.py:789)send (requests/adapters.py:667)send (requests/sessions.py:703)_request (odps/rest.py:273)request (odps/rest.py:200)post (odps/rest.py:297)_call_tunnel (odps/tunnel/tabletunnel.py:335)call_with_retry (odps/utils.py:930)_create_or_reload_session (odps/tunnel/tabletunnel.py:345)_init (odps/tunnel/tabletunnel.py:354)__init__ (odps/tunnel/tabletunnel.py:305)create_upload_session (odps/tunnel/tabletunnel.py:976)call_with_retry (odps/utils.py:930)open_writer (odps/models/table.py:810)write_table (odps/core.py:739)write_data_thread (utils.py:143)run (threading.py:870)_bootstrap_inner (threading.py:932)_bootstrap (threading.py:890)
Thread 531542 (idle): "Thread-6"getaddrinfo (socket.py:918)create_connection (urllib3/util/connection.py:60)_new_conn (urllib3/connection.py:199)connect (urllib3/connection.py:279)send (http/client.py:950)_send_output (http/client.py:1010)endheaders (http/client.py:1250)request (urllib3/connection.py:441)_make_request (urllib3/connectionpool.py:495)urlopen (urllib3/connectionpool.py:789)send (requests/adapters.py:667)send (requests/sessions.py:703)_request (odps/rest.py:273)request (odps/rest.py:200)post (odps/rest.py:297)_call_tunnel (odps/tunnel/tabletunnel.py:335)call_with_retry (odps/utils.py:930)_create_or_reload_session (odps/tunnel/tabletunnel.py:345)_init (odps/tunnel/tabletunnel.py:354)__init__ (odps/tunnel/tabletunnel.py:305)create_upload_session (odps/tunnel/tabletunnel.py:976)call_with_retry (odps/utils.py:930)open_writer (odps/models/table.py:810)write_table (odps/core.py:739)write_data_thread (utils.py:143)run (threading.py:870)_bootstrap_inner (threading.py:932)_bootstrap (threading.py:890)
Thread 531641 (idle): "QueueFeederThread"wait (threading.py:302)_feed (multiprocessing/queues.py:227)run (threading.py:870)_bootstrap_inner (threading.py:932)_bootstrap (threading.py:890)
原因分析
- 从 py-spy dump 的输出来看,程序所有线程均处于 idle(空闲阻塞)状态,且数据未处理完成,核心原因是读取和写入 ODPS 表的线程都阻塞在网络连接环节,导致整个数据处理流程(读→处理→写)停滞。以下是详细解读
一、线程角色与阻塞点分析
输出中包含 7 个线程,可分为 3 类,每类线程的阻塞点高度一致,均与网络操作相关:
- 主线程(MainThread,Thread 530658)
_wait_for_tstate_lock (threading.py:1027)
join (threading.py:1011)
work (LLM_predict_pname_table_auto_process.py:206)
<module> (LLM_predict_pname_table_auto_process.py:227)
角色:程序主入口线程,负责协调整体流程。
阻塞点:在 work 函数中调用了 join() 方法(threading.Thread.join()),正在等待其他子线程(读取、处理、写入线程)执行完毕。
为什么阻塞:子线程(如下述的读取 / 写入线程)未完成工作,导致主线程长期处于 “等待子线程结束” 的状态。
- 读取 ODPS 数据的线程(Thread 531534,“Thread-2”)
getaddrinfo (socket.py:918) # 核心阻塞点
create_connection (urllib3/util/connection.py:60)
...(中间调用链)...
read_data_thread (utils.py:94) # 你的读取线程函数
角色:负责从 ODPS 表读取数据(read_data_thread 是自定义的读取线程函数)。
阻塞点:卡在 socket.getaddrinfo() 函数 —— 这是 Python 底层用于解析域名、获取网络地址的系统调用(如将 ODPS 服务的域名解析为 IP 地址)。
为什么阻塞:getaddrinfo 是网络连接的第一步,若域名解析失败、DNS 无响应、网络不通,或 ODPS 服务的域名无法访问,该调用会长期阻塞(无超时或超时时间过长),导致读取线程无法获取数据,后续处理流程无数据可处理。
- 写入 ODPS 数据的线程(Thread 531536~531542,“Thread-3” 到 “Thread-6”)
getaddrinfo (socket.py:918) # 核心阻塞点
create_connection (urllib3/util/connection.py:60)
...(中间调用链)...
write_data_thread (utils.py:143) # 你的写入线程函数
角色:负责将处理结果写入 ODPS 表(write_data_thread 是自定义的写入线程函数)。
阻塞点:与读取线程完全一致,同样卡在 socket.getaddrinfo(),说明写入线程在尝试连接 ODPS 服务时,也无法完成域名解析或网络连接。
为什么阻塞:写入操作依赖 ODPS 的上传接口(create_upload_session 等),需要先建立网络连接,若网络环境有问题(与读取线程的问题相同),写入线程会同步阻塞。
- 队列 feeder 线程(QueueFeederThread,531641)
wait (threading.py:302)
_feed (multiprocessing/queues.py:227)
角色:负责进程间队列的数据传递(multiprocessing.Queue 的内部辅助线程)。
阻塞点:卡在队列的 _feed 方法(等待数据写入队列)。
为什么阻塞:由于读取线程未成功获取数据,队列中没有数据可传递;同时写入线程阻塞也可能导致队列消费停滞,最终 feeder 线程因 “无数据可处理” 而空闲。
二、核心结论:网络连接问题导致全流程阻塞
所有线程的 idle 状态均源于读取和写入 ODPS 的线程卡在网络连接的第一步(域名解析),具体表现为:
读取线程无法从 ODPS 获取数据 → 处理环节无数据输入;
写入线程无法向 ODPS 写入结果 → 即使有部分处理结果也无法输出;
主线程等待所有子线程完成 → 因子线程阻塞而同步等待;
队列辅助线程因无数据流转而空闲。
最终导致 “数据未处理完成,但程序整体处于 idle 状态”—— 本质是网络层阻塞导致业务层所有环节停滞。
三、可能的原因及排查方向
- 网络环境问题(最可能)
DNS 解析失败:ODPS 服务的域名(如 service.odps.aliyun.com)无法被解析为 IP 地址(可能是 DNS 服务器故障、本地 DNS 配置错误)。
验证:在终端执行 nslookup service.odps.aliyun.com 或 ping service.odps.aliyun.com,若无法解析或超时,说明 DNS 有问题。
网络连通性差:程序所在机器与 ODPS 服务的网络链路不通(如防火墙拦截、网段限制、VPN 断开)。
验证:用 telnet 测试 ODPS 端口(通常是 443):telnet service.odps.aliyun.com 443,若无法连接,说明网络被拦截。
ODPS 服务临时故障:ODPS 服务端(尤其是隧道服务 tabletunnel)临时不可用,导致客户端连接请求无响应。
验证:检查 ODPS 控制台的服务状态,或尝试用其他机器连接同一 ODPS 表,排除服务端问题。 - ODPS SDK 配置问题
超时设置缺失:ODPS SDK(odps 库)未设置网络超时,导致 getaddrinfo 或连接操作无超时限制,长期阻塞。
检查:查看 utils.py 中 read_data_thread 和 write_data_thread 的实现,是否为 ODPS 操作设置了超时参数(如 socket.setdefaulttimeout(30) 或 SDK 层面的超时配置)。
并发连接数过高:同时启动了多个写入线程(Thread-3 到 Thread-6 共 4 个),可能触发 ODPS 服务的限流或本地网络的连接数限制,导致部分连接被挂起。
四、解决方案建议
- 添加网络超时配置:
在 ODPS 操作前设置全局网络超时(避免无限阻塞):
import socket
socket.setdefaulttimeout(30) # 所有网络操作超时30秒
或在 ODPS SDK 中配置超时(参考官方文档,如 odps 库的 tunnel 配置)。
2. 排查网络连通性:
联系运维确认机器是否能访问 ODPS 服务的域名和端口;
临时切换网络环境(如连接 VPN)测试是否恢复。
3. 减少写入线程并发数:
当前有 4 个写入线程(Thread-3 到 Thread-6),可能超过网络或服务端承载能力,尝试减少并发数(如改为 1-2 个)。
4. 增加网络错误重试机制:
在 read_data_thread 和 write_data_thread 中,对网络相关操作添加重试逻辑(如用 tenacity 库),避免单次失败导致全流程阻塞。
通过以上排查,核心是解决 getaddrinfo 阻塞的问题(网络或 DNS),恢复 ODPS 的读写连接后,程序的 idle 状态会解除,数据处理流程将正常推进。
以上阻塞点分析判断依据
- 在分析程序阻塞环节时,核心依据是线程调用栈的顶层函数及其所属的操作链路。调用栈(Call Stack)记录了函数的调用关系,最顶层的函数是线程当前正在执行的操作,也是阻塞发生的具体位置。结合函数的功能、所属模块(如网络库、线程库、业务代码),可以精准定位阻塞环节。以下是具体判断过程和依据:
一、判断逻辑:从 “顶层函数” 到 “操作环节”
每个线程的调用栈是 “从上到下” 的函数调用链:
最顶层的函数(第一行)是线程当前正在执行且未返回的操作(即阻塞点)。
下层函数是 “调用它的父函数”,形成完整的操作链路(如 “业务代码→SDK→网络库→系统调用”)。
通过分析顶层函数的功能、所属模块,结合下层函数的业务上下文(如自定义的read_data_thread、write_data_thread),可确定阻塞发生在哪个业务环节(读取、写入、主线程协调等)。
二、各线程阻塞环节的判断依据
- 主线程(MainThread,Thread 530658):阻塞在 “等待子线程结束”
调用栈顶层:
_wait_for_tstate_lock (threading.py:1027)
join (threading.py:1011)
work (LLM_predict_pname_table_auto_process.py:206)
判断依据:
顶层函数是threading.py中的_wait_for_tstate_lock,其直接上层是join()方法 —— 这是 Python 线程中Thread.join()的内部实现,功能是 “等待子线程执行完毕”。
再上层是业务代码work(LLM_predict_pname_table_auto_process.py:206),说明主线程在work函数中调用了join(),正在等待其他子线程(读取、写入线程)结束。
结论:主线程阻塞在 “等待子线程完成” 的协调环节,因子线程未结束而长期等待。
- 读取 ODPS 数据的线程(Thread 531534,“Thread-2”):阻塞在 “ODPS 读取的网络连接初始化”
调用栈顶层:
getaddrinfo (socket.py:918)
create_connection (urllib3/util/connection.py:60)
_new_conn (urllib3/connection.py:199)
...(中间链路)...
read_data_thread (utils.py:94) # 自定义读取线程函数
判断依据:
顶层函数是socket.py中的getaddrinfo—— 这是 Python 底层的网络系统调用,功能是 “解析域名并获取对应的 IP 地址和端口”(网络连接的第一步,如将 ODPS 服务的域名service.odps.aliyun.com解析为 IP)。
下层函数链路清晰指向网络连接过程:create_connection(创建连接)→_new_conn(初始化连接)→ODPS SDK 的open_record_reader(打开 ODPS 读取器)→最终到自定义的read_data_thread(业务层的读取线程函数)。
结论:读取线程的核心操作是 “从 ODPS 读取数据”,但在最开始的 “网络连接初始化(域名解析)” 环节就阻塞了,导致后续无法获取数据。
- 写入 ODPS 数据的线程(Thread 531536~531542):阻塞在 “ODPS 写入的网络连接初始化”
调用栈顶层:
getaddrinfo (socket.py:918)
create_connection (urllib3/util/connection.py:60)
...(中间链路)...
write_data_thread (utils.py:143) # 自定义写入线程函数
判断依据:
顶层函数同样是socket.getaddrinfo,与读取线程完全一致,说明写入线程也卡在 “域名解析” 这一步。
下层函数链路指向 ODPS 写入操作:create_upload_session(创建 ODPS 上传会话)→open_writer(打开写入器)→write_table(写入 ODPS 表)→最终到自定义的write_data_thread(业务层的写入线程函数)。
结论:写入线程的核心操作是 “向 ODPS 写入结果”,但同样在 “网络连接初始化(域名解析)” 环节阻塞,导致无法写入数据。
- 队列辅助线程(QueueFeederThread):阻塞在 “等待队列数据”
调用栈顶层:
wait (threading.py:302)
_feed (multiprocessing/queues.py:227)
判断依据:
顶层函数是threading.py中的wait,来自multiprocessing/queues.py的_feed方法 —— 这是进程间队列(multiprocessing.Queue)的内部辅助线程,功能是 “将数据从主进程传递到子进程队列”。
_feed方法中的wait说明队列中没有数据可传递(读取线程未产生数据,或写入线程未消费数据),导致辅助线程空闲等待。
结论:因上游读取线程未获取数据,队列无数据流转,辅助线程阻塞在 “等待数据” 环节。
三、总结:核心判断原则
顶层函数定阻塞点:每个线程的阻塞点一定是调用栈的最顶层函数(当前正在执行的操作)。
函数功能定操作类型:通过顶层函数的所属模块(如socket→网络操作,threading→线程协调,multiprocessing/queues→队列操作)判断是网络、线程还是队列问题。
业务函数定环节:通过调用栈中自定义的业务函数(如read_data_thread→读取环节,write_data_thread→写入环节)定位到具体的业务流程(读 / 写 / 协调)。
最终,所有线程的阻塞都可追溯到 “读取和写入 ODPS 时的网络连接初始化(域名解析)失败”,这是导致全流程停滞的根因。
以上是整个问题的分析过程, 希望在以后的开发中不要遇到这样的问题