☰
ARM64下psycopg3连接GaussDB coredump排查与修复
2026/10/10 3:49:57 网站建设 项目流程

上个月替朋友排查了一个挺典型的故障:ARM64 服务器上的 Python 服务连接 GaussDB 时反复 coredump。现象很干脆,进程起来后还没等执行查询就消失,日志里只有一行 Segmentation fault,外加一个几十 MB 的 core 文件。最后问题锁定在 psycopg3 的 C 扩展和底层 libpq 动态库上,跟业务代码本身没什么关系。

这篇文章把整个排查过程整理一遍,从现场信息收集、gdb 看栈,到最后重新编译 psycopg3 并绑定正确的 libpq,尽量写成一份可复用的排查手册。如果你正在 ARM64 环境上部署 Python 应用,或者被 psycopg3 崩得没脾气的,可以照着这个思路走一遍。

1. 先把问题边界划清楚

1.1 崩溃到底发生在哪一步

这类问题最忌讳一上来就改代码。第一步要做的是确认崩溃发生在哪个阶段。

我当时的做法很简单:写一个最小脚本,在 import、connect、query、close 四个环节分别打点。原理是让程序告诉我们“走到哪里才死”,从而圈定排查范围。

import psycopg print("step 1: import ok") conn = psycopg.connect( "host=127.0.0.1 port=5432 dbname=postgres " "user=app_user password=xxx connect_timeout=5" ) print("step 2: connect ok") cur = conn.cursor() cur.execute("select 1") print("step 3: query ok") conn.close() print("step 4: close ok")

现场的结果是:step 2之后进程直接消失,core dump 落在崩溃目录。这说明问题发生在建立连接、认证或 SSL 握手环节,跟查询逻辑无关。如果崩在step 1,方向就完全不同,大概率是 import 阶段加载 C 扩展时出了问题;崩在step 4,则要怀疑连接关闭时的资源释放。

这一步花不了几分钟,却能避免后续在错误的方向上浪费一整天。

1.2 环境信息清单

拿到崩溃点之后,我会把环境信息一次性收集齐。整流排查最怕“盲人摸象”,每一项信息都可能决定下一步走向。

下面是我建议收集的项目,可以当成一张检查表:

项目需要记录的内容为什么关键
CPU 架构uname -m 的结果必须是 aarch64,所有 C 扩展二进制必须匹配
操作系统发行版和内核版本,如某 Linux 4.19决定 libc、OpenSSL 等底层库版本
Python 版本python3 --version决定 .so 文件名中的 cpython 标签
psycopg 版本pip show psycopg 的结果确认是否真的安装了 v3,以及是否装了 psycopg-c
libpq 来源系统自带还是 GaussDB 客户端库决定认证和 SSL 协议走哪一套实现
部署方式是 venv 打包传输、容器镜像,还是本机 pip 安装决定 C 扩展架构是否匹配
core 文件ulimit -c 状态、core_pattern 配置没有 core 文件后面全白干

排查这类问题,第一件事不是改代码,而是把环境信息收集齐。少一项都可能走弯路。

1.3 先澄清一下 psycopg3 这个名字

很多人会踩一个命名坑:PyPI 上真正要装的包叫psycopg,不是psycopg3。v3 的实现分几部分:核心是纯 Python,可选 C 扩展是psycopg-c,预编译的二进制扩展是psycopg-binary。在 Python 里导入psycopg后,如果同时能看到psycopg_c被加载,就说明走的是 C 扩展路径。

python -c "import psycopg, psycopg_c; print(psycopg.__version__); print(psycopg_c.__file__)"

C 扩展本身是对 libpq 的包装。也就是说,psycopg3 的很多关键操作最终会落到 libpq.so 上。coredump 出在 psycopg_c 里,往往只是表象,真正的根源可能在 libpq 上。这也是后面排查的核心逻辑:不要只看 Python 层面的代码,要往 C 扩展和动态库里挖。

2. 从 core 文件反推崩溃现场

2.1 先把 core 留住

core 文件是崩溃现场的第一手证据。很多机器默认 ulimit -c 是 0,或者 core_pattern 指向了 systemd-coredump,导致你明明看到进程退了,却找不到 dump 文件。

检查三件事:

ulimit -c cat /proc/sys/kernel/core_pattern ls -lh /var/lib/systemd/coredump/ 2>/dev/null

如果 ulimit 是 0,临时放开:

ulimit -c unlimited

如果 core_pattern 带着 systemd-coredump,用coredumpctl也能拿到信息。实在不行可以临时改内核参数,把 core 统一丢到固定目录:

sysctl -w kernel.core_pattern=/tmp/core.%e.%p

注意,这个设置重启后会失效,生产环境不要图省事永久改它,只需要在排查期间临时开启。

2.2 gdb 打开 core 的基本姿势

有了 core 文件之后,用 gdb 打开。最关键的一点是:gdb 后面跟的 Python 可执行文件,必须和崩溃时的进程是同一个,或者至少是同一个版本的编译产物。如果服务跑在 venv 里,就要用 venv 里的 python,而不是 /usr/bin/python3。

gdb /opt/app/venv/bin/python /tmp/core.python.40221 (gdb) bt (gdb) info sharedlibrary (gdb) thread apply all bt (gdb) quit

bt 是看主线程调用栈,info sharedlibrary 看崩溃时加载了哪些动态库,thread apply all bt 看所有线程的栈。如果 core 文件几十 MB,加载可能要等一会,可以用批处理模式快速拿栈:

gdb -batch -c /tmp/core.python.40221 -ex bt -ex quit /opt/app/venv/bin/python

这样输出会简短很多,方便先做个初步判断。

2.3 三类典型崩溃栈的判读

拿到的调用栈不同,排查方向完全不同。我根据经验把 psycopg3 的 coredump 分成三类:

第一类,栈顶落在 psycopg_c 内部,比如PQresult相关解析、对象析构、引用计数操作。这类通常和 C 扩展的生命周期管理有关,比如连接对象跨线程使用、连接在未关闭状态下被 GC 回收等。

第二类,栈顶落在 libpq.so.5 里,比如pqReadData、PQconnectPoll、SSL_read。这类几乎都是底层通信库的问题,重点查 libpq 来自哪里,SSL 库是否冲突。我们这次就属于这一类。

第三类,栈里的函数名是??,或者地址完全不可读。这种大概率是二进制架构不匹配,比如把一个 x86_64 的 .so 塞到了 ARM64 环境里,gdb 拿到指令后无法正确解析。

举例说明,一个典型的示意栈长这样:

#0 __memcpy_generic () at /usr/lib/aarch64-linux-gnu/libc.so.6 #1 0x0000ffff8a1e2f40 in PQconnectPoll (conn=...) from /opt/gaussdb/app/lib/libpq.so.5 #2 0x0000ffff89c10d50 in psycopg_c.pq.PQconnectStart (conninfo=...) from .../_psycopg.cpython-310-aarch64-linux-gnu.so #3 ...

注意这是示意栈,不是现场原样。但它反映了一个常见结构:上层调用 psycopg_c,psycopg_c 调用 libpq,libpq 在连接轮询时崩溃。看到这种栈,就要把注意力集中到 libpq 的加载路径和编译方式上。

3. 为什么 ARM64 环境特别容易踩这个坑

3.1 复制粘贴安装方式带来的架构错配

ARM64 服务器的 coredump 里,频率最高的根因其实是“架构不对”。

思路是这样的:pip install psycopg-binary的时候,pip 会根据当前机器的平台标签选择对应的 wheel。如果你在一台 x86_64 的开发机上pip download,然后把 site-packages 整个打包传到 ARM64 服务器,或者把虚拟环境目录直接 rsync 过去,那你拿到的 C 扩展就是 x86_64 的。

问题在于 Python 启动时并不会主动检查所有 .so 的架构,而是加载到哪个模块才去解析哪个模块。所以经常出现这种情况:业务代码跑着,日志正常,看起来一切都没问题,一旦执行到某条路径触碰 C 扩展,进程就突然段错误。这种“半死不活”的状态比一启动就崩更坑人。

确认架构的命令:

file /opt/app/venv/lib/python3.10/site-packages/psycopg_c/_psycopg.cpython-310-aarch64-linux-gnu.so readelf -h /opt/app/venv/lib/python3.10/site-packages/psycopg_c/_psycopg*.so | grep -i machine

正常的输出应该是Machine: AArch64。如果看到X86-64,恭喜,问题找到了。

3.2 两个 libpq 打架

架构对了,不代表万事大吉。第二个高发原因是 libpq 动态库被加载错了。

psycopg3 的 C 扩展在运行时要寻找 libpq。动态库搜索顺序大体是:LD_LIBRARY_PATH指定的路径、ELF 里记录的 RUNPATH/RPATH、系统默认路径。如果一台机器上同时装了 PostgreSQL 的 libpq 和 GaussDB 的客户端 libpq,两个库文件名都是libpq.so.5,那么谁被找到就完全取决于搜索路径的顺序。

这里有一个常见误区:很多人觉得 libpq 就是 libpq,能用就行。实际上 GaussDB 虽然是 PostgreSQL 生态,但客户端库在认证协议、错误处理、SSL 协商细节上可能有自己的实现。psycopg3 编译的时候对着 PostgreSQL 头文件编译,运行的时候却加载了 GaussDB 的 libpq,或者反过来,都有可能在某个函数调用处踩到不一致的 ABI 约定,直接段错误。

检查当前到底加载了哪个库:

ldd /opt/app/venv/lib/python3.10/site-packages/psycopg_c/_psycopg*.so | grep pq LD_DEBUG=libs /opt/app/venv/bin/python -c "import psycopg_c" 2>&1 | grep libpq

LD_DEBUG=libs会打印动态链接器的全部搜索过程,信息量很大,但 grep 一下就能看到先找到了哪个路径下的 libpq。注意这两条命令需要在崩溃环境下跑,不能拿开发机代替。

3.3 SSL 和认证阶段是重灾区

回到我们这次的实际场景:崩溃点发生在连接阶段。DRL 排查惯例是优先看 SSL 相关调用。因为 GaussDB 默认开启 SSL/TLS,libpq 在握手阶段会调用 OpenSSL 的SSL_read、SSL_write等函数。

如果系统里存在多个 OpenSSL 版本,比如系统升级后 libssl.so.3 和 libssl.so.1.1 共存,而 libpq 编译时链接的是 libssl.so.1.1,运行环境却因为路径优先级加载了 libssl.so.3,那么函数符号偏移一旦对不上,崩溃几乎是必然的。

检查 libpq 依赖的 OpenSSL 也很简单:

ldd /opt/gaussdb/app/lib/libpq.so.5 | grep ssl

如果发现 libpq 和 psycopg_c 各自依赖不同版本的 libssl,或者某个依赖路径根本不存在,那这一条基本就能定责。实际生产环境里,OpenSSL 多版本共存很常见,尤其在那种“不敢动老环境”的服务器上。

4. 在 ARM64 上重新编译并绑定正确的 libpq

4.1 准备编译环境

问题定位清楚之后,修复思路就明确了:在 ARM64 环境上,直接用 GaussDB 配套的客户端库源码编译 psycopg3,让 C 扩展和 libpq 的版本、架构完全一致。

首先确认编译工具齐全。源码编译 psycopg-c 需要 gcc、make,以及 Python 头文件。

gcc --version make --version python3-config --include

然后找到 GaussDB 客户端库自带的 pg_config。这个 pg_config 决定了编译 psycopg-c 时去哪个目录找头文件和库文件。很多现场就栽在这里:系统装了 PostgreSQL 的 libpq-dev,结果 pg_config 指向的是 PostgreSQL 而不是 GaussDB,编译出来又接回了错误的 libpq。

检查一下 pg_config 输出的路径是不是你要的那一个:

pg_config --includedir pg_config --libdir

如果确认没问题,导出环境变量:

export PG_CONFIG=/opt/gaussdb/app/bin/pg_config

像这种依赖底层库的 C 扩展,我的原则是:编译时指定路径,不要依赖运行时环境变量碰运气。这会省掉后续大量的“为什么这台机器行、那台机器不行”的问题。

4.2 源码编译安装

在开始编译之前,先清掉旧包,避免残留的二进制包干扰现场。

pip uninstall -y psycopg psycopg-c psycopg-binary

然后强制从源码构建 C 扩展。注意--no-binary=psycopg-c的意思是让 pip 不要下载预编译好的 psycopg-c wheel,而是老老实实在本机编译:

pip install "psycopg[c]" --no-binary=psycopg-c

如果编译过程中报找不到libpq-fe.h,说明 pg_config 路径不对,或者 CPATH、LDFLAGS 没有指向正确位置。这种情况下可以手动补:

export CPATH=/opt/gaussdb/app/include export LDFLAGS="-L/opt/gaussdb/app/lib -Wl,-rpath,/opt/gaussdb/app/lib" pip install "psycopg[c]" --no-binary=psycopg-c

编译完成后,第一时间验证 C 扩展是否真的链到了 GaussDB 的 libpq:

python -c "import psycopg, psycopg_c; print(psycopg.__version__); print(psycopg_c.__file__)" ldd $(python -c "import psycopg_c; print(psycopg_c.__file__)") | grep -E 'pq|ssl'

看到libpq.so.5 => /opt/gaussdb/app/lib/libpq.so.5的时候,基本可以放心一半。

4.3 运行期动态库路径别踩雷

编译链接成功并不等于运行期一定能找到。如果 GaussDB 的 libpq 目录不在系统默认搜索路径里,运行 Python 时依然会加载失败或加载错库。

最直接的验证方式:

/opt/app/venv/bin/python -c "import psycopg; psycopg.connect('host=127.0.0.1 dbname=postgres user=app_user password=xxx connect_timeout=5')"

如果这里报libpq.so.5: cannot open shared object file,那就是运行时路径的问题。给 Python 进程设置:

export LD_LIBRARY_PATH=/opt/gaussdb/app/lib:$LD_LIBRARY_PATH

但我不太建议在全局环境变量里加这个路径。一台生产机器上可能同时有多个应用,全局指定可能把别家应用的 libpq 也带偏。更稳妥的做法是写进服务的 systemd unit 里,只对当前服务生效。

示例:

[Service] Environment=LD_LIBRARY_PATH=/opt/gaussdb/app/lib ExecStart=/opt/app/venv/bin/python /opt/app/sync.py

这样做的好处是,路径关系被固化在服务配置里,任何人部署都能看到,不会出现“我这里能跑,你那不行”的玄学。

4.4 回归验证不能省

修复之后,不能只跑一个 select 1 就算完。coredump 类问题经常是偶发的,必须做一轮完整的回归。覆盖连接、SSL、参数化查询、事务、并发、连接关闭等路径。

我当时用一个并发小脚本压了一遍:

from concurrent.futures import ThreadPoolExecutor import psycopg DSN = "host=127.0.0.1 dbname=postgres user=app_user password=xxx sslmode=require" def work(i): with psycopg.connect(DSN) as conn: with conn.cursor() as cur: cur.execute("select %s, version()", (i,)) return cur.fetchone() with ThreadPoolExecutor(max_workers=8) as pool: results = list(pool.map(work, range(200))) print(len(results))

这里有一个容易被忽视的点:连接对象不要跨线程共享。psycopg3 的连接对象不是线程安全的,多线程场景应该用线程池各自建连接,或者配合连接池模块。很多并发场景下的 coredump,其实不是环境问题,而是连接对象在多线程间传递导致 C 扩展内部状态被破坏。

5. 常见问题速查与排查心得

5.1 问题速查表

把这次排查中遇到的典型问题整理成一张表,后续再遇到可以直接对照。

现场现象可能原因优先处理方式
import 后报libpq.so.5: cannot open shared object file运行时找不到 libpq设置 LD_LIBRARY_PATH,或重新编译时指定 rpath
一 connect 就段错误,栈顶在 libpq 里加载了错误版本的 libpqldd 确认链接路径,统一到 GaussDB 客户端库
import 正常,执行某些查询时报段错误C 扩展架构不匹配file 和 readelf 检查 .so 的 Machine 字段
栈顶在 SSL_read / SSL_writeOpenSSL 多版本共存检查 libpq 链接的 libssl,统一版本
多线程并发连接偶发崩溃连接对象跨线程共享每线程独立建连,或使用连接池
core 文件完全找不到系统没开 core dumpulimit -c unlimited,检查 core_pattern

这条表看着简单,但每一条背后都是真实环境的眼泪。尤其“架构不匹配”这条,我曾经见过一个团队把它当成业务代码 bug 查了整整两天。

5.2 值得养成的三个排查习惯

第一个习惯是顺序固定:先file,再ldd,最后才gdb。很多人一听说 coredump 就直接开 gdb,结果栈看了半天发现二进制压根就是错架构的,白白浪费时间。file 和 ldd 各一条命令,十几秒就能判断方向。

第二个习惯是保留最小复现脚本。排查问题的过程中一定会写很多临时脚本,别删,挑一个最简的版本留在仓库里。以后升级 Python、升级 psycopg、升级数据库客户端库,都先跑一遍这个脚本。它能帮你把问题拦截在测试环境,而不是线上半夜三点。

第三个习惯是学会管理 core 文件。调试信息不全的时候,core 文件几乎等于废纸。所以生产环境要记录当时的 binary 版本,最好连同编译参数一起归档。gdb 打开 core 后看到??不可读,往往就是因为 binary 和 core 不是同一份产物。

5.3 这次排查得到的几点经验

ARM64 离线环境部署 psycopg3,尽量不要从 x86_64 机器拷贝 venv 或者 site-packages。Python 的纯 Python 包可以随便拷,但 C 扩展是编译产物,架构不对就是一颗定时炸弹。

GaussDB 的 Python 驱动连接,如果厂商提供了配套客户端库,优先用它自己的 libpq,不要顺手拿 PostgreSQL 的 libpq 顶替。接口虽然兼容,底层实现不完全一样,平时没事不代表高并发和 TLS 握手时也没事。

最后分享一个定位技巧:如果 gdb 栈里看到的地址非常奇怪,比如指向 0x0,或者函数名全是??,先别急着深挖逻辑,回头检查架构和库路径。这一类“表面看不懂”的崩溃,绝大多数是二进制层面出了问题,而不是代码逻辑写错了。

排查这类问题,我现在已经养成了条件反射:听到 coredump,第一句问架构对不对,第二句问是不是拷贝的 venv,第三句才问崩溃栈。这个顺序帮我省了太多时间。如果你也在 ARM64 上跑 psycopg3,建议把上面的命令存一份,下次遇到问题照着排查,比临时查文档要快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询