☰
Databasus 后端开发工作流:从 .env 配置、make 运行测试,到 goose 迁移与 Swagger 文档
2026/9/25 2:34:45 网站建设 项目流程
  • 数据库
  • 灾备

【免费下载链接】databasus

PostgreSQL backup tool with Point-In-Time-Recovery and restore verification

项目地址:https://gitcode.com/gh_mirrors/po/databasus
点击查看免费下载

本篇以 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_MODEdevelopment运行模式,代码校验只允许development/production,为空直接报错退出
DATABASE_DSNhost=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_DBSTRINGURL 形式的postgres://...make migration-up/make migration-down使用的连接串
LOG_LEVEL/LOG_FILE_IS_ENABLED/OPEN_TELEMETRY_URLinfo/true/ 空日志级别、文件日志开关、OTLP 日志外发地址
TEST_PARALLEL_WORKERS8测试并行度,决定-p值与测试库分片数量
TEST_LOGICAL_POSTGRES_16_PORT5004逻辑备份共享测试库端口
TEST_PHYSICAL_POSTGRES_17_PORT/TEST_PHYSICAL_POSTGRES_18_PORT5007/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自己定位监听者:

  1. 用printf ':%04X' $(PORT)把端口转成十六进制,在/proc/net/tcp与/proc/net/tcp6中查找状态为0A(LISTEN)且端口匹配的四元组,拿到 socket 的 inode;
  2. 遍历/proc/<pid>/fd,找到持有socket:[inode]的进程并kill -9,杀监听进程也会连带结束它的父进程go run。

go run ./cmd启动后,backend/cmd/main.go 的main()按固定顺序完成启动装配,值得按调用链理解一遍:

  1. 命令行选项解析(parseCommandLineOptions):支持--test-storage(存取并删除一个本地存储探针,由config.StorageProbeFlagName定义)、--list-admins、--disable-2fa、--new-password+--email(重置密码)等运维入口;另有一个特殊子命令databasus healthcheck(os.Args[1] == "healthcheck"时走runHealthcheckCommand()后退出),docker compose 里databasus-local服务就靠它做健康检查。
  2. 自动迁移:runMigrations()用子进程执行goose -dir ./migrations up,并把GOOSE_DRIVER=postgres与.env中的DATABASE_DSN注入子进程环境;失败则进程直接退出。这意味着make run每次启动都会先保证 schema 是最新的,make migration-up只是它的手工等价物。
  3. 目录准备:files_utils.EnsureDirectories确保DataFolder与TempFolder(即上文databasus-data下的两个目录)存在。
  4. 中间件链:middleware.AssignRequestID()→middleware.LogAccess→ 带日志的 panic recovery →NoStoreCacheControl→ GZIP(排除.png/.jpg/.svg等已压缩扩展名)。
  5. CORS:仅在ENV_MODE=development时开启且AllowOrigins: *,并ExposeHeaders回传X-Request-Id。
  6. 路由注册(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 等全部受保护路由挂进去。
  7. 依赖装配:setUpDependencies()依次调用各 feature 的SetupDependencies()(databases、backups、restores、healthcheck、audit_logs、notifiers、storages、备份配置、physical backup、verification、任务取消、telemetry)。
  8. 后台任务:runBackgroundTasks()在一个可取消的context下启动十余个后台协程——逻辑备份调度器、验证调度器、逻辑/物理备份清理器、restore 调度器、healthcheck 执行服务、审计日志清理、下载 token 清理、物理 WAL 流 supervisor、存储文件删除 worker、登录码清理、telemetry 服务;每个都用runWithPanicLogging包一层 recover,把 panic 连同堆栈写入日志而不是让进程崩溃。
  9. 前端挂载:mountFrontend把./ui/build作为静态目录,未命中 API 路由的请求回退到index.html(SPA 行为)。backend/ui/readme.md 说明:生产构建时前端产物会放进这个目录。
  10. 优雅停机: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/...
  1. clean-testcontainers:按label=org.testcontainers过滤出遗留的 testcontainers 容器并docker rm -f,没有则打印no leftover testcontainers;
  2. 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;
  3. cleanup_test_db:独立的小型 Go 程序(backend/cmd/cleanup_test_db/main.go),在迁移前先清理测试库残留;
  4. 分片迁移 + 并行测试:对 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。

两个容易踩的坑在源码注释里都写了:

  1. 文档在第二次启动才可见。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";
  2. production 模式不自动生成:config.GetEnv().EnvMode == EnvModeProduction时直接返回,生产环境依赖构建期已生成的 swagger 包。

七、速查:backend 目录开发命令一览

命令实际执行说明
make runkill-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-fedoralibedit 垫片 + 上述流程Fedora 上绕过libedit.so.2差异
make lintgolangci-lint fmt && golangci-lint run(./cmd/..../internal/...)提交前检查
make migration-create name=Xgoose -dir ./migrations create X sql生成迁移骨架
make migration-up/migration-downgoose ... "$(GOOSE_DBSTRING)" up/down手工迁移/回滚
make migration-up-testgoose ... "$(GOOSE_TEST_DBSTRING)" up对测试库迁移
make swaggerswag 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

项目地址:https://gitcode.com/gh_mirrors/po/databasus
点击查看免费下载

相关推荐

上一篇:国家中小学智慧教育平台电子课本终极下载指南:一键批量获取PDF教程
下一篇:PostgreSQL高可用性与备份恢复工具全解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询