ProxySQL Cluster Simulator 初始状态清洗:init_state_rm_invalid_readers.jq 过滤器实战解析

原创2026-10-08 21:34:062,294 阅读
文章标签:后端数据库负载均衡

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 hostgroup 2272、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

这是过滤器的核心判定逻辑:

  1. 遍历 proxysql_init_state 的每条记录 $init;
  2. 在 matching_writers_readers 中查找同 hostgroup_id、同 hostname 的记录;
  3. 如果命中则视为"无效/误放置的读者"丢弃(empty),否则保留;
  4. 清洗后的初始状态与 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 找出初始状态里所有读者,再对照 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):

  1. 编译对应模式:make build_deps -j$(nproc) && make testreadonly -j$(nproc) && make build_tap_test -j$(nproc);
  2. 启动 ProxySQL:./src/proxysql --sqlite3-server -f -c ${CLUSTER_SIM_DIR}/configs/readonly_sim_conf.cnf -D ./readonly_sim_datadir;
  3. 用 ./cluster_simulator --mode simulate -f next_test.json 模拟出 proxysql_init_state/proxysql_final_state,若手工编写/生成的状态含误放置条目,先以 init_state_rm_invalid_readers.jq 清洗;
  4. 将清洗后的状态回填 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 正是消除这类数据污染的最小可靠工具。

登录后查看全文
proxysql