从 PostgreSQL 换到 ClickHouse:高吞吐缓存查询的现实取舍

2026-07-08 29 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

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) 适合 hitmiss 这类低基数字段。

真实系统中,你还需要压测不同排序键。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 放大到每一次查询里。


相关推荐