当前位置:首页>python>Python扫MySQL百万大表,断连的锅在代码不在数据库

Python扫MySQL百万大表,断连的锅在代码不在数据库

  • 2026-10-25 18:08:20
Python扫MySQL百万大表,断连的锅在代码不在数据库
凌晨两点,后台任务的报警把我震醒:2013,Lost connection to MySQL server during query。重跑一次,好了,接着睡。第二天凌晨,又断。

那阵子我在做政务系统的数据同步,Python + SQLAlchemy + PyMySQL,扫的是百万级的 MySQL 大表。断连反反复复折腾了好几轮,最后发现一件事:锅不在数据库,在我们自己写的代码里。

下面这些,是三个项目踩完坑之后我记下来的东西。你可以直接拿去自查。

图注:数据管道断在凌晨——MySQL断连的典型瞬间

▍01 一条铁律,先记下来

扫描和处理,必须分开。先把结果集快速读完、释放连接,再去内存里处理。处理的时候,手里不握任何 MySQL 连接。

听起来简单?但我们翻车的每一处,都是违反了它:边读边处理、处理时把连接攥在手里、generator 惰性迭代做耗时空转。百万级数据量下,踩一条崩一条。

▍02 断连之前,先分清三种超时

报错不可怕,可怕的是分不清谁在超时。

wait_timeout,默认 8 小时,管的是空闲连接。闲置太久被服务端回收,下次取出来用就报 2006,MySQL server has gone away。

net_write_timeout,默认只有 60 秒,管的是服务端发数据、你收得太慢。fetchone 读到一半断掉的 2013,多半是它。

net_read_timeout,默认 30 秒,方向反过来,是服务端等客户端发数据。

这里有个高频误判:很多人一看 2013,扭头就去调大 wait_timeout。没用。2013 during query 卡的是 net_write_timeout,两个参数根本不挨着。参数没毛病,有毛病的是代码架构。

图注:三种 timeout,三种命运——别再傻傻分不清

▍03 三种写法,三种死法

最先中招的,是 SQLAlchemy 的 generator 配 yield_per。文档写得像流式,但在 PyMySQL 上它不是真正的服务端游标,行还是先缓存进客户端。内存照涨,连接照攥,两头都没占到便宜。

第二个坑更隐蔽。有人换成了 SSCursor,服务端游标,内存确实稳了——结果在读取循环里 submit 慢任务,用 wait 卡并发。并发一堵,fetchone 就停,停满 60 秒,服务端直接挂断。SSCursor 只管内存,不管你处理时还握着连接这件事。

还有一个坑埋在连接池:不设 pool_pre_ping,取到一条早被回收的死连接,用的时候才炸;不设 pool_recycle,空闲连接越堆越多,最后 1040,Too many connections。

▍04 后来我们怎么改的

分两段。

第一段,老老实实用 SSCursor,在 with 块里一口气 fetchone 读完,装进 list,出块就释放连接。循环里只有读,没有别的。

第二段,连接还了,再开线程池在内存里跑。这一段里任务卡多久都无所谓,反正不碰数据库。

百万行、字段精简的话,内存大概两三百 MB,扛得住。连接池配上 pool_pre_ping=True,pool_recycle 压到服务端 wait_timeout 以下,查询做成幂等——中途挂了整轮重跑也安全。改完之后,那类报警再没响过。

图注:先读完再处理——两阶段架构根治方案

▍05 排查的时候,从报错倒推

fetch 途中报 2013,去查处理阶段是不是握着连接;

闲置之后报 2006,去查 pool_pre_ping 和 pool_recycle;

连接数 1040,去查池大小和 finally 里有没有释放。

至于 SSCursor——记住它只解决内存。内存和超时一起解决的,是两阶段。

▍06 送你一段提示词,直接粘给 AI

现在很多人写这类代码是让 AI 代劳的。但你不说清楚要求,它大概率给你生成一套"generator + yield_per 边读边处理"的标准答案——正是第一种死法。

下面这段,是我踩完坑之后整理的前置要求。让 AI 写扫描代码之前,直接整段粘进对话:

【MySQL 大数据量扫描硬性要求】

1. 禁止用 SQLAlchemy generator + yield_per/mappings 做惰性流式迭代并边迭代边处理;大表扫描必须用 PyMySQL SSCursor 真服务端游标,且必须两阶段:阶段 1 在 with 块内一次性 fetchone 循环读完入内存 list,连接释放后才 return;阶段 2 在内存里并发处理,不持有任何 MySQL 连接。

2. SSCursor 创建方式:s.connection() 完成 dialect 初始化后,用 raw_conn.cursor(pymysql.cursors.SSCursor) 显式创建;不要在 connect_args 里传 cursorclass。

3. 读取循环内禁止 submit 业务任务、禁止 wait/阻塞、禁止任何远程调用,保持纯读。

4. 连接池必须设 pool_pre_ping=True,pool_recycle 低于服务端 wait_timeout。

5. 说明内存占用估算(列要精简),并配套幂等设计:已处理行可被过滤,失败可重试。

6. 远程跨网络访问 MySQL 时,注意 net_write_timeout(默认 60 秒)陷阱,不要靠调大服务端 timeout 掩盖架构问题。

(长按可复制整段)

把这段存下来。下次让 AI 写扫描代码之前先粘上,比断连之后连夜救火省心多了。

下次再被凌晨的断连报警震醒,先别骂数据库,回头看看自己的读取循环。

最新文章

随机文章