MySQL客户端通过流式分片和协议帧封装接收查询结果:服务端逐行发送Column Definition帧和Row帧,客户端按MySQL Protocol规范逐帧解析,每帧以3字节长度+1字节序号标识边界,大字段可能被拆分为多个子帧。
SELECT 到本地变量的链路MySQL 不是“把结果集打包发过去”就完事——它用流式分片 + 协议帧封装的方式传输,客户端必须按 MySQL Protocol 规范逐帧解析。你看到的 mysql_fetch_row() 或 cursor.fetchall(),本质是在消费一个 TCP 流里的多个二进制帧。
Column Definition 帧 + 第一行 Row 帧packet length (3 bytes) + sequence ID (1 byte) 开头,客户端靠这个识别帧边界(不是靠换行或 JSON 分隔)TEXT/BLOB)可能被拆成多个 Row Data 子帧(MYSQL_TYPE_LONG_BLOB 场景下常见)fetchone()),服务端会在 socket send buffer 满后阻塞在 write(),触发 wait_timeout 断连mysql_real_connect() 后不发数据,但 mysql_query() 会卡住?因为 mysql_query() 是同步阻塞调用,它内部做了三件事:发送 query 帧 → 循环 recv 直到收到完整响应帧 → 解析并缓存元数据。卡住通常不是网络问题,而是服务端还没返回第一个帧——可能正在执行慢查询、锁等待,或结果集太大导致 send buffer 积压。
net_write_timeout(默认 60s),超时会断开连接并报错 Lost connection to MySQL server during query
SHOW PROCESSLIST 中看到状态为 Sending data,说明服务端仍在构造/发送结果,不是卡在网络EAGAIN/EWOULDBLOCK 并轮询,Python/Java 驱动已封装这一层驱动默认把整张结果集 load 到内存(如 MySQLdb 的 fetchall()、Go 的 rows.Scan() 全部展开),和 MySQL 服务端是否流式发送无关。真正控制内存的是客户端读取方式。
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false 启用 unbuffered 模式,fetch() 一次只拿一行,但要求必须读完否则连接会被 closesscursor = conn.cursor(MySQLdb.cursors.SSCursor),配合 fetchone() 流式读,避免 fetchall()
statement.setFetchSize(Integer.MIN_VALUE) 触发流式读(MySQL Connector/J 特有),否则即使 ResultSet 很大也全缓存在 driver heapSELECT id, content FROM articles WHERE created_at > '2025-01-01' LIMIT 1000000;
用 tcpdump -i lo port 3306 -w mysql.pcap 抓包后 Wireshark 打开,你会看到大量小包(通常 ≤ 16KB),每个包 payload 是一个或多个 MySQL protocol frame。关键不是看 SQL 文本,而是关注:
packet length 字段是否突增(比如从 32 字节跳到 8192 字节)→ 对应一个大 Row 的开始sequence ID 是否递增且无跳变 → 判断是否丢帧(MySQL 协议本身无重传,丢帧 = 连接异常中断)0xFF 开头的错误帧(如 0xFF 0x15 0x00 #HY000...)→ 表示服务端主动终止了这次 query 流程read_timeout 和 write_timeout 是两回事;所