数据库

网关用 PostgreSQL 保存请求日志、用户账号、API key,以及所有需要在重启后保留的管理设置和运行时设置。本页介绍存了哪些数据、表结构是怎么建出来的,以及如何备份、恢复和重置。

一定要有数据库吗?

不用。DB_ENABLED 默认为 true,但用 DB_ENABLED=false 启动的网关照样能正常路由请求,只是没有请求历史、没有用户账号、不能签发 API key,也没有那些依赖已存设置的管理功能。

看 /health 时要注意区分,两种「没有数据库」的状态返回的结果并不一样:

{"status": "healthy",   "routes_configured": 3, "database_configured": false, "database_connected": false}
{"status": "unhealthy", "reason": "database_unavailable_at_startup", "database_configured": true, "database_connected": false}

第一种是部署本来就没要数据库。第二种是要了却没连上,所以报告 unhealthy,让负载均衡器不再把流量转给它。

连接设置

这些设置由 apps/backend/serving/config/settings.py 里的 Settings 从 .env 或进程环境变量中读取。

变量

默认值

说明

DB_ENABLED

true

false / 0 / no 中任意一个都会彻底禁用数据库。

DB_HOST

localhost

在 Compose 服务栈里,后端容器中的这个值会被覆盖为 postgres。

DB_PORT

5432

在 Compose 里这是宿主机一侧的端口映射;容器内部始终连 5432。

DB_NAME

hybridinference

Compose 要求必填(DB_NAME must be set in .env file)。

DB_USER

postgres

Compose 要求必填。

DB_PASSWORD

(空)

Compose 要求必填。

DB_STORE_FULL_CONTENT

false

是否原样存储 prompt 和响应。见请求日志与隐私。

ERASURE_FENCE_SECRET

(空)

已删除账号记录所用的密钥,这些记录保证删掉的账号不会被写回来。只设置一次,以后永远不要改;见删除账号相关的密钥。

ERASURE_FENCE_PROTOCOL_READY

false

开启永久删除账号功能。见删除账号相关的密钥。

仓库自带的服务栈(deploy/docker/docker-compose.yml)运行的是 postgres:16,用 -E UTF8 --locale=C.UTF-8 初始化,发布在 127.0.0.1:${DB_PORT:-5432},只监听回环地址。要从别的机器访问,请用 SSH 隧道,不要放宽这个绑定。

表结构是怎么创建出来的

应用在启动时自己创建和迁移表结构,重启不会改动已经存在的部分,也不需要另外运行迁移工具。每条语句都是 CREATE TABLE IF NOT EXISTS,所以下面这些建表代码即使有两处定义了同一张表,也不会冲突:

代码

创建什么

apps/backend/serving/storage/log_schema.py 里的 ensure_api_logs_schema

api_logs 和 api_stats_hourly,连同它们的列和索引

apps/backend/serving/storage/database.py 里的 DatabaseLogger._create_tables

先调用上面那个,再建认证/管理相关的表

apps/backend/serving/storage/postgres_operational.py 里的 PostgresOperationalStore.initialize

运行数据表(设置、覆盖项、provider 注册表)

apps/backend/serving/storage/responses_store.py 里的 ResponseStore.initialize

openai_responses

apps/backend/serving/grants.py 和 apps/backend/serving/admin/geo_demand_rollup.py

agent_grants;以及 geo_hourly_* 汇总表

所以所谓「迁移」,就是把网关指向一个空数据库:启动网关,表就建好了。

运维之前,有两点值得先了解:

重启只补缺失的部分。每次启动都会先检查哪些列和索引已经存在,只执行真正缺少的 ALTER/CREATE INDEX 语句,所以表结构没有变化时,重启不会拿任何强表锁。这一点很重要:ALTER TABLE 在 Postgres 判断 IF NOT EXISTS 之前就会申请 ACCESS EXCLUSIVE 锁,而排队等待的排他锁会把后面所有读请求都堵住。

拿不到锁的迁移会推迟执行,不会导致启动失败。DDL 阶段设置了 3 秒的 lock_timeout,拿不到锁就抛出 SchemaLockUnavailable,不会一直等;调用方会在后台重试,启动流程照常继续。最常见的持锁者是长时间运行的 pg_dump,它可能在 api_logs 上持有 ACCESS SHARE 锁好几个小时。如果你有定时备份,那么在某次新增列的部署之后,日志里偶尔出现一行迁移被推迟的记录是正常的。

如果你要给 api_logs 加一列,只在 log_schema.py 里加;请求日志的表结构只在那一处定义。

删除账号相关的密钥

管理员永久删除用户时,网关还会在 erasure_fence 表里记下这个账号的带密钥指纹,这样即使有请求日志正在写入,也没法把这个用户的数据写回来。这由两项设置控制:

  • ERASURE_FENCE_SECRET 是这些指纹所用的密钥。网关第一次连上某个数据库启动时,会记下这个密钥的指纹——哪怕还没有删除过任何账号——此后只要密钥对不上,它就拒绝启动。所以要在第一次启动之前给它设一个独立的随机值,在重启和各个副本之间保持一致,并且永远不要更改。留空时,它会改用 API_KEY_SECRET,记下的也就是这个值;之后再改 API_KEY_SECRET,后端同样会拒绝启动。如果遇到不匹配,恢复原来的值即可;千万不要为了绕过它去删除 fence 行或它们的元数据。

  • ERASURE_FENCE_PROTOCOL_READY=true 开启永久删除。要等所有写 api_logs 的进程都升级到会检查 fence 的版本,才能打开它;在那之前保持 false。

各张表存了什么

分组

表

存放的内容

请求历史

api_logs, api_stats_hourly, provider_hourly_stats

每个请求一行(模型、provider、token、延迟、TTFT、状态、成本),外加仪表盘用的小时级汇总

账号与认证

users, api_keys, auth_sessions, login_events, email_verification_tokens, password_reset_tokens, identity_auth_codes

用户记录、哈希后的 API key 及其配额、refresh token 会话、登录历史

管理操作

admin_audit_log, signup_allowed_domains, site_settings, site_updates, email_broadcasts, email_broadcast_recipients

管理变更的审计记录、注册策略、运行时设置、公告、群发投递状态

运行时路由覆盖项

provider_definitions, provider_api_keys, provider_route_configs, provider_route_candidates, provider_weight_overrides, disabled_providers, disabled_provider_env_keys, provider_env_key_min_roles, model_visibility_overrides, model_concurrency_exemptions

在管理控制台里不改 YAML 就能调整的所有路由设置——见从管理控制台做运行时配置

成本与配额

user_daily_cost

每个用户每天的花费,用于配额控制

RouteWise

routewise_probe_samples, routewise_probe_leases

延迟探测样本,以及防止两个 worker 同时探测的租约

Responses API

openai_responses

启用内容存储时保存下来的 /v1/responses 状态

地理分析

geo_hourly_coverage, geo_hourly_demand

按国家统计的小时级请求数和 token 数;只有聚合值,不存储 IP 地址

agent 授权

agent_grants

网关为外部 agent 控制平面签发的短期授权,只能用于限定的模型

账号删除

erasure_fence

已永久删除账号的带密钥指纹;见删除账号相关的密钥

在运行中的数据库上执行 \dt,就能列出所有表。

请求日志与隐私

DB_STORE_FULL_CONTENT 默认为 false。这个默认值不是对已存文本做脱敏,而是根本不写入文本。关闭时,api_logs.prompt、api_logs.response 和 api_logs.request_payload 都写入 NULL,/v1/responses 的状态也不会保存。

不管开没开,不含内容的派生列都会记录,因为仪表盘读的是这些列,不必去 de-TOAST 请求载荷。这些列包括:token 数、成本、延迟和 TTFT,对话结构(num_turns、num_user_turns、num_tool_calls),以及最新一条用户消息的指纹(last_user_msg_chars、last_user_msg_entropy、last_user_msg_hash)。

打开后会保存完整的 prompt 和响应。打开之前,先想清楚你的用户能否接受。

请求属于哪个会话

api_logs.session_id 把同一段对话的请求归到一起。客户端可以用网关自己的 X-Session-ID 头来设置它,但编程 agent 不会发这个头,而是各自带着自己的会话 id;请求里带的是哪一种,网关就读哪一种:

从哪里读取

由谁发送

X-Session-ID 头

任何遵循网关自身约定的客户端;只要出现就优先采用

session-id / thread-id 头(session_id / conversation_id 这两种写法也认)

Codex CLI,每个请求都带上本次运行的标识

x-session-affinity / x-opencode-session 头

OpenCode 及其分支 Kilo Code:如果 provider 不是它们自家的,就在 X-Session-ID 之外再发这个亲和性头;如果是自家的 provider,则发 x-opencode-session

请求体里的 metadata.session_id 或 client_metadata.session_id

把会话和其他信息标注在同一处的客户端;Codex 用的是 client_metadata

x-claude-code-session-id 头

Claude Code,每个请求都带

请求体里的 metadata.user_id

Claude Code,它把设备、账号和本次运行的信息都塞进 Anthropic 的 user_id 这一个字段里。有两种格式,见下文

从 2.1.78 版起,Claude Code 以 JSON 对象的形式发送 metadata.user_id——{"device_id": …, "account_uuid": …, "session_id": …}——在此之前则是 user_<hash>_account_<uuid>_session_<uuid> 形式的字符串。网关两种都认。subagent 的请求里还带有 parent_session_id,网关不用它:这个字段指的是启动这个 subagent 的会话,不是当前会话。

部分 OpenCode 版本和 Kilo Code 分支在较新的请求路径上不发送会话 id。OpenCode 已经在上游修复了这个问题(sst/opencode#43188);在 Kilo Code 跟进同样的修复之前,它的请求都会记录为没有会话。

管理控制台的 Recent Requests 视图会在每一行的客户端下方显示会话,点击会话,列表就只显示这段对话的请求。

同一行里的 metadata.session_id_source 记录会话 id 来自上面哪个来源。这些来源全部由客户端提供,鉴权、计费和限流都不看会话 id;超过 128 个字符或带控制字符的会话 id 会直接丢弃,不做记录。和上面的派生列一样,无论 DB_STORE_FULL_CONTENT 是否打开,会话都会被记录:它只是客户端给请求打的标签,不属于对话内容。

备份

直接从容器里做 custom 格式的 pg_dump:

docker exec hybridinference-postgres \
  pg_dump -U "$DB_USER" -d "$DB_NAME" -Fc > hybridinference-$(date +%F).dump

不要给 docker exec 加 -t:分配 TTY 会破坏二进制流。

恢复到已有的、正在运行的数据库:

docker exec -i hybridinference-postgres \
  pg_restore -U "$DB_USER" -d "$DB_NAME" --clean --if-exists < hybridinference-2026-01-01.dump

往正在运行的数据库上做恢复之前,先用 docker stop hybridinference-backend 停掉后端;不要用 make down,它会连 Postgres 一起停掉。备份里有哈希后的 API key;如果 DB_STORE_FULL_CONTENT 打开过,还有用户的 prompt 内容,请妥善保管。

重置

删除数据库没有看上去那么简单,因为 Postgres 的卷在 deploy/docker/docker-compose.yml 里声明成了 external::

volumes:
  postgres_data:
    external: true
    name: hybridinference_postgres_data

docker compose down --volumes 不会删除外部卷。make down 也不会,它根本不传 --volumes。靠这两个命令来「重置」,所有数据都会原封不动地留着,而且没有任何提示。要按名字删除这个卷:

make down
docker volume rm hybridinference_postgres_data    # destroys all data
make up                                           # recreates an empty volume

make up 依赖 docker-volumes 这个 target,它会在具名卷不存在时重新创建,所以服务栈会连上一个空数据库启动,由启动时的初始化逻辑重建表结构。只要以后还有可能想找回这些数据,就先做一份 dump。

可运行的示例(make demo-reset DISTRIBUTION=example)用的是示例专属的卷,不会删掉 hybridinference_postgres_data。

查看数据库

docker exec -it hybridinference-postgres psql -U "$DB_USER" -d "$DB_NAME"

可以先从这两条命令开始:\dt 查看所有表,\d api_logs 查看请求日志的列。make ps 会列出端口绑定,Postgres 和 pgAdmin 都应该显示为 127.0.0.1:...;如果不是,说明数据库对本机以外也开放了监听。

可选:pgAdmin

Compose 服务栈自带一个 pgAdmin 服务,给喜欢图形界面的人用。它受 profile 控制:不指定 admin profile,它就不会启动。它完全是可选的,上面的 psql 什么都能做。

启动与重启

这个 profile 必须带在每一条 Compose 命令上,而不只是第一条:

make up   COMPOSE_PROFILES=admin
make down COMPOSE_PROFILES=admin

漏掉它不会有明显的报错,但启动和停止都会出错:不带 profile 的 make up 会启动其他所有服务,唯独跳过 pgAdmin;不带 profile 的 make down 会删掉其他所有东西,却留着 pgAdmin 容器继续运行(Compose 随后会报告网络仍在使用)。所以重启时,停止和启动两步都要带上 COMPOSE_PROFILES=admin。

认证

有两道关卡可以保护 pgAdmin,默认只开了控制台那一道:

  • PGADMIN_CONFIG_SERVER_MODE 默认为 False,此时 pgAdmin 不需要登录它自己的账号。在 .env 里把它设为 True,pgAdmin 就会要求输入 PGADMIN_EMAIL / PGADMIN_PASSWORD(这两个变量的默认值分别是 admin@local.dev / admin,启用前先改掉)。

  • 通过控制台的 /pgadmin/ 访问时,请求要经过一个 Next.js route handler(apps/frontend/src/app/pgadmin/[[...path]]/route.ts)检查是否为管理员会话,它会请后端验证调用方。直接访问 pgAdmin 发布的端口(127.0.0.1:${PGADMIN_PORT:-5050},只监听回环地址)时,这道关卡不起作用,请用 SSH 隧道:

    ssh -L 5050:127.0.0.1:5050 <user>@<your-gateway-host>
    

pgAdmin 的主密码(master password)提示永远不会出现:PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED 在 Compose 文件里写死为 "False",没有环境变量可以修改。

注册数据库

  1. Servers → 右键 → Register → Server。

  2. General:名字随便起。

  3. Connection:host 填 postgres,port 填 5432,maintenance database 填 DB_NAME,username 填 DB_USER,password 填 DB_PASSWORD——都用容器内部的值,不是宿主机那侧的端口映射。

重置 pgAdmin

pgAdmin 保存的连接存放在 hybridinference_pgadmin_data 里,这是一个普通的项目本地卷(和 Postgres 的卷不同,它不是 external 卷):

make down COMPOSE_PROFILES=admin
docker volume rm hybridinference_pgadmin_data     # destroys saved connections only
make up   COMPOSE_PROFILES=admin