☰
ProxySQL 变量加载可见反馈设计:为 LOAD ... VARIABLES 命令增加 Records/Updated/Rejected/Unknown 统计
2026/10/9 10:06:51 网站建设 项目流程
  • 后端
  • 数据库
  • 负载均衡

【免费下载链接】proxysql

High-performance proxy for MySQL and PostgreSQL

项目地址:https://gitcode.com/gh_mirrors/pr/proxysql
点击查看免费下载

导读

本文围绕 ProxySQL 中LOAD MYSQL VARIABLES TO RUNTIME(以及 pgsql、admin、sqlite3-server、ClickHouse、LDAP、TSDB 等模块的同类命令)的一个经典痛点展开:当global_variables中某行变量的值无法通过模块侧校验时,管理端客户端只能看到Query OK, 0 rows affected,被拒绝的值在日志之外毫无提示地重置或删除。本文基于仓库中的设计文档(2026-06-14-mysql-variables-validation-feedback-design.md),结合 Admin_FlushVariables.cpp、proxysql_admin.h 与 Admin_Handler.cpp 的现行实现,完整梳理该问题的根因、FlushVariableStats返回值透传方案、OK 包info字段的格式化,以及对应的 TAP 回归测试设计。读完本文,你将能理解为何修改量极小却效果显著,并掌握在新版本 ProxySQL 中如何解读Records/Updated/Rejected/Unknown反馈、如何用mysql_info()编写自动化断言,以及该改动在协议、SQLite 结构与并发模型上的零侵入性。

问题背景:静默拒绝让运维"失明"

ProxySQL 的配置分三层:disk(持久化)、main(内存中的管理态)、runtime(实际生效态)。管理员通常先UPDATE global_variables修改内存态配置,再执行LOAD MYSQL VARIABLES TO RUNTIME使其生效。问题在于:LOAD命令在逐行调用模块的set_variable()校验时,如果某行值不合法(超出范围、格式错误),并不会向客户端返回任何错误。

issue #1288 给出了经典复现(mysql 客户端):

mysql> UPDATE global_variables SET variable_value='0' WHERE variable_name='mysql-max_connections'; Query OK, 1 row affected (0.00 sec) mysql> LOAD MYSQL VARIABLES TO RUNTIME; Query OK, 0 rows affected (0.00 sec) <-- no signal that '0' was rejected

此时错误只出现在ProxySQL_Admin的日志里:

[WARNING] Impossible to set variable max_connections with value "0". Resetting to current "102400".

同样的问题还存在于:

  • mysql-max_stmts_cache(issue #853);
  • pgsql、admin、sqlite3-server、ClickHouse、LDAP、TSDB 六个模块——它们共享同一条通用 flush 路径;
  • pgsql 模块还有一份几乎完全重复的独立循环副本,同样需要单独修复。

现有实现中的拒绝语义(源码佐证)

在 Admin_FlushVariables.cpp 的flush_GENERIC_variables__process__database_to_runtime中,逐行处理逻辑为:

  • 调用对应模块的set_variable()(mysql 走GloMTH->set_variable,admin 走set_variable,sqliteserver 走GloSQLite3Server->set_variable等);
  • 若返回false且replace为真:
    • 若模块能取到当前值val:proxy_warning记录"值非法,重置为当前值",并执行INSERT OR REPLACE INTO global_variables ...把行重置为 runtime 当前值;
    • 若取不到val:区分三种情况——variables_to_delete_silently(如session_debug)静默删除并计数;variables_deprecated(如forward_autocommit)打印proxy_error后删除;其余情况(模块不认识的变量名)打印proxy_warning后删除,并计入unknown;
  • 若set_variable()返回true:记录变量已更新,并处理variables_special_values特殊动作(如 mysql 的default_charset/default_collation_connection后处理)。

在整个旧实现中,这些计数都只落在日志里,客户端毫无感知——这正是"静默拒绝"的本质。

设计目标与方案选型

Goal

LOAD <module> VARIABLES TO RUNTIME执行后,管理端客户端能看到一条反馈,报告本次加载中"接受多少条、拒绝多少条",通过 MySQL OK 包的info字段返回。既有日志输出与global_variables的重置/删除语义保持不变。

Option 1:返回值透传(最终采用)

设计文档选定方案是向通用 flush helper 增加一个小的FlushVariableStats结构体,由各模块的 flush 包装函数与load_*_variables_to_runtime内联助手返回,最后由admin_handler_command_load_or_save格式化info字符串并交给既有的send_ok_msg_to_client。之所以"免费",是因为 MySQL_Protocol::generate_pkt_OK 与 PgSQL_Protocol::generate_ok_packet 本来就把第三个msg参数写入 OK 包的info字段,客户端渲染零成本。

被否决的两个备选:

  • Option 2(out 参数):不够 C++17 风格;proxysql_admin.h中内联的load_*_variables_to_runtime助手需要追加尾部引用参数,在头文件里很别扭;
  • Option 3(把统计暂存到GloAdmin):补丁最小,但引入了共享可变状态,为将来并发管理会话的场景必须加互斥锁保护,且数据流是隐式的。

数据结构:FlushVariableStats

该结构体已落地在 proxysql_admin.h:

struct FlushVariableStats { int records = 0; // 从 global_variables 中读到的本模块行数 int updated = 0; // set_variable() 返回 true 的行数 int rejected = 0; // 值越界 / 格式错误 / 只读(admin 模块)而被拒绝的行数 int unknown = 0; // 模块不认识的变量名(疑似陈旧磁盘文件)的行数 };

rejected与unknown拆分的设计意图:让用户一眼区分"变量名拼错/版本不支持"(unknown,多半是陈旧 disk 文件)与"值本身合法但模块不接受"(rejected)。admin 模块的只读变量拒绝(如version)也归入rejected——用户输入的值没错,只是模块不接受写入。

透传路径:从通用 helper 到 OK 包

通用路径(mysql、admin、sqliteserver、tsdb、clickhouse、ldap)

flush_GENERIC_variables__process__database_to_runtime返回类型改为FlushVariableStats,在每个分支分别执行stats.rejected++/stats.unknown++/stats.updated++。其调用点在 Admin_FlushVariables.cpp 中分别对应:

  • admin:flush_admin_variables___database_to_runtime(第 284 行)
  • mysql:flush_mysql_variables___database_to_runtime(第 478 行)
  • sqliteserver:flush_sqliteserver_variables___database_to_runtime(第 750 行)
  • tsdb:flush_tsdb_variables___database_to_runtime(第 773 行)
  • clickhouse:flush_clickhouse_variables___database_to_runtime(第 869 行,受PROXYSQLCLICKHOUSE编译开关保护)
  • ldap:flush_ldap_variables___database_to_runtime(第 1225 行)

这六个包装函数统一从void改为返回FlushVariableStats。flush_mysql_variables___database_to_runtime中default_charset/default_collation_connection的后处理逻辑保持不变,通用调用的返回值原样向上冒泡。

头文件中的内联助手同样返回结构体(proxysql_admin.h):

FlushVariableStats load_mysql_variables_to_runtime(const std::string& checksum = "", const time_t epoch = 0) { return flush_mysql_variables___database_to_runtime(admindb, true, checksum, epoch); }

ldap 与 pgsql 的对应助手分别位于 proxysql_admin.h 与 proxysql_admin.h。

PgSQL 路径:独立的重复循环

flush_pgsql_variables___database_to_runtime(Admin_FlushVariables.cpp)不走通用 helper,它有自己的逐行循环(SELECT substr(variable_name,7) vn, ... WHERE variable_name LIKE 'pgsql-%')。由于这份副本是独立演进的历史遗留,同样的改动要在此处单独应用一遍:包装循环、返回FlushVariableStats、经由proxysql_admin.h的load_pgsql_variables_to_runtime透传。其中对session_debug、forward_autocommit(deprecated,见 issue #3253)等特殊分支的计数逻辑与通用路径保持一致。

管理端处理器格式化

Admin_Handler.cpp 中 mysql 分支的处理:

if (is_admin_command_or_alias(LOAD_MYSQL_VARIABLES_FROM_MEMORY, query_no_space, query_no_space_length)) { ProxySQL_Admin* SPA = (ProxySQL_Admin*)pa; FlushVariableStats stats = SPA->load_mysql_variables_to_runtime(); proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded mysql variables to RUNTIME\n"); char info[160]; snprintf(info, sizeof(info), "Records: %d Updated: %d Rejected: %d Unknown: %d", stats.records, stats.updated, stats.rejected, stats.unknown); SPA->send_ok_msg_to_client(sess, info, 0, query_no_space); return false; }

同样的编辑应用于:

  • LOAD_PGSQL_VARIABLES_FROM_MEMORY(Admin_Handler.cpp);
  • LOAD LDAP VARIABLES TO RUNTIME(Admin_Handler.cpp)。

send_ok_msg_to_client本身不改,其第三个参数msg流入MySQL_Protocol::generate_pkt_OK(..., msg, ...)与PgSQL_Protocol::generate_ok_packet(..., msg, ...),最终写入协议 OK 包的info字段。mysql与psql命令行客户端会把该字段打印在Query OK, X rows affected的下一行,无需任何协议改动。

加载成功后的实际效果(mysql 客户端):

mysql> LOAD MYSQL VARIABLES TO RUNTIME; Query OK, 0 rows affected (0.00 sec) Records: 3 Updated: 1 Rejected: 2 Unknown: 0

保持不变的行为(兼容性契约)

  • 逐行的proxy_warning/proxy_error日志不变,运维脚本与日志采集器无需改动;
  • global_variables的replace语义不变:拒绝行仍重置为当前 runtime 值,未知行仍被删除;
  • 三个非管理端的load_mysql_variables_to_runtime调用点(ProxySQL_Admin.cpp 引导路径、ProxySQL_Cluster.cpp 校验和同步)忽略返回值,行为不变;
  • OK 包的affected_rows字段保持 0——它统计 SQLINSERT/UPDATE的行数,不是被设置的变量数;
  • LOAD ... VARIABLES FROM MEMORY/LOAD ... VARIABLES TO MEMORY别名走同一个is_admin_command_or_alias分支,用户看到相同反馈。

范围边界:哪些路径不纳入

设计文档明确划出 Out of Scope:

  • LOAD MYSQL VARIABLES TO MEMORY(disk 路径):不调用set_variable,不产生校验;
  • SET mysql-...快捷命令:走admin_handler_command_set另一条路径,已自行记录警告,且维护者明确拒绝了"逐 DML 校验"的提议(过于复杂);
  • SHOW WARNINGS集成:用户选择只做 info 反馈;
  • 日志中的完整信息(变量名、值、当前值)不复制进 OK 包:那会把单行撑到 1 KiB 以上,也不符合 mysql 客户端展示形态。

测试设计:TAP 回归测试

仓库中已落地两个测试文件:

  • test/tap/tests/reg_test_1288-load-mysql-variables-feedback-t.cpp
  • test/tap/tests/reg_test_1288-load-pgsql-variables-feedback-t.cpp

文件名遵循目录中既有 issue/回归测试的reg_test_NNNN-...-t.cpp约定。mysql 测试的核心思路(三个场景):

  1. 全部非法:UPDATE mysql-max_connections='0'、mysql-monitor_ping_interval='foo'、mysql-max_stmts_cache='1000'后LOAD,用mysql_info()解析响应 info,断言包含Records: 3 Updated: 0 Rejected: 2 Unknown: 1,并验证runtime_global_variables中被拒值已重置为 runtime 默认值;
  2. 全部合法:写入合法值(如mysql-monitor_ping_interval=2000)后LOAD,断言Records: 1 Updated: 1 Rejected: 0 Unknown: 0;
  3. 混合:一合法一非法,断言Records: 2 Updated: 1 Rejected: 1 Unknown: 0。

pgsql 测试(pgsql-max_connections='0'非法、'100'合法)单独覆盖,因为 pgsql flush helper 有自己的重复循环,独立测试可防止该副本回归。两个测试都在test/tap/groups/groups.json注册,走既有 TAP 基建(make build_tap_tests、run-tests-isolated.bash)。测试自行清理:恢复原始global_variables行并重跑LOAD,保证退出时集群状态已知。

测试代码片段(reg_test_1288-load-mysql-variables-feedback-t.cpp):

MYSQL_QUERY(admin, "UPDATE global_variables SET variable_value='0' WHERE variable_name='mysql-max_connections'"); MYSQL_QUERY(admin, "UPDATE global_variables SET variable_value='foo' WHERE variable_name='mysql-monitor_ping_interval'"); MYSQL_QUERY(admin, "INSERT OR REPLACE INTO global_variables(variable_name, variable_value) VALUES('mysql-bogus_var_xyz', '1')"); if (mysql_query(admin, "LOAD MYSQL VARIABLES TO RUNTIME")) { ... } const char* info = mysql_info(admin); bool has_rejected_2 = info && strstr(info, "Rejected: 2") != NULL; bool has_unknown_1 = info && strstr(info, "Unknown: 1") != NULL; ok(has_rejected_2 && has_unknown_1, "LOAD info includes 'Rejected: 2' and 'Unknown: 1'");

随后测试还校验runtime_global_variables中mysql-max_connections被重置为先前合法值('10000')而非'0',并在收尾时恢复原值、删除mysql-bogus_var_xyz并重跑LOAD。

兼容性与风险评估

设计文档给出的兼容性结论:

  • 无线上协议变更:OK 包的info字段本就是标准 MySQL/PostgreSQL 协议的一部分;忽略它的客户端(如mysql --batch、libmariadb 消费者)不受影响;
  • 无 SQL DDL / SQLite schema / admin 变量变更;不新增 mutex、不新增线程。

风险评估为Low:

  • 改动是纯增量的:一个返回值加一次snprintf,既有的逐行重置/删除语义保留,依赖静默拒绝的 admin 脚本在global_variables与runtime_global_variables中看到的终态不变;
  • 对客户端唯一可见的行为变化是Query OK之后多出Records: N Updated: X ...一行。用grep精确匹配"单独一行Query OK"的监控脚本需要小幅调整以兼容下一行——mysql与psql客户端本来就会打印这一行;
  • 格式串固定且人类可读,不引入需要文档化与解析的机器可读计数器。

结语

FlushVariableStats方案用约一个结构体、一次snprintf的代价,把"静默拒绝"变成管理端可见的即时反馈,同时严格保留了日志、重置/删除语义与协议兼容性。从 设计文档 到 flush 实现、管理端处理器 与 TAP 测试,整条链路在仓库中完整可查。对于 ProxySQL 运维与二次开发者,这不仅是一个具体的告警改进案例,也展示了该项目"以最小侵入换取最大可观测性"的工程取舍范式。

  • 后端
  • 数据库
  • 负载均衡

【免费下载链接】proxysql

High-performance proxy for MySQL and PostgreSQL

项目地址:https://gitcode.com/gh_mirrors/pr/proxysql
点击查看免费下载

相关推荐

上一篇:JavaScript Email Validation in 30 seconds of code: A Practical Guide to Syntax Checks and Beyond
下一篇:使用 Fisher-Yates 算法对数组洗牌——jstips 实用技巧深度解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询