ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Databasus ADR-0013 深度解析:按版本复用共享内存测试数据库,平衡测试速度与 CI 内存

Databasus ADR-0013 深度解析:按版本复用共享内存测试数据库,平衡测试速度与 CI 内存 数据库灾备【免费下载链接】databasusPostgreSQL backup tool with Point-In-Time-Recovery and restore verification项目地址https://gitcode.com/gh_mirrors/po/databasus点击查看免费下载本文基于 Databasus 仓库中的架构决策记录 ADR-0013讲清楚一个后端测试基础设施的核心问题当一个数据库备份工具需要对 PostgreSQL、MySQL、MariaDB、MongoDB 的十几个版本逐一做集成测试时如何同时满足测试快和CI 内存有界这两个互相矛盾的目标。读完本文你将掌握 Databasus 采用的每版本一个容器、跨测试复用 数据目录放 RAM 并行包 advisory lock 工作槽隔离四层方案以及 backend/internal/util/testing/containers/ 与 backend/internal/config/config.go 中对应的源码实现。背景两种旧方案为什么会失败Databasus 的数据库测试会把同一组检查跑在每种引擎的多个版本上。在采纳本决策之前团队先后尝试过两套方案都在 16 GB 内存的 CI 机器上失败了。旧方案一一个巨型 docker-compose 全版本常驻最初的做法是启动时把所有引擎的所有版本一次性拉起并且全程保持运行docker-compose up (everything, all the time) postgres:12 .. postgres:18 mysql:5.7 .. mysql:8.4 mariadb:10.6 .. mariadb:12.0 mongo:4.0 .. mongo:8.0 ────────────────────────────── ~50 containers alive at once → out of RAM约 50 个容器同时存活16 GB CI 机器直接耗尽内存。更糟的是所有测试包共享同一套数据库彼此之间没有隔离。旧方案二每个测试一个全新容器朴素的修复思路是让每个测试独立启动自己的数据库、用完即杀test 1 → boot DB → run → kill DB test 2 → boot DB → run → kill DB ... (40 times per package) → too slow数据库冷启动需要 20–60 秒而单个测试包有 40 多个测试函数直接撞上 15 分钟超时。由此得到本决策要解决的双重目标测试快、内存有界且测试之间互不干扰。决策每个版本一个容器跨该版本的所有测试复用核心思路是把启动一次的代价从每个测试摊销到每个版本上boot postgres:16 (once) ├─ run test 1 ├─ run test 2 └─ run test 3 shut down postgres:16 → boot postgres:17 ...每个矩阵包只声明一次版本列表用外层t.Run循环版本进入版本子测试时通过StartPostgres/StartMysql等辅助函数启动服务器然后把原本独立的测试函数作为内层子测试依次跑完。StartXxx会注册t.CleanupGo 在版本子测试返回时执行它——因此容器在下一个版本启动之前就被拆除任意时刻每个测试包内只有一个矩阵容器存活。又因为每个go test包是独立进程其容器只属于自己。源码印证每版本编排器模式这一模式在逻辑备份测试中的落地见 backup_restore_test.go。外层按版本循环注释中直接引用了 ADR-0013// Test_PostgresqlBackupRestore_AcrossSupportedVersions boots each PostgreSQL version once, runs // every backup/restore test function against it as a subtest, then shuts it down before the next // version. Only one matrix container is alive per package at a time. See ADR-0013. func Test_PostgresqlBackupRestore_AcrossSupportedVersions(t *testing.T) { for _, dbVersion : range postgresVersions { t.Run(dbVersion.name, func(t *testing.T) { endpoint : containers.StartPostgres(t, dbVersion.image) t.Run(Test_BackupAndRestorePostgresql_RestoreIsSuccesful, func(t *testing.T) { t.Run(CPU1 streamed, ...) t.Run(CPU4 directory, ...) }) // …其余备份/恢复测试函数均为内层子测试 }) } }容器生命周期由 containers.go 中的startContainer统一托管启动成功后立即注册t.Cleanup子测试结束时调用container.Terminate保证容器只活过创建它的那个测试这一语义。测试作者的一条硬规则由于同一版本下服务器被多个子测试复用、且子测试按顺序执行凡创建固定名称对象不带随机后缀的表、用户的测试必须以DROP ... IF EXISTS开头。同一版本内的子测试之间没有隔离这条规则是复用方案能够安全成立的前提。让每次启动都更快数据库文件放进 RAM第二个加速手段是把每个引擎的数据目录挂到 tmpfs 上而不是 overlay 文件系统——这样 fsync 密集的冷启动initdb和写密集的物理恢复全部跑在内存上。tmpfs 挂载与固定容量在 containers.go 中这个选项被集中定义为一处常量// dataDirTmpfsOptions mounts a containers data directory on tmpfs (RAM) instead of the overlay // filesystem, so the fsync-heavy cold init of the SQL engines is RAM-fast. The size is pinned // because Dockers tmpfs default is half the host RAM, which is unsafe to reserve per container // under go test -pN; 512m dwarfs every test fixture and tmpfs only consumes the bytes written. const dataDirTmpfsOptions rw,size512m这里有两个工程细节值得注意为什么必须钉死size512mDocker 的 tmpfs 默认容量是宿主内存的一半。在go test -pN下每个包都会起容器若按默认值预留几个并行容器就把宿主的可保留内存吃光了。512m 远大于任何测试夹具而 tmpfs 只按实际写入的字节数占内存所以固定值既安全又够用。每个引擎的挂载点不同PostgreSQL 挂/var/lib/postgresql/dataMySQL 家族挂/var/lib/mysqlMongoDB 挂/data/db见 mysql.go、mariadb.go、mongodb.go。PostgreSQL 还有一个版本相关的坑18 起官方镜像把 PGDATA 和数据卷上移到了/var/lib/postgresql若仍按 14–17 的旧路径挂 tmpfs会在旧路径留下一个空目录被 entrypoint 的布局检测直接拒绝服务器在就绪前就退出了。这段处理见 postgres.go 的postgresDataDir它按镜像 tag 解析主版本号PostgresMajorVersion18 及以上返回父卷路径不可解析的 tag 回退到 18 之前的路径。关闭持久性为一次性服务器换取速度因为测试服务器用完即弃所有引擎都关闭了崩溃安全相关的持久性开关引擎关闭的参数源码位置PostgreSQLfsyncoff、full_page_writesoff、synchronous_commitoffpostgres.gopostgresRequestMySQL / MariaDBinnodb-flush-log-at-trx-commit0、innodb-doublewrite0、sync-binlog0、skip-log-binmysql.gomysqlFamilyCmdmysqlFamilyCmd同时携带 utf8mb4 字符集默认值MySQL 与 MariaDB 共用mysql.go 还会对mysql:8.0特例追加--default-authentication-pluginmysql_native_password8.4 起该插件被移除后续版本不追加。就绪判定比端口通了更严格并行启动多个容器时CPU 争抢下冷启动远长于独占时间因此启动超时给得很宽PostgreSQL 240 秒postgres.go、MySQL/MariaDB 300 秒mysql.go——注释说明快速主机就绪即返回这个上限没有额外成本。就绪策略本身也做了针对性设计。PostgreSQL 的 entrypoint 会先起一个仅 socket 的临时服务器做 initdb然后重启真正的服务器ready to accept connections 日志会出现两次postgresReady 用WithOccurrence(2)等待第二次出现并叠加端口监听避免与 socket-only 临时服务器竞态。MySQL 侧则干脆数日志行数不可靠临时 initdb 服务器、MySQL 8.x 的 X Plugin 都会额外打日志改为wait.ForSQL完成一次真实的 root 握手并选择 entrypoint 创建的数据库从而同时证明端口可连且 initdb 已完成mysqlFamilyReady。并行跑测试包并用工作槽隔离每个包第三个手段是把速度找回来让测试包并行执行。入口在 backend/MakefileTEST_PARALLEL_WORKERS ? 8 test: clean-testcontainers pull-testcontainers TEST_PARALLEL_WORKERS$(TEST_PARALLEL_WORKERS) go run ./cmd/cleanup_test_db for i in $$(seq 0 $$(( $(TEST_PARALLEL_WORKERS) - 1 ))); do \ dbstring$$(echo $(GOOSE_TEST_DBSTRING) | sed -E s#/([^/?])\?#/\1_w$$i?#); \ echo migrating slot $$i; \ goose -dir ./migrations postgres $$dbstring up || exit 1; \ done TESTCONTAINERS_RYUK_DISABLEDtrue TEST_PARALLEL_WORKERS$(TEST_PARALLEL_WORKERS) \ go test -p$(TEST_PARALLEL_WORKERS) -count1 -failfast -timeout 15m ./internal/...要点-p8让 8 个包同时跑。由于每个包任意时刻只有一个矩阵容器存活峰值内存被钉在约 10–11 GBADR-0013 给出的数据而不是旧方案中所有容器同时存活的无界状态make test会先为 0..N-1 号槽位各自建库并跑一次 goose 迁移——用sed把测试 DSN 里的库名改写成base_wi这正是每个工作槽位私有元数据库的来源clean-testcontainers通过labelorg.testcontainers过滤残留容器并强制删除兜底-timeout或os.Exit时t.Cleanup被跳过的情形pull-testcontainers则用grep扫出源码里所有测试镜像名并行拉取避免并行包同时拉镜像互相拖慢。工作槽的申领Postgres advisory lock8 个包同时启动共享同一套元数据 Postgres 与备份基础设施谁来分配我是几号工人config.go 给出了答案每个真实的go test二进制进程启动时在系统库上按序尝试pg_try_advisory_lock(base slot)拿到第一个空闲槽位并把持有锁的连接锚在全局变量slotLockConn上直到进程退出——注释明确说明连接一旦被关闭或 GC锁就会释放、槽位会被中途抢走所以这个只赋值的引用本身就是 GC 根// slotLockConn holds the system-DB connection whose session owns this workers // advisory lock. It must live for the whole process: closing it (or letting it // be garbage-collected) releases the lock and frees the slot for another worker // mid-run. var slotLockConn *sql.Conn func claimTestWorkerSlot(testDsn string, pool int) int { // …对 [0, pool) 内每个槽位执行 // SELECT pg_try_advisory_lock($1) -- $1 945_000_000 slot // 命中即返回 slot否则每 100ms 重试直到 testSlotClaimTimeout60s }几个参数值得留意见 config.go锁键基址testSlotAdvisoryLockBase 945_000_000槽位 N 用baseN与业务 advisory lock 空间错开testSlotClaimTimeout 60 * time.Second专门吸收go test -p交接窗口——下一个包启动时上一个进程可能还没退出、锁还没释放若 60 秒仍无空闲槽位直接报错并提示TEST_PARALLEL_WORKERS must be the go test -p value即槽池大小必须不小于并行度defaultTestParallelWorkers 8的注释强调它必须与go test -p的取值一致否则并发的包无法各自领到独立库只有可执行文件名含.test的进程才申领槽位cleanup_test_db等运维工具保留基础 DSN需要对所有槽位操作config.go。每个槽位拿到什么claimTestWorkerSlotAndSelectMetadataDatabase领到槽位后把 DSN 中的库名改写为base_w{slot}config.go随后该包内的一切共享状态都落在自己的槽位里。按 ADR-0013 的描述槽位提供了自己的元数据库命名…_w{slot}自己的Valkey/Redis 数据库即槽位序号自己的缓存前缀w{slot}:该前缀同时给备份节点注册表 backups/backups/backuping/nodes/registry.go 中的每个 Redis 键与 pub/sub 频道打标。于是两个并排的包绝不会互相触碰对方的数据库、缓存条目、备份节点或 pub/sub 频道。被否决的备选方案ADR-0013 的 Alternatives considered 一节完整记录了否决理由这里保持原样继承备选方案否决理由docker-compose 全版本同时启动约 50 个容器在 16 GB CI 上耗尽内存且所有包共享同一套数据库、无隔离每个测试一个全新容器每个包 40 次冷启动 × 20–60s → 触发 15 分钟超时数据库文件放磁盘而非 RAM慢的环节恰恰是冷启动与写密集恢复时的 fsync对反正会丢的服务器放 RAM 直接消除该开销影响与约束正面收益原先两次超时于 900 秒的包现在约 30 秒跑完整个测试套件从直接超时变为约 1.5–4 分钟完成ADR-0013 Consequences峰值内存有界且可预测——每个包一个矩阵容器并行的包之间完全隔离。代价与使用约束由于服务器在版本内被多个子测试复用作者必须对固定名称对象写DROP ... IF EXISTS并且这些子测试不得加t.Parallel——同一版本内它们是顺序执行的RAM 中的数据易失对一次性测试服务器而言可以接受但这些容器没有任何崩溃安全若 CI runner 内存不足应调低TEST_PARALLEL_WORKERS例如设为 6。中性项与兜底在硬-timeout或os.Exit场景下t.Cleanup会被跳过残留容器由 Makefile/CI 的 label 清扫即上文clean-testcontainers的org.testcontainers过滤兜底。后续演进Valkey 替换说明2026-09-06ADR-0013 文末带有更新说明基于 Valkey 的测试状态已被进程内缓存、进程内 publish-subscribe 提供者和进程内限流提供者取代每个测试二进制现在自己拥有那份短暂状态。这意味着槽位机制的职责收窄——工作槽仍然隔离共享的元数据库而原先需要w{slot}:前缀和 Redis 数据库号去隔离的缓存键、pub/sub 频道已随 Valkey 的移除而消失。阅读上述每个槽位拿到什么一节时应以这条更新为准Redis 相关的隔离属于该文档描述方案的历史形态当前仓库中对应代码已不再存在。关键文件索引内容路径决策原文本文主体adr/0013-reuse-shared-in-memory-test-databases.mdStartXxx容器辅助函数与共享管道backend/internal/util/testing/containers/postgres.go、mysql.go、mariadb.go、mongodb.go、containers.go每版本编排器模式backend/internal/features/tests/logical/postgresql/backup_restore_test.go物理备份 E2E 按版本拆包pg17/pg18 各自独立并行二进制backend/internal/features/tests/physical/postgresql/pg17/、pg18/工作槽申领与元数据库改写backend/internal/config/config.go并行度、槽位迁移与容器清扫backend/Makefile物理备份测试是这个方案的一个特别注脚pg17 与 pg18 各占一个测试包两个主版本作为彼此隔离、可并行的独立测试二进制运行每个二进制自带自己的控制面和一次性的源库/恢复目标容器——与逻辑测试单包内逐版本串行复用形成互补。小结ADR-0013 给出的是一条可复用的测试基础设施方法论把版本作为容器复用的边界而不是测试用例用 tmpfs 加持久性关闭抹掉一次性服务器的启动成本用go test -p找回并行速度再用 advisory lock 工作槽保证并行包共享控制面时互不串扰。其结果是有界的峰值内存与从超时到分钟级的套件耗时而代价只是两条对测试作者的纪律固定名对象必须DROP ... IF EXISTS复用版本内的子测试不得t.Parallel。赞分享数据库灾备【免费下载链接】databasusPostgreSQL backup tool with Point-In-Time-Recovery and restore verification项目地址https://gitcode.com/gh_mirrors/po/databasus点击查看免费下载相关推荐matcha.css vs 其他CSS框架为什么这个7kB的库值得一试matcha.css vs 其他CSS框架为什么这个7kB的库值得一试 在当今前端开发领域CSS框架的选择往往让人眼花缭乱。从庞大的Bootstrap到轻量数据库灾备高性能无锁共享内存数据库——SimDB深度解析高性能无锁共享内存数据库——SimDB深度解析 项目基础介绍及主要编程语言 SimDB是一款由C11编写的高性能键值存储系统它集成了共享内存、跨平台兼容、Lance 数据库测试指南内存与 IO 用量测试及 Bytehound 内存剖析Lance 数据库测试指南内存与 IO 用量测试及 Bytehound 内存剖析 导读 本文基于 Lance多模态 AI 的开源湖仓格式的 rust/la数据库向量数据库数据湖全文检索上一篇UABEAUnity资源双向编辑系统的技术架构深度解析下一篇高性能跨平台Unity资源双向编辑架构解析从资源提取到完整编辑的技术实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表