test: rehearse MySQL GTID manual promotion
This commit is contained in:
5
Makefile
5
Makefile
@@ -1,6 +1,6 @@
|
|||||||
GO ?= go
|
GO ?= go
|
||||||
|
|
||||||
.PHONY: fmt test vet build integration sdk-check protocol-check schedule-backlog
|
.PHONY: fmt test vet build integration mysql-replication sdk-check protocol-check schedule-backlog
|
||||||
fmt:
|
fmt:
|
||||||
$(GO) fmt ./...
|
$(GO) fmt ./...
|
||||||
cd sdk/go && $(GO) fmt ./...
|
cd sdk/go && $(GO) fmt ./...
|
||||||
@@ -29,6 +29,9 @@ build:
|
|||||||
integration:
|
integration:
|
||||||
./scripts/test-mysql.sh
|
./scripts/test-mysql.sh
|
||||||
|
|
||||||
|
mysql-replication:
|
||||||
|
./scripts/test-mysql-replication.sh
|
||||||
|
|
||||||
sdk-check:
|
sdk-check:
|
||||||
./scripts/test-sdk-isolation.sh
|
./scripts/test-sdk-isolation.sh
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ P4/P5定时等待与后续查询轮次的实现及回归记录见[时钟报告](
|
|||||||
5. 已补[`orbit-load`](api/load.md)有界受理负载工具及24次/1000次隔离样本,见[受理报告](reports/phase-6-load.md)。新增独立[`orbit-exec-load`](api/execution-load.md),通过真实Worker、任务结果和尝试历史核对,提供短/混合/中长任务的受理、排队、授权执行及完成耗时分布;具体环境、原始JSON和失败分母见[执行测量报告](reports/phase-6-execution-load.md)。新增[`orbit-workflow-load`](api/workflow-load.md),用本次发布版本的双分支并行流程,逐个核对不同任务、唯一尝试、来源、精确输出与本地调用;24/100实例及24实例截止负例已重新构建回归并绑定源码/二进制哈希,见[流程测量报告](reports/phase-6-workflow-load.md)。新增[`orbit-schedule-load`](api/schedule-load.md),已完成24/128次定时集中触发与24次计划量的截止负例,保存实际分母、唯一任务/尝试、来源与暂停收尾证据,见[定时测量报告](reports/phase-6-schedule-load.md)。Go 1.27.1 模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24个可运行 SDK 示例真实服务串跑和 result-inbox/task-outbox 真实业务集成已复跑通过。已补[长期积压与资源曲线样本](reports/phase-6-backlog.md):32×8 共256个触发点,在1个Worker/容量1和180秒截止下保留173个成功、83个未核验任务,并以独立数据库读取记录等待积压曲线;该单节点样本不替代目标拓扑容量。SDK受控重连风暴及[真实慢消费者背压](reports/phase-6-slow-consumer.md)已补边界回归;下一步补指标采集对照、长期存储增长、跨节点重连与目标数据库拓扑容量。
|
5. 已补[`orbit-load`](api/load.md)有界受理负载工具及24次/1000次隔离样本,见[受理报告](reports/phase-6-load.md)。新增独立[`orbit-exec-load`](api/execution-load.md),通过真实Worker、任务结果和尝试历史核对,提供短/混合/中长任务的受理、排队、授权执行及完成耗时分布;具体环境、原始JSON和失败分母见[执行测量报告](reports/phase-6-execution-load.md)。新增[`orbit-workflow-load`](api/workflow-load.md),用本次发布版本的双分支并行流程,逐个核对不同任务、唯一尝试、来源、精确输出与本地调用;24/100实例及24实例截止负例已重新构建回归并绑定源码/二进制哈希,见[流程测量报告](reports/phase-6-workflow-load.md)。新增[`orbit-schedule-load`](api/schedule-load.md),已完成24/128次定时集中触发与24次计划量的截止负例,保存实际分母、唯一任务/尝试、来源与暂停收尾证据,见[定时测量报告](reports/phase-6-schedule-load.md)。Go 1.27.1 模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24个可运行 SDK 示例真实服务串跑和 result-inbox/task-outbox 真实业务集成已复跑通过。已补[长期积压与资源曲线样本](reports/phase-6-backlog.md):32×8 共256个触发点,在1个Worker/容量1和180秒截止下保留173个成功、83个未核验任务,并以独立数据库读取记录等待积压曲线;该单节点样本不替代目标拓扑容量。SDK受控重连风暴及[真实慢消费者背压](reports/phase-6-slow-consumer.md)已补边界回归;下一步补指标采集对照、长期存储增长、跨节点重连与目标数据库拓扑容量。
|
||||||
6. 已补[隔离备份恢复演练](api/backup-restore.md):源库和恢复库均为一次性 MySQL 容器,Go 1.27.1 构建的当前二进制在逻辑备份恢复后核对迁移账本、核心记录数量、固定索引和零外键;证据见[恢复报告](reports/phase-6-backup-restore.md)。该演练不替代生产备份介质、复制切换、跨版本恢复或 RPO/RTO 验收。
|
6. 已补[隔离备份恢复演练](api/backup-restore.md):源库和恢复库均为一次性 MySQL 容器,Go 1.27.1 构建的当前二进制在逻辑备份恢复后核对迁移账本、核心记录数量、固定索引和零外键;证据见[恢复报告](reports/phase-6-backup-restore.md)。该演练不替代生产备份介质、复制切换、跨版本恢复或 RPO/RTO 验收。
|
||||||
7. 已补[A18 数据库短暂不可用恢复演练](api/db-recovery.md):Go 1.27.1 构建的当前二进制在数据库停止期间返回 readiness 503,同卷恢复后返回 200,并用原权限范围继续受理任务;证据见[数据库恢复报告](reports/phase-6-db-recovery.md)。该演练不替代主从切换、复制确认、跨故障域或 RPO/RTO 验收。
|
7. 已补[A18 数据库短暂不可用恢复演练](api/db-recovery.md):Go 1.27.1 构建的当前二进制在数据库停止期间返回 readiness 503,同卷恢复后返回 200,并用原权限范围继续受理任务;证据见[数据库恢复报告](reports/phase-6-db-recovery.md)。该演练不替代主从切换、复制确认、跨故障域或 RPO/RTO 验收。
|
||||||
|
8. 已补[MySQL GTID 复制与手工提升演练](reports/phase-6-mysql-replication.md):隔离源库/副本通过真实 Orbit HTTP 受理任务,源库停止后副本提升、结构核验和原幂等键重放通过;自动故障切换、复制延迟上限、目标拓扑及 RPO/RTO 仍待验收。
|
||||||
|
|
||||||
### P3 功能范围核对
|
### P3 功能范围核对
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- 文档版本:0.2
|
- 文档版本:0.2
|
||||||
- 整理日期:2026-09-17
|
- 整理日期:2026-09-17
|
||||||
- 阶段:P0–P5平台功能已通过阶段回归;[P4/P5原范围核对](reports/phase-4-5.md)和[首批故障边界](reports/phase-6-fault-boundaries.md)保留证据边界。P6/P7已补进程观测、有界业务状态采样、凭据轮换/撤销、提交不明切点、隔离备份恢复、数据库短暂不可用恢复、Go 1.27.1 模块门禁、真实服务示例和独立业务模块回归;生产全局聚合、目标拓扑容量、复制切换、RPO/RTO、跨版本兼容和发布审批继续推进,尚未完成全平台生产验收,数据库保持无外键。
|
- 阶段:P0–P5平台功能已通过阶段回归;[P4/P5原范围核对](reports/phase-4-5.md)和[首批故障边界](reports/phase-6-fault-boundaries.md)保留证据边界。P6/P7已补进程观测、有界业务状态采样、凭据轮换/撤销、提交不明切点、隔离备份恢复、数据库短暂不可用恢复、隔离 MySQL GTID 复制与手工提升、Go 1.27.1 模块门禁、真实服务示例和独立业务模块回归;生产全局聚合、目标拓扑容量、自动复制切换、RPO/RTO、跨版本兼容和发布审批继续推进,尚未完成全平台生产验收,数据库保持无外键。
|
||||||
- 适用范围:Orbit 统一平台,包含 Pulse 任务调度模块、Weave 流程编排模块与公共能力。平台面向多个业务系统复用。
|
- 适用范围:Orbit 统一平台,包含 Pulse 任务调度模块、Weave 流程编排模块与公共能力。平台面向多个业务系统复用。
|
||||||
|
|
||||||
## 阅读顺序
|
## 阅读顺序
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
| 定时集中触发测量 | 已实现 | [有界计划测量工具](schedule-load.md) |
|
| 定时集中触发测量 | 已实现 | [有界计划测量工具](schedule-load.md) |
|
||||||
| `scripts/test-backup-restore.sh` 隔离备份恢复演练 | 已实现一次性 MySQL 逻辑备份、独立恢复、核心计数及零外键核验 | [备份恢复演练](backup-restore.md) |
|
| `scripts/test-backup-restore.sh` 隔离备份恢复演练 | 已实现一次性 MySQL 逻辑备份、独立恢复、核心计数及零外键核验 | [备份恢复演练](backup-restore.md) |
|
||||||
| `scripts/test-db-recovery.sh` MySQL 短暂不可用恢复演练 | 已实现 readiness 503、同卷恢复 200 及恢复后任务受理核验 | [数据库恢复演练](db-recovery.md) |
|
| `scripts/test-db-recovery.sh` MySQL 短暂不可用恢复演练 | 已实现 readiness 503、同卷恢复 200 及恢复后任务受理核验 | [数据库恢复演练](db-recovery.md) |
|
||||||
|
| `scripts/test-mysql-replication.sh` MySQL GTID 复制与手工提升演练 | 已实现隔离源库/副本复制、源库停止、副本提升、结构核验和原幂等键重放;不提供自动切换 | [复制与手工提升报告](../reports/phase-6-mysql-replication.md) |
|
||||||
| `/v1/worker/connect` | P2 已实现并通过阶段验收 | [Worker 契约](worker.md)、[Schema](worker-schema.md) |
|
| `/v1/worker/connect` | P2 已实现并通过阶段验收 | [Worker 契约](worker.md)、[Schema](worker-schema.md) |
|
||||||
| 任务相对延迟 `delay_ms` | 已实现数据库计时、原键恢复与实际执行例子 | [任务接口](tasks.md)、[延迟示例](../examples/delayed-task.md) |
|
| 任务相对延迟 `delay_ms` | 已实现数据库计时、原键恢复与实际执行例子 | [任务接口](tasks.md)、[延迟示例](../examples/delayed-task.md) |
|
||||||
| 任务冻结策略与 Worker `retryable` | P3 安全重试已实现 | [安全重试](retries.md) |
|
| 任务冻结策略与 Worker `retryable` | P3 安全重试已实现 | [安全重试](retries.md) |
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 受理、派发、流程推进和恢复吞吐 | 固定硬件、数据量、任务/流程混合模型,p50/p95/p99和资源曲线 | 已有受理样本、[真实Worker短/混合/中长执行样本](reports/phase-6-execution-load.md)、[Weave双分支流程样本](reports/phase-6-workflow-load.md)及[定时集中触发/积压曲线样本](reports/phase-6-backlog.md);目标硬件资源曲线、生产容量和HA仍未验收 |
|
| 受理、派发、流程推进和恢复吞吐 | 固定硬件、数据量、任务/流程混合模型,p50/p95/p99和资源曲线 | 已有受理样本、[真实Worker短/混合/中长执行样本](reports/phase-6-execution-load.md)、[Weave双分支流程样本](reports/phase-6-workflow-load.md)及[定时集中触发/积压曲线样本](reports/phase-6-backlog.md);目标硬件资源曲线、生产容量和HA仍未验收 |
|
||||||
| 100万+企业规模推导 | [容量模型](19-capacity-model.md):租户、队列、连接、事件、索引和保存周期的参数化推导 | 已建立计算方法和演算样例;目标业务数据、硬件、拓扑和生产容量仍未验收,不能由企业数直接承诺 |
|
| 100万+企业规模推导 | [容量模型](19-capacity-model.md):租户、队列、连接、事件、索引和保存周期的参数化推导 | 已建立计算方法和演算样例;目标业务数据、硬件、拓扑和生产容量仍未验收,不能由企业数直接承诺 |
|
||||||
| MySQL 复制、切换、RPO/RTO | 目标拓扑上的实际切换、复制确认、恢复点和恢复时间 | 未验收 |
|
| MySQL 复制、切换、RPO/RTO | [隔离 GTID 复制与手工提升演练](reports/phase-6-mysql-replication.md);目标拓扑上的实际切换、复制确认、恢复点和恢复时间 | 隔离源库/副本复制、手工提升和原幂等键重放已通过;自动切换、目标拓扑、RPO/RTO仍未验收 |
|
||||||
| 慢消费者、重连风暴、磁盘和锁等待 | 受控故障负载及积压清空证据 | SDK 受控连续断线和真实 HTTP/WebSocket 容量1慢消费者已回归(见[重连报告](reports/phase-6-reconnect-storm.md)、[慢消费者报告](reports/phase-6-slow-consumer.md));跨节点风暴、磁盘和锁等待未验收 |
|
| 慢消费者、重连风暴、磁盘和锁等待 | 受控故障负载及积压清空证据 | SDK 受控连续断线和真实 HTTP/WebSocket 容量1慢消费者已回归(见[重连报告](reports/phase-6-reconnect-storm.md)、[慢消费者报告](reports/phase-6-slow-consumer.md));跨节点风暴、磁盘和锁等待未验收 |
|
||||||
| 跨版本 SDK/服务端 | 至少一个正式旧标签与候选版本的双向矩阵 | 当前仓库无正式标签,未验收 |
|
| 跨版本 SDK/服务端 | 至少一个正式旧标签与候选版本的双向矩阵 | 当前仓库无正式标签,未验收 |
|
||||||
|
|
||||||
|
|||||||
37
docs/reports/phase-6-mysql-replication.md
Normal file
37
docs/reports/phase-6-mysql-replication.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# P6:MySQL GTID 复制与手工提升演练
|
||||||
|
|
||||||
|
验证日期:2026-09-20。新增 `scripts/test-mysql-replication.sh`,在两个一次性 MySQL 8.4 容器中使用原生 GTID 复制,先由源库承载真实 Orbit HTTP 受理,再停止源库、提升副本并用原幂等键重放请求。演练不连接已有数据库,也不修改生产数据。
|
||||||
|
|
||||||
|
## 实际环境与命令
|
||||||
|
|
||||||
|
使用 Docker `golang:1.27.1` 交叉构建 macOS arm64 的当前 `orbit`、`orbit-migrate` 和 `orbit-admin`,随后在宿主机执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ORBIT_BIN=/tmp/orbit-go127-darwin/orbit \
|
||||||
|
ORBIT_MIGRATE_BIN=/tmp/orbit-go127-darwin/orbit-migrate \
|
||||||
|
ORBIT_ADMIN_BIN=/tmp/orbit-go127-darwin/orbit-admin \
|
||||||
|
ORBIT_TEST_MYSQL_IMAGE=mysql:8.4 \
|
||||||
|
sh scripts/test-mysql-replication.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本使用普通 Docker bridge 映射宿主机端口,避免 Docker Desktop 的 host network 只对容器可见;副本通过源容器 IP 和内部 3306 端口连接。两个数据库均启用 `gtid_mode=ON`、`enforce_gtid_consistency=ON`、行格式 binlog 和独立 `server_id`。源库和副本均随机生成密码,副本提升前保持 `read_only`/`super_read_only`。
|
||||||
|
|
||||||
|
## 实际结果
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"replication_io_sql":"Yes:Yes","replicated_task_rows":1,"source_stopped":true,"manual_promotion_verified":true,"idempotent_replay_same_task":true,"automatic_failover":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
真实步骤包括:
|
||||||
|
|
||||||
|
1. 迁移源库并签发通用应用凭据;
|
||||||
|
2. 对源库执行带 GTID 的逻辑基线导入副本;
|
||||||
|
3. 建立复制并核对 `Replica_IO_Running=Yes`、`Replica_SQL_Running=Yes`;
|
||||||
|
4. 通过源库上的 Orbit HTTP 提交任务,确认副本收到任务记录;
|
||||||
|
5. 停止源库,停止复制并提升副本为可写;
|
||||||
|
6. 用原凭据和原幂等键启动 Orbit,重放后返回同一个任务身份;
|
||||||
|
7. 用 `orbit-admin verify` 核对提升后的迁移版本、固定索引和零外键。
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
这次演练证明 MySQL 原生 GTID 复制、手工提升、Orbit 重新连接和幂等重放可以在隔离拓扑中工作;它不提供自动故障探测、虚拟 IP、代理路由或零停机切换。没有测量复制延迟上限、网络分区、双主写入、磁盘损坏、跨故障域、备份介质、RPO/RTO 或生产容量。上线前仍需在批准的 MySQL 主从/集群拓扑中确定切换执行人、读写路由、复制确认、恢复点和恢复时间,并完成业务副作用核对。
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
| Go 1.27.1 task-outbox 真实业务集成 | 独立发送方 MySQL 8.4.10;真实 Orbit HTTP;`go test -race -run Integration ./...`,含 CLI、三处 COMMIT 回执丢失、HTTP 回执丢失、发送进程强杀后租约接管、预算和错误分类 | PASS;测试包 32.964 秒,初始化与 demo 也完成;原操作键恢复为单一平台任务 |
|
| Go 1.27.1 task-outbox 真实业务集成 | 独立发送方 MySQL 8.4.10;真实 Orbit HTTP;`go test -race -run Integration ./...`,含 CLI、三处 COMMIT 回执丢失、HTTP 回执丢失、发送进程强杀后租约接管、预算和错误分类 | PASS;测试包 32.964 秒,初始化与 demo 也完成;原操作键恢复为单一平台任务 |
|
||||||
| Go 1.27.1 备份恢复脚本 | Go 1.27.1 交叉构建 macOS arm64 二进制;`scripts/test-backup-restore.sh`;MySQL 8.4 | PASS;源/恢复核心计数均为 `1,1,1,1,28`,迁移校验完整、固定索引6个、外键0 |
|
| Go 1.27.1 备份恢复脚本 | Go 1.27.1 交叉构建 macOS arm64 二进制;`scripts/test-backup-restore.sh`;MySQL 8.4 | PASS;源/恢复核心计数均为 `1,1,1,1,28`,迁移校验完整、固定索引6个、外键0 |
|
||||||
| Go 1.27.1 数据库短暂不可用脚本 | Go 1.27.1 交叉构建 macOS arm64 二进制;`scripts/test-db-recovery.sh`;MySQL 8.4 同卷重启 | PASS;readiness `200 → 503 → 200`,恢复后使用原凭据成功受理任务 |
|
| Go 1.27.1 数据库短暂不可用脚本 | Go 1.27.1 交叉构建 macOS arm64 二进制;`scripts/test-db-recovery.sh`;MySQL 8.4 同卷重启 | PASS;readiness `200 → 503 → 200`,恢复后使用原凭据成功受理任务 |
|
||||||
|
| Go 1.27.1 MySQL GTID 复制与手工提升 | 两个一次性 MySQL 8.4 bridge 容器;真实 Orbit HTTP;`scripts/test-mysql-replication.sh` | PASS;复制 IO/SQL 均为 `Yes`,任务记录复制到副本,源库停止后手工提升、结构核验和原幂等键重放通过;自动切换未验证 |
|
||||||
| Orbit HTTP/metrics 子进程 | 同一共享网络命名空间;`TestIntegrationMetricsProcess`;metrics 启用/禁用各一组 | PASS,3.081 秒;监听隔离、隐私、后台采样和优雅退出均通过 |
|
| Orbit HTTP/metrics 子进程 | 同一共享网络命名空间;`TestIntegrationMetricsProcess`;metrics 启用/禁用各一组 | PASS,3.081 秒;监听隔离、隐私、后台采样和优雅退出均通过 |
|
||||||
| 真实Worker执行测量 | `orbit-exec-load`;最终短/混合/中长及截止不足四组,环境及原始JSON见[测量报告](phase-6-execution-load.md) | 96+96+6项全部逐项核验,长任务18次续租;24项截止负例按预期非零退出。SDK全模块竞态/vet/build通过,生产容量未验收 |
|
| 真实Worker执行测量 | `orbit-exec-load`;最终短/混合/中长及截止不足四组,环境及原始JSON见[测量报告](phase-6-execution-load.md) | 96+96+6项全部逐项核验,长任务18次续租;24项截止负例按预期非零退出。SDK全模块竞态/vet/build通过,生产容量未验收 |
|
||||||
| 真实Weave流程测量 | `orbit-workflow-load`;24/100实例、固定发布版本双分支并行及24实例截止负例,见[流程测量报告](phase-6-workflow-load.md) | 124个正常实例、248个不同任务/唯一尝试和本地调用逐项关联;负例2成功/22未核验,非零退出。重新构建并保存源码/二进制哈希,替换旧证据;生产容量未验收 |
|
| 真实Weave流程测量 | `orbit-workflow-load`;24/100实例、固定发布版本双分支并行及24实例截止负例,见[流程测量报告](phase-6-workflow-load.md) | 124个正常实例、248个不同任务/唯一尝试和本地调用逐项关联;负例2成功/22未核验,非零退出。重新构建并保存源码/二进制哈希,替换旧证据;生产容量未验收 |
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
|
|
||||||
此前使用容器网络别名运行存储集成时,两组 COMMIT 故障测试曾因自身 `127.0.0.1` 明文连接前置条件被拒绝;本轮已改用共享网络命名空间单独复跑并通过,因此不再把它们列为未验证场景。共享网络命名空间只解决测试进程的 loopback 观察条件,不改变生产部署约束。
|
此前使用容器网络别名运行存储集成时,两组 COMMIT 故障测试曾因自身 `127.0.0.1` 明文连接前置条件被拒绝;本轮已改用共享网络命名空间单独复跑并通过,因此不再把它们列为未验证场景。共享网络命名空间只解决测试进程的 loopback 观察条件,不改变生产部署约束。
|
||||||
|
|
||||||
当前已完成仓库声明 Go 1.27.1 下的模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24 个可运行 SDK 示例的真实服务串跑,以及 result-inbox/task-outbox 两个独立业务模块的真实 MySQL/HTTP/进程故障集成;SDK/服务端跨标签矩阵仍未完成。新增长期积压和慢消费者样本见[报告](phase-6-backlog.md)与[慢消费者报告](phase-6-slow-consumer.md),仍未升级为生产容量结论。此前 Docker Go 1.25 共享网络结果作为历史对照保留,不替代 Go 1.27.1 集成回归、目标拓扑和版本兼容门禁。
|
当前已完成仓库声明 Go 1.27.1 下的模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24 个可运行 SDK 示例的真实服务串跑、两个独立业务模块的真实 MySQL/HTTP/进程故障集成,以及隔离 MySQL GTID 复制和手工提升演练;SDK/服务端跨标签矩阵仍未完成。新增长期积压和慢消费者样本见[报告](phase-6-backlog.md)与[慢消费者报告](phase-6-slow-consumer.md),仍未升级为生产容量结论。此前 Docker Go 1.25 共享网络结果作为历史对照保留,不替代 Go 1.27.1 集成回归、目标拓扑和版本兼容门禁。
|
||||||
|
|
||||||
历史 macOS arm64 + Go 1.27.1 + loopback MySQL 的完整切点记录见[故障边界报告](phase-6-fault-boundaries.md);该记录与本轮 Go 1.27.1 模块门禁互相独立,仍不能替代声明工具链下的无过滤 MySQL 集成复跑。
|
历史 macOS arm64 + Go 1.27.1 + loopback MySQL 的完整切点记录见[故障边界报告](phase-6-fault-boundaries.md);该记录与本轮 Go 1.27.1 模块门禁互相独立,仍不能替代声明工具链下的无过滤 MySQL 集成复跑。
|
||||||
|
|
||||||
|
|||||||
217
scripts/test-mysql-replication.sh
Executable file
217
scripts/test-mysql-replication.sh
Executable file
@@ -0,0 +1,217 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Rehearses native MySQL GTID replication and a documented manual promotion.
|
||||||
|
# This is an isolated topology check; it does not provide automatic failover.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
TASK_DOCKER=${DOCKER:-docker}
|
||||||
|
TASK_MYSQL_IMAGE=${ORBIT_TEST_MYSQL_IMAGE:-mysql:8.4}
|
||||||
|
TASK_ORBIT=${ORBIT_BIN:-bin/orbit}
|
||||||
|
TASK_MIGRATE=${ORBIT_MIGRATE_BIN:-bin/orbit-migrate}
|
||||||
|
TASK_ADMIN=${ORBIT_ADMIN_BIN:-bin/orbit-admin}
|
||||||
|
TASK_SOURCE_NAME="orbit-ha-source-$$"
|
||||||
|
TASK_REPLICA_NAME="orbit-ha-replica-$$"
|
||||||
|
TASK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/orbit-ha.XXXXXX")
|
||||||
|
TASK_SERVER_PID=
|
||||||
|
TASK_SOURCE_PORT=${ORBIT_TEST_SOURCE_PORT:-}
|
||||||
|
TASK_REPLICA_PORT=${ORBIT_TEST_REPLICA_PORT:-}
|
||||||
|
TASK_LAST_HOST_PORT=
|
||||||
|
TASK_SOURCE_IP=
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$TASK_SERVER_PID" ]; then
|
||||||
|
kill "$TASK_SERVER_PID" >/dev/null 2>&1 || true
|
||||||
|
wait "$TASK_SERVER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
"$TASK_DOCKER" rm -f "$TASK_SOURCE_NAME" "$TASK_REPLICA_NAME" >/dev/null 2>&1 || true
|
||||||
|
for task_artifact in "$TASK_DIR"/*; do
|
||||||
|
if [ -f "$task_artifact" ]; then unlink "$task_artifact"; fi
|
||||||
|
done
|
||||||
|
rmdir "$TASK_DIR" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
if [ ! -x "$TASK_ORBIT" ] || [ ! -x "$TASK_MIGRATE" ] || [ ! -x "$TASK_ADMIN" ]; then
|
||||||
|
printf '%s\n' 'orbit, orbit-migrate and orbit-admin binaries are required; build them before this rehearsal.' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
TASK_SOURCE_PASSWORD=$(openssl rand -hex 24)
|
||||||
|
TASK_REPLICA_PASSWORD=$(openssl rand -hex 24)
|
||||||
|
TASK_REPLICA_USER_PASSWORD=$(openssl rand -hex 16)
|
||||||
|
TASK_DATABASE=orbit_test
|
||||||
|
|
||||||
|
start_mysql() {
|
||||||
|
container=$1
|
||||||
|
password=$2
|
||||||
|
host_port=$3
|
||||||
|
server_id=$4
|
||||||
|
port_mapping="127.0.0.1::3306"
|
||||||
|
if [ -n "$host_port" ]; then port_mapping="127.0.0.1:$host_port:3306"; fi
|
||||||
|
"$TASK_DOCKER" run -d --name "$container" --network bridge \
|
||||||
|
-e MYSQL_ROOT_PASSWORD="$password" -e MYSQL_DATABASE="$TASK_DATABASE" \
|
||||||
|
-p "$port_mapping" "$TASK_MYSQL_IMAGE" --mysqlx=OFF --server-id="$server_id" --log-bin=mysql-bin \
|
||||||
|
--relay-log=relay-bin --binlog-format=ROW --gtid-mode=ON \
|
||||||
|
--enforce-gtid-consistency=ON --log-replica-updates=ON >/dev/null
|
||||||
|
ready=0
|
||||||
|
for task_attempt in $(seq 1 90); do
|
||||||
|
if "$TASK_DOCKER" exec "$container" sh -c \
|
||||||
|
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 -e "SELECT 1" >/dev/null 2>&1'; then
|
||||||
|
ready=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! "$TASK_DOCKER" inspect -f '{{.State.Running}}' "$container" 2>/dev/null | grep -qx true; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$ready" -ne 1 ]; then
|
||||||
|
printf 'MySQL container did not become ready: %s\n' "$container" >&2
|
||||||
|
"$TASK_DOCKER" logs --tail 40 "$container" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TASK_LAST_HOST_PORT=$("$TASK_DOCKER" port "$container" 3306/tcp | sed 's/.*://')
|
||||||
|
}
|
||||||
|
|
||||||
|
source_mysql() {
|
||||||
|
"$TASK_DOCKER" exec -e MYSQL_PWD="$TASK_SOURCE_PASSWORD" "$TASK_SOURCE_NAME" \
|
||||||
|
mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
replica_mysql() {
|
||||||
|
"$TASK_DOCKER" exec -e MYSQL_PWD="$TASK_REPLICA_PASSWORD" "$TASK_REPLICA_NAME" \
|
||||||
|
mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_mysql "$TASK_SOURCE_NAME" "$TASK_SOURCE_PASSWORD" "$TASK_SOURCE_PORT" 1
|
||||||
|
TASK_SOURCE_PORT=$TASK_LAST_HOST_PORT
|
||||||
|
TASK_SOURCE_IP=$("$TASK_DOCKER" inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$TASK_SOURCE_NAME")
|
||||||
|
start_mysql "$TASK_REPLICA_NAME" "$TASK_REPLICA_PASSWORD" "$TASK_REPLICA_PORT" 2
|
||||||
|
TASK_REPLICA_PORT=$TASK_LAST_HOST_PORT
|
||||||
|
if [ "$TASK_SOURCE_PORT" = "$TASK_REPLICA_PORT" ]; then
|
||||||
|
printf '%s\n' 'source and replica host ports collided; set ORBIT_TEST_SOURCE_PORT and ORBIT_TEST_REPLICA_PORT explicitly' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TASK_SOURCE_DSN="root:$TASK_SOURCE_PASSWORD@tcp(127.0.0.1:$TASK_SOURCE_PORT)/$TASK_DATABASE"
|
||||||
|
TASK_REPLICA_DSN="root:$TASK_REPLICA_PASSWORD@tcp(127.0.0.1:$TASK_REPLICA_PORT)/$TASK_DATABASE"
|
||||||
|
|
||||||
|
ORBIT_MYSQL_DSN="$TASK_SOURCE_DSN" "$TASK_MIGRATE" up >/dev/null
|
||||||
|
ORBIT_MYSQL_DSN="$TASK_SOURCE_DSN" "$TASK_ADMIN" bootstrap \
|
||||||
|
--application ha-rehearsal --namespace default --environment test --queue failover \
|
||||||
|
--output "$TASK_DIR/credentials.json" >/dev/null
|
||||||
|
|
||||||
|
read TASK_TOKEN TASK_QUEUE <<EOF
|
||||||
|
$(python3 - "$TASK_DIR/credentials.json" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open(sys.argv[1], encoding="utf-8") as handle:
|
||||||
|
value = json.load(handle)
|
||||||
|
print(value["token"], value["queue_id"])
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
source_mysql -e "CREATE USER IF NOT EXISTS 'orbit_replica'@'%' IDENTIFIED BY '$TASK_REPLICA_USER_PASSWORD'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'orbit_replica'@'%'; FLUSH PRIVILEGES"
|
||||||
|
if ! "$TASK_DOCKER" exec -e MYSQL_PWD="$TASK_SOURCE_PASSWORD" "$TASK_SOURCE_NAME" \
|
||||||
|
mysqldump -uroot --protocol=tcp -h127.0.0.1 -P3306 \
|
||||||
|
--single-transaction --set-gtid-purged=ON --routines --triggers --events --hex-blob \
|
||||||
|
--no-tablespaces "$TASK_DATABASE" > "$TASK_DIR/orbit.sql" 2>"$TASK_DIR/mysqldump.log"; then
|
||||||
|
cat "$TASK_DIR/mysqldump.log" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"$TASK_DOCKER" exec -i -e MYSQL_PWD="$TASK_REPLICA_PASSWORD" "$TASK_REPLICA_NAME" \
|
||||||
|
mysql -uroot --protocol=tcp -h127.0.0.1 -P3306 "$TASK_DATABASE" < "$TASK_DIR/orbit.sql"
|
||||||
|
|
||||||
|
replica_mysql -e "CHANGE REPLICATION SOURCE TO SOURCE_HOST='$TASK_SOURCE_IP', SOURCE_PORT=3306, SOURCE_USER='orbit_replica', SOURCE_PASSWORD='$TASK_REPLICA_USER_PASSWORD', SOURCE_AUTO_POSITION=1, GET_SOURCE_PUBLIC_KEY=1; START REPLICA"
|
||||||
|
replica_mysql -e "SET GLOBAL super_read_only=ON; SET GLOBAL read_only=ON"
|
||||||
|
|
||||||
|
ORBIT_MYSQL_DSN="$TASK_SOURCE_DSN" ORBIT_HTTP_ADDR=127.0.0.1:0 "$TASK_ORBIT" >"$TASK_DIR/source-server.log" 2>&1 &
|
||||||
|
TASK_SERVER_PID=$!
|
||||||
|
TASK_HTTP_PORT=
|
||||||
|
for task_attempt in $(seq 1 30); do
|
||||||
|
TASK_HTTP_PORT=$(sed -n 's/.*"address":"127.0.0.1:\([0-9]*\)".*/\1/p' "$TASK_DIR/source-server.log" | head -n 1)
|
||||||
|
if [ -n "$TASK_HTTP_PORT" ]; then break; fi
|
||||||
|
if ! kill -0 "$TASK_SERVER_PID" 2>/dev/null; then cat "$TASK_DIR/source-server.log" >&2; exit 1; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ -z "$TASK_HTTP_PORT" ]; then cat "$TASK_DIR/source-server.log" >&2; exit 1; fi
|
||||||
|
|
||||||
|
TASK_OPERATION_ID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
cat >"$TASK_DIR/task.json" <<EOF
|
||||||
|
{"queue_id":"$TASK_QUEUE","handler":"ha.rehearsal","handler_version":"v1","payload_version":"v1","payload":{"phase":"source"},"recovery_safety":"RECONCILE_ONLY","retry":{"max_attempts":1}}
|
||||||
|
EOF
|
||||||
|
TASK_SOURCE_RESPONSE=$(curl -fsS --max-time 5 -X POST "http://127.0.0.1:$TASK_HTTP_PORT/v1/tasks" \
|
||||||
|
-H "Authorization: Bearer $TASK_TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-H "Idempotency-Key: $TASK_OPERATION_ID" --data-binary @"$TASK_DIR/task.json")
|
||||||
|
TASK_SOURCE_ID=$(python3 - "$TASK_SOURCE_RESPONSE" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
value = json.loads(sys.argv[1])
|
||||||
|
task = value.get("data", {}).get("task", {})
|
||||||
|
if not task.get("id"):
|
||||||
|
raise SystemExit("source did not accept task")
|
||||||
|
print(task["id"])
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
kill "$TASK_SERVER_PID" >/dev/null 2>&1 || true
|
||||||
|
wait "$TASK_SERVER_PID" 2>/dev/null || true
|
||||||
|
TASK_SERVER_PID=
|
||||||
|
|
||||||
|
TASK_REPLICA_ROWS=0
|
||||||
|
for task_attempt in $(seq 1 60); do
|
||||||
|
TASK_REPLICA_ROWS=$(replica_mysql --batch --skip-column-names "$TASK_DATABASE" -e \
|
||||||
|
"SELECT COUNT(*) FROM pulse_tasks" 2>/dev/null || true)
|
||||||
|
if [ "$TASK_REPLICA_ROWS" = 1 ]; then break; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$TASK_REPLICA_ROWS" != 1 ]; then
|
||||||
|
replica_mysql -e 'SHOW REPLICA STATUS\G' >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TASK_REPLICATION_STATUS=$(replica_mysql -e 'SHOW REPLICA STATUS\G' 2>/dev/null | \
|
||||||
|
awk -F': ' '/Replica_IO_Running:/{io=$2} /Replica_SQL_Running:/{sql=$2} END{print io ":" sql}')
|
||||||
|
case "$TASK_REPLICATION_STATUS" in
|
||||||
|
Yes:Yes) ;;
|
||||||
|
*) printf 'replication was not healthy: %s\n' "$TASK_REPLICATION_STATUS" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
"$TASK_DOCKER" stop "$TASK_SOURCE_NAME" >/dev/null
|
||||||
|
TASK_SOURCE_STOPPED=true
|
||||||
|
replica_mysql -e 'STOP REPLICA; RESET REPLICA ALL; SET GLOBAL super_read_only=OFF; SET GLOBAL read_only=OFF'
|
||||||
|
ORBIT_MYSQL_DSN="$TASK_REPLICA_DSN" "$TASK_ADMIN" verify >/dev/null
|
||||||
|
|
||||||
|
ORBIT_MYSQL_DSN="$TASK_REPLICA_DSN" ORBIT_HTTP_ADDR=127.0.0.1:0 "$TASK_ORBIT" >"$TASK_DIR/replica-server.log" 2>&1 &
|
||||||
|
TASK_SERVER_PID=$!
|
||||||
|
TASK_HTTP_PORT=
|
||||||
|
for task_attempt in $(seq 1 30); do
|
||||||
|
TASK_HTTP_PORT=$(sed -n 's/.*"address":"127.0.0.1:\([0-9]*\)".*/\1/p' "$TASK_DIR/replica-server.log" | head -n 1)
|
||||||
|
if [ -n "$TASK_HTTP_PORT" ]; then break; fi
|
||||||
|
if ! kill -0 "$TASK_SERVER_PID" 2>/dev/null; then cat "$TASK_DIR/replica-server.log" >&2; exit 1; fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ -z "$TASK_HTTP_PORT" ]; then cat "$TASK_DIR/replica-server.log" >&2; exit 1; fi
|
||||||
|
|
||||||
|
TASK_REPLAY_RESPONSE=$(curl -fsS --max-time 5 -X POST "http://127.0.0.1:$TASK_HTTP_PORT/v1/tasks" \
|
||||||
|
-H "Authorization: Bearer $TASK_TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-H "Idempotency-Key: $TASK_OPERATION_ID" --data-binary @"$TASK_DIR/task.json")
|
||||||
|
TASK_REPLAY_ID=$(python3 - "$TASK_REPLAY_RESPONSE" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
value = json.loads(sys.argv[1])
|
||||||
|
task = value.get("data", {}).get("task", {})
|
||||||
|
if not task.get("id"):
|
||||||
|
raise SystemExit("promoted replica did not accept idempotent replay")
|
||||||
|
print(task["id"])
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
if [ "$TASK_REPLAY_ID" != "$TASK_SOURCE_ID" ]; then
|
||||||
|
printf 'idempotent replay changed task: source=%s promoted=%s\n' "$TASK_SOURCE_ID" "$TASK_REPLAY_ID" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '{"replication_io_sql":"%s","replicated_task_rows":%s,"source_stopped":true,"manual_promotion_verified":true,"idempotent_replay_same_task":true,"automatic_failover":false}\n' \
|
||||||
|
"$TASK_REPLICATION_STATUS" "$TASK_REPLICA_ROWS"
|
||||||
Reference in New Issue
Block a user