MySQL 5.6 本地复现 profile 实施计划
MySQL 5.6 本地复现 profile 实施计划
状态:已确认并完成实施(用户在 mysql56 CI 门禁合并后回复「加上」),实现记录见
docs/pull-requests/2026-09-24-mysql56-contract-ci-gate.md 之后的
docs/pull-requests/2026-09-24-mysql56-compose-profile.md。
日期:2026-09-24
1. 目标
让贡献者能在本地一键起出与 CI mysql56 相同的 MySQL 下限环境并复跑同一批契约测试,同时把 CI
唯一没覆盖的那一半——生产 charset(utf8mb4_general_ci)下的 767-byte 索引前缀限制——放进同一条
路径。
2. 非目标
- 不改集群部署形态:
mysql(8.4)服务、两个 Server、relay、证书流程一概不动。 - 不做 Schema、API、协议、生产代码变更。
- 不把 profile 变成默认启动的服务,也不给它加
restart策略(一次性测试库应在重启后保持停止)。 - 不引入 etcd 或 8.4 contract 矩阵(仍属 deferred,见
docs/operations/completeness-checklist.md)。
3. 架构决策
- P-D1 放在
docker-compose.cluster.yml而不是新文件:下限环境与被验证对象(集群模式的 MySQL 管理数据)同源,新增一份 compose 文件只会多一个漂移点。代价是必须做到「默认不启动」, 由profiles承担。 - P-D2 profile 名与服务名同名
mysql56:与docker-compose.local.yml的agent/clientprofile 惯例一致,--profile mysql56 up -d mysql56无需记忆额外名字。 - P-D3 只绑
127.0.0.1:3307:5.6 无 TLS,发布到0.0.0.0等于把可写的匿名库放到局域网上; 3307 避开本机已有的 MySQL 8 与集群mysql服务。 - P-D4 charset 用
conf.d而不是initdb.d:/docker-entrypoint-initdb.d/*.sql只在数据目录 为空时执行,卷复用后会静默回到 latin1;/etc/mysql/conf.d每次启动都生效,MYSQL_DATABASE也按该 charset 创建,行为与生产一致且可复现。 - P-D5 不放宽
innodb_large_prefix:一旦设置,「所有索引键 ≤767 byte」这条被测命题就永远测不 出来。测试断言直接把它钉死。 - P-D6 版本单一来源靠测试守护:CI job 与 profile 若各自写死版本,一次
mysql:5.6 → 8.0的 改动就会悄悄改变「已验证」的含义。deploy/mysql56/mysql56_service_test.go比较两处 image 值。 - P-D7 口令默认占位值可覆盖:
${MYSQL56_TEST_PASSWORD:-tunnelmesh}。集群mysql服务用${MYSQL_PASSWORD:?}强制要求是对的(真实数据),但:?会在任何对该文件的插值上生效、 与 profile 无关,会让既有集群三步引导流程直接启动失败——这是回归,必须避免。
4. 技术栈与规格引用
- Docker Compose v2(
--profile、顶层volumes、healthcheck)、镜像mysql:5.6。 migrations/ddl.sql(唯一权威全量 DDL)、internal/storage.OpenMySQL(默认auto-init=true)。- 既有门禁事实与命令:
docs/development/testing.md、.github/workflows/ci.yml的mysql56job。 - InnoDB 767-byte 前缀限制依据:
docs/operations/schema-upgrades.md、docs/operations/connection-pool.md。
5. 全局约束
- 不把口令、DSN 或任何生产凭据写入仓库;默认值是本地一次性测试库占位符,且在文档中显式标注不得用于真实数据。
- 交付产物与说明分离:
deploy/只放可直接使用的文件,步骤留在docs/(deploy/README.md的既有约定)。 deploy/*/..._test.go是仓库一致性检查,scripts/build-release.sh按目录白名单打包,因此deploy/mysql56/不会进入发布归档。
6. 文件清单
| 文件 | 变更 |
|---|---|
docker-compose.cluster.yml |
新增 opt-in mysql56 服务(image、profiles、env、127.0.0.1:3307 端口、conf.d 挂载、healthcheck)与顶层 tunnelmesh-mysql56-test-data 卷 |
deploy/mysql56/utf8mb4.cnf |
新增:[mysqld] character-set-server=utf8mb4、collation-server=utf8mb4_general_ci,并写明为什么不能设 innodb_large_prefix |
deploy/mysql56/mysql56_service_test.go |
新增:三条一致性测试(版本单一来源、opt-in 且只绑回环、conf.d 内容/挂载与被测命题) |
docs/development/testing.md |
本地复跑命令改为 profile 路径并补 -p 1/multiStatements 原因;新增 CI 与 profile 的分工(charset 边界由谁覆盖) |
docs/deployment/docker.md |
集群模式新增「MySQL 5.6 契约环境(可选)」小节 |
deploy/README.md |
目录表登记 mysql56/utf8mb4.cnf 与一致性测试 |
docs/operations/completeness-checklist.md |
charset 端到端项转为已完成,另立 8.4 矩阵为 deferred |
| 计划与本 PR 记录 + 四份生成索引 | python3 scripts/gen_doc_index.py |
7. 任务间接口
- 启动:
docker compose -f docker-compose.cluster.yml --profile mysql56 up -d mysql56。 - 复跑:
TUNNELMESH_TEST_MYSQL_DSN='tunnelmesh:tunnelmesh@tcp(127.0.0.1:3307)/tunnelmesh_test?parseTime=true&tls=false&multiStatements=true' go test -p 1 ./internal/storage ./internal/registry -count=1 -run MySQL。 - 销毁:
docker compose -f docker-compose.cluster.yml --profile mysql56 down -v mysql56。 - 契约测试侧不变:
OpenMySQL仍需空库 + 建表权限;MYSQL_DATABASE名字与 CI 保持一致(tunnelmesh_test)。
8. TDD 步骤(本节为补记)
实际执行顺序是先落 compose 服务与 utf8mb4.cnf,再写一致性测试,所以不存在「先看到红灯再实现」
的原始输出,本节只记录已验证事实,不虚构当时的红灯。等价证据是三条断言的反向验证:人为破坏被测
对象后必须失败,每条都实际执行并还原。
| 破坏 | 命令 | 实际失败输出 |
|---|---|---|
移走 utf8mb4.cnf |
go test ./deploy/mysql56 -count=1 -run TestMySQL56ServerConfig |
mysql56_service_test.go:131: open utf8mb4.cnf: no such file or directory |
把 profile 镜像改成 mysql:8.0(制造与 CI 的版本漂移) |
go test ./deploy/mysql56 -count=1 -run TestMySQL56ProfileMatches |
CI image "mysql:5.6" != ../../docker-compose.cluster.yml image "mysql:8.0": the MySQL floor version must have one source |
删掉 profiles: ["mysql56"](变成随集群栈启动) |
go test ./deploy/mysql56 -count=1 -run TestMySQL56ProfileIsOptIn |
profiles = [], want [mysql56] so up for the cluster never starts it |
- 断言自身的缺陷也暴露过一次:最初写成整文件
strings.Contains(config, "innodb_large_prefix"), 因 cnf 的注释里解释性地提到该词而失败,说明它检查的不是选项行;改为逐行扫描、跳过#注释。 - 还原后:
go test ./deploy/mysql56 -count=1→ ok。 - 回归:
go test ./... -count=1、go test -race ./... -count=1 -timeout 30m、go vet ./...、git diff --check、gofmt -l deploy/mysql56(无输出)。
9. 验证命令与预期
go test ./deploy/... -count=1 # 三条 profile 一致性测试通过
go test ./... -count=1 # 无回归:本次不改生产代码
go vet ./... # 通过
git diff --check # 无空白错误
ruby -ryaml -e 'YAML.load_file("docker-compose.cluster.yml")' # YAML 可解析
未在本机执行、需在 CI 或有 Docker 的机器上补做(本机无容器运行时,/usr/local/bin/docker 是悬空符号链接):
docker compose -f docker-compose.cluster.yml config # 默认不含 mysql56,且不需要 MYSQL56_* 变量
docker compose -f docker-compose.cluster.yml --profile mysql56 config # 含 mysql56,healthcheck/挂载可渲染
docker compose -f docker-compose.cluster.yml --profile mysql56 up -d mysql56 # 健康检查转 healthy
10. 回滚注意事项
- 纯增量的开发/测试环境资产:
git revert合并提交即可,无 Schema、无生产代码路径、无配置分发。 - 若
deploy/mysql56/utf8mb4.cnf挂载在新版 Docker 上报错,可临时删掉该 volumes 行退回 latin1; 但这会让「生产 charset 下建表」重新变成未验证项,必须同步把docs/operations/completeness-checklist.md的对应项改回未完成。 - 已存在的
tunnelmesh-mysql56-test-data卷只含测试数据,down -v或docker volume rm清理即可。