ProxySQL Cluster Simulator 实战:用 jq 过滤器定位 writer hostgroup 中的服务器

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

ProxySQL Cluster Simulator 实战:用 jq 过滤器定位 writer hostgroup 中的服务器

导读

在 ProxySQL 的 cluster_simulator 测试体系中,payload(载荷文件)以 JSON 形式完整描述 mysql_servers、mysql_replication_hostgroups、监控探测结果以及 ProxySQL 期望的初始/最终状态。随着测试场景从单服务器扩展为多服务器 × 多 hostgroup,手工比对 payload 中动辄数十上百条服务器记录变得极其困难。本文以 servers_in_writer_hostgroups.jq 过滤器为核心,讲解如何用 jq 快速"只取出配置为 writer 的服务器",并延伸到 reader 过滤、去重语义、hostgroup 编号约定与 payload 校验等实战技能,读完可直接用于日常的 simulator payload 调试与测试结果核查。

关联文档与过滤器定位

本文对应的核心文档位于 servers_in_writer_hostgroups.md,其描述极简:

Show all the servers configured as writers at mysql_servers.

而真正承载逻辑的是与之同名的 servers_in_writer_hostgroups.jq 过滤器文件。在 payloads_processing 目录 下,官方为每一类常用分析场景都配套了"过滤器文件 + 说明文档"的组合,形成一套可复用的 jq 工具箱,成员包括:

为什么要"过滤出 writer 服务器"?

在 READ_ONLY 监控模式下,ProxySQL 依据后端服务器的 read_only 状态,自动把服务器在 writer_hostgroup 与 reader_hostgroup 之间移动(相关监控逻辑与字段定义可参见 readonly_utils.cpp、galera_utils.cpp、grouprep_utils.cpp)。payload 里的 mysql_servers 是用户初始配置的服务器列表,它可能同时包含 writer 与 reader 两种角色;而 proxysql_init_state / proxysql_final_state 则是 ProxySQL 在监控介入前后的期望状态。

此时会产生一个核心疑问:用户配置在 mysql_servers 里的服务器,到底哪些真的落在某个 writer_hostgroup 上?这直接决定:

  1. 测试断言是否正确:状态比对脚本(如 payloads_processing/README.md 中给出的差集过滤器)需要先拿到"应该出现在 writer hostgroup 的服务器集合",才能判断初始状态是否符合预期;
  2. hostgroup 移动逻辑是否可验证:只有先明确 writer 集合,才能核对 RO=0 → RO=1 之后服务器是否被正确移入 reader hostgroup(见 payloads_creation/README.md 中列的 transition 矩阵);
  3. 无效配置是否会被发现:例如示例 read_only_test_multiple_servers_multiple_hostgroups_invalid.json 这类"invalid"样例,正是需要靠过滤工具快速揪出配置与期望状态不一致之处。

逐行拆解 servers_in_writer_hostgroups.jq

过滤器全文如下:

.[0]
    | [.mysql_servers[] as $server
        | (.mysql_replication_hostgroups[] | if $server.hostgroup_id == .writer_hostgroup then $server else empty end)]
    | unique_by([.hostgroup_id, .hostname])
    | .[]

下面按管道顺序逐段解释:

1. .[0] —— 选中第一个测试场景

simulator payload 的顶层是一个数组,每个元素是一次独立的测试场景(可包含多个 test)。例如 simple_test_payload.json 顶层就是 [ {...} ]。.[0] 取出数组第一个场景对象,后续所有操作都限定在该场景内。若要遍历全部场景,可改成 .[] 并在下游自行组织输出结构。

2. 笛卡尔积式的双数组交叉过滤

[.mysql_servers[] as $server
    | (.mysql_replication_hostgroups[] | if $server.hostgroup_id == .writer_hostgroup then $server else empty end)]

这是整个过滤器的核心,需要理解 jq 的求值顺序:

  • .mysql_servers[] as $server 将 mysql_servers 中的每一条服务器记录依次绑定到变量 $server;
  • 对每个 $server,再展开 .mysql_replication_hostgroups[],对每一条复制组定义执行条件判断:
    • 若 $server.hostgroup_id == .writer_hostgroup,则输出这条 $server;
    • 否则输出 empty(不产生任何输出,等价于丢弃);
  • 由于 mysql_replication_hostgroups[] 展开在外层迭代之内,这里形成了服务器 × 复制组定义的交叉组合:一台服务器可能命中多条复制组的 writer_hostgroup(即同时是多个 writer hostgroup 的成员),此时同一台服务器会输出多次。

外层 [...] 把这些结果收集成一个数组,供后续去重处理。

3. unique_by([.hostgroup_id, .hostname]) —— 按角色维度去重

| unique_by([.hostgroup_id, .hostname])

一台服务器在同一 hostgroup 中只会有一条 mysql_servers 记录(hostname + port 唯一),但上述交叉过滤可能因为同一服务器命中多条复制组定义而产生重复输出。这里按 [.hostgroup_id, .hostname] 组合键去重,保留每个(hostgroup, hostname)组合的唯一条目。

值得注意:这里刻意没有把 port 纳入去重键。在 READ_ONLY 集群场景中,同主机名通常对应同一个 MySQL 实例(端口固定为 3306),按 hostgroup + hostname 去重既保证结果精简,又与原文档"以 hostname 为主"的语义一致。若你的 payload 中同一 hostname 存在多个端口实例,建议自行将键扩展为 [.hostgroup_id, .hostname, .port]。

4. .[] —— 打平数组,逐条输出

数组被展开为独立的 JSON 对象流,每行输出一个命中 writer hostgroup 的服务器记录。

实战运行:在真实 payload 上验证

用仓库自带的 READ_ONLY 多服务器多 hostgroup 示例来实际跑一遍:

cd test/deps/cluster_simulator
jq -c -f docs/payloads_processing/jq_filters/servers_in_writer_hostgroups.jq \
  docs/payloads_processing/examples/read_only_test_multiple_servers_multiple_hostgroups_invalid.json

输出(已按 -c 压缩为每行一条记录):

{"hostgroup_id":2271,"hostname":"127.1.1.11","port":3306,"comment":"mysql01"}
{"hostgroup_id":2271,"hostname":"127.1.1.12","port":3306,"comment":"mysql02"}
{"hostgroup_id":2271,"hostname":"127.1.1.13","port":3306,"comment":"mysql03"}
{"hostgroup_id":2273,"hostname":"127.1.1.12","port":3306,"comment":"mysql02"}
{"hostgroup_id":2275,"hostname":"127.1.1.11","port":3306,"comment":"mysql01"}
{"hostgroup_id":2275,"hostname":"127.1.1.12","port":3306,"comment":"mysql02"}
{"hostgroup_id":2275,"hostname":"127.1.1.13","port":3306,"comment":"mysql03"}
{"hostgroup_id":2277,"hostname":"127.1.1.12","port":3306,"comment":"mysql02"}
{"hostgroup_id":2279,"hostname":"127.1.1.12","port":3306,"comment":"mysql02"}
{"hostgroup_id":2279,"hostname":"127.1.1.13","port":3306,"comment":"mysql03"}

对照示例 payload 中的配置可以立刻验证语义:

  • mysql_servers 中配置了 5 个 writer hostgroup(2271、2273、2275、2277、2279)和多个 reader hostgroup(2272、2274、2276、2278、2280)对应的服务器;
  • 过滤器只保留了命中 writer_hostgroup 的记录:例如 127.1.1.11 出现在 2271 与 2275(两条 writer 定义)中,而它配置在 2274、2278(reader hostgroup)的记录被正确排除;
  • 同一服务器的重复输出(如 2271 中三次输出不同 hostname)说明它同时属于多个 writer hostgroup——这正是"服务器可以同时配置在多个 hostgroup"的体现(参见 payloads_creation/README.md 对单服务器多 hostgroup 场景的描述)。

再看一个细节:127.1.1.11 在 mysql_servers 中配置了 2271/2274/2275/2278 四个 hostgroup,其中 2271 与 2275 是 writer,2274 与 2278 是 reader。输出恰好只有 2271 与 2275 两条记录——去重 + 条件过滤共同保证了准确性。

过滤器家族:读者视角的对照与组合

理解了 writer 过滤,读者过滤 servers_in_reader_hostgroups.jq 几乎同构,仅把条件换成 .reader_hostgroup:

.[0]
    | [.mysql_servers[] as $server
        | (.mysql_replication_hostgroups[] | if $server.hostgroup_id == .reader_hostgroup then $server else empty end) ]
    | unique_by([.hostgroup_id, .hostname])
    | .[]

对同一示例运行可得到 2274/2278 中的 reader 记录(如 127.1.1.11 与 127.1.1.13 在 2274,127.1.1.11 在 2278)。两个过滤器组合起来,就能完整划分 mysql_servers 中"writer 侧"与"reader 侧"两个集合。

更进一步的变体还有:

  • init_state_writers_only.jq:交叉比 mysql_servers 与 proxysql_init_state,只保留在两个数据源中都停留在 writer hostgroup 的服务器(先按 writer_hostgroup 求交集,再排除同时出现在 reader_hostgroup 的服务器),用于核对"监控介入前用户配置与初始期望是否一致";
  • hostgroups_by_type_by_hostname.jq:按 hostname 与状态来源(proxysql_init_state / proxysql_final_state)反查某台服务器出现在哪些 reader/writer hostgroup,可直接观察 RO=0 → RO=1 时服务器从 writer 侧整体迁往 reader 侧的过渡过程;
  • mv_readers_to_writers.jq:利用"hostgroup_id 按顺序编排"的约定,对 proxysql_init_state 中每条记录执行 hostgroup_id += 1,从而把 writer 侧服务器批量"平移"为 reader 侧,是构造期望状态的便捷手段。

配套约定:hostgroup_id 的顺序编排

多过滤器依赖一个隐含约定:writer 与 reader 的 hostgroup_id 是成对连续编排的。以 read_only_test_multiple_servers_multiple_hostgroups.json 为例:

"mysql_replication_hostgroups": [
    { "writer_hostgroup": 2271, "reader_hostgroup": 2272, "check_type": "read_only", "comment": "" },
    { "writer_hostgroup": 2273, "reader_hostgroup": 2274, "check_type": "read_only", "comment": "" },
    { "writer_hostgroup": 2275, "reader_hostgroup": 2276, "check_type": "read_only", "comment": "" },
    { "writer_hostgroup": 2277, "reader_hostgroup": 2278, "check_type": "read_only", "comment": "" },
    { "writer_hostgroup": 2279, "reader_hostgroup": 2280, "check_type": "read_only", "comment": "" }
]

writer_hostgroup 与紧随其后的 reader_hostgroup 相差恒为 1。这正是 mv_readers_to_writers.jq 文档中"Since hostgroups_id are chosen to be sequential"一句所描述的事实:因为编号连续,所以 hostgroup_id += 1 就能把 writer 记录转换为等价的 reader 记录。理解这一约定,你才能判断 unique_by 的去重键与 +=1 之类的变换在什么前提下成立。

结合状态差集做完整校验

payloads_processing/README.md 给出了结合 exp_proxysql_state 与 act_proxysql_state 的差集过滤器,可用于核对模拟器输出与期望的差异:

# 找出期望状态中有、实际状态中缺失的服务器
jq --compact-output ".results[0] | (.exp_proxysql_state? - .act_proxysql_state?) | .[]" simulator_output.json

# 找出实际状态中有、期望状态中缺失的服务器
jq --compact-output ".results[0] | (.act_proxysql_state? - .exp_proxysql_state?) | .[]" simulator_output.json

实战中的典型工作流是:先用 servers_in_writer_hostgroups.jq 从 payload 中抽出 writer 集合,再用上述差集过滤器对比 simulator_output.json,从而定位"是否少了一台 writer"或"多了一台不该出现的 writer"。若 jq 已无法表达复杂的转换逻辑,可退回 payload.js 这个 NodeJS 模板,用完整的 JavaScript 能力处理 payload(JSON 本身即是合法 JavaScript,详见 payloads_processing/README.md)。

使用前提与注意事项

  1. 过滤器假设 payload 为数组结构:.[0] 依赖顶层是测试场景数组。仓库中的所有示例 payload(如 simple_test_payload.json)均符合这一结构;
  2. mysql_replication_hostgroups 必须存在:READ_ONLY 类 payload 通过该字段定义 writer/reader 对;若你的 payload 使用 mysql_galera_hostgroups(GALERA 模式,见 simple_test_payload.json)或 mysql_group_replication_hostgroups,字段名不同,需自行改写过滤器中的键名;
  3. 去重键不含 port:默认按 [.hostgroup_id, .hostname] 去重,适用于一主机一实例的场景;多端口实例需扩展去重键;
  4. writer 与 reader 编号连续:mv_readers_to_writers 类过滤器依赖"编号 +1 即为对应 reader"的约定,修改 hostgroup_id 编排后这些过滤器需要同步调整;
  5. jq 版本:unique_by、as $var 等语法在 jq 1.5+ 均可用,仓库文档默认使用系统 jq。

小结

servers_in_writer_hostgroups.jq 虽只有三行逻辑,却是理解 simulator payload 中"服务器—hostgroup—角色"三角关系的入口。它用 mysql_replication_hostgroups 的 writer_hostgroup 作为判定基准,通过交叉过滤与 unique_by 去重,把 mysql_servers 中所有 writer 角色服务器精确抽取出来,与 servers_in_reader_hostgroups.jq、init_state_writers_only.jq 等过滤器配合,即可完成从单服务器到多服务器 × 多 hostgroup 的任意 READ_ONLY 场景的配置核查与状态校验。下一次面对庞大的 simulator payload,不妨先跑一遍它,让 writer 集合一目了然。

登录后查看全文
proxysql