注册
Django使用连接池异常退出解决方法
专栏/技术分享/ 文章详情 /

Django使用连接池异常退出解决方法

青空 2026/09/18 237 0 0
摘要

1. 背景与结论

1.1. 背景

客户在使用dmDjango连接池时出现了异常,报错如下

image.png

经过检验,能够在复现环境中复现客户“在libdmdpi中发生divide error,随后因SIGFPE退出”的错误,错误码和崩溃位置也一致(libdmdpi-d9dd6fd8.so 内固定偏移0x239EAE的除零停机)。

这是因为当前dmPython_pool连接池的设计是进程内池,一个连接池仅供一个进程使用,进程内多线程并发是安全的(互斥依靠 threading.Condition 实现,但内存锁只对同一进程内的线程有效)

而billiard在linux上创建子进程时默认使用fork方式,导致子进程原样复制来父进程的连接池和池内连接,子进程之间、子进程和父进程之间共用了同一个连接池,且互斥失效:并发使用时会出现协议应答错乱;

如果跨进程使用,某个进程归还连接或关闭连接池后,会导致其他进程的连接状态和服务端的会话状态冲突,驱动主动触发异常(对应的是除零操作,信号是SIGFPE)停机

一旦有一个进程注销了池里的连接或者整个池,服务端的会话就结束了,但其他进程仍持有“可用”状态的连接副本,继续使用或再次注销时。驱动检测到连接状态和服务端会话状态冲突,会主动触发异常停机,在libdmdpi-d9dd6fd8.so上对应的实现就是除零实现停机,所以会出现divide error

而观察市面上常见的关系型数据库(Oracle、PG及其衍生),大多都不保证连接池的多进程安全,均在文档中建议在fork后再建立连接池

1.2. 结论

所以建议做法是每个进程使用自己的连接池,连接池不要跨进程使用

在本案例中就是先fork建立子进程,不要继承父进程的连接池,在创建子进程之后各自建立各自的连接池。如果继承了父进程的连接池,则各子进程应该丢弃继承的连接池,在首次访问数据库时再各自重建自己的连接池

可以参考demo,在客户给的demo基础上进行了修改(还打开了连接池),经测试不会触发“在libdmdpi中发生divide error,随后因SIGFPE退出”的相同错误

2. 示例说明

2.1. 测试环境

复现测试环境如下:

组件 版本
操作系统 Kylin-Server-V10-SP3-2403 (x86_64)
达梦数据库 DM8
Python 3.10
Django 4.1.13
dmPython 2.5.38
dmPython_pool 1.0.0
dmDjango 3.0.10
libdmdpi 通过pip安装,libdmdpi-d9dd6fd8.so
billiard 4.2.4
celery 5.3.6

2.2. 修改内容

基于复现包dm_celery_core_repro修改,仅改动了两个文件:

  1. dm_celery_repro/celery.py:在worker_process_init信号(每个 fork 出的子进程启动时执行一次)的处理函数中,增加丢弃继承的连接池的逻辑:
# 修复: 子进程内保住继承池对象的引用, 防止 GC 触发 __del__ 注销父进程的连接 _inherited_pools: list = [] @signals.worker_process_init.connect def worker_process_init_handler(**_kwargs): # 修复: fork 出的子进程丢弃继承的连接池, 首次访问数据库时在本进程内重建。 # 步骤 1: 借出的连接归还继承池(close 对池化引擎只是还池, 不注销) # 步骤 2: 池对象先保引用再清空字典(不能调 close: close 会向父进程的 # socket 发注销包, 导致父进程会话失效甚至驱动停机) ……(和原来一样,略过) from django_dmpython_pool.dmDjango.base import DatabaseWrapper _inherited_pools.extend(DatabaseWrapper._connection_pools.values()) DatabaseWrapper._connection_pools.clear() logger.info( "worker_process_init: inherited pool discarded (kept %d ref(s)), will rebuild per-worker", len(_inherited_pools), ) except Exception: logger.exception("worker_process_init: failed to discard inherited pool")

image.png

image.png

需要注意,池对象必须先保存引用再清空字典,否则池对象被回收时会触发析构逻辑,向父进程的连接发送注销包,同样会引发崩溃;不能调用pool.close()或close_pool(),效果等同于向父进程的 socket 发送注销包。

  1. run_crash_mode.sh:增加连接池开关(DJANGO_DM_ENABLE_POOL_OPTIONS=true 及池容量参数),并将任务等待窗从 8 秒调整为 20 秒。

2.3. 测试方式

用法与原复现包用法一致,在复现包目录下设置运行环境变量后执行一键脚本即可,

cd /tmp/dm_celery_core_repro_pool_fixed export PATH=/Work/billiard/venv_demo/bin:$PATH export SECKER_DB_NAME=DAMENG SECKER_DB_USER=SYSDBA SECKER_DB_PASSWORD=Dameng123 export SECKER_DB_HOST=127.0.0.1 SECKER_DB_PORT=5237 export REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASS=dmrepro123 export PYTHONUNBUFFERED=4 sh run_crash_mode.sh

测试条件与原复现包的crash模式一致:父进程在fork每个子进程前预先打开达梦连接、保留任务执行前关闭连接的动作。且已启用连接池。

但建议在fork之后才打开连接,这里只是说明即使在fork前打开连接也能正常处理

  1. 按第 2.3 节运行一键脚本(本次验证使用 4 个 worker、投递 16 个任务)。
  2. 观察控制台与日志文件中的 worker 启动、任务执行和收尾过程。
  3. 通过 dmesg 检查内核是否记录新的异常。

2.3.1. 预期结果

修复后运行的判断标准如下(以4个并发、16个任务为例)

  1. 每个 worker 启动时输出两条 worker_process_init 日志,表明继承池已丢弃、将按进程重建:
[INFO/ForkPoolWorker-1] worker_process_init: Django DB connections closed
[INFO/ForkPoolWorker-1] worker_process_init: inherited pool discarded (kept 1 ref(s)), will rebuild per-worker
  • 有 4 条 inherited pool discarded(4 个 worker 都丢弃了继承池)
  • 有 4 条 DB probe ok … row=(1,) 且无一条失败
  • worker: Warm shutdown 正常收尾
  • 整个输出里没有 divide error、-70019、exited with signal
  1. 全部任务执行成功,每条任务输出 DB probe ok 且查询结果正确:
[INFO/ForkPoolWorker-1] [Celery_Task] DB probe ok task id=... iteration=0 row=(1,)
[INFO/ForkPoolWorker-1] Task probe.tasks.polling_offline_log_import_check[...] succeeded in 0.0029s
  1. worker 正常收尾(Warm shutdown),全程没有worker异常退出。
  2. 内核日志无新增记录(修复前同场景会记录 trap divide error … in libdmdpi-d9dd6fd8.so)。

2.3.2. 实际验证结果

测试结果是4个任务全部入队且全部执行成功,没有出现divide error、-70019、signal退出

观察项 修复前 修复后
worker进程 被信号8(SIGFPE) 杀死 全部正常存活
内核日志 divide error @ libdmdpi-d9dd6fd8.so偏移0x239EAE 无新增异常记录
主进程日志 -70019网络通讯失败 无异常
任务执行 中断或拿到错乱结果 全部执行成功、结果正确
修复后丢弃继承池的日志 无 每个 worker 一条 inherited pool discarded

说明通过“子进程丢弃继承的连接池,在首次访问数据库时再各自重建自己的连接池”的做法,成功在业务侧解决了“在libdmdpi中发生divide error,随后因SIGFPE退出”的问题

3. 附录

3.1. 参考材料

链接内的文字是对该链接的简要说明,冒号后面的英文是链接内的原文

  1. celery billard文档:billiard在linux上创建子进程时默认使用fork方式:On all non-Windows, non-macOS platforms the default remains fork via ForkContext. On macOS, _default_context is now initialized with SpawnContext, which routes process creation through popen_spawn_posix.Popen instead of popen_fork.Popen.
  2. PG数据库Python驱动的Github Issue:连接池属于进程内池,不能在多个进程之间共享:The pool is an in-process pool: you can instantiate one in a process and several threads or asyncio tasks can use it concurrently, but it cannot be shared across processes.
  3. Oracle Python驱动文档:连接池必须在同一个进程内创建、使用和关闭。如果在不同的进程之间共享连接池或连接,将会导致不可预测的行为。:Python-oracledb driver connection pools must be created, used, and closed within the same process. Sharing pools or connections across processes has unpredictable behavior.
  4. [Django Ticket#31637:在使用 multiprocessing.Pool() 或其他进程分叉相关 API 时,可能会遇到一些问题]When using multiprocessing.Pool() or other process-forking APIs, one might bump into “django.db.utils.OperationalError: SSL error: decryption failed or bad record mac ”or similar inconsistency errors in the child processes, since the socket is passed down into the forked process.
  5. Django Ticket#36957:多个工作进程会同时读写同一个套接字,从而破坏了 PostgreSQL 的通信协议:Django’s PostgreSQL backend stores psycopg_pool.ConnectionPool objects in a class-level dict (DatabaseWrapper._connection_pools). When gunicorn (or any pre-forking server) forks worker processes, all children inherit references to the same pool objects — and crucially, the same underlying TCP sockets to PostgreSQL. Multiple workers then read/write the same socket concurrently, corrupting the PostgreSQL wire protocol.

3.2. 运行结果文本

[root@localhost dm_celery_core_repro_pool_fixed]# export DM_REPRO_CONCURRENCY=4
[root@localhost dm_celery_core_repro_pool_fixed]# sh run_crash_mode.sh
enqueued index=0 task_id=add31502-3c13-46b0-8d64-fd97b0ced2aa queue=dm_repro_crash
enqueued index=1 task_id=58ec37bf-b660-44e4-b77d-3774cc7e9b7b queue=dm_repro_crash
enqueued index=2 task_id=0d359053-5338-4944-b792-fb58f3f85ff5 queue=dm_repro_crash
enqueued index=3 task_id=4afa2cde-98d2-45b0-91fe-7b05c66852b4 queue=dm_repro_crash
/Work/billiard/venv_demo/lib/python3.10/site-packages/celery/platforms.py:829: SecurityWarning: You're running the worker with superuser privileges: this is
absolutely not recommended!

Please specify a different user using the --uid option.

User information: uid=0 euid=0 gid=0 egid=0

  warnings.warn(SecurityWarning(ROOT_DISCOURAGED.format(

 -------------- celery@prefork_dm_repro_crash_4 v5.3.6 (emerald-rush)
--- ***** -----
-- ******* ---- Linux-4.19.90-89.11.v2401.ky10.x86_64-x86_64-with-glibc2.28 2026-09-18 11:00:56
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app:         dm_celery_repro:0x7f0cd0c888e0
- ** ---------- .> transport:   redis://:**@127.0.0.1:6379/0
- ** ---------- .> results:     disabled://
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
 -------------- [queues]
                .> dm_repro_crash   exchange=dm_repro_crash(direct) key=dm_repro_crash


[tasks]
  . probe.tasks.polling_offline_log_import_check

[2026-09-18 11:00:56,263: INFO/MainProcess] worker_before_create_process: parent opened Django DB connection row=(1,)
[2026-09-18 11:00:56,314: INFO/MainProcess] worker_before_create_process: parent opened Django DB connection row=(1,)
[2026-09-18 11:00:56,326: INFO/ForkPoolWorker-1] worker_process_init: Django DB connections closed
[2026-09-18 11:00:56,327: INFO/ForkPoolWorker-1] worker_process_init: inherited pool discarded (kept 1 ref(s)), will rebuild per-worker
[2026-09-18 11:00:56,393: INFO/MainProcess] worker_before_create_process: parent opened Django DB connection row=(1,)
[2026-09-18 11:00:56,403: INFO/ForkPoolWorker-2] worker_process_init: Django DB connections closed
[2026-09-18 11:00:56,404: INFO/ForkPoolWorker-2] worker_process_init: inherited pool discarded (kept 1 ref(s)), will rebuild per-worker
[2026-09-18 11:00:56,467: INFO/MainProcess] worker_before_create_process: parent opened Django DB connection row=(1,)
[2026-09-18 11:00:56,481: INFO/ForkPoolWorker-3] worker_process_init: Django DB connections closed
[2026-09-18 11:00:56,481: INFO/ForkPoolWorker-3] worker_process_init: inherited pool discarded (kept 1 ref(s)), will rebuild per-worker
[2026-09-18 11:00:56,616: WARNING/MainProcess] /Work/billiard/venv_demo/lib/python3.10/site-packages/celery/worker/consumer/consumer.py:507: CPendingDeprecationWarning: The broker_connection_retry configuration setting will no longer determine
whether broker connection retries are made during startup in Celery 6.0 and above.
If you wish to retain the existing behavior for retrying connections on startup,
you should set broker_connection_retry_on_startup to True.
  warnings.warn(

[2026-09-18 11:00:56,624: INFO/ForkPoolWorker-4] worker_process_init: Django DB connections closed
[2026-09-18 11:00:56,625: INFO/ForkPoolWorker-4] worker_process_init: inherited pool discarded (kept 1 ref(s)), will rebuild per-worker
[2026-09-18 11:00:56,638: INFO/MainProcess] Connected to redis://:**@127.0.0.1:6379/0
[2026-09-18 11:00:56,640: WARNING/MainProcess] /Work/billiard/venv_demo/lib/python3.10/site-packages/celery/worker/consumer/consumer.py:507: CPendingDeprecationWarning: The broker_connection_retry configuration setting will no longer determine
whether broker connection retries are made during startup in Celery 6.0 and above.
If you wish to retain the existing behavior for retrying connections on startup,
you should set broker_connection_retry_on_startup to True.
  warnings.warn(

[2026-09-18 11:00:56,644: INFO/MainProcess] mingle: searching for neighbors
[2026-09-18 11:00:57,677: INFO/MainProcess] mingle: all alone
[2026-09-18 11:00:57,716: INFO/MainProcess] celery@prefork_dm_repro_crash_4 ready.
[2026-09-18 11:01:01,061: INFO/MainProcess] Task probe.tasks.polling_offline_log_import_check[add31502-3c13-46b0-8d64-fd97b0ced2aa] received
[2026-09-18 11:01:01,064: INFO/ForkPoolWorker-1] [Celery_Task] scheduled DB probe task id=add31502-3c13-46b0-8d64-fd97b0ced2aa repeat=1
[2026-09-18 11:01:01,065: INFO/ForkPoolWorker-1] [Celery_Task] closing all Django DB connections before query task id=add31502-3c13-46b0-8d64-fd97b0ced2aa
[2026-09-18 11:01:01,083: INFO/ForkPoolWorker-1] [Celery_Task] DB probe ok task id=add31502-3c13-46b0-8d64-fd97b0ced2aa iteration=0 row=(1,)
[2026-09-18 11:01:01,084: INFO/ForkPoolWorker-1] Task probe.tasks.polling_offline_log_import_check[add31502-3c13-46b0-8d64-fd97b0ced2aa] succeeded in 0.020270170003641397s: {'task_id': 'add31502-3c13-46b0-8d64-fd97b0ced2aa', 'repeat': 1}
[2026-09-18 11:01:01,084: INFO/ForkPoolWorker-1] task_postrun: probe.tasks.polling_offline_log_import_check [add31502-3c13-46b0-8d64-fd97b0ced2aa] executed in 0.0200s
[2026-09-18 11:01:01,130: INFO/MainProcess] Task probe.tasks.polling_offline_log_import_check[58ec37bf-b660-44e4-b77d-3774cc7e9b7b] received
[2026-09-18 11:01:01,132: INFO/ForkPoolWorker-1] [Celery_Task] scheduled DB probe task id=58ec37bf-b660-44e4-b77d-3774cc7e9b7b repeat=1
[2026-09-18 11:01:01,132: INFO/ForkPoolWorker-1] [Celery_Task] closing all Django DB connections before query task id=58ec37bf-b660-44e4-b77d-3774cc7e9b7b
[2026-09-18 11:01:01,134: INFO/ForkPoolWorker-1] [Celery_Task] DB probe ok task id=58ec37bf-b660-44e4-b77d-3774cc7e9b7b iteration=0 row=(1,)
[2026-09-18 11:01:01,134: INFO/ForkPoolWorker-1] Task probe.tasks.polling_offline_log_import_check[58ec37bf-b660-44e4-b77d-3774cc7e9b7b] succeeded in 0.002888534014346078s: {'task_id': '58ec37bf-b660-44e4-b77d-3774cc7e9b7b', 'repeat': 1}
[2026-09-18 11:01:01,134: INFO/ForkPoolWorker-1] task_postrun: probe.tasks.polling_offline_log_import_check [58ec37bf-b660-44e4-b77d-3774cc7e9b7b] executed in 0.0029s
[2026-09-18 11:01:01,191: INFO/MainProcess] Task probe.tasks.polling_offline_log_import_check[0d359053-5338-4944-b792-fb58f3f85ff5] received
[2026-09-18 11:01:01,193: INFO/ForkPoolWorker-1] [Celery_Task] scheduled DB probe task id=0d359053-5338-4944-b792-fb58f3f85ff5 repeat=1
[2026-09-18 11:01:01,193: INFO/ForkPoolWorker-1] [Celery_Task] closing all Django DB connections before query task id=0d359053-5338-4944-b792-fb58f3f85ff5
[2026-09-18 11:01:01,195: INFO/ForkPoolWorker-1] [Celery_Task] DB probe ok task id=0d359053-5338-4944-b792-fb58f3f85ff5 iteration=0 row=(1,)
[2026-09-18 11:01:01,195: INFO/ForkPoolWorker-1] Task probe.tasks.polling_offline_log_import_check[0d359053-5338-4944-b792-fb58f3f85ff5] succeeded in 0.0026033629837911576s: {'task_id': '0d359053-5338-4944-b792-fb58f3f85ff5', 'repeat': 1}
[2026-09-18 11:01:01,195: INFO/ForkPoolWorker-1] task_postrun: probe.tasks.polling_offline_log_import_check [0d359053-5338-4944-b792-fb58f3f85ff5] executed in 0.0025s
[2026-09-18 11:01:01,244: INFO/MainProcess] Task probe.tasks.polling_offline_log_import_check[4afa2cde-98d2-45b0-91fe-7b05c66852b4] received
[2026-09-18 11:01:01,246: INFO/ForkPoolWorker-1] [Celery_Task] scheduled DB probe task id=4afa2cde-98d2-45b0-91fe-7b05c66852b4 repeat=1
[2026-09-18 11:01:01,247: INFO/ForkPoolWorker-1] [Celery_Task] closing all Django DB connections before query task id=4afa2cde-98d2-45b0-91fe-7b05c66852b4
[2026-09-18 11:01:01,249: INFO/ForkPoolWorker-1] [Celery_Task] DB probe ok task id=4afa2cde-98d2-45b0-91fe-7b05c66852b4 iteration=0 row=(1,)
[2026-09-18 11:01:01,249: INFO/ForkPoolWorker-1] Task probe.tasks.polling_offline_log_import_check[4afa2cde-98d2-45b0-91fe-7b05c66852b4] succeeded in 0.003110619989456609s: {'task_id': '4afa2cde-98d2-45b0-91fe-7b05c66852b4', 'repeat': 1}
[2026-09-18 11:01:01,249: INFO/ForkPoolWorker-1] task_postrun: probe.tasks.polling_offline_log_import_check [4afa2cde-98d2-45b0-91fe-7b05c66852b4] executed in 0.0032s

worker: Warm shutdown (MainProcess)
[root@localhost dm_celery_core_repro_pool_fixed]#
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服