- 数据库
- 灾备
【免费下载链接】databasus
PostgreSQL backup tool with Point-In-Time-Recovery and restore verification
本篇以 backend/README.md 为主线,讲解 Databasus 后端(Go + Gin + GORM + PostgreSQL)的完整本地开发闭环:如何用仓库根目录的.env作为统一配置源、如何用make run/make test/make lint启动与验证代码、如何用 goose 管理数据库迁移、如何生成并访问 Swagger 接口文档。读完你可以独立完成一次从拉取代码到跑通全量测试、再手工创建并回滚一条迁移的完整流程,并能对照 backend/Makefile 与 backend/cmd/main.go 理解每条命令背后的真实行为。
一、前置步骤:仓库根目录的 .env 是唯一配置源
README 的第一步要求:把仓库根目录的.env.example复制为根目录下的.env,并注明这是后端、前端和 docker compose 共用的单一配置源(single source for backend, frontend, and docker compose)。
这个"根目录"的位置不是约定俗成,而是由后端代码强制的。backend/internal/config/config.go 的loadEnvVariables()会先定位backend/go.mod所在的目录,再取其父目录作为.env的加载路径:
envPath := filepath.Join(filepath.Dir(backendRoot), ".env") ... if err := godotenv.Load(envPath); err != nil { log.Error("error loading .env file from repo root", "path", envPath, "error", err) logger.ExitAfterFlush(1) }也就是说:即使你从backend/目录或任意子目录启动进程,配置都统一从仓库根目录的.env读取,读不到会直接退出(ExitAfterFlush(1))。加载链是godotenv读文件 →cleanenv.ReadEnv绑定到EnvVariables结构体,整个加载由sync.OnceFunc包裹,进程内只执行一次。
.env.example中的关键配置项可以分为几组:
| 配置项 | 示例值 | 作用 |
|---|---|---|
ENV_MODE | development | 运行模式,代码校验只允许development/production,为空直接报错退出 |
DATABASE_DSN | host=localhost user=postgres password=Q1234567 dbname=databasus port=5437 sslmode=disable | 开发库连接串(Postgres 17,docker compose 中映射到宿主机 5437 端口) |
TEST_DATABASE_DSN | ... port=5438 ... | 测试专用库,make test用它避免污染开发库 |
GOOSE_DBSTRING/GOOSE_TEST_DBSTRING | URL 形式的postgres://... | make migration-up/make migration-down使用的连接串 |
LOG_LEVEL/LOG_FILE_IS_ENABLED/OPEN_TELEMETRY_URL | info/true/ 空 | 日志级别、文件日志开关、OTLP 日志外发地址 |
TEST_PARALLEL_WORKERS | 8 | 测试并行度,决定-p值与测试库分片数量 |
TEST_LOGICAL_POSTGRES_16_PORT | 5004 | 逻辑备份共享测试库端口 |
TEST_PHYSICAL_POSTGRES_17_PORT/TEST_PHYSICAL_POSTGRES_18_PORT | 5007/5008 | 物理备份测试库端口 |
SMTP_HOST等 | test-mailpit/1025 | 开发环境 SMTP,.env.example默认指向本地 mailpit |
DATABASUS_URL | 空 | 用于邮件中的外链域名 |
从源码结构看,还有几个派生路径不需要你配置:config.go会把databasus-data/backups、databasus-data/temp、databasus-data/secret.key固定放在仓库根目录(即backend的上一级),备份文件、临时目录与密钥都落在这里。
.env准备好后,需要把开发依赖容器拉起来。仓库根的 docker-compose.yml 提供了全套开发服务:
backend-db:postgres:17,宿主机${BACKEND_DB_PORT:-5437},对应DATABASE_DSN;backend-db-test:postgres:17,宿主机 5438,专供make test使用;test-logical-postgres-16(5004)、test-physical-postgres-17(5007)、test-physical-postgres-18(5008):测试共享 fixture,其中两个物理备份实例显式设置了wal_level=logical、summarize_wal=on、wal_keep_size=512MB并追加了允许复制度量的pg_hba规则(compose 中的注释解释了原因:Docker 网桥下宿主机连接呈现为桥接子网 IP,默认pg_hba只允许127.0.0.1/32的 replication 连接);wal-backup-postgres-18+wal-backup-loader:持续产生 WAL 的"手动远端 WAL 测试台",loader 侧车每 10 秒插入随机行、每 3 轮执行CHECKPOINT; SELECT pg_switch_wal()。
注意区分:TEST_LOGICAL_POSTGRES_16_PORT等变量指向的是 compose 里"常驻"的 fixture 容器;而按版本的 PG 14–18 逻辑测试、SSL/mTLS 场景则走 testcontainers 临时容器,无需固定端口(见config.go中TestLogicalPostgres16Port字段旁的注释)。
二、运行后端:make run 与端口抢占清理
README 给出启动命令:
make run对应 backend/Makefile 中的定义:
PORT ?= 4005 run: kill-stale go run ./cmd值得细看的是前置目标kill-stale。它的注释解释了动机:一个残留的make run会一直占用 4005 端口并用上一次构建的二进制应答;第二次启动若绑定失败只会记一条错误日志、不提供服务,于是请求会静默打到旧进程上。由于开发容器里没有lsof/fuser,该目标通过/proc自己定位监听者:
- 用
printf ':%04X' $(PORT)把端口转成十六进制,在/proc/net/tcp与/proc/net/tcp6中查找状态为0A(LISTEN)且端口匹配的四元组,拿到 socket 的 inode; - 遍历
/proc/<pid>/fd,找到持有socket:[inode]的进程并kill -9,杀监听进程也会连带结束它的父进程go run。
go run ./cmd启动后,backend/cmd/main.go 的main()按固定顺序完成启动装配,值得按调用链理解一遍:
- 命令行选项解析(
parseCommandLineOptions):支持--test-storage(存取并删除一个本地存储探针,由config.StorageProbeFlagName定义)、--list-admins、--disable-2fa、--new-password+--email(重置密码)等运维入口;另有一个特殊子命令databasus healthcheck(os.Args[1] == "healthcheck"时走runHealthcheckCommand()后退出),docker compose 里databasus-local服务就靠它做健康检查。 - 自动迁移:
runMigrations()用子进程执行goose -dir ./migrations up,并把GOOSE_DRIVER=postgres与.env中的DATABASE_DSN注入子进程环境;失败则进程直接退出。这意味着make run每次启动都会先保证 schema 是最新的,make migration-up只是它的手工等价物。 - 目录准备:
files_utils.EnsureDirectories确保DataFolder与TempFolder(即上文databasus-data下的两个目录)存在。 - 中间件链:
middleware.AssignRequestID()→middleware.LogAccess→ 带日志的 panic recovery →NoStoreCacheControl→ GZIP(排除.png/.jpg/.svg等已压缩扩展名)。 - CORS:仅在
ENV_MODE=development时开启且AllowOrigins: *,并ExposeHeaders回传X-Request-Id。 - 路由注册(
setUpRoutes):所有路由挂在/api/v1下;先挂 Swagger UI 路由v1.GET("/docs/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler));随后注册公开路由(用户认证、healthcheck、版本、agent、逻辑/物理备份的公共路由、验证 agent 路由等),再用users_middleware.AuthMiddleware建立protected组,把 workspaces、disk、notifiers、storages、databases、backups(逻辑/物理)、restores、healthcheck、backup config(逻辑/物理)、audit_logs、user management/settings、verification 等全部受保护路由挂进去。 - 依赖装配:
setUpDependencies()依次调用各 feature 的SetupDependencies()(databases、backups、restores、healthcheck、audit_logs、notifiers、storages、备份配置、physical backup、verification、任务取消、telemetry)。 - 后台任务:
runBackgroundTasks()在一个可取消的context下启动十余个后台协程——逻辑备份调度器、验证调度器、逻辑/物理备份清理器、restore 调度器、healthcheck 执行服务、审计日志清理、下载 token 清理、物理 WAL 流 supervisor、存储文件删除 worker、登录码清理、telemetry 服务;每个都用runWithPanicLogging包一层 recover,把 panic 连同堆栈写入日志而不是让进程崩溃。 - 前端挂载:
mountFrontend把./ui/build作为静态目录,未命中 API 路由的请求回退到index.html(SPA 行为)。backend/ui/readme.md 说明:生产构建时前端产物会放进这个目录。 - 优雅停机:
startServerWithGracefulShutdown监听SIGINT/SIGTERM,给正在处理的请求 10 秒宽限期,之后再FlushAndCloseSinks冲刷日志。
服务地址是写死的serverAddr = ":4005"(main.go 第 70 行),所以 Makefile 里的PORT ?= 4005与 Swagger 文档里的http://localhost:4005是同一个来源。
三、运行测试:make test 的四阶段流水线
README 给出make test作为测试入口。backend/Makefile 中的完整定义远比"跑一遍 go test"复杂,可以拆成四个阶段:
TEST_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_DISABLED=true TEST_PARALLEL_WORKERS=$(TEST_PARALLEL_WORKERS) go test -p=$(TEST_PARALLEL_WORKERS) -count=1 -failfast -timeout 15m ./internal/...clean-testcontainers:按label=org.testcontainers过滤出遗留的 testcontainers 容器并docker rm -f,没有则打印no leftover testcontainers;pull-testcontainers:用grep -rhoE扫描./internal中所有"mysql|mysql|mariadb|mongo|postgres:版本号"以及quay.io/minio/minio镜像引用,sort -u后并发(xargs -P 4)拉取,每个镜像最多重试 3 次,失败则退出。注释还解释了为什么裸镜像名的正则要求 tag 以数字开头:避免物理备份测试里形如postgres:postgres的chown参数被误识别成镜像 tag;cleanup_test_db:独立的小型 Go 程序(backend/cmd/cleanup_test_db/main.go),在迁移前先清理测试库残留;- 分片迁移 + 并行测试:对 0..7 每个 slot,用
sed把GOOSE_TEST_DBSTRING里的库名改写成<库名>_w<i>(例如databasus→databasus_w0…databasus_w7),逐个跑goose up;然后以-p=8 -count=1 -failfast -timeout 15m运行./internal/...全量测试,并禁用 testcontainers 的 Ryuk 清理容器。
为什么是 8 个_wN分片?答案在 backend/internal/config/config.go:
defaultTestParallelWorkers = 8,注释明确要求它必须与go test -p的值一致,"这样每个正在运行的包都能领取一个隔离的元数据库";- 任何可执行文件名以
.test结尾的测试进程会触发claimTestWorkerSlotAndSelectMetadataDatabase():它先连到postgres系统库,从pg_try_advisory_lock(945_000_000 + slot)起顺序尝试抢占会话级咨询锁,抢到哪个 slot 就把自己的 DSN 改写为对应的<dbname>_w<slot>;锁由一个全局持有的*sql.Conn(slotLockConn)锚定到进程退出,注释里专门说明"关闭它(或被 GC)就会释放锁,让 slot 在半路被别的 worker 抢走"; - 抢占有 60 秒的
testSlotClaimTimeout重试窗口,用来吸收go test -p启动下一个包时上一个进程尚未退出的交接间隙;抢不到空闲 slot 会打印提示TEST_PARALLEL_WORKERS must be >= the go test -p value并退出。
这解释了 Makefile 中"先给 8 个 slot 各自跑一遍 goose up"与"测试进程按咨询锁各自认领 slot"的呼应关系:Makefile 负责预建8 套带完整 schema 的分片库,config 层负责在运行时原子地分配它们,使并行包之间互不污染。测试模式下DATABASE_DSN会被TEST_DATABASE_DSN覆盖(env.DatabaseDsn = env.TestDatabaseDsn),这是测试不污染开发库的最后一道保证。
Fedora 环境的 libedit 兼容垫片
Makefile 还针对 Fedora 提供了test-fedora与run-fedora两个目标。背景写在注释里:assets/tools中自带的数据库客户端二进制是 Debian 构建的,链接libedit.so.2——这是 Debian 在下游打补丁产生的 soname;Fedora 发行的是上游的libedit.so.0。两者 ABI 相同,所以libedit-shim目标直接软链改名:
LIBEDIT_SHIM_DIR ?= $(HOME)/.local/lib/databasus-compat libedit-shim: @libedit=/usr/lib64/libedit.so.0; \ [ -e "$$libedit" ] || libedit=$$(ldconfig -p | awk '/libedit\.so\.0 / {print $$NF; exit}'); \ ... ln -sf "$$libedit" $(LIBEDIT_SHIM_DIR)/libedit.so.2之后test-fedora/run-fedora就是带着LD_LIBRARY_PATH=$(LIBEDIT_SHIM_DIR)再执行test/run。因为后端启动时会逐一启动所有捆绑客户端做健康检查(tools.LogAndExitIfClientToolsBroken,在 config 加载末尾调用),所以跑测试和跑服务都需要这个垫片。
四、提交前检查:make lint
README 提示提交前确保安装了golangci-lint,然后执行:
make lint其实现一步到位地做格式化与静态检查:
lint: golangci-lint fmt ./cmd/... ./internal/... && golangci-lint run ./cmd/... ./internal/...即先golangci-lint fmt统一格式,再golangci-lint run跑全部 linter,检查范围覆盖backend/cmd与backend/internal两个模块路径。后端的具体编码规范(控制器组织、依赖注入的位置参数写法、后台服务必须用atomic.Bool防重入、slog 日志上下文规则等)在 backend/AGENTS.md 中有系统说明,lint 正是这些规范的机器化执行。
五、数据库迁移:make migration-create / migration-up / migration-down
README 的 Migrations 一节给出三个命令,全部基于 goose 目录,从20250605090323_init.sql一路排到20260920215925_add_two_factor_auth.sql,覆盖用户、workspace、存储、通知器、审计日志、两因素认证等全部 schema 演进):
# 创建一条迁移(生成带时间戳前缀的 SQL 骨架,之后手工填充内容) make migration-create name=MIGRATION_NAME # 应用迁移 make migration-up # 最近一次迁移失败时回滚 make migration-down对应 Makefile 定义:
migration-create: goose -dir ./migrations create $(name) sql migration-up: goose -dir ./migrations postgres "$(GOOSE_DBSTRING)" up migration-up-test: goose -dir ./migrations postgres "$(GOOSE_TEST_DBSTRING)" up migration-down: goose -dir ./migrations postgres "$(GOOSE_DBSTRING)" down几点实操要点:
name=参数直接透传给goose create $(name) sql,生成backend/migrations/<时间戳>_<name>.sql的 up/down 骨架,README 与 AGENTS 都强调先让工具生成骨架、再手工填充 SQL;migration-up/migration-down走GOOSE_DBSTRING(.env中 URL 形式的连接串),而migration-up-test走GOOSE_TEST_DBSTRING——这条目标没写进 README,但make test流水线里每轮都会对 8 个分片库自动执行等价的goose up;- 手工
migration-up其实只是开发便捷入口:make run启动时runMigrations()(main.go 中)会以子进程方式执行goose -dir ./migrations up并注入GOOSE_DRIVER=postgres与DATABASE_DSN,失败即退出。所以正常开发流里,启动服务本身就会把迁移带到位; - README 只写了
migration-down用于"最近一次迁移失败时回滚",这与 goose 的down语义一致:回退一个已应用的版本。
backend/AGENTS.md 对迁移 SQL 本身有强制风格约束,写迁移时值得对照:仅支持 PostgreSQL;主键 UUID 用gen_random_uuid();时间列必须TIMESTAMPTZ;表、约束、索引必须分开声明(先建表,每个约束/索引独立语句);列类型对齐、约束子句各占一行。
六、Swagger 文档:make swagger 与启动期自动再生成
README 的 Swagger 一节:
# 生成 swagger 文档 make swagger # 文档地址 http://localhost:4005/api/v1/docs/swagger/index.html#/Makefile 中该目标的实现:
swagger: swag init -g ./cmd/main.go -o swagger即以 backend/cmd/main.go 为入口文件扫描注解,把生成物输出到backend/swagger目录。入口文件顶部有全局注解:
// @title Databasus Backend API // @version 1.0 // @description API for Databasus // @host localhost:4005 // @BasePath /api/v1 // @schemes http这就解释了为什么文档 URL 是/api/v1前缀且宿主写死localhost:4005——与serverAddr、setUpRoutes中的v1 := r.Group("/api/v1")完全对齐。main.go通过_ "databasus-backend/swagger"空导入把生成物编译进二进制,运行时经ginSwagger.WrapHandler(swaggerFiles.Handler)挂在/api/v1/docs/swagger/*any。
两个容易踩的坑在源码注释里都写了:
- 文档在第二次启动才可见。
generateSwaggerDocs()(非 production 模式下)会以后台 goroutine 调swag init -d <cwd> -g cmd/main.go -o swagger重新生成,但生成的是 Go 文件,要重启服务才会编译进去。main.go 中generateSwaggerDocs上方的注释原话即:"docs appear after second launch, because Swagger is generated into Go files"; - production 模式不自动生成:
config.GetEnv().EnvMode == EnvModeProduction时直接返回,生产环境依赖构建期已生成的 swagger 包。
七、速查:backend 目录开发命令一览
| 命令 | 实际执行 | 说明 |
|---|---|---|
make run | kill-stale→go run ./cmd | 清理 4005 端口残留监听者后启动后端 |
make test | 清 testcontainers → 拉镜像 → 清测试库 → 8 分片 goose up →go test -p=8 -count=1 -failfast -timeout 15m ./internal/... | 全量测试,库隔离靠_wN分片 + 咨询锁 |
make test-fedora/run-fedora | libedit 垫片 + 上述流程 | Fedora 上绕过libedit.so.2差异 |
make lint | golangci-lint fmt && golangci-lint run(./cmd/..../internal/...) | 提交前检查 |
make migration-create name=X | goose -dir ./migrations create X sql | 生成迁移骨架 |
make migration-up/migration-down | goose ... "$(GOOSE_DBSTRING)" up/down | 手工迁移/回滚 |
make migration-up-test | goose ... "$(GOOSE_TEST_DBSTRING)" up | 对测试库迁移 |
make swagger | swag init -g ./cmd/main.go -o swagger | 重新生成接口文档 |
关键文件索引:命令定义见 backend/Makefile,启动流程与路由见 backend/cmd/main.go,环境加载与测试分片见 backend/internal/config/config.go,配置模板见 .env.example,开发容器拓扑见 docker-compose.yml,编码规范见 backend/AGENTS.md。按照"cp .env.example .env→ 起 compose 依赖 →make run→make test→make lint"的顺序走完,即构成一次标准开发迭代;每次改动 schema 时再叠加make migration-create与make migration-up/down即可。
- 数据库
- 灾备
【免费下载链接】databasus
PostgreSQL backup tool with Point-In-Time-Recovery and restore verification
相关推荐
Gitea 源码开发工作流:从 make build 到 Swagger 校验的完整构建与调试指南
Gitea 源码开发工作流:从 make build 到 Swagger 校验的完整构建与调试指南 Gitea 采用前后端分离的构建体系:Go 编写后端、Typ
后端代码托管研发协作CI/CD从 Stoplight 迁移到 Scalar:OpenAPI 文档、Markdown 指南与工作流的一站式迁移指南
从 Stoplight 迁移到 Scalar:OpenAPI 文档、Markdown 指南与工作流的一站式迁移指南 本指南以 Scalar 开源 API 平台为
开发工具API 工具前端Relay 开发工作流:从配置 Relay Compiler 到生成运行时产物
Relay 开发工作流:从配置 Relay Compiler 到生成运行时产物 本篇技术指南以 Relay v14 文档中的 Workflow 章节为核心,讲解
前端开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考