几百个前端连接,后端也几乎 1:1:ProxySQL 的复用被什么关掉了
项目地址:sysown/proxysql
现象
应用侧几百个连接,stats_mysql_connection_pool 里后端连接数也差不多几百条,几乎 1:1。一个前端连接对应一条后端连接,复用等于没开。
MySQL 是 thread-per-connection,每个连接在后端占一个线程和一份会话内存。ProxySQL 自己做线程池、做连接复用,官方 FAQ 的说法是:数万个客户端连接可以用几百个后端连接撑住。所以 1:1 这个数字本身就是异常信号。
全局开关是 mysql-multiplexing,默认 true。注意它默认就是开的——全局开着仍然可能被别的原因关掉;反过来,全局关掉就永不复用。
复用的前提
复用成立的条件只有一个:后端连接在两次请求之间必须是无状态的。
一条后端连接刚服务完会话 A,被还回池里,下一个借到它的是会话 B。如果 A 在这条连接上留下了事务、表锁、用户变量、临时表、prepared statement,B 就会看到一个不属于它的世界。所以 ProxySQL 一旦检测到这类状态,就把这条会话钉在专用后端连接上,等状态消失再放开。
禁用条件按恢复方式分两类:可恢复的,和该连接生命周期内永久不恢复的。
可恢复的禁用条件
1. 活跃事务。 触发就是 BEGIN / START TRANSACTION,或者 autocommit=0 下对事务表的第一条查询。COMMIT 或 ROLLBACK 之后恢复。
2. 表锁。 LOCK TABLE ... / LOCK TABLES ... / FLUSH TABLES WITH READ LOCK,直到执行 UNLOCK TABLES 才恢复。
3. SQL_LOG_BIN=0。 直到设回 1 恢复。
SET SQL_LOG_BIN=0;
-- 期间该会话不复用
SET SQL_LOG_BIN=1;
4. mysql-auto_increment_delay_multiplex,默认 5。 1.4.14 引入。收到一个带 last_insert_id 的 OK 包之后,接下来 N 条查询禁用复用。官方说明:即使设成 0,SELECT LAST_INSERT_ID()、SELECT LAST_INSERT_ID() LIMIT 1、SELECT @@IDENTITY 也能被正确处理;只有当应用用别的方式依赖 LAST_INSERT_ID() 才需要大于 0,不用的话建议降到 0。
5. mysql-connection_delay_multiplex_ms,默认 0。 查询执行完后不立刻把后端连接还池,而是保持 N 毫秒禁用复用,让同一个前端连接的连续查询落在同一条后端上——典型的批量写入场景。这个变量只影响复用,不影响路由。
6. mysql-handle_warnings,默认 1。 产生 warning 后,在下一条查询到来前临时禁用复用。如果下一条正好是 SHOW WARNINGS 或 SHOW COUNT(*) FROM WARNINGS,就在同一条连接上执行,之后重新启用。例外是 SELECT @@warning_count——执行它会让该连接永久禁用复用,这一条要归到下一类。
永久的禁用条件
7. GET_LOCK()。 一经执行,该后端连接在生命周期内不再复用。
SELECT GET_LOCK('job_lock', 10);
8. query_digest 里含 @ 的语句——用户变量、会话变量。永不再开。维护者在 issue #4873 里的说明:用户自定义变量下复用和路由都必须禁用,因为客户端预期在其后的任何查询里都能找到这个变量;这不需要额外加规则去改,默认就是这个行为。配套的那条 Unable to parse unknown SET query ... Setting lock_hostgroup WARNING 对 SET @i 这类语句是预期行为,可以忽略。
同类的确定性命令还有一批,同样永久:
SET SQL_SAFE_UPDATES=?; -- mysql --safe-updates
SET SQL_SELECT_LIMIT=?;
SET MAX_JOIN_SIZE=?;
SET FOREIGN_KEY_CHECKS=...;
SET UNIQUE_CHECKS=...;
SET AUTO_INCREMENT_INCREMENT=...; -- 1.4.4+
SET AUTO_INCREMENT_OFFSET=...; -- 1.4.4+
SET GROUP_CONCAT_MAX_LEN=...; -- 1.4.4+
文档标注的例外:SELECT @@tx_isolation 和 SELECT @@version 是硬编码白名单,因为很多应用每条连接都要跑一遍。对应变量 mysql-keep_multiplexing_variables,默认值 trx_isolation,version,含义是「这些变量被查询时不导致禁用复用」,扩展它就能把更多 @@ 变量加进白名单。
9. SQL_CALC_FOUND_ROWS。 出现即永久禁用。
10. CREATE TEMPORARY TABLE。 永久禁用。
11. 文本协议 PREPARE。 指 SQL 文本形式的 PREPARE stmt FROM '...',不是二进制协议。永久禁用,而且没有关闭机制——之后执行 DEALLOCATE PREPARE 也不会恢复。文档明确标注这是已知限制。
12. 二进制协议 COM_STMT_PREPARE。 只要该会话有至少一条打开的 prepared statement,就绑定一条专用后端连接。恢复条件只有客户端用 COM_STMT_CLOSE 把全部语句关掉。没有自动超时;应用 prepare 完不 close,这条后端连接就被这个会话占着。
三处容易误判的地方
文本 PREPARE 的 DEALLOCATE 不复位。 上面第 11 条。看到 DEALLOCATE PREPARE 就以为状态清干净了,是错的。
连接归还池时,prepared statement 不会被拆掉。 这叫 lazy retention:后端连接回池,上面的 PS 仍然保留。下一个借到这条连接的客户端,在 global-to-backend id 映射仍有效时可以直接复用,省掉一次 COM_STMT_PREPARE。代价是后端连接上会累积 PS 状态。相关变量 mysql-max_stmts_cache,默认 10000,最小 1024,最大 1048576。
禁用复用不等于禁用路由。 执行 CREATE TEMPORARY TABLE 之后,同一张临时表上的 SELECT 如果被规则路由到另一个 hostgroup,会直接拿到 'schemaname.temporary_tablename' doesn't exist。官方建议这种场景用 query rules 把临时表相关的语句固定到同一 hostgroup。
autocommit=0 与 ORM 习惯
mysql-autocommit_false_is_transaction 默认 false。设为 true 时,autocommit=0 的连接会被当作事务处理,不回连接池。官方建议不要改这个变量。
issue #1009 里维护者解释过一件事:SET autocommit=0 本身并不开启事务,数据库层是在对事务表执行第一条查询时才真正开启。不少 ORM 和驱动连接一建立就发 SET autocommit=0,这个习惯很糟——会开出一个可能跑几小时的只读事务;而且如果把 autocommit=0 当成事务,读写分离就永远用不上。
多数会话变量不会被自动识别
Multiplexing 文档写明:多数 session 变量不会被自动处理。比如 SET TX_ISOLATION=? 不会禁用复用。所有客户端用同一个隔离级别、且是默认值时,这是特性;混着用就是 bug。
另外两个明确会禁用复用的写法,来自 mysql-default_transaction_read / mysql-default_tx_isolation 条目:
-- 不被支持,自动禁用复用
SET TRANSACTION READ WRITE;
SET TRANSACTION READ ONLY;
-- 必须写成
SET SESSION TRANSACTION READ WRITE;
SET SESSION TRANSACTION READ ONLY;
-- 不被支持,自动禁用
SET TRANSACTION ISOLATION LEVEL value;
-- 必须写成
SET SESSION TRANSACTION ISOLATION LEVEL value;
mysql-query_digests 必须保持开启。判定临时表、SQL_CALC_FOUND_ROWS、GET_LOCK 都依赖 digest。
定位到具体是哪条语句
从 v2 起,stats_mysql_processlist 带 extended_info 字段,是个 JSON:
SELECT extended_info FROM stats_mysql_processlist;
看 backend 部分的字段:
MultiplexDisabled: true—— 被全局mysql-multiplexing关掉了sql_log_bin: "0"—— 对应第 3 条status: {found_rows, get_lock, lock_tables, no_multiplex, temporary_table, user_variable}—— 任意一个为true就命中了对应的禁用条件server_status: 33——SERVER_STATUS_IN_TRANS是 MySQL 状态位的第一位(0x0001),事务活跃时该值为奇数last_errno: 0—— 非 0 时复用被禁用,直到回到 0autocommit: "OFF"—— 配合mysql-autocommit_false_is_transaction=true时视为事务MultiplexDisabled_ext—— 还会把「潜在事务」条件算进去
session 对象里 locked_on_hostgroup 不等于 -1,表示该会话被锁在某个 hostgroup,也就是复用已经禁用。
文档说明这份列表不是全集,其他全局变量也要查。
调优手段
mysql_query_rules.multiplex 三个取值:0 禁用复用,1 启用复用,2 表示「这条含 @ 的查询不要因此禁用复用」。
应用发 SET SQL_SELECT_LIMIT 时,两条规则一起用:
INSERT INTO mysql_query_rules (active,match_digest,multiplex) VALUES
('1','^SET SQL_SELECT_LIMIT=?',0),
(1,'^SET SQL_SELECT_LIMIT=DEFAULT',1);
应用发 SELECT @@max_allowed_packet,想避免禁用:
INSERT INTO mysql_query_rules (active,match_digest,multiplex) VALUES
('1','^SELECT @@max_allowed_packet',2);
顺带缓存这条查询:
INSERT INTO mysql_query_rules (active,match_digest,multiplex,cache_ttl) VALUES
('1','^SELECT @@max_allowed_packet',2,60000);
完全丢弃某条会导致禁用的 SET,直接返回 OK:
INSERT INTO mysql_query_rules (active,match_digest,OK_msg) VALUES
('1','^SET @@wait_timeout = ?','');
mysql-keep_multiplexing_variables 默认 trx_isolation,version。
还有一层是会话变量默认值机制:ProxySQL 维护一组 mysql-default_XXX 全局变量,比如 mysql-default_time_zone=SYSTEM。只有当客户端设的值与默认不同,才会往后端下发,以此提高复用率。常用的会话设置尽量对齐默认值。
OFFLINE_SOFT 下的残留连接
复用因为上面任一原因被禁用时,活跃连接会留在已经进入 OFFLINE_SOFT 的节点上,查询也继续发到那个节点。用连接池的应用要在故障切换前主动回收连接。
只有「活跃事务」这一种原因禁用时,事务结束后连接才会自动迁走,其余情况不会。
一个还没实测的点
文本协议 PREPARE 的永久禁用没有关闭机制,靠 DEALLOCATE PREPARE 也不行,理论上只能重建前端连接才能解除。但它在上层连接池(比如 HikariCP 这类)里被无感持有的时候,怎么在不停应用的前提下强制回收,我没有实测。二进制协议那边还有一个 mysql-max_stmts_cache 到顶之后的行为——是拒绝新的还是淘汰旧的——也没测。
参考: