feat(ansible): MySQL 交付剧本接入 GTID 异步主从复制

- mysql_expected_hosts 按 replica_count 推导预期节点数
- 主节点判定改为 mysql_primary_host 显式指定,IP 经 hostvars 回退解析
- 副本节点等待主节点端口就绪后执行 CHANGE REPLICATION SOURCE
  (defaults-extra-file 临时凭据文件 + SOURCE_AUTO_POSITION + GET_SOURCE_PUBLIC_KEY)
- 复制线程验证含 IO/SQL Running 与错误字段空值检查
  (锚点带 ^[[:space:]]* 适配 SHOW REPLICA STATUS\G 的字段名前导空格)
This commit is contained in:
Hungerdream
2026-07-31 18:12:15 +08:00
parent 0ca3af39dc
commit 69a12d778e
2 changed files with 139 additions and 17 deletions
+69 -8
View File
@@ -61,7 +61,13 @@
mgr_group_name: "{{ group_replication_group_name | default('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') }}"
# --- expected node count per topology ---
mysql_expected_hosts: "{{ {'standalone': 1, 'primary_replica': 2, 'mgr_3': 3}[topology | default('standalone')] }}"
mysql_expected_hosts: "{{ 1 if (topology | default('standalone')) == 'standalone' else ((replica_count | default(1) | int) + 1 if (topology | default('standalone')) == 'primary_replica' else 3) }}"
# 平台为 primary_replica 显式传入主从节点;standalone 仍按旧逻辑工作。
mysql_primary_host_value: "{{ mysql_primary_host | default(ansible_play_hosts_all[0]) }}"
mysql_primary_ip_value: "{{ mysql_primary_ip | default(hostvars[mysql_primary_host_value].ansible_host | default(mysql_primary_host_value)) }}"
# 交付只暴露 root 管理员;复制链路复用已配置好的 root@%。
mysql_replication_user: "root"
mysql_replication_password: "{{ mysql_root_password_value }}"
# --- secrets (prefer launch extra_vars; fall back to AWX credential-injected env) ---
mysql_root_password_value: "{{ mysql_root_password | default(lookup('ansible.builtin.env', 'XINFRA_MYSQL_ROOT_PASSWORD'), true) }}"
@@ -355,7 +361,7 @@
mysql_node_index: "{{ ansible_play_hosts_all.index(inventory_hostname) }}"
mysql_node_role: >-
{{ 'standalone' if topology == 'standalone' else
('primary' if ansible_play_hosts_all.index(inventory_hostname) == 0 else
('primary' if inventory_hostname == mysql_primary_host_value else
('replica' if topology == 'primary_replica' else 'mgr')) }}
# host-wide unique: platform may pass explicit mysql_server_id; otherwise derive
# port + node index (ports are unique per host in the pool model).
@@ -479,6 +485,64 @@
failed_when: false
no_log: true
- name: Wait for the replica to reach the primary SQL port
ansible.builtin.wait_for:
host: "{{ mysql_primary_ip_value }}"
port: "{{ mysql_port_value }}"
timeout: 60
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
- name: Configure and start GTID replication on the replica
ansible.builtin.shell:
cmd: |
set -euo pipefail
client_file="$(mktemp)"
sql_file="$(mktemp)"
trap 'rm -f "$client_file" "$sql_file"' EXIT
chmod 600 "$client_file" "$sql_file"
cat >"$client_file" <<'EOF'
[client]
user=root
password={{ mysql_root_password_value }}
socket={{ mysql_run_dir }}/mysql.sock
EOF
cat >"$sql_file" <<'EOF'
STOP REPLICA;
RESET REPLICA ALL;
CHANGE REPLICATION SOURCE TO SOURCE_HOST='{{ mysql_primary_ip_value }}', SOURCE_PORT={{ mysql_port_value }}, SOURCE_USER='{{ mysql_replication_user }}', SOURCE_PASSWORD='{{ mysql_replication_password | replace("'", "''") }}', SOURCE_AUTO_POSITION=1, GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;
EOF
/usr/bin/mysql --defaults-extra-file="$client_file" <"$sql_file"
executable: /bin/bash
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
no_log: true
- name: Verify GTID replication threads
ansible.builtin.shell:
cmd: |
set -euo pipefail
client_file="$(mktemp)"
trap 'rm -f "$client_file"' EXIT
chmod 600 "$client_file"
cat >"$client_file" <<'EOF'
[client]
user=root
password={{ mysql_root_password_value }}
socket={{ mysql_run_dir }}/mysql.sock
EOF
status="$(/usr/bin/mysql --defaults-extra-file="$client_file" -e 'SHOW REPLICA STATUS\G')"
grep -q 'Replica_IO_Running: Yes' <<<"$status"
grep -q 'Replica_SQL_Running: Yes' <<<"$status"
grep -qE '^[[:space:]]*Last_IO_Error:[[:space:]]*$' <<<"$status"
grep -qE '^[[:space:]]*Last_SQL_Error:[[:space:]]*$' <<<"$status"
executable: /bin/bash
register: mysql_replica_status
retries: 12
delay: 5
until: mysql_replica_status.rc == 0
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
no_log: true
- name: Notify healthcheck started
ansible.builtin.uri:
url: "{{ delivery_callback_url_value }}"
@@ -521,13 +585,10 @@
failed_when: false
no_log: true
- name: Note pending HA runtime orchestration
- name: Note pending Group Replication orchestration
ansible.builtin.debug:
msg: >-
topology={{ topology }} deployed with HA-ready config (GTID/binlog/relay/GR settings in place),
but replication wiring (CHANGE REPLICATION SOURCE) and Group Replication bootstrap
(START GROUP_REPLICATION) are not yet automated — nodes start config-ready only.
when: topology != 'standalone'
msg: "MGR remains unsupported; primary_replica GTID replication is configured and healthy."
when: topology == 'mgr_3'
run_once: true
- name: Notify platform registration handoff