Momentic 为 AI 驱动的软件测试平台重构了缓存系统:规模达到每天超过 200 万次查询、总计约 200 亿条记录,同时把平均响应延迟维持在约 250 ms。关键变化不是给 PostgreSQL 再加一层补丁,而是把缓存查询负载迁移到列式数据库 ClickHouse。
这类迁移值得后端团队关注,因为它不是“数据库谁更强”的抽象争论,而是一个典型场景:数据量极大、查询模式相对明确、读吞吐和聚合/过滤效率比事务能力更重要。
为什么 PostgreSQL 会开始吃力
PostgreSQL 是通用型关系数据库,强在事务、一致性、索引能力、复杂 SQL 和生态成熟度。但当缓存系统里堆到数十亿甚至数百亿条记录时,瓶颈常常不是“能不能查”,而是“能不能稳定、便宜、低延迟地查”。
缓存查询通常有几个特征:
- 写入量大,但很多数据是追加式的。
- 查询往往围绕少数维度过滤,例如项目、测试用例、提交 SHA、时间窗口、缓存 key。
- 很多请求只需要读取少数列,而不是整行对象。
- 历史数据体量巨大,冷热分层明显。
行式数据库在读取整行、处理事务、频繁更新时很自然;列式数据库则适合“只读几列、扫很多行、快速过滤和聚合”的负载。ClickHouse 正是沿着这个方向设计的:列式存储、压缩友好、向量化执行,并通过 MergeTree 系列表引擎处理大规模追加数据。
ClickHouse 适合的是查询形状,不是所有缓存
Momentic 的案例里,迁移到 ClickHouse 后支撑了每天超过 200 万次查询、约 200 亿条总记录,并保持约 250 ms 平均响应延迟。这个结果说明:当缓存查询更像分析型读负载,而不是强事务型 OLTP 负载时,列式数据库会非常有优势。
但这不代表所有缓存都该迁移。
如果你的缓存需要大量单行更新、严格事务、外键约束、复杂跨表写入一致性,PostgreSQL 仍然更合适。如果你的访问模式是 get(key) 点查,Redis 或专门的 KV 存储可能比 ClickHouse 更直接。
ClickHouse 更适合这样的缓存/索引层:
- 记录可追加,更新可以少做或异步修正。
- 查询条件稳定,可以提前设计排序键和分区键。
- 数据量足够大,列式压缩和扫描收益明显。
- 可以接受最终一致或批量写入模型。
换句话说,迁移前先画查询路径,不要先选数据库。
可以这样实践:用 ClickHouse 建一个测试缓存表
下面是一个可本地运行的最小实验,用 Docker 启动 ClickHouse,并创建一个面向“测试结果缓存”的表。它不是 Momentic 的真实 schema,而是一个可改造的练习模型,用来验证列式查询、排序键和 TTL 的基本手感。
新建 docker-compose.yml:
services:
clickhouse:
image: clickhouse/clickhouse-server:24.8
container_name: clickhouse-cache-demo
ports:
- "8123:8123"
- "9000:9000"
environment:
CLICKHOUSE_DB: cache_demo
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: ""
ulimits:
nofile:
soft: 262144
hard: 262144
启动服务:
docker compose up -d
创建表:
docker exec -i clickhouse-cache-demo clickhouse-client --database cache_demo <<'SQL'
CREATE TABLE IF NOT EXISTS test_cache_events
(
project_id String,
cache_key String,
commit_sha String,
test_name String,
status LowCardinality(String),
duration_ms UInt32,
created_at DateTime,
payload_hash String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (project_id, cache_key, created_at)
TTL created_at + INTERVAL 90 DAY DELETE;
SQL
插入一些样例数据:
docker exec -i clickhouse-cache-demo clickhouse-client --database cache_demo <<'SQL'
INSERT INTO test_cache_events VALUES
('proj-a', 'linux-node18-auth', 'a1b2c3', 'login_spec', 'hit', 120, now() - INTERVAL 10 MINUTE, 'hash-001'),
('proj-a', 'linux-node18-auth', 'a1b2c4', 'login_spec', 'hit', 95, now() - INTERVAL 5 MINUTE, 'hash-002'),
('proj-a', 'linux-node18-auth', 'a1b2c5', 'login_spec', 'miss', 840, now() - INTERVAL 1 MINUTE, 'hash-003'),
('proj-b', 'macos-node20-cart', 'd4e5f6', 'cart_spec', 'hit', 210, now() - INTERVAL 3 MINUTE, 'hash-004');
SQL
查询某个项目和缓存 key 的近期命中情况:
docker exec -i clickhouse-cache-demo clickhouse-client --database cache_demo <<'SQL'
SELECT
status,
count() AS events,
round(avg(duration_ms), 2) AS avg_duration_ms
FROM test_cache_events
WHERE project_id = 'proj-a'
AND cache_key = 'linux-node18-auth'
AND created_at >= now() - INTERVAL 1 HOUR
GROUP BY status
ORDER BY events DESC;
SQL
这个例子里有几个设计点值得注意:
ORDER BY (project_id, cache_key, created_at)对应常见查询条件,让 ClickHouse 少扫无关数据。PARTITION BY toYYYYMM(created_at)方便按月管理历史数据。TTL created_at + INTERVAL 90 DAY DELETE适合缓存类数据自动过期。LowCardinality(String)适合hit、miss这类低基数字段。
真实系统中,你还需要压测不同排序键。ClickHouse 的表设计不像 PostgreSQL 那样靠事后加多个 B-tree 索引补救,排序键一旦选错,查询会立刻暴露代价。
从 PostgreSQL 迁移时,别只迁表
PostgreSQL 到 ClickHouse 的迁移,真正要改的是数据写入和查询习惯。
在 PostgreSQL 里,开发者常常习惯这样做:一条记录来了就 INSERT,状态变了就 UPDATE,查询慢了就加索引。ClickHouse 更喜欢批量写入、追加数据、按查询路径设计物理布局。
一个常见改造方向是:
# 假设从 PostgreSQL 导出最近一天缓存事件,再导入 ClickHouse。
# 请把连接串、表名和字段改成你的实际环境。
psql "$POSTGRES_DSN" \
-c "COPY (SELECT project_id, cache_key, commit_sha, test_name, status, duration_ms, created_at, payload_hash FROM test_cache_events WHERE created_at >= now() - interval '1 day') TO STDOUT WITH CSV" \
| docker exec -i clickhouse-cache-demo clickhouse-client --database cache_demo \
--query="INSERT INTO test_cache_events FORMAT CSV"
这段命令适合做迁移演练或离线回填。生产环境通常还要考虑:
- 双写或 CDC,避免迁移窗口内丢数据。
- 幂等写入策略,避免重放时产生重复记录。
- 查询结果对账,确认 ClickHouse 和 PostgreSQL 在关键窗口内一致。
- 回滚路径,保留 PostgreSQL 查询通道直到新系统稳定。
如果缓存语义允许重复事件,再通过查询取最新记录,ClickHouse 会更舒服;如果业务要求原地更新唯一行,就要谨慎设计,例如使用 ReplacingMergeTree,或者把 ClickHouse 只作为查询加速层。
采用前的检查清单
可以把这类迁移当作一次负载分离:PostgreSQL 继续负责事务型核心数据,ClickHouse 承接大规模缓存查询、历史检索和聚合统计。
落地前建议检查这些问题:
- 查询是否主要按固定维度过滤,而不是任意字段随机组合。
- 数据是否能追加写入,更新和删除是否可以延后处理。
- 是否能接受批量写入和最终一致。
- 是否有明确的数据保留周期,例如 30 天、90 天或 180 天。
- 是否已经用真实查询做过排序键、分区键和压缩效果验证。
- 是否准备了迁移期间的双写、对账和回滚方案。
Momentic 的经验说明,在合适的查询形状下,从 PostgreSQL 迁到 ClickHouse 可以把缓存系统推到更大的数据规模和更稳定的响应延迟。但这不是简单替换数据库驱动,而是重新设计存储模型、写入路径和查询边界。做对了,ClickHouse 会像一台为扫描和过滤调校过的引擎;做错了,它也会把错误的 schema 放大到每一次查询里。