用共享內(nèi)存測試數(shù)據(jù)庫,平衡測試速度與 CI 內(nèi)存)
數(shù)據(jù)庫災(zāi)備【免費(fèi)下載鏈接】databasusPostgreSQL backup tool with Point-In-Time-Recovery and restore verification項(xiàng)目地址https://gitcode.com/gh_mirrors/po/databasus點(diǎn)擊查看免費(fèi)下載本文基于 Databasus 倉庫中的架構(gòu)決策記錄 ADR-0013講清楚一個(gè)后端測試基礎(chǔ)設(shè)施的核心問題當(dāng)一個(gè)數(shù)據(jù)庫備份工具需要對 PostgreSQL、MySQL、MariaDB、MongoDB 的十幾個(gè)版本逐一做集成測試時(shí)如何同時(shí)滿足測試快和CI 內(nèi)存有界這兩個(gè)互相矛盾的目標(biāo)。讀完本文你將掌握 Databasus 采用的每版本一個(gè)容器、跨測試復(fù)用 數(shù)據(jù)目錄放 RAM 并行包 advisory lock 工作槽隔離四層方案以及 backend/internal/util/testing/containers/ 與 backend/internal/config/config.go 中對應(yīng)的源碼實(shí)現(xiàn)。背景兩種舊方案為什么會(huì)失敗Databasus 的數(shù)據(jù)庫測試會(huì)把同一組檢查跑在每種引擎的多個(gè)版本上。在采納本決策之前團(tuán)隊(duì)先后嘗試過兩套方案都在 16 GB 內(nèi)存的 CI 機(jī)器上失敗了。舊方案一一個(gè)巨型 docker-compose 全版本常駐最初的做法是啟動(dòng)時(shí)把所有引擎的所有版本一次性拉起并且全程保持運(yùn)行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 個(gè)容器同時(shí)存活16 GB CI 機(jī)器直接耗盡內(nèi)存。更糟的是所有測試包共享同一套數(shù)據(jù)庫彼此之間沒有隔離。舊方案二每個(gè)測試一個(gè)全新容器樸素的修復(fù)思路是讓每個(gè)測試獨(dú)立啟動(dòng)自己的數(shù)據(jù)庫、用完即殺test 1 → boot DB → run → kill DB test 2 → boot DB → run → kill DB ... (40 times per package) → too slow數(shù)據(jù)庫冷啟動(dòng)需要 20–60 秒而單個(gè)測試包有 40 多個(gè)測試函數(shù)直接撞上 15 分鐘超時(shí)。由此得到本決策要解決的雙重目標(biāo)測試快、內(nèi)存有界且測試之間互不干擾。決策每個(gè)版本一個(gè)容器跨該版本的所有測試復(fù)用核心思路是把啟動(dòng)一次的代價(jià)從每個(gè)測試攤銷到每個(gè)版本上boot postgres:16 (once) ├─ run test 1 ├─ run test 2 └─ run test 3 shut down postgres:16 → boot postgres:17 ...每個(gè)矩陣包只聲明一次版本列表用外層t.Run循環(huán)版本進(jìn)入版本子測試時(shí)通過StartPostgres/StartMysql等輔助函數(shù)啟動(dòng)服務(wù)器然后把原本獨(dú)立的測試函數(shù)作為內(nèi)層子測試依次跑完。StartXxx會(huì)注冊t.CleanupGo 在版本子測試返回時(shí)執(zhí)行它——因此容器在下一個(gè)版本啟動(dòng)之前就被拆除任意時(shí)刻每個(gè)測試包內(nèi)只有一個(gè)矩陣容器存活。又因?yàn)槊總€(gè)go test包是獨(dú)立進(jìn)程其容器只屬于自己。源碼印證每版本編排器模式這一模式在邏輯備份測試中的落地見 backup_restore_test.go。外層按版本循環(huán)注釋中直接引用了 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, ...) }) // …其余備份/恢復(fù)測試函數(shù)均為內(nèi)層子測試 }) } }容器生命周期由 containers.go 中的startContainer統(tǒng)一托管啟動(dòng)成功后立即注冊t.Cleanup子測試結(jié)束時(shí)調(diào)用container.Terminate保證容器只活過創(chuàng)建它的那個(gè)測試這一語義。測試作者的一條硬規(guī)則由于同一版本下服務(wù)器被多個(gè)子測試復(fù)用、且子測試按順序執(zhí)行凡創(chuàng)建固定名稱對象不帶隨機(jī)后綴的表、用戶的測試必須以DROP ... IF EXISTS開頭。同一版本內(nèi)的子測試之間沒有隔離這條規(guī)則是復(fù)用方案能夠安全成立的前提。讓每次啟動(dòng)都更快數(shù)據(jù)庫文件放進(jìn) RAM第二個(gè)加速手段是把每個(gè)引擎的數(shù)據(jù)目錄掛到 tmpfs 上而不是 overlay 文件系統(tǒng)——這樣 fsync 密集的冷啟動(dòng)initdb和寫密集的物理恢復(fù)全部跑在內(nèi)存上。tmpfs 掛載與固定容量在 containers.go 中這個(gè)選項(xiàng)被集中定義為一處常量// 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這里有兩個(gè)工程細(xì)節(jié)值得注意為什么必須釘死size512mDocker 的 tmpfs 默認(rèn)容量是宿主內(nèi)存的一半。在go test -pN下每個(gè)包都會(huì)起容器若按默認(rèn)值預(yù)留幾個(gè)并行容器就把宿主的可保留內(nèi)存吃光了。512m 遠(yuǎn)大于任何測試夾具而 tmpfs 只按實(shí)際寫入的字節(jié)數(shù)占內(nèi)存所以固定值既安全又夠用。每個(gè)引擎的掛載點(diǎn)不同PostgreSQL 掛/var/lib/postgresql/dataMySQL 家族掛/var/lib/mysqlMongoDB 掛/data/db見 mysql.go、mariadb.go、mongodb.go。PostgreSQL 還有一個(gè)版本相關(guān)的坑18 起官方鏡像把 PGDATA 和數(shù)據(jù)卷上移到了/var/lib/postgresql若仍按 14–17 的舊路徑掛 tmpfs會(huì)在舊路徑留下一個(gè)空目錄被 entrypoint 的布局檢測直接拒絕服務(wù)器在就緒前就退出了。這段處理見 postgres.go 的postgresDataDir它按鏡像 tag 解析主版本號(hào)PostgresMajorVersion18 及以上返回父卷路徑不可解析的 tag 回退到 18 之前的路徑。關(guān)閉持久性為一次性服務(wù)器換取速度因?yàn)闇y試服務(wù)器用完即棄所有引擎都關(guān)閉了崩潰安全相關(guān)的持久性開關(guān)引擎關(guān)閉的參數(shù)源碼位置PostgreSQLfsyncoff、full_page_writesoff、synchronous_commitoffpostgres.gopostgresRequestMySQL / MariaDBinnodb-flush-log-at-trx-commit0、innodb-doublewrite0、sync-binlog0、skip-log-binmysql.gomysqlFamilyCmdmysqlFamilyCmd同時(shí)攜帶 utf8mb4 字符集默認(rèn)值MySQL 與 MariaDB 共用mysql.go 還會(huì)對mysql:8.0特例追加--default-authentication-pluginmysql_native_password8.4 起該插件被移除后續(xù)版本不追加。就緒判定比端口通了更嚴(yán)格并行啟動(dòng)多個(gè)容器時(shí)CPU 爭搶下冷啟動(dòng)遠(yuǎn)長于獨(dú)占時(shí)間因此啟動(dòng)超時(shí)給得很寬PostgreSQL 240 秒postgres.go、MySQL/MariaDB 300 秒mysql.go——注釋說明快速主機(jī)就緒即返回這個(gè)上限沒有額外成本。就緒策略本身也做了針對性設(shè)計(jì)。PostgreSQL 的 entrypoint 會(huì)先起一個(gè)僅 socket 的臨時(shí)服務(wù)器做 initdb然后重啟真正的服務(wù)器ready to accept connections 日志會(huì)出現(xiàn)兩次postgresReady 用WithOccurrence(2)等待第二次出現(xiàn)并疊加端口監(jiān)聽避免與 socket-only 臨時(shí)服務(wù)器競態(tài)。MySQL 側(cè)則干脆數(shù)日志行數(shù)不可靠臨時(shí) initdb 服務(wù)器、MySQL 8.x 的 X Plugin 都會(huì)額外打日志改為wait.ForSQL完成一次真實(shí)的 root 握手并選擇 entrypoint 創(chuàng)建的數(shù)據(jù)庫從而同時(shí)證明端口可連且 initdb 已完成mysqlFamilyReady。并行跑測試包并用工作槽隔離每個(gè)包第三個(gè)手段是把速度找回來讓測試包并行執(zhí)行。入口在 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/...要點(diǎn)-p8讓 8 個(gè)包同時(shí)跑。由于每個(gè)包任意時(shí)刻只有一個(gè)矩陣容器存活峰值內(nèi)存被釘在約 10–11 GBADR-0013 給出的數(shù)據(jù)而不是舊方案中所有容器同時(shí)存活的無界狀態(tài)make test會(huì)先為 0..N-1 號(hào)槽位各自建庫并跑一次 goose 遷移——用sed把測試 DSN 里的庫名改寫成base_wi這正是每個(gè)工作槽位私有元數(shù)據(jù)庫的來源clean-testcontainers通過labelorg.testcontainers過濾殘留容器并強(qiáng)制刪除兜底-timeout或os.Exit時(shí)t.Cleanup被跳過的情形pull-testcontainers則用grep掃出源碼里所有測試鏡像名并行拉取避免并行包同時(shí)拉鏡像互相拖慢。工作槽的申領(lǐng)Postgres advisory lock8 個(gè)包同時(shí)啟動(dòng)共享同一套元數(shù)據(jù) Postgres 與備份基礎(chǔ)設(shè)施誰來分配我是幾號(hào)工人config.go 給出了答案每個(gè)真實(shí)的go test二進(jìn)制進(jìn)程啟動(dòng)時(shí)在系統(tǒng)庫上按序嘗試pg_try_advisory_lock(base slot)拿到第一個(gè)空閑槽位并把持有鎖的連接錨在全局變量slotLockConn上直到進(jìn)程退出——注釋明確說明連接一旦被關(guān)閉或 GC鎖就會(huì)釋放、槽位會(huì)被中途搶走所以這個(gè)只賦值的引用本身就是 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) 內(nèi)每個(gè)槽位執(zhí)行 // SELECT pg_try_advisory_lock($1) -- $1 945_000_000 slot // 命中即返回 slot否則每 100ms 重試直到 testSlotClaimTimeout60s }幾個(gè)參數(shù)值得留意見 config.go鎖鍵基址testSlotAdvisoryLockBase 945_000_000槽位 N 用baseN與業(yè)務(wù) advisory lock 空間錯(cuò)開testSlotClaimTimeout 60 * time.Second專門吸收go test -p交接窗口——下一個(gè)包啟動(dòng)時(shí)上一個(gè)進(jìn)程可能還沒退出、鎖還沒釋放若 60 秒仍無空閑槽位直接報(bào)錯(cuò)并提示TEST_PARALLEL_WORKERS must be the go test -p value即槽池大小必須不小于并行度defaultTestParallelWorkers 8的注釋強(qiáng)調(diào)它必須與go test -p的取值一致否則并發(fā)的包無法各自領(lǐng)到獨(dú)立庫只有可執(zhí)行文件名含.test的進(jìn)程才申領(lǐng)槽位cleanup_test_db等運(yùn)維工具保留基礎(chǔ) DSN需要對所有槽位操作config.go。每個(gè)槽位拿到什么claimTestWorkerSlotAndSelectMetadataDatabase領(lǐng)到槽位后把 DSN 中的庫名改寫為base_w{slot}config.go隨后該包內(nèi)的一切共享狀態(tài)都落在自己的槽位里。按 ADR-0013 的描述槽位提供了自己的元數(shù)據(jù)庫命名…_w{slot}自己的Valkey/Redis 數(shù)據(jù)庫即槽位序號(hào)自己的緩存前綴w{slot}:該前綴同時(shí)給備份節(jié)點(diǎn)注冊表 backups/backups/backuping/nodes/registry.go 中的每個(gè) Redis 鍵與 pub/sub 頻道打標(biāo)。于是兩個(gè)并排的包絕不會(huì)互相觸碰對方的數(shù)據(jù)庫、緩存條目、備份節(jié)點(diǎn)或 pub/sub 頻道。被否決的備選方案ADR-0013 的 Alternatives considered 一節(jié)完整記錄了否決理由這里保持原樣繼承備選方案否決理由docker-compose 全版本同時(shí)啟動(dòng)約 50 個(gè)容器在 16 GB CI 上耗盡內(nèi)存且所有包共享同一套數(shù)據(jù)庫、無隔離每個(gè)測試一個(gè)全新容器每個(gè)包 40 次冷啟動(dòng) × 20–60s → 觸發(fā) 15 分鐘超時(shí)數(shù)據(jù)庫文件放磁盤而非 RAM慢的環(huán)節(jié)恰恰是冷啟動(dòng)與寫密集恢復(fù)時(shí)的 fsync對反正會(huì)丟的服務(wù)器放 RAM 直接消除該開銷影響與約束正面收益原先兩次超時(shí)于 900 秒的包現(xiàn)在約 30 秒跑完整個(gè)測試套件從直接超時(shí)變?yōu)榧s 1.5–4 分鐘完成ADR-0013 Consequences峰值內(nèi)存有界且可預(yù)測——每個(gè)包一個(gè)矩陣容器并行的包之間完全隔離。代價(jià)與使用約束由于服務(wù)器在版本內(nèi)被多個(gè)子測試復(fù)用作者必須對固定名稱對象寫DROP ... IF EXISTS并且這些子測試不得加t.Parallel——同一版本內(nèi)它們是順序執(zhí)行的RAM 中的數(shù)據(jù)易失對一次性測試服務(wù)器而言可以接受但這些容器沒有任何崩潰安全若 CI runner 內(nèi)存不足應(yīng)調(diào)低TEST_PARALLEL_WORKERS例如設(shè)為 6。中性項(xiàng)與兜底在硬-timeout或os.Exit場景下t.Cleanup會(huì)被跳過殘留容器由 Makefile/CI 的 label 清掃即上文clean-testcontainers的org.testcontainers過濾兜底。后續(xù)演進(jìn)Valkey 替換說明2026-09-06ADR-0013 文末帶有更新說明基于 Valkey 的測試狀態(tài)已被進(jìn)程內(nèi)緩存、進(jìn)程內(nèi) publish-subscribe 提供者和進(jìn)程內(nèi)限流提供者取代每個(gè)測試二進(jìn)制現(xiàn)在自己擁有那份短暫狀態(tài)。這意味著槽位機(jī)制的職責(zé)收窄——工作槽仍然隔離共享的元數(shù)據(jù)庫而原先需要w{slot}:前綴和 Redis 數(shù)據(jù)庫號(hào)去隔離的緩存鍵、pub/sub 頻道已隨 Valkey 的移除而消失。閱讀上述每個(gè)槽位拿到什么一節(jié)時(shí)應(yīng)以這條更新為準(zhǔn)Redis 相關(guān)的隔離屬于該文檔描述方案的歷史形態(tài)當(dāng)前倉庫中對應(yīng)代碼已不再存在。關(guān)鍵文件索引內(nèi)容路徑?jīng)Q策原文本文主體adr/0013-reuse-shared-in-memory-test-databases.mdStartXxx容器輔助函數(shù)與共享管道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 各自獨(dú)立并行二進(jìn)制backend/internal/features/tests/physical/postgresql/pg17/、pg18/工作槽申領(lǐng)與元數(shù)據(jù)庫改寫backend/internal/config/config.go并行度、槽位遷移與容器清掃backend/Makefile物理備份測試是這個(gè)方案的一個(gè)特別注腳pg17 與 pg18 各占一個(gè)測試包兩個(gè)主版本作為彼此隔離、可并行的獨(dú)立測試二進(jìn)制運(yùn)行每個(gè)二進(jìn)制自帶自己的控制面和一次性的源庫/恢復(fù)目標(biāo)容器——與邏輯測試單包內(nèi)逐版本串行復(fù)用形成互補(bǔ)。小結(jié)ADR-0013 給出的是一條可復(fù)用的測試基礎(chǔ)設(shè)施方法論把版本作為容器復(fù)用的邊界而不是測試用例用 tmpfs 加持久性關(guān)閉抹掉一次性服務(wù)器的啟動(dòng)成本用go test -p找回并行速度再用 advisory lock 工作槽保證并行包共享控制面時(shí)互不串?dāng)_。其結(jié)果是有界的峰值內(nèi)存與從超時(shí)到分鐘級(jí)的套件耗時(shí)而代價(jià)只是兩條對測試作者的紀(jì)律固定名對象必須DROP ... IF EXISTS復(fù)用版本內(nèi)的子測試不得t.Parallel。贊分享數(shù)據(jù)庫災(zāi)備【免費(fèi)下載鏈接】databasusPostgreSQL backup tool with Point-In-Time-Recovery and restore verification項(xiàng)目地址https://gitcode.com/gh_mirrors/po/databasus點(diǎn)擊查看免費(fèi)下載相關(guān)推薦matcha.css vs 其他CSS框架為什么這個(gè)7kB的庫值得一試matcha.css vs 其他CSS框架為什么這個(gè)7kB的庫值得一試 在當(dāng)今前端開發(fā)領(lǐng)域CSS框架的選擇往往讓人眼花繚亂。從龐大的Bootstrap到輕量數(shù)據(jù)庫災(zāi)備高性能無鎖共享內(nèi)存數(shù)據(jù)庫——SimDB深度解析高性能無鎖共享內(nèi)存數(shù)據(jù)庫——SimDB深度解析 項(xiàng)目基礎(chǔ)介紹及主要編程語言 SimDB是一款由C11編寫的高性能鍵值存儲(chǔ)系統(tǒng)它集成了共享內(nèi)存、跨平臺(tái)兼容、Lance 數(shù)據(jù)庫測試指南內(nèi)存與 IO 用量測試及 Bytehound 內(nèi)存剖析Lance 數(shù)據(jù)庫測試指南內(nèi)存與 IO 用量測試及 Bytehound 內(nèi)存剖析 導(dǎo)讀 本文基于 Lance多模態(tài) AI 的開源湖倉格式的 rust/la數(shù)據(jù)庫向量數(shù)據(jù)庫數(shù)據(jù)湖全文檢索上一篇UABEAUnity資源雙向編輯系統(tǒng)的技術(shù)架構(gòu)深度解析下一篇高性能跨平臺(tái)Unity資源雙向編輯架構(gòu)解析從資源提取到完整編輯的技術(shù)實(shí)現(xiàn)創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考