如何配置数据库生产环境
数据库是产品最核心的基础设施之一。开发阶段可以用 SQLite 或本地 Docker 凑合,但一旦上线,数据丢失、连接耗尽、慢查询拖垮整站的代价远比你想象的高。生产环境的数据库配置涉及选型、连接管理、备份恢复、性能调优和安全加固五个方面,每一项都需要在上线前落实。
本章聚焦 AI 产品出海场景,讨论如何从「开发能跑」升级到「生产可用」。
数据库服务选型
对于出海团队,自建 PostgreSQL / MySQL 的运维成本极高——你需要处理操作系统补丁、数据库版本升级、磁盘扩容、主从切换、SSL 证书轮换等一系列基础设施工作。绝大多数场景应优先选择托管型数据库服务。选型时核心关注以下维度:
选型考量维度:
- 引擎类型:PostgreSQL 在 JSONB 支持、全文搜索、扩展生态方面更成熟,适合 AI 产品常见的非结构化数据存储场景;MySQL 生态更大,工具链更丰富,团队招聘更容易。
- 弹性伸缩:流量波动大的 AI 产品(如发布后的首波用户涌入)需要 Serverless 或快速扩容能力,避免临时升配导致的服务中断。
- 数据合规:出海产品需要关注数据存储区域。GDPR 要求欧盟用户数据存储在欧洲区域,CCPA 对加州用户数据有类似要求。选择数据库服务时确认其数据中心覆盖你目标市场所在的区域。
- 备份与恢复能力:是否支持 PITR(Point-in-Time Recovery)、备份保留周期、恢复速度。
- 计费模型:固定规格适合流量稳定的场景;按用量计费适合早期产品或流量波动大的场景。注意隐藏成本——数据传输费用、备份存储费用、连接池附加费用。
主流托管数据库对比
| 服务 | 引擎 | Serverless | 自动备份 | 分支能力 | 连接池内置 | 起步价格 |
|---|---|---|---|---|---|---|
| Supabase | PostgreSQL | 否(固定规格) | 是(每日) | 否 | PgBouncer | $25/月起 |
| Neon | PostgreSQL | 是 | 是(PITR) | 是 | 内置 | 按用量计费 |
| PlanetScale | MySQL (Vitess) | 是 | 是(PITR) | 是(Schema branching) | 内置 | $39/月起 |
| Railway | PostgreSQL / MySQL | 否 | 是 | 否 | 需自行配置 | $5/月起 |
选型建议:
- AI 产品 MVP / 中小型项目:Neon 的 Serverless PostgreSQL 是性价比最高的选择。按需计费、自动暂停、支持分支,特别适合开发迭代频繁的早期阶段。
- 需要 Auth + Storage + DB 全家桶:Supabase 提供完整的 BaaS 体验,适合需要实时订阅(Realtime)、对象存储、身份认证一站式集成的场景。
- 团队已熟悉 MySQL 生态:PlanetScale 的 Git-like schema branching 对多人协作非常友好,但注意它不支持外键约束(由 Vitess 架构限制)。
- 需要多数据库或自定义配置:Railway 提供灵活的部署方式,适合需要 Redis + PostgreSQL + 自定义 Worker 的场景。
生产环境配置
选好数据库服务后,需要针对生产环境做一系列配置调优。以下以 PostgreSQL 为例说明关键配置项。
连接池配置
数据库连接是有限资源。每个连接占用内存、文件描述符和 CPU 上下文切换开销。当并发请求超过数据库可承受的连接数时,系统会直接拒绝新连接,导致请求 500 错误。
连接池的核心思路是:复用已有连接,而不是为每个请求创建新连接。
连接池参数配置参考:
| 参数 | 说明 | 推荐值 |
|---|---|---|
max_connections | 连接池最大连接数 | CPU 核数 × 2 + 1 |
min_idle | 最小空闲连接数 | 总连接数的 20%~30% |
connection_timeout | 获取连接的超时时间 | 5~10 秒 |
idle_timeout | 空闲连接回收时间 | 5~10 分钟 |
max_lifetime | 连接最大生命周期 | 30 分钟~1 小时 |
statement_timeout | 单条 SQL 最大执行时间 | 30 秒(OLTP) |
// Prisma 连接池配置示例
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
// 连接池参数通过 connection string 配置
// postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=10
})如果使用 Supabase,可以启用 Supavisor(内置连接池),支持从应用侧直连或通过 pooler 连接。对于 Serverless 场景(Vercel / Cloudflare Workers),务必使用 pooler endpoint 而非直连数据库。
Serverless 场景的特殊考量:
Serverless 函数(如 Vercel Functions、Cloudflare Workers)的运行特点是实例短暂、并发高、冷启动频繁。每次函数冷启动都会创建新的数据库连接,如果多个函数实例同时运行,连接数会快速膨胀,远超数据库的 max_connections 限制。
解决方案是部署一个独立的连接池层:
- Supabase:内置 Supavisor 连接池,支持 Transaction 和 Session 两种模式。Serverless 场景使用 Transaction 模式,所有查询通过一个共享连接多路复用。
- Neon:内置连接池支持 Autoscaling,冷启动时自动创建连接,空闲时自动释放。
- 自建 PgBouncer:部署在 VPC 内的独立服务,应用通过 PgBouncer 间接连接数据库。需要自行管理 PgBouncer 的高可用和监控。
应用层 ──(连接池协议)──▶ PgBouncer / Supavisor ──(少量长连接)──▶ PostgreSQL
关键原则:应用到连接池的连接是短暂的,连接池到数据库的连接是持久的。这样数据库实际承载的连接数 = 连接池实例数 × 每个实例的连接数,而不是应用实例数。
资源限制与日志
资源限制配置:
| 配置项 | 说明 | 推荐值 |
|---|---|---|
work_mem | 排序 / Hash 操作的内存 | 4~64 MB(视查询复杂度) |
shared_buffers | 共享缓冲区 | 物理内存的 25% |
effective_cache_size | 查询优化器估算的系统缓存 | 物理内存的 50%~75% |
maintenance_work_mem | VACUUM / CREATE INDEX 的内存 | 256~512 MB |
max_wal_size | WAL 日志最大保留量 | 1~2 GB |
checkpoint_completion_target | Checkpoint 分散系数 | 0.9 |
日志配置:
log_statement = 'ddl':记录所有 DDL 操作(ALTER / CREATE / DROP)log_min_duration_statement = 1000:记录执行超过 1 秒的慢查询log_connections = on/log_disconnections = on:记录连接建立和断开log_lock_waits = on:记录锁等待超过deadlock_timeout的事件
Neon 和 Supabase 都支持通过控制台直接修改 postgresql.conf 参数,修改后会自动 apply 并重启实例。
备份策略
备份是生产环境最后的保险。没有经过恢复测试的备份等于没有备份。
备份方式对比
| 备份方式 | 原理 | RPO | RTO | 适用场景 |
|---|---|---|---|---|
| 逻辑备份(pg_dump) | SQL 文本导出 | 取决于备份频率 | 分钟~小时 | 小库、迁移、版本归档 |
| 物理备份(WAL 归档) | 持续归档 WAL 日志 | 秒级(PITR) | 分钟 | 生产环境标配 |
| 快照备份 | 存储层快照 | 取决于快照频率 | 秒级 | 大库快速恢复 |
| 跨区复制 | 异步流复制到其他区域 | 秒~分钟 | 分钟 | 灾备 |
生产环境推荐策略:
- 自动备份(必选):启用 PITR(Point-in-Time Recovery),Neon 和 Supabase Pro 均支持。保留至少 7 天的恢复窗口,金融类应用建议 30 天以上。
- 手动备份(重要操作前):每次执行批量迁移、DDL 变更、大表 TRUNCATE 前,手动触发一次
pg_dump快照并上传到 S3 或独立存储桶。 - 恢复测试(定期执行):每季度至少做一次恢复演练。从备份恢复到一个临时实例,验证数据完整性和应用可连接性,记录恢复耗时。
灾备与多区域策略:
对于全球化产品,单区域备份可能不够。如果数据中心所在的区域发生不可恢复的故障(极端情况下),你需要有能力在另一个区域快速恢复服务。灾备方案设计需要考虑 RTO(恢复时间目标)和 RPO(恢复点目标):
| 灾备级别 | 架构 | RTO | RPO | 成本 |
|---|---|---|---|---|
| 单区域 + 备份 | 所有数据在一个区域,定期备份到其他区域 | 小时级 | 小时级 | 低 |
| 跨区域只读副本 | 主库在一个区域,只读副本在其他区域 | 分钟级 | 秒~分钟 | 中 |
| 多活架构 | 多个区域都有可读写副本 | 秒级 | 零 | 高 |
对于大多数 AI 产品,「单区域 + 跨区域备份」是合理的起步方案。当用户量增长到需要全球低延迟访问时,再考虑引入跨区域只读副本。
# 手动逻辑备份示例
pg_dump -h db.xxx.supabase.co -U postgres -Fc -Z9 \
--schema=public myapp_production \
> backup_$(date +%Y%m%d_%H%M%S).dump
# 上传到 S3
aws s3 cp backup_*.dump s3://my-backups/db/性能优化
数据库上线后的性能问题通常不是「数据库不够快」,而是「查询写得不够好」或「缺了关键索引」。
索引优化
索引是查询性能的第一道防线。常见场景和推荐的索引类型:
| 查询模式 | 推荐索引 | 示例 |
|---|---|---|
| 等值查询(WHERE id = ?) | B-tree(默认) | CREATE INDEX idx_users_email ON users(email) |
| 范围查询(WHERE created_at > ?) | B-tree | CREATE INDEX idx_orders_created ON orders(created_at DESC) |
| 全文搜索(WHERE content LIKE '%keyword%') | GIN + tsvector | CREATE INDEX idx_posts_search ON posts USING GIN(to_tsvector('english', content)) |
| JSONB 字段查询 | GIN | CREATE INDEX idx_data_meta ON data USING GIN(metadata jsonb_path_ops) |
| 模糊匹配(WHERE name ILIKE '%xxx%') | pg_trgm GIN | CREATE INDEX idx_users_name_trgm ON users USING GIN(name gin_trgm_ops) |
索引维护注意事项:
- 大表加索引使用
CREATE INDEX CONCURRENTLY,避免锁表 - 定期通过
pg_stat_user_indexes检查未使用的索引,及时清理 - 每张表的索引数量控制在 5~8 个以内,过多索引会拖慢写入性能
查询优化
-- 查找慢查询
SELECT query, calls, total_exec_time / calls AS avg_time,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;常见的查询优化手段:
- 避免 SELECT *:只查需要的列,减少网络传输和内存占用
- 分页查询用游标而非 OFFSET:
WHERE id > ? ORDER BY id LIMIT 20性能远优于OFFSET 10000 LIMIT 20 - 批量操作代替循环:使用
INSERT ... VALUES (...), (...), (...)或COPY代替逐行插入 - 合理使用 CTE 和子查询:避免过度嵌套,必要时用
MATERIALIZED关键字控制执行计划 - EXPLAIN ANALYZE 验证:不要猜执行计划,用
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)看实际执行情况
缓存策略
数据库前面加缓存可以显著降低读压力:
| 缓存层 | 适用场景 | 工具 |
|---|---|---|
| 应用层缓存 | 高频读、低频写的配置数据 | Redis / Upstash |
| 查询结果缓存 | 相同查询参数反复执行 | Redis + TTL |
| 边缘缓存 | 公开内容的全球加速 | Cloudflare KV / CDN |
| ORM 层缓存 | 模型级别的查询去重 | Prisma extensions / DataLoader |
安全配置
数据库安全是底线。一个裸露的数据库实例被扫描到后,几分钟内就可能被爆破。
安全配置清单
| 配置项 | 说明 | 必须 |
|---|---|---|
| 强密码策略 | 至少 32 位随机字符串,定期轮换 | ✅ |
| 网络隔离 | 仅允许应用服务器 IP 段访问,关闭公网访问 | ✅ |
| TLS 加密 | 强制 SSL 连接(sslmode=require) | ✅ |
| 最小权限 | 每个服务使用独立数据库角色,仅授予必要权限 | ✅ |
| 行级安全(RLS) | 多租户场景下在数据库层做数据隔离 | 推荐 |
| 审计日志 | 记录所有 DDL 和敏感 DML 操作 | 推荐 |
| 敏感字段加密 | PII 字段(email、phone)使用 pgcrypto 应用层加密 | 推荐 |
-- 创建只读角色
CREATE ROLE app_readonly WITH LOGIN PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE myapp TO app_readonly;
GRANT USAGE ON SCHEMA public TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
-- 创建读写角色(不含 DDL 权限)
CREATE ROLE app_readwrite WITH LOGIN PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE myapp TO app_readwrite;
GRANT USAGE ON SCHEMA public TO app_readwrite;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_readwrite;
-- 注意:不授予 CREATE / ALTER / DROP 权限环境变量管理:数据库连接串不要硬编码在代码或配置文件中,使用 Vercel Environment Variables、AWS Secrets Manager 或 Doppler 等工具管理。生产环境和开发环境必须使用不同的数据库实例和连接串。
监控与告警
数据库上线后不是一劳永逸的,需要持续监控关键指标并设置告警阈值。
核心监控指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 连接数使用率 | 当前连接数 / 最大连接数 | > 80% |
| 磁盘使用率 | 已用存储 / 总存储 | > 85% |
| CPU 使用率 | 数据库实例 CPU 占用 | > 70%(持续 5 分钟) |
| 慢查询数量 | 执行超过 1 秒的查询数 | > 10 次/分钟 |
| 死锁数量 | 并发事务互相等待导致死锁 | > 0 |
| 复制延迟 | 主从复制的数据延迟 | > 10 秒 |
| WAL 堆积量 | 未清理的 WAL 日志量 | > 5 GB |
监控工具选型:
- 数据库服务自带:Neon 和 Supabase 都提供内置的监控面板,展示连接数、CPU、存储、查询性能等指标,适合早期产品。
- Datadog / New Relic:全栈可观测性平台,支持数据库指标采集、慢查询追踪、自定义告警规则,适合已经在使用这类平台做 APM 的团队。
- Grafana + Prometheus:开源自建方案,通过
postgresql_exporter采集指标,灵活但需要自己维护。
建议至少配置以下告警规则,并在团队通讯工具(Slack / 飞书 / 钉钉)中接收通知:
- 连接数使用率 > 80% 持续 2 分钟
- 磁盘使用率 > 85%
- 出现连续 3 次以上的连接失败
- 单条查询执行超过 30 秒
数据库生产环境配置流程
案例分析
案例一:AI 写作助手——从 Supabase 迁移到 Neon
某 AI 写作助手产品初期使用 Supabase 作为 BaaS 平台。随着用户量增长到 5 万 DAU,遇到两个问题:一是 Supabase 的固定规格实例无法应对流量高峰,低峰时资源浪费严重;二是 Supabase 的 PgBouncer 连接池在 Serverless 函数场景下表现不够理想,频繁出现连接数打满的问题。
迁移到 Neon 后:
- Serverless 自动扩缩容解决了高峰期的连接问题
- Branching 功能让每个开发者都有独立的数据库副本,schema 变更先在分支上验证,合并到 main 后再发布
- PITR 备份让数据恢复窗口从 24 小时缩短到秒级
- 连接池在冷启动时的表现优于自建 PgBouncer
迁移的核心工作是将 Supabase Auth 替换为独立身份服务(Clerk),其他数据层的 Prisma schema 和查询逻辑无需改动。
案例二:AI 客服平台——多租户 RLS 方案
一个面向 B 端客户的 AI 客服平台使用 PostgreSQL 存储对话历史和知识库数据。早期用 tenant_id 字段做逻辑隔离,但在应用层频繁出现跨租户查询的 bug。
切换到 PostgreSQL 行级安全策略(RLS)后:
-- 启用 RLS
ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
ALTER TABLE knowledge_base ENABLE ROW LEVEL SECURITY;
-- 创建策略
CREATE POLICY tenant_isolation ON conversations
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- 应用层设置当前租户
SET LOCAL app.current_tenant = 'tenant-uuid-here';即使应用代码遗漏了 WHERE tenant_id = ? 条件,数据库层也会自动过滤,从根本上杜绝了跨租户数据泄露的风险。配合最小权限角色设计,应用服务只有 SELECT / INSERT / UPDATE / DELETE 权限,无法执行 TRUNCATE 或 DROP 操作。
上线前检查清单
- 数据库服务选择了合适的规格和区域(与用户群就近)
- 连接池参数已根据并发量调优,并设置了超时和最大生命周期
-
statement_timeout已设置,防止慢查询拖垮连接池 - 慢查询日志已开启(
log_min_duration_statement≤ 1000ms) - 自动备份已启用,PITR 恢复窗口 ≥ 7 天
- 至少执行过一次备份恢复演练,并记录了恢复耗时
- 所有数据库连接使用 TLS 加密(
sslmode=require) - 应用服务使用最小权限角色,无超级用户连接
- 数据库连接串通过环境变量或密钥管理服务注入,未硬编码
- 网络访问已限制,生产数据库不暴露公网端口
- 关键表和字段有合适的索引,
EXPLAIN ANALYZE已验证高频查询 - 多租户场景已实现数据隔离(RLS 或应用层强制过滤)
- 监控告警已配置(连接数使用率 > 80%、慢查询数、磁盘使用率)