new-api 这类中转程序通常同时支持 SQLite、MySQL 和 PostgreSQL,安装文档也就一句"按需选择"。但在一台 2G 内存的小机器上,这个选择是有真实代价的——光是数据库进程本身就可能吃掉可用内存的四分之一。
先给一个锚点。我实测一台 2 核 2G 的云主机,上面跑着 nginx、Redis 和两个 Go 服务,MariaDB 的进程 RSS 是:
mysql 14.5% 291932 KB /usr/libexec/mariadbd
285 MB,是这台机器上最大的单个进程。而整机 available 只有 1.1 G。换句话说,你还没开始跑业务,数据库已经拿走了四分之一的可用余量。
选型的真正依据:写入量 ≈ 请求量
在动手比较之前,先想清楚 API 中转站的负载长什么样,否则比较是没有意义的。
- 读:API key 校验、用户配置、配额查询。数据量小、可缓存、频率高——三种数据库都轻松应对,不构成差异。
- 写:每次请求落一条用量/计费记录。这是全部的关键:写入速率基本等于请求速率。
所以问题可以化简成一句:你每秒有几次请求?
| 请求量 | 每天写入行数 | SQLite 是否够用 |
|---|---|---|
| 1 req/s | 约 8.6 万 | 够,且很轻松 |
| 10 req/s | 约 86 万 | 勉强够,需调好 WAL 和 busy_timeout |
| 50 req/s | 约 430 万 | 不够,写入会明显排队 |
| 多实例部署 | —— | 不行,SQLite 无法跨进程跨机共享 |
大多数个人自建的中转站落在第一档,很多人却直接上了 MySQL——在小机器上这笔开销是白花的。
三种数据库的内存画像
max_connections 配错,比 shared_buffers 配错更容易出事。并发写入模型的差异
SQLite:如果选它,这几个 pragma 不能少
默认配置的 SQLite 在并发场景下表现很差,而这常被误当成"SQLite 不能上生产"的证据。实际要配的就几行:
PRAGMA journal_mode = WAL; -- 读写不再互相阻塞。默认的 delete 模式才是性能灾难
PRAGMA busy_timeout = 5000; -- 遇锁等 5 秒而不是立刻报 SQLITE_BUSY
PRAGMA synchronous = NORMAL; -- 配合 WAL 的合理折中
PRAGMA foreign_keys = ON; -- 默认是关的,很多人不知道
PRAGMA cache_size = -32000; -- 负数表示 KB,这里约 32 MB
其中 busy_timeout 是最容易漏、后果最直接的一条。不配它,两个并发写会有一个立刻收到 SQLITE_BUSY 失败——表现出来就是"偶发写入失败",而你会以为是 SQLite 不稳定,其实只是它在如实告诉你"锁被占了",而你没让它等。
synchronous = NORMAL 的取舍要说清:配合 WAL 时它能抵御应用崩溃,但操作系统崩溃或断电可能丢掉最近几个已提交事务。对用量日志这种损失几条可接受的数据没问题;如果这张表直接用于扣款对账,就该用 FULL。
备份不能直接 cp。 WAL 模式下 -wal 文件里有还没合并进主库的数据,直接复制主文件会拿到一个不一致的快照:
# ✓ 正确
sqlite3 /path/to/app.db "VACUUM INTO '/backup/app-$(date +%F).db'"
# ❌ 错误:WAL 模式下这样拷出来的库可能缺最近的写入
cp /path/to/app.db /backup/
MySQL / MariaDB:小机器上的调优
如果已经在用 MySQL 系,285 MB 是可以压下来的:
[mysqld]
# 关掉它就能省几十 MB,个人站几乎不需要
performance_schema = OFF
innodb_buffer_pool_size = 96M
innodb_buffer_pool_instances = 1 # 池子小的时候拆成多个实例反而浪费
innodb_log_file_size = 32M
# 每个连接都要分配缓冲区,这个数不是免费的
max_connections = 30
key_buffer_size = 8M
tmp_table_size = 16M
max_heap_table_size = 16M
performance_schema = OFF 是性价比最高的一条——它默认开启,为性能诊断收集大量数据,而个人站基本用不上。代价是出问题时少了一套诊断视图,需要时再临时打开即可。
max_connections = 30 看着很小,但要算清楚:中转程序的连接池通常配 10–20,加上你偶尔用客户端连进去看数据,30 足够。这个数配成 151(默认值)不会立刻占用内存,但它决定了内存占用的上限,在没有 swap 的机器上等于把 OOM 的可能性留在了那里。
PostgreSQL:注意它是进程模型
PostgreSQL 和 MySQL 最大的运维差异在这里:每个连接是一个独立的操作系统进程,不是线程。 所以连接数对内存的影响比 MySQL 显著得多。
shared_buffers = 128MB # 通用建议是内存的 25%,但共享机器上别照抄
max_connections = 20 # 进程模型下这个数很值钱
work_mem = 2MB # 注意:单次查询可能同时用掉好几份
maintenance_work_mem = 32MB
effective_cache_size = 512MB # 这只是给优化器的提示,不实际占用
work_mem 有个容易踩的坑:它是每个排序/哈希操作的上限,不是每个连接的上限。一个复杂查询里有多个排序节点时会各分配一份,并发多个这样的查询,内存就会超出直觉。小机器上宁可配小,让它落盘排序。
如果应用的连接池配得比较大,考虑在前面加 pgbouncer 做连接复用——但这又是一个常驻进程,在 2G 机器上要权衡它值不值。
比选型更要紧的问题:日志表会无限长大
这是本文最想强调的一点。无论你选哪个数据库,真正会在半年后咬你的是用量日志表。
机制很简单:每个请求写一行,从不删除。按前面的换算:
- 10 req/s → 每天 86 万行 → 一年约 3.1 亿行
- 每行假设 300 字节 → 一年约 90 GB
后果不是"磁盘满了"这么简单,而是一连串的连带效应:查询变慢 → 管理后台的用量统计页面开始超时 → 备份时间越来越长 → 最后磁盘真的满了,而磁盘满会让数据库直接拒绝写入,整个中转站停摆。
所以从第一天就该有保留策略:
-- 最朴素但有效:定期删除旧数据 + 保留聚合结果
DELETE FROM logs WHERE created_at < NOW() - INTERVAL 90 DAY;
-- 删之前先把要长期保留的口径聚合出来
INSERT INTO usage_daily (day, user_id, model, requests, tokens)
SELECT DATE(created_at), user_id, model, COUNT(*), SUM(total_tokens)
FROM logs
WHERE created_at < NOW() - INTERVAL 90 DAY
GROUP BY 1, 2, 3;
两点提醒:
- 大表上的
DELETE本身就是个事故来源。 一次删几千万行会长时间持锁、把 binlog/WAL 撑爆。要分批删(每次几千行、中间 sleep),或者用按时间分区的表直接DROP PARTITION——后者是从一开始就该做的设计。 - SQLite 删完记得
VACUUM,否则文件大小不会回落(空间被标记为可复用但不归还给文件系统)。注意VACUUM需要与库等大的临时空间,磁盘快满时反而做不了——所以别等到那时候。
决策建议
| 场景 | 选择 | 理由 |
|---|---|---|
| 个人自建、单实例、几 req/s | SQLite | 零常驻内存,省下的 285 MB 在 2G 机器上是实打实的。配好 WAL 和 busy_timeout 就够 |
| 2G 机器、已有 MySQL 在跑 | 复用现有 MySQL | 再起一个数据库进程是纯浪费。调优比换数据库收益大 |
| 请求量大 / 计划多实例 | PostgreSQL 或 MySQL | SQLite 无法跨进程共享,这是硬限制而非性能问题 |
| 已经在用 Docker 编排 | 跟着编排走 | 容器化环境里 SQLite 的数据卷管理反而更麻烦 |
| 需要复杂用量分析查询 | PostgreSQL | 窗口函数、CTE、JSON 查询能力更强,统计报表写起来省事 |
最后一条实践建议:先从 SQLite 开始不是妥协。 但要知道 SQLite → MySQL 的迁移不是无痛的——SQLite 是动态类型(type affinity),日期存储没有原生类型,AUTOINCREMENT 语义也不同。如果你已经能预见到半年内会上多实例,那一开始就选 MySQL 或 PostgreSQL,省掉一次迁移。
相关阅读
- new-api 部署选型:2G 小内存机器上裸二进制比 Docker 省多少 —— 那台机器 285 MB 的 MariaDB 实测数据出自这里
- 大模型 API 中转的超时与重试:一次 504 可能让你双倍计费 —— 用量日志正是核对重复计费的依据
- Caddy 还是 nginx 给 API 中转做反代:证书自动化是决定性差异 —— 同一台小机器上的另一项运维取舍