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——在小机器上这笔开销是白花的。

三种数据库的内存画像

数据库常驻内存(2G 机器 · available ≈ 1126 MB)SQLite≈ 0 MB 常驻库跑在应用进程里,没有独立进程MariaDB285 MB 实测约 150 MB(关掉 performance_schema 并调小 buffer pool 后)PostgreSQLshared_buffers 128 MB(默认)+ 每个连接一个独立进程,随连接数线性增长只有 285 MB 那个数是实测值,其余为默认配置下的典型量级。PostgreSQL 的总占用高度依赖 max_connections —— 它是进程模型,不是线程模型。SQLite 仍会用到系统 page cache,但那部分是可回收的,不计入进程 RSS。
PostgreSQL 那一行的画法是有意的:它的内存不是一个固定数字,而是「基础 + 连接数 × 每连接」。 在小机器上 max_connections 配错,比 shared_buffers 配错更容易出事。

并发写入模型的差异

并发写请求的处理方式SQLite写 A写 B写 C单写者队列串行化db 文件WAL 模式下读不阻塞,但写仍然一个一个来。busy_timeout 必须配MySQLPostgreSQL写 A写 B写 C连接/工作进程连接/工作进程连接/工作进程行级锁真并发写,但代价是常驻内存和连接管理。选型问题因此变成:你的写入速率会不会撞上 SQLite 的单写者上限。
SQLite 的"单写者"经常被误解成"不能并发"。WAL 模式下读写是可以并发的, 受限的只有写与写之间。对写入速率等于请求速率的中转站来说,这个限制什么时候变成问题是可以直接算出来的。

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/sSQLite零常驻内存,省下的 285 MB 在 2G 机器上是实打实的。配好 WAL 和 busy_timeout 就够
2G 机器、已有 MySQL 在跑复用现有 MySQL再起一个数据库进程是纯浪费。调优比换数据库收益大
请求量大 / 计划多实例PostgreSQL 或 MySQLSQLite 无法跨进程共享,这是硬限制而非性能问题
已经在用 Docker 编排跟着编排走容器化环境里 SQLite 的数据卷管理反而更麻烦
需要复杂用量分析查询PostgreSQL窗口函数、CTE、JSON 查询能力更强,统计报表写起来省事

最后一条实践建议:先从 SQLite 开始不是妥协。 但要知道 SQLite → MySQL 的迁移不是无痛的——SQLite 是动态类型(type affinity),日期存储没有原生类型,AUTOINCREMENT 语义也不同。如果你已经能预见到半年内会上多实例,那一开始就选 MySQL 或 PostgreSQL,省掉一次迁移。

相关阅读