本文主要说明一类自托管媒体库(以 Octans 后端为例)在选用 PostgreSQL 作为可选数据库 Provider 时,如何完成安全部署停服双向迁移部署冒烟;同时划清破坏性重建(删除并重建整个业务库)的适用场景、数据影响与「无备份、无窗口计划绝不在生产执行」的边界。密码与主机一律占位。

两套流程容易被混成「升级就是重建」。实际上:

流程是否保留业务数据典型用途生产可用性
安全部署 / schema migration保留(空库建表或在已有库上升级 schema)新部署、Provider 切换后的正式接入可按变更窗口执行
停服迁移(SQLite ↔ Postgres)保留(工具复制业务表)从默认 SQLite 切到 Postgres,或回切可按变更窗口执行
破坏性重建DROP DATABASE不保留0.x 测试库、明确可清空的预发 / 临时实例默认禁止;无备份与回滚计划不得执行

下面先讲安全路径,再单独写破坏性重建,避免命令串台。

边界与前提

  • 默认 Provider 仍是 SQLite;PostgreSQL 必须显式配置 Database__Provider=Postgres
  • Provider 切换是停服操作,不支持在线切库。
  • SQLite 与 PostgreSQL 之间的双向迁移走专用迁移工具;目标库必须先用目标 Provider 的 migration 建好 schema,且业务表为空;工具失败后不会自动清理目标库。
  • 破坏性重建不是数据迁移,也不是版本升级的默认路径。
  • 生产密码不要写进仓库;优先环境变量、systemd drop-in、容器 secret 或平台 secret 注入。

版本底线(以当前项目支持矩阵为准,落地前请对照目标版本发布说明):

PostgreSQL 版本状态说明
16支持底线最低支持 major
17 / 18支持通常纳入本地部署 smoke 矩阵
15 及以下不支持启动期应 fail-fast

新 major 一般不会在 day 0 自动宣称支持;常见策略是等到 x.1 / x.2,并通过迁移、启动、扫库、任务查询等冒烟后再扩矩阵。

安全部署:配置与接入

环境变量示例

export Database__Provider="Postgres"
export Database__Postgres__Url="postgresql://octans:<password>@<pg-host>:5432/octans"
export Database__Postgres__SslMode="Prefer"

appsettings.Prod.json 结构示例

{
  "Database": {
    "Provider": "Postgres",
    "Postgres": {
      "Url": "postgresql://octans:<password>@<pg-host>:5432/octans",
      "SslMode": "Prefer"
    }
  }
}

生产环境不要把真实密码提交到仓库。

systemd drop-in(推荐避免密码进主 unit)

[Service]
Environment=Database__Provider=Postgres
Environment="Database__Postgres__Url=postgresql://octans:<password>@127.0.0.1:5432/octans"
Environment=Database__Postgres__SslMode=Prefer

更稳妥的做法是把连接串放进仅 root 可读的 env 文件(例如 /etc/octans/octans-db.env,权限 0600),由 drop-in 引用,再:

sudo systemctl daemon-reload
sudo systemctl restart octans.service
sudo journalctl -u octans.service -f

Docker Compose environment 示例

services:
  octans:
    environment:
      Database__Provider: Postgres
      Database__Postgres__Url: "postgresql://octans:<password>@postgres:5432/octans"
      Database__Postgres__SslMode: Prefer

服务端启用 TLS 时,SslMode 不要随便写 Disable;按实际策略使用 Prefer / Require / VerifyCA / VerifyFull

新部署推荐步骤

  1. 准备 PostgreSQL 16+,创建业务库 octans 与专用账号(owner)。
  2. 确认编码 / locale / 时区策略(开发容器常见:UTF8C.UTF-8UTC)。
  3. 停止后端
  4. 配置 Database__Provider=PostgresDatabase__Postgres__UrlDatabase__Postgres__SslMode
  5. 对目标库执行 EF migration(上下文为 PostgreSQL 对应 DbContext)。
  6. 启动后端,确认日志无 Provider / 版本 / 连接错误。
  7. 登录并做一次扫库 / 任务查询冒烟。

手工执行 migration 的示意(端口与路径按环境替换):

Database__Provider=Postgres \
Database__Postgres__Url="postgresql://octans:<password>@127.0.0.1:<pg-port>/octans" \
Database__Postgres__SslMode="Disable" \
dotnet ef database update --context PostgresAppDbContext -- --environment Dev

停服迁移:SQLite ↔ PostgreSQL

这是保留数据的正式切换路径。所有命令都应在服务停止后执行;迁移期间不要同时启动后端或后台 worker。

从 SQLite 切到 PostgreSQL

  1. 停止后端。
  2. 备份当前 SQLite 库、配置目录与媒体元数据目录;不要在原库上做破坏性试验。
  3. 准备空业务库的 PostgreSQL 16+ 实例。
  4. 对目标库只跑 schema migration,不复制业务数据。
  5. 使用发布产物的迁移工具(正式演练优先自包含发布,而不是临时 dotnet run)。
  6. dry-run:若目标非空、有 pending migration、活动任务、未完成恢复、运行时锁或字段扫描失败,先处理。
  7. migrate(建议 --search-index-mode rebuild),完成后看自动 / 独立 verify 报告并归档。
  8. 切换 Provider 与连接配置,启动后端并冒烟。

连接串示意(密码占位):

Host=<pg-host>;Port=5432;Database=octans;Username=octans;Password=<password>;Application Name=octans-migration

不建议用手工 SQL 拼接替代迁移工具:Guid、boolean、瞬时点 / 日历日期、搜索派生字段、计算列与 sequence 等语义需要工具显式处理。

瞬时点策略注意:

  • reject:拒绝没有明确 UTC kind / offset 的瞬时点。
  • assume-utc:仅在确认历史值应按 UTC 解释时使用,并保留迁移报告。
  • 历史 SQLite 库通常先 dry-run --instant-timestamp-policy reject 看阻断项。

从 PostgreSQL 回切到 SQLite

同样是停服操作:备份 Postgres → 准备新的 SQLite 文件路径(不覆盖可回滚备份)→ 空 schema migration → dry-run / migrate / verify → 切回 Database__Provider=Sqlite → 启动与冒烟。

连接池与超时(安全运维侧)

应用侧通常直接使用 Npgsql 连接池。Application Name 建议固定(例如 octans),便于 pg_stat_activity 过滤。

计算连接上限至少满足:

实例数 * 每实例 Maximum Pool Size + 运维连接 + 备份/监控连接 <= PostgreSQL max_connections

不要把 Maximum Pool Size 单独调大来掩盖连接泄漏或慢查询。Timeout / Command Timeout 是暴露问题的边界,不是吞错降级策略。

连接池排查可先过滤应用名:

select
  pid,
  usename,
  application_name,
  state,
  wait_event_type,
  wait_event,
  now() - xact_start as xact_age,
  left(query, 160) as query
from pg_stat_activity
where application_name = 'octans'
order by xact_start nulls last, backend_start;

部署冒烟

本地 / 预发可用部署冒烟脚本做冷启链路(启动 PG → migration → 冷启后端 → 登录 → 固定 smoke 媒体库 → 扫库 → 任务 Completed)。注意:

  • 脚本会冷启后端,运行前需避免端口冲突;可换空闲 API base 或先停现有实例。
  • 需要本机 ffmpeg 生成可被 MediaInfo 识别的短视频 fixture。
  • smoke 媒体库通常会被复用固定名称,配置漂移时应 fail-fast,而不是静默改写。

破坏性重建:定义、影响与禁区

什么叫破坏性重建

这里的「破坏性重建」指:

  1. 停止应用(容器或 systemd 服务)。
  2. 删除 PostgreSQL 中已有的业务库(例如 octans)。
  3. 重新创建同名空库并修正 public schema 权限。
  4. 启动新版本应用,依赖启动期 migration 重建 schema。

不保留任何数据库数据。适合:

  • 0.x 阶段测试库;
  • 预发 / 开发库;
  • 已书面确认「数据可丢」的临时实例。

何时绝不执行(或必须先有计划)

在下列任一条件下,不要把破坏性重建当作升级手段:

  • 库中存在不可再生的账号、权限、媒体库配置、刮削结果、播放进度、收藏、任务历史等。
  • 没有可验证的 pg_dump / 物理备份与恢复演练记录。
  • 没有变更窗口、回滚负责人与「重建失败如何回到旧库」的书面步骤。
  • 生产或准生产环境,且业务未明确签字允许清空。
  • 只是「想升级 schema」——应走 migration 或停服迁移工具,而不是 DROP DATABASE

一句话:破坏性重建默认禁止上生产;没有备份与回滚计划,就没有执行资格。

影响范围

执行 DROP DATABASE 后会全部丢失(非穷尽):

  • 用户账号、登录会话与权限;
  • 媒体库配置、根路径、排除规则与库权限;
  • 扫描出的媒体项、文件、音视频 / 字幕轨与资产关系;
  • 刮削结果、provider 缓存、评分与演职员关系;
  • 配置中心写入数据库的值(含 metadata provider API key);
  • 播放进度、收藏、稍后看、任务历史与任务日志。

通常不会被该流程直接删除:

  • 宿主机上的真实媒体文件;
  • 应用数据 volume 中的非 PostgreSQL 文件(元数据缓存、播放缓存、日志等);
  • PostgreSQL 服务中的其他数据库。

注意:库清空后,volume 里旧图片 / 缓存可能变成孤立文件;本文只处理数据库,不顺带清数据目录。

执行前强制检查清单

  1. 确认目标:host / port / 库名 / owner 与变更单一致(写错库名的代价是整库清空)。
  2. 确认数据可丢,或已完成并校验备份。
  3. 停止应用,确认无业务连接占用目标库。
  4. 维护账号与 ~/.pgpass(权限 0600)准备就绪;密码不进文档与仓库。

环境变量示意(主机占位):

export PGHOST=<pg-host>
export PGPORT=5432
export PGUSER=postgres
export PGMAINTDB=postgres
export OCTANS_DB=octans
export OCTANS_OWNER=octans
pg_isready -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$OCTANS_DB"

确认库与角色存在、再确认连接占用:

-- 在维护库上查询目标库大小与 owner
SELECT datname,
       pg_get_userbyid(datdba) AS owner,
       pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database
WHERE datname = 'octans';

-- 查占用连接
SELECT pid, usename, application_name, client_addr, state, query_start
FROM pg_stat_activity
WHERE datname = 'octans'
ORDER BY backend_start;

若仍有残留连接:先确认应用已停,再 pg_terminate_backend 终止目标库上非本会话连接。

应用已停示例(Docker):

docker ps --filter name=octans
docker stop octans   # 仍 Up 时

删除与重建(高危命令区)

以下命令会永久删除业务库。仅在检查清单全部勾选后执行。

# DROP:确认 :db 变量无误
psql -X -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGMAINTDB" \
  -v ON_ERROR_STOP=1 -v db="$OCTANS_DB" <<'SQL'
SELECT format('DROP DATABASE IF EXISTS %I', :'db')
\gexec
SQL

# 确认 0 rows
psql -X -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGMAINTDB" \
  -v ON_ERROR_STOP=1 -v db="$OCTANS_DB" \
  -c "SELECT datname FROM pg_database WHERE datname = :'db';"

重建空库并保证应用 owner 可在 public 建表:

psql -X -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGMAINTDB" \
  -v ON_ERROR_STOP=1 -v db="$OCTANS_DB" -v owner="$OCTANS_OWNER" <<'SQL'
SELECT format('CREATE DATABASE %I OWNER %I', :'db', :'owner')
\gexec
SQL

psql -X -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$OCTANS_DB" \
  -v ON_ERROR_STOP=1 -v owner="$OCTANS_OWNER" <<'SQL'
SELECT format('ALTER SCHEMA public OWNER TO %I', :'owner')
\gexec
SELECT format('GRANT USAGE, CREATE ON SCHEMA public TO %I', :'owner')
\gexec
SQL

确认 public 尚无业务表后再启动应用。

启动应用与验证

镜像若默认 Database__ApplyMigrationsOnStartup=true,空库启动时会在监听 HTTP 前做 Provider 校验与 EF migration。关键环境变量:

Database__Provider=Postgres
Database__Postgres__Url=postgresql://octans:<password>@<pg-host>:5432/octans
Database__Postgres__SslMode=<Prefer|Require|Disable|...>

验证要点:

  1. 应用日志:校验与 migration 成功,再进入 Web 服务。
  2. ready 接口(端口按部署替换):curl --fail http://127.0.0.1:<listen-port>/api/ready
  3. __EFMigrationsHistory 中有当前版本预期 migration id。
  4. public 下业务表已创建;表数量不是长期契约,重点是 migration 成功、ready 正常、可进入初始化流程。

重建后的业务动作

破坏性重建后系统回到「全新安装」:

  1. Web 初始化,创建 root。
  2. 重新填写 metadata provider API key 等配置中心项。
  3. 重新创建媒体库与排除规则。
  4. 重新扫库 / 按需刮削。
  5. 旧账号不可用;媒体文件本身未被 DROP DATABASE 删除,但与库记录的绑定需重建。

常见问题速查

现象优先排查
启动提示仅支持 PostgreSQL 16.0+URL 是否指错实例;select version();;升级服务端,勿绕过启动校验
DROP DATABASE 报 database is being accessed应用是否已停;pg_stat_activitypg_terminate_backend
容器 / 服务 unhealthyURL / 密码 / SslMode / owner 的 public 建表权限 / 镜像 tag;首次 migration 是否超过 healthcheck start_period
pending migration 未自动应用是否把 Database__ApplyMigrationsOnStartup 显式改成了 false
连接池耗尽长事务、扫库/刮削并发、实例数 × pool 是否超过 max_connections
配置像「生效了却不对」环境变量优先级高于 appsettings;systemd / 容器里是否残留旧变量

配置不匹配的常见组合:

  • Provider=Postgres 但 URL 未配或指错环境;
  • SslMode
  • Provider=Sqlite 仍残留 Postgres 环境变量,排障时误判配置来源。

实践建议(收口)

  1. 先分清路径:要保留数据 → migration / 迁移工具;可丢测试数据 → 才考虑破坏性重建。
  2. 任何删库命令前再读一遍 host、库名、变更单与备份校验结果。
  3. 生产变更:备份 → 停服 → 变更 → 冒烟 → 保留回滚窗口;把迁移 / verify 报告当变更记录存档。
  4. 文档与工单中统一使用 <password><pg-host> 等占位符,避免粘贴真实连接串。

相关阅读

同系列还可对照:

来源

本文合并改写自 Octans 项目 usage 层《后端 PostgreSQL 部署指南》与《PostgreSQL 破坏性重建指南》。命令与路径已通用化;主机、密码与镜像仓库已脱敏为占位符;具体支持版本与 migration id 以目标环境当前发布说明为准。