ProxySQL Cluster Simulator 初始状态清洗:init_state_rm_invalid_readers.jq 过滤器实战解析
ProxySQL Cluster Simulator 初始状态清洗:init_state_rm_invalid_readers.jq 过滤器实战解析
本指南深入剖析 ProxySQL Cluster Simulator 中用于清洗测试 payload 的 jq 过滤器 init_state_rm_invalid_readers.jq:它用于修复 proxysql_init_state 中"在 mysql_servers 里根本没有定义为 reader、却被误放到 reader hostgroup 的无效服务器条目",是 READ_ONLY 多主机组模拟场景下修正初始状态的必备工具。读完本文,你将掌握该过滤器的四阶段 jq 数据流、invalid payload 的典型结构特征,并能独立将其运用于 Cluster Simulator 测试 payload 的生成与修复。
为什么需要清洗 payload:Cluster Simulator 的测试输入从哪来
ProxySQL Cluster Simulator(源码位于 test/deps/cluster_simulator/README.md)是一个利用 ProxySQL 通过内部 SQLite 伪造服务器状态的机制、按 GALERA、READONLY、GROUP_REPLICATION 等模式模拟集群状态迁移的测试工具。它的输入输出都是 JSON payload,一个 payload 内包含 mysql_servers(配置给 ProxySQL 的服务器)、mysql_replication_hostgroups(读写主机组映射)、proxysql_init_state(期望的初始状态)、proxysql_final_state(变更后的期望状态)等成员。
由于 payload 会随测试规模迅速变大变复杂,仓库的 payload 处理说明 明确建议用工具化方式维护它们:用 jq 快速过滤分析、用 Node.js 做复杂的生成与改造。其中 jq_filters 目录存放了一批即取即用的过滤器,覆盖"展示读者/写者""迁移读者到写者""按主机名分组主机组""删除无效读者"等典型操作,init_state_rm_invalid_readers.jq 就是其中之一。
问题场景:invalid payload 里被"误放"的 reader
以示例文件 read_only_test_multiple_servers_multiple_hostgroups_invalid.json 为例,它模拟的是"多台服务器、多个 replication hostgroup、writer_is_also_reader: 1、三台服务器由 read_only=0 切换为 read_only=1"的场景。其核心配置如下:
- 5 组
mysql_replication_hostgroups:2271→2272、2273→2274、2275→2276、2277→2278、2279→2280(writer_hostgroup → reader_hostgroup),全部check_type: "read_only"; mysql_servers中只定义了部分主机组条目,例如127.1.1.11出现在2271/2274/2275/2278,127.1.1.12出现在2271/2273/2275/2277/2279,127.1.1.13出现在2271/2274/2275/2279——注意 reader hostgroup2272、2276、2280在mysql_servers中完全没有对应条目;proxysql_init_state却把2272:127.1.1.11、2272:127.1.1.13、2276:127.1.1.11、2276:127.1.1.13、2280:127.1.1.13都标成了ONLINE。
这些条目就是文档所说的"在 mysql_servers 中找不到定义、却被错误放置在 proxysql_init_state 的 readers":既然 ProxySQL 的 mysql_servers 表中根本没有这些主机组的服务器,初始状态就不应该出现它们,否则后续 verify 模式必然报 verification_error。过滤器的作用就是把这批无效条目从 proxysql_init_state 中剔除,产出可用的初始状态。
过滤器全貌:一条 jq 数据流里的四阶段
完整过滤器源码位于 init_state_rm_invalid_readers.jq,全文件不到 40 行,按 | 分为四个阶段,每一阶段都重建中间对象。
阶段一:提取三份原始数据
.[0]
| {
init_state: .proxysql_init_state,
replication_hostgroups: .mysql_replication_hostgroups,
readers: [
.mysql_replication_hostgroups[] as $hg
| .mysql_servers[]
| select(.hostgroup_id == $hg.reader_hostgroup)
] | map({hostgroup_id, hostname, port, status: "ONLINE", comment}),
exp_writers: [
.mysql_servers[] as $server
| .proxysql_init_state[] as $init_server
| .mysql_replication_hostgroups[]
| if $server.hostgroup_id == .writer_hostgroup and
$init_server.hostgroup_id == .writer_hostgroup
then $server
else empty end
]
}
.[0]:payload 顶层是数组,测试对象放在[0];readers:遍历所有mysql_replication_hostgroups,从mysql_servers中挑出hostgroup_id等于某个reader_hostgroup的服务器——即在mysql_servers中显式定义为读者的服务器,统一标注status: "ONLINE"并保留comment。这是后续"无条件保留"的合法读者集合;exp_writers:通过mysql_servers、proxysql_init_state、mysql_replication_hostgroups三方笛卡尔积,筛选出"hostgroup_id 等于某个 writer_hostgroup、且该 writer hostgroup 在初始状态中出现过"的服务器,即经初始状态验证过的写者。
在本例中,readers 得到 2274:127.1.1.11、2274:127.1.1.13、2278:127.1.1.11(mysql_servers 里仅有的三个 reader hostgroup 条目);exp_writers 得到 mysql_servers 中所有 writer hostgroup 条目(2271/2273/2275/2277/2279 的服务器)。
阶段二:推导"写者同时是读者"的候选集合
| {
init_state: .init_state,
replication_hostgroups: .replication_hostgroups,
readers: .readers,
exp_writers: .exp_writers,
matching_writers_readers: [
.exp_writers[] as $writer | .replication_hostgroups[] as $hg
| if $writer.hostgroup_id == $hg.writer_hostgroup
then $writer else empty end
| $writer.hostgroup_id = $hg.reader_hostgroup
] | map({hostgroup_id, hostname, port, status: "ONLINE", comment})
}
这一阶段把阶段一的 exp_writers 按 mysql_replication_hostgroups 映射:对每个写者,找到它所属的 writer_hostgroup,再把 hostgroup_id 改写为对应的 reader_hostgroup,得到 matching_writers_readers——即"如果 writer_is_also_reader 生效,这些 (reader_hostgroup, hostname) 组合会由写者转变而来"的候选集合。它表达的是:某台服务器虽然没被显式定义在 reader hostgroup,但理论上存在成为该读者主机组成员的路径。
阶段三:剔除"误放置"的读者,再并入显式读者
| [
.init_state[] as $init | [.matching_writers_readers[]
| select($init.hostgroup_id == .hostgroup_id and $init.hostname ==.hostname)]
| if length != 0 then empty else $init end
] + .readers
这是过滤器的核心判定逻辑:
- 遍历
proxysql_init_state的每条记录$init; - 在
matching_writers_readers中查找同hostgroup_id、同hostname的记录; - 如果命中则视为"无效/误放置的读者"丢弃(
empty),否则保留; - 清洗后的初始状态与
readers(显式定义的合法读者)拼接合并。
之所以"命中候选集合即删除":结合示例 payload 可知,2272:127.1.1.11 这类条目虽然在 matching_writers_readers 中存在(因为 mysql_servers 里有 writer 2271:127.1.1.11),但它在 mysql_servers 的 reader hostgroup 中并无任何显式定义——它只是基于 writer_is_also_reader 的推测产物,却被错误写进了初始状态。而 2274:127.1.1.11 这类条目既在 mysql_servers 中显式定义(属于 readers),又不匹配 matching_writers_readers(mysql_servers 中没有 writer 2273:127.1.1.11),因此被完整保留。
阶段四:按主机名与主机组去重输出
| unique_by([.hostname, .hostgroup_id]) | .[]
最后按 (hostname, hostgroup_id) 键去重,逐条输出结果。因为清洗后的 init_state 与 readers 可能有重叠(如 2274:127.1.1.11 两边都会出现),unique_by 保证每条 (hostgroup_id, hostname) 组合只出现一次,得到干净、无重复的初始状态流。
运行方式与输出解读
对示例 payload 执行(在 jq_filters 目录 下,或以仓库绝对路径引用过滤器与示例):
jq -c -f jq_filters/init_state_rm_invalid_readers.jq examples/read_only_test_multiple_servers_multiple_hostgroups_invalid.json
输出为 16 行紧凑 JSON(-c 紧凑模式),每行一条 {hostgroup_id, hostname, port, status, comment}:
{"hostgroup_id":2271,"hostname":"127.1.1.11","port":3306,"status":"ONLINE","comment":"mysql01"}
{"hostgroup_id":2273,"hostname":"127.1.1.11","port":3306,"status":"ONLINE","comment":"mysql01"}
{"hostgroup_id":2274,"hostname":"127.1.1.11","port":3306,"status":"ONLINE","comment":"mysql01"}
{"hostgroup_id":2275,"hostname":"127.1.1.11","port":3306,"status":"ONLINE","comment":"mysql01"}
{"hostgroup_id":2277,"hostname":"127.1.1.11","port":3306,"status":"ONLINE","comment":"mysql01"}
{"hostgroup_id":2278,"hostname":"127.1.1.11","port":3306,"status":"ONLINE","comment":"mysql01"}
{"hostgroup_id":2271,"hostname":"127.1.1.12","port":3306,"status":"ONLINE","comment":"mysql02"}
{"hostgroup_id":2273,"hostname":"127.1.1.12","port":3306,"status":"ONLINE","comment":"mysql02"}
{"hostgroup_id":2275,"hostname":"127.1.1.12","port":3306,"status":"ONLINE","comment":"mysql02"}
{"hostgroup_id":2277,"hostname":"127.1.1.12","port":3306,"status":"ONLINE","comment":"mysql02"}
{"hostgroup_id":2279,"hostname":"127.1.1.12","port":3306,"status":"ONLINE","comment":"mysql02"}
{"hostgroup_id":2271,"hostname":"127.1.1.13","port":3306,"status":"ONLINE","comment":"mysql01"}
{"hostgroup_id":2273,"hostname":"127.1.1.13","port":3306,"status":"ONLINE","comment":"mysql03"}
{"hostgroup_id":2274,"hostname":"127.1.1.13","port":3306,"status":"ONLINE","comment":"mysql03"}
{"hostgroup_id":2275,"hostname":"127.1.1.13","port":3306,"status":"ONLINE","comment":"mysql03"}
{"hostgroup_id":2279,"hostname":"127.1.1.13","port":3306,"status":"ONLINE","comment":"mysql03"}
将输入 proxysql_init_state(21 条)与输出对比,被剔除的正是 5 条无效 reader:
| 被删除的条目 | 原因 |
|---|---|
2272:127.1.1.11 |
reader hostgroup 2272 在 mysql_servers 中无定义 |
2272:127.1.1.13 |
reader hostgroup 2272 在 mysql_servers 中无定义 |
2276:127.1.1.11 |
reader hostgroup 2276 在 mysql_servers 中无定义 |
2276:127.1.1.13 |
reader hostgroup 2276 在 mysql_servers 中无定义 |
2280:127.1.1.13 |
reader hostgroup 2280 在 mysql_servers 中无定义 |
同时可以验证保留逻辑的正确性:writer hostgroup(2271/2273/2275/2277/2279)的条目原样保留;2274:127.1.1.11、2274:127.1.1.13、2278:127.1.1.11 因在 mysql_servers 中显式定义而保留;输出中不含任何 2272/2276/2280 的条目。清洗后的状态与实际 mysql_servers 定义完全对齐,可直接回填为新的 proxysql_init_state。
与其他过滤器的配合:payload 处理工具箱
jq_filters 目录是一组互补的清洗/分析工具(各过滤器均附 .md 说明文档):
- all_init_state_readers.jq:列出
proxysql_init_state中所有属于 reader hostgroup 的服务器,用于核对"初始状态里有哪些读者"; - all_init_state_writers.jq:对称地列出所有写者;
- mv_readers_to_writers.jq:把读者条目迁移为写者条目,适合构造角色翻转场景;
- servers_in_reader_hostgroups.jq / servers_in_writer_hostgroups.jq:按读写主机组抽取服务器;
- hostgroups_by_type_by_hostname.jq:按
$type(writer/reader)与$hostname两个参数化变量过滤指定主机组下的服务器。
实际使用中,可以先跑 all_init_state_readers.jq 找出初始状态里所有读者,再对照 mysql_servers 确认哪些属于"无效读者",最后用 init_state_rm_invalid_readers.jq 一键清洗。完整可参考 payloads_processing 的 README,其中还给出了通用的状态差异比对过滤器:
jq --compact-output ".results[0] | (.exp_proxysql_state? - .act_proxysql_state?) + (.act_proxysql_state? - .exp_proxysql_state?) | .[]" simulator_output.json
它用于对比模拟结果中期望状态与实际状态的差集,快速定位"缺了哪台服务器"。
扩展到 READ_ONLY 测试 payload 的实战链路
仓库中真实的 READ_ONLY 测试用例位于 test/deps/cluster_simulator/tests/readonly_tests_payloads,其中 read_only_test_multiple_servers_multiple_hostgroups.json 等便是此类清洗后产出的合法 payload。完整的开发流程(详见 Cluster Simulator README):
- 编译对应模式:
make build_deps -j$(nproc) && make testreadonly -j$(nproc) && make build_tap_test -j$(nproc); - 启动 ProxySQL:
./src/proxysql --sqlite3-server -f -c ${CLUSTER_SIM_DIR}/configs/readonly_sim_conf.cnf -D ./readonly_sim_datadir; - 用
./cluster_simulator --mode simulate -f next_test.json模拟出proxysql_init_state/proxysql_final_state,若手工编写/生成的状态含误放置条目,先以init_state_rm_invalid_readers.jq清洗; - 将清洗后的状态回填 payload,再用
./cluster_simulator --mode verify -f next_test.json验证,得到{"err_type": "none", "err_msg": ""}即通过。
若清洗逻辑复杂到 jq 难以表达,payloads_processing 的 README 也建议退回到 Node.js:JSON 本身是合法 JavaScript,可用 node/payload.js 作为模板编写更可读的生成与改造程序。无论是 jq 还是 Node,核心原则一致:proxysql_init_state 必须严格与 mysql_servers 的实际定义对齐,任何凭空出现在 reader hostgroup 的服务器都会导致模拟验证失败,而 init_state_rm_invalid_readers.jq 正是消除这类数据污染的最小可靠工具。