1. 长驻线程里的 2013 报错,到底卡在哪
Django 项目用 pymysql 连 MySQL,跑一段时间后定时任务或常驻线程突然抛Lost connection to MySQL server during query,错误码 2013。这个报错本身不神秘,它说的是:你手里这个 TCP 连接,在真正发 SQL 的那一刻,已经被 MySQL 服务端单方面关掉了,客户端却还以为它活着。
MySQL 有个wait_timeout(默认常见 28800 秒,也就是 8 小时)和interactive_timeout,空闲超过这个时间的连接会被服务端回收。Web 请求场景下 Django 每个请求结束会走close_old_connections,连接被及时归还或重建,所以很少撞上。但定时任务、threading.Thread起的常驻线程、Celery worker 里的长循环,它们不经过请求生命周期,连接一挂就是几小时,下一次执行 SQL 时直接踩雷。
这篇要解决的就是这条链路:先讲清close_old_connections和连接池的机制,再给一份可复制的settings.json/config.toml骨架,把数据库连接配置和 TaoToken 统一 Key 的接入示例放在一起,最后用复现报错、验证连接恢复的具体动作,帮你建立一条稳定的排查路径。适合正在被 2013 折磨的 Django 后端,也适合想把 AI 调用和数据库配置统一管理的团队。
2. 先把 TaoToken 的 Key 和接入点准备好
排查这类问题经常需要一边翻日志、一边让模型帮你读 traceback、生成修复代码。如果每个工具都单独配一套 Key,管理成本会很高。TaoToken 的思路是给你一个统一的 Key,兼容 OpenAI 风格的接口,模型对话、编码 Agent、控制台管理都走同一个入口。
你需要先拿到 Key,再决定用在哪。三个入口按用途分:
- 模型对话(临时问报错、贴 traceback 让模型分析):https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- Coding Plan(长期在 IDE / Agent 里改 Django 代码):https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
- API Keys 管理(生成、轮换 Key):https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,API 基址是https://taotoken.net/api(这个地址不加 UTM 参数,直接用于代码里的 base_url)。控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。
注意:Key 只放在环境变量或本地配置文件里,不要提交进 Git。下面给的
config.toml骨架里,Key 字段留空,用环境变量注入。
3. 可复制的配置骨架:settings.json 与 config.toml
3.1 Django 侧 settings.json 片段
把数据库连接参数和连接健康检查相关的配置集中管理,避免散落在settings.py各处。下面这份settings.json是给配置加载器读的骨架,字段名按你的加载逻辑调整即可。
{ "database": { "ENGINE": "django.db.backends.mysql", "NAME": "your_db", "USER": "your_user", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": 3306, "CONN_MAX_AGE": 0, "OPTIONS": { "charset": "utf8mb4", "connect_timeout": 10, "read_timeout": 30, "write_timeout": 30 } }, "taotoken": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "model": "gpt-4o-mini" } }关键点解释:CONN_MAX_AGE设为 0 表示每个请求结束后关闭连接,这是 Web 请求场景下最省心的做法;但常驻线程不吃这套,所以还要配合close_old_connections。connect_timeout和read_timeout是 pymysql 支持的参数,能避免连接卡死时无限等待。
3.2 config.toml 骨架
如果你更习惯 TOML,这份等价:
[database] engine = "django.db.backends.mysql" name = "your_db" user = "your_user" password = "your_password" host = "127.0.0.1" port = 3306 conn_max_age = 0 [database.options] charset = "utf8mb4" connect_timeout = 10 read_timeout = 30 write_timeout = 30 [taotoken] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model = "gpt-4o-mini"3.3 在 settings.py 里加载并注入
import json import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent with open(BASE_DIR / "settings.json", "r", encoding="utf-8") as f: _cfg = json.load(f) DATABASES = { "default": { "ENGINE": _cfg["database"]["ENGINE"], "NAME": _cfg["database"]["NAME"], "USER": _cfg["database"]["USER"], "PASSWORD": _cfg["database"]["PASSWORD"], "HOST": _cfg["database"]["HOST"], "PORT": _cfg["database"]["PORT"], "CONN_MAX_AGE": _cfg["database"]["CONN_MAX_AGE"], "OPTIONS": _cfg["database"]["OPTIONS"], } } TAOTOKEN_BASE_URL = _cfg["taotoken"]["base_url"] TAOTOKEN_API_KEY = os.environ.get(_cfg["taotoken"]["api_key_env"], "")4. 让连接不再失效:close_old_connections 与 create_cursor 覆盖
4.1 方案一:在长驻任务前调用 close_old_connections
Django 的close_old_connections会遍历所有数据库连接,检查is_usable(),不可用就关闭,下次用的时候重新建。它不判断"空闲多久",只判断"当前是否还能用",所以放在每次数据库操作前调用最稳。
from django.db import close_old_connections def long_running_task(): while True: close_old_connections() # 这里再执行 ORM 操作 from myapp.models import Task Task.objects.filter(status="pending").update(status="running") time.sleep(60)如果你用的是 Celery,可以在task_prerun信号里统一挂上,避免每个任务都手写:
from celery.signals import task_prerun from django.db import close_old_connections @task_prerun.connect def _close_old_conn(**kwargs): close_old_connections()4.2 方案二:覆盖 create_cursor,让每次取 cursor 前 ping 一次
Django 所有 model 操作最终都会走到DatabaseWrapper.create_cursor。pymysql 的connection.ping(reconnect=True)会检测连接,断了就重连。把这两件事绑在一起,等于给每次查询加了一道保险。
在项目某个 App 的__init__.py或专门的db_patch.py里写:
import pymysql pymysql.version_info = (1, 4, 13, "final", 0) pymysql.install_as_MySQLdb() from django.db.backends.mysql.base import CursorWrapper, DatabaseWrapper from django.utils import asyncio def create_cursor(self, name=None): self.connection.ping(reconnect=True) cursor = self.connection.cursor() return CursorWrapper(cursor) DatabaseWrapper.create_cursor = asyncio.async_unsafe(create_cursor)然后在settings.py顶部或manage.py里 import 这个模块,确保补丁在 Django 初始化数据库前生效。实测下来,这个方案对定时任务和常驻线程都有效,代价是每次查询多一次 ping 往返,QPS 极高的场景要评估。
注意:
ping(reconnect=True)在连接已断时会重建连接,但重建后 Django 的连接状态需要同步,所以补丁要放在DatabaseWrapper层面,而不是自己另起一个连接。
4.3 连接池视角:什么时候该上池
CONN_MAX_AGE大于 0 时,Django 会复用连接,但复用的连接同样可能被服务端回收。如果你用django-db-connection-pool或 SQLAlchemy 池,要确认池的pool_recycle小于 MySQL 的wait_timeout,否则池里拿出来的还是死连接。一个常见配置是pool_recycle=28000,比默认wait_timeout=28800小一点,留出安全边界。
5. 验证请求:复现报错并确认连接恢复
5.1 先复现 2013
把 MySQL 的wait_timeout临时调小,方便快速复现:
SET GLOBAL wait_timeout = 10; SET GLOBAL interactive_timeout = 10;然后跑一个不调用close_old_connections的脚本:
import time import django import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") django.setup() from myapp.models import Task Task.objects.count() print("first query ok") time.sleep(15) Task.objects.count() print("second query ok")第二次查询大概率抛pymysql.err.OperationalError: (2013, 'Lost connection to MySQL server during query')。这就是你要的复现。
5.2 加上修复后再跑
在两次查询之间插入close_old_connections(),或者启用create_cursor补丁,再跑一遍。第二次查询应该正常返回,日志里能看到 pymysql 重连的痕迹。如果还是报错,检查补丁是否在django.setup()之前 import 生效。
5.3 用 TaoToken 辅助读 traceback
把完整 traceback 贴到模型对话里,让它帮你定位是哪一层连接失效:
import os from openai import OpenAI client = OpenAI( base_url="https://taotoken.net/api", api_key=os.environ["TAOTOKEN_API_KEY"], ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是 Django 数据库排障助手,只输出可执行的修复步骤。"}, {"role": "user", "content": "报错:pymysql.err.OperationalError: (2013, 'Lost connection to MySQL server during query'),场景是 Celery 长驻 worker。"}, ], ) print(resp.choices[0].message.content)返回结果会给出close_old_connections和ping(reconnect=True)的具体落点,和你上面手动做的对得上,相当于多一个交叉验证。
6. 本篇常见错排查
补丁没生效:create_cursor覆盖必须在 Django 加载数据库后端之前执行。放在settings.py里 import 最稳,放在manage.py里要注意 import 顺序。
ping 太频繁拖慢查询:每次取 cursor 都 ping,QPS 高时开销明显。折中做法是只在长驻任务入口调用close_old_connections,Web 请求路径不动。
CONN_MAX_AGE 设太大:设成 600 秒以上,但 MySQLwait_timeout只有 300 秒,连接在池里就死了。让CONN_MAX_AGE或pool_recycle小于wait_timeout。
多线程共享连接:Django 的数据库连接是线程局部的,但如果你手动把 connection 对象传给别的线程,会出问题。每个线程自己走 ORM,别共享 connection。
Key 相关报错:如果 TaoToken 调用返回 401,先确认TAOTOKEN_API_KEY环境变量已导出,再检查 base_url 是不是https://taotoken.net/api。Key 的生成和轮换在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入细节看 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
验证模型是否通:想快速确认 Key 能用,直接去模型对话页发一条消息,比写脚本快:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。
长期编码场景:如果你要在 IDE 或 Agent 里持续改 Django 代码,用 Coding Plan 更合适,不用每次手动贴 Key:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
最后留一个我踩过的坑:close_old_connections放在循环里调用时,别放在try/except的except分支里,否则连接已经坏了才去关,等于没关。放在每次数据库操作之前,才是正确姿势。