PostHog ClickHouse 查询基准测试套件实战指南:用 ASV 持续追踪查询性能回归
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
本文是一份面向 PostHog 后端开发者的 ClickHouse 查询基准测试(Benchmark)实战指南。文章以 ee/benchmarks/README.md 为核心骨架,结合 benchmarks.py、helpers.py、asv.conf.json 与 measure.sh 等仓库源码,系统讲解基准测试套件的架构、本地运行方法、PR 性能回归检测流程、新用例编写规范以及历史结果回填技巧。读完本文,你将掌握如何在本地复现 PostHog 官方基准测试、读懂各项 ClickHouse 指标(耗时、读取行数、内存占用)的含义,并为自己的查询优化 PR 添加可追踪的性能证据。
基准测试套件定位:为什么 PostHog 需要一套 ClickHouse 查询基准
PostHog 的产品分析、会话回放、用户行为洞察等核心能力,几乎都建立在 ClickHouse 之上的大量复杂查询之上。随着数据量增长与查询逻辑演进,一次不经意的 SQL 改动或物化列策略调整,都可能在线上引发显著的查询性能回退。
ee/benchmarks/README.md 明确指出:ee/benchmarks是PostHog ClickHouse 查询的基准测试套件(benchmark suite),其目标是在时间轴上持续追踪 ClickHouse 查询的性能改进(tracks performance improvements to clickhouse queries over time)。这意味着它不是一次性压测工具,而是与 CI、PR 流程深度绑定的长期性能治理基础设施。
为了获得跨时间的稳定可比较结果,套件坚持两个关键设计决策:
- 基于 airspeed velocity(asv)驱动:asv 是专门面向 Python 项目的基准测试框架,负责环境构建、多次采样、结果归档与回归检测;
- 使用预填充数据的稳定 ClickHouse 节点:基准测试始终跑在一个数据形态固定的 ClickHouse 节点上,避免因线上数据增长导致结果失真,从而保证不同提交之间的性能差异真实反映代码变化。
历史基准测试结果由独立的PostHog/benchmark-results仓库承载,套件本身只负责“生产”结果,不负责长期保存。
套件文件布局:五个文件组成的性能观测体系
在继续之前,先整体了解ee/benchmarks目录的结构:
| 文件 | 职责 |
|---|---|
| README.md | 使用文档:安装、运行、回填、FAQ |
| asv.conf.json | ASV 框架配置文件,定义项目路径、构建命令、结果目录等 |
| benchmarks.py | 基准用例集合,定义被测查询与测试环境准备逻辑 |
| helpers.py | 核心支撑库:Django/ClickHouse 环境初始化、@benchmark_clickhouse装饰器、查询统计采集 |
| measure.sh | 独立于 ASV 的单查询测量脚本,支持火焰图与 EXPLAIN |
其中benchmarks.py与helpers.py是套件的主体:前者描述“测什么”,后者解决“怎么稳定地测”。
安装与本地运行:从零跑起一套基准
环境准备
基准测试由 asv 驱动,而 asv 依赖 virtualenv(或 Anaconda 发行版)来隔离基准运行环境。按 README 的说明,安装只需:
pip install asv virtualenv需要特别说明的是:PostHog 的基准用例直接运行在 Django 应用上下文中(见下文helpers.py),因此本地运行前还需要保证项目本身的 Python 依赖可用,并且能够连上目标 ClickHouse 节点。
一键本地运行
README 给出了标准的本地运行流程。核心是两步:先声明机器环境,再执行基准:
# 1) 设置机器环境(machine 标识用于区分不同运行环境的结果) asv machine --machine ci-benchmarks --config ee/benchmarks/asv.conf.json # 2) 运行全部基准(X 替换为实际凭据) CLICKHOUSE_HOST=X CLICKHOUSE_USER=X CLICKHOUSE_PASSWORD=X CLICKHOUSE_DATABASE=posthog asv run --config ee/benchmarks/asv.conf.json这里的环境变量直接决定了基准用例执行时连接的 ClickHouse 实例。从 helpers.py 可以看到,套件运行时还强制注入了两个环境变量:
os.environ["POSTHOG_DB_NAME"] = "posthog_test" os.environ["DJANGO_SETTINGS_MODULE"] = "posthog.settings"也就是说,无论外部传入的CLICKHOUSE_DATABASE是什么,Django 侧的POSTHOG_DB_NAME恒为posthog_test,这是 PostHog 测试环境与生产环境隔离的一部分。随后脚本把仓库根目录加入sys.path并调用django.setup(),确保基准用例可以像正常后端代码一样使用 ORM、模型与 ClickHouse 客户端。
快速迭代单个用例
整套基准耗时较长,日常开发中最常用的是 README 提供的“快速单测”模式:
asv run --config ee/benchmarks/asv.conf.json --bench track_lifecycle --quick--bench track_lifecycle是一个正则表达式,匹配任何名称包含track_lifecycle的用例;--quick则只让每个用例运行一次,用于快速验证逻辑正确性而非采集稳定数据。关于asv run的更多参数(如--steps、--date-period、--record-samples),README 建议查阅 asv 官方文档。
基准用例剖析:QuerySuite 到底在测什么
benchmarks.py 是套件的用例主体,其核心是一个名为QuerySuite的类。先看它的类级配置:
class QuerySuite: timeout = 3000.0 # Timeout for the whole suite version = "v001" # Version. Incrementing this will invalidate previous resultstimeout = 3000.0:整个套件的超时上限(秒),避免单个提交的异常查询拖垮整轮基准;version = "v001":套件版本号。这是一个容易被忽略却非常重要的设计——一旦基准用例本身发生结构性变化(例如新增了被测查询、改变了数据准备逻辑),旧版本的结果与新版本不可直接比较,此时应递增版本号使历史结果失效,防止回归检测出现误判。
当前QuerySuite定义了 5 个基准用例,全部通过@benchmark_clickhouse装饰器标记为被测目标:
| 用例名 | 被测功能 | 特别说明 |
|---|---|---|
track_earliest_timestamp | 查询团队最早事件时间戳 | 对应 timestamp_utils.py 中的get_earliest_timestamp_unfiltered,是许多查询的时间范围默认值来源 |
track_event_property_values | 事件属性取值查询($browser) | 在no_materialized_columns()上下文内执行,模拟无物化列场景 |
track_event_property_values_materialized | 同上,但允许使用物化列 | 与上一用例成对,用于量化物化列收益 |
track_person_property_values | 用户属性取值查询($browser) | 同样在无物化列上下文内执行 |
track_person_property_values_materialized | 同上,但允许使用物化列 | 与上一用例成对 |
四个属性取值用例实际复用同一个执行入口_run_event_property_values/_run_person_property_values,它们都通过PropertyValuesQueryRunner运行一个标准的PropertyValuesQuery(property_type区分 EVENT 与 PERSON),并以ExecutionMode.CALCULATE_BLOCKING_ALWAYS模式强制同步阻塞执行——确保查询完整跑完,指标采集落在真实执行上,而不是被缓存或异步机制“糊弄”过去。
setup:基准数据准备的关键
QuerySuite.setup()是每个提交运行前都会执行的准备逻辑,其作用有三:
- 物化列准备:按照模块级常量
MATERIALIZED_PROPERTIES的定义,为events表物化$current_url、$event_type、$host,为person表物化$browser、email,并通过backfill_materialized_columns(..., backfill_period=timedelta(days=1_000))回填过去 1000 天的历史数据。materialize与backfill_materialized_columns均来自 ee/clickhouse/materialized_columns/analyze.py,与生产环境的物化列管理共用同一套实现,保证基准环境与生产语义一致。 - 团队数据准备:由于基准服务器上的数据约定
ID=2,setup 会优先查找Team id=2,不存在则创建一个名为 "The Bakery" 的团队并指定id=2,确保后续查询总是作用于同一份团队数据。 - 人群(Cohort)准备:查找或创建名为 "benchmarking cohort" 的人群,其筛选条件为
person.email包含.com,创建后立即调用cohort.calculate_people_ch(pending_version=0)计算人群成员,供未来人群相关基准用例使用。
这套 setup 逻辑保证了无论跑在哪个提交上,基准数据的“形状”都保持一致,这正是 README 强调的“稳定结果”的落地实现。
运行原理:@benchmark_clickhouse装饰器如何采集指标
性能基准最怕三件事:结果抖动、采样不足、指标口径不一致。helpers.py 通过一个装饰器和一个查询统计函数解决了这些问题。
单次执行的指标采集
def run_query(fn, *args): uuid = str(UUIDT()) tag_queries(kind="benchmark", id=f"{uuid}::${fn.__name__}") try: fn(*args) return get_clickhouse_query_stats(uuid) finally: reset_query_tags()执行前,先用tag_queries给当前线程的后续查询打上benchmark:{uuid}::${函数名}的标签;执行后,get_clickhouse_query_stats通过SYSTEM FLUSH LOGS强制落盘查询日志,再从system.query_log中按标签匹配出该次执行产生的所有查询:
SELECT query_duration_ms, read_rows, read_bytes, memory_usage FROM system.query_log WHERE query NOT LIKE '%%query_log%%' AND query LIKE %(matcher)s AND type = 'QueryFinish'最终聚合为四个指标:query_count(查询次数)、ch_query_time(ClickHouse 总耗时毫秒)、read_rows(读取行数)、read_bytes(读取字节数)、memory_usage(内存占用)。读取行数与内存占用是比单纯耗时更稳健的性能信号——它们不受机器负载抖动影响,能更真实地反映查询的“计算量”。
装饰器的采样策略
def benchmark_clickhouse(fn): @wraps(fn) def inner(*args): samples = [run_query(fn, *args)["ch_query_time"] for _ in range(4)] return {"samples": samples, "number": len(samples)} return inner每个用例默认连续执行4 次采样,并以 ASV 标准的{"samples": [...], "number": 4}结构返回——这正好对应 README 中--quick模式“只运行一次”的行为差异(快速模式下number为 1)。4 次采样的均值与分布由 asv 负责统计,用于计算置信区间与回归判定。
no_materialized_columns:量化物化列收益的开关
@contextmanager def no_materialized_columns(): "Allows running a function without any materialized columns being used in query" cast(Any, get_enabled_materialized_columns)._cache = { ("events",): (now(), {}), ("person",): (now(), {}), } yield cast(Any, get_enabled_materialized_columns)._cache = {}PostHog 的查询编译器通过get_enabled_materialized_columns判断属性是否可走物化列。该上下文管理器临时把此函数对events、person的缓存置为空映射,让编译器认为“没有可用物化列”,从而在同一数据、同一查询下获得“无物化列 vs 有物化列”的对照数据。这正是 benchmarks.py 中四对..._values/..._materialized用例的意义:用同一把尺子量化 PostHog 物化列特性带来的真实性能收益。
在 CI 中持续运行:master 每日基准与 PR 性能检测
README 的 FAQ 部分描述了基准套件在团队协作流程中的实际地位:
- master 分支每日自动运行:基准测试每天对 master 分支执行一次,形成持续的性能时间序列,任何历史回归都能通过时间轴发现;
- PR 标记
performance触发评论:如果你的分支包含显著的查询性能改动,给 PR 打上performance标签,CI action 会在 PR 上自动运行基准并评论基准结果,把性能影响直接呈现在评审上下文中。
这意味着性能治理不是“事后追责”,而是嵌入到了日常代码评审流程中。若你的改动涉及查询性能(例如调整了 HogQL 编译器、物化列逻辑或 ClickHouse 查询生成),记得主动添加performance标签以获取基准反馈。
新增基准用例:两条规则
README 给出的新增规范非常精简:
Edit the
benchmarks.pyfile as needed. Use@benchmark_clickhousedecorator to select tests to run
即:
- 在 benchmarks.py 的
QuerySuite类中添加新的方法; - 用
@benchmark_clickhouse装饰该方法,即可被 asv 自动发现并纳入基准。
结合现有代码,写一个新用例至少要注意两点:一是方法内应真正执行到目标查询(可参考现有用例通过PropertyValuesQueryRunner等 Query Runner 触发完整查询链路);二是若用例依赖特定数据形态(如人群、物化列),需在setup()中补充对应的数据准备逻辑,否则基准环境无法保证一致性。
回填历史基准:把性能曲线补到过去
当你需要评估“过去一段时间”的查询性能走势,或者新基准用例需要历史基线时,README 提供了回填(backfilling)流程:
# 1) 将历史结果仓库克隆到套件目录下 # 将 benchmark-results 克隆到 ee/benchmarks/results # 2) 对过去约 4 天(或任意历史区间)的提交运行基准 CLICKHOUSE_HOST=X CLICKHOUSE_USER=X CLICKHOUSE_PASSWORD=X CLICKHOUSE_DATABASE=posthog \ asv run --config ee/benchmarks/asv.conf.json --date-period 4d master~500.. # 3) 发布结果并提交到 benchmark-results 仓库 asv publishmaster~500..表示从 master 向前 500 个提交直到当前 HEAD 的全部历史提交;--date-period 4d则按时间窗口选取提交。回填完成后asv publish会生成静态 HTML 报告(输出目录对应 asv.conf.json 中的html_dir: results/docs),将增量结果合入历史仓库。README 同时提示:若对回填细节有疑问,可直接以仓库中的 benchmark GitHub Action 工作流作为执行参考。
measure.sh:脱离 ASV 的单查询深度剖析
除了 ASV 套件,ee/benchmarks还提供了一个独立的测量脚本 measure.sh,用于对单个 SQL 文件进行针对性分析。其定位与 ASV 互补:ASV 负责长期回归追踪,measure.sh 负责“当场把一条查询的性能与执行计划看透”。
用法与参数
./ee/benchmarks/measure.sh \ --clickhouse-server clickhouse-server \ --tunnel-server some-server \ --password PW \ --query-file some-query.sql支持的全部参数如下:
| 参数 | 含义 |
|---|---|
-h, --help | 打印帮助信息 |
-q, --query-file | 要测量的查询文件路径 |
-s, --clickhouse-server | ClickHouse 服务器地址 |
-t, --tunnel-server | 用于 SSH 隧道访问 ClickHouse 的中转服务器 |
-u, --user | ClickHouse 用户(默认default) |
-p, --password | ClickHouse 用户密码 |
--explain | 输出查询执行计划(EXPLAIN PIPELINE graph=1, header=1) |
--drop-cache | 执行前清空 ClickHouse mark cache(切勿在生产环境使用) |
--no-flamegraphs | 跳过火焰图生成 |
执行细节
脚本会为查询附加一组剖析设置后发送执行,包括开启内省函数(allow_introspection_functions=1)、实时与 CPU 剖析器采样周期(40ms)、内存剖析步长(1MB)与采样概率(0.01)、禁用未压缩缓存(use_uncompressed_cache=0)并限制最大执行时间 400 秒——这些设置确保能采集到采样栈与内存火焰图所需的信号。
查询执行完毕后,脚本依次执行SYSTEM FLUSH LOGS,从system.query_log反查该查询的query_id,并输出event_time、query_duration_ms、read_rows、read_bytes、result_rows、memory_usage、涉及表与列等完整统计。若未禁用火焰图,还会调用clickhouse-flamegraph工具按 query-id 生成火焰图并用浏览器打开,便于直观定位热点函数。
若指定了--tunnel-server,脚本会先通过ssh -L建立本地端口转发(8124 -> clickhouse-server:8123),让本机以localhost:8124访问 ClickHouse,适合无法直连内网节点的场景。--drop-cache通过SYSTEM DROP MARK CACHE在每次执行前清空 mark 缓存,用于测量冷缓存下的真实性能,README 与脚本帮助信息都明确警告不要在生产环境使用。
FAQ 速查:把常用操作汇总成一张表
| 场景 | 操作 |
|---|---|
| 检查我的 PR 是否影响查询性能 | 在 PR 上添加performance标签,CI 会自动运行基准并评论结果 |
| 本地装好工具 | pip install asv virtualenv |
| 跑全部基准 | 先asv machine --machine ci-benchmarks --config ee/benchmarks/asv.conf.json,再带环境变量执行asv run --config ee/benchmarks/asv.conf.json |
| 快速迭代单个用例 | asv run --config ee/benchmarks/asv.conf.json --bench track_lifecycle --quick |
| 添加新用例 | 在 benchmarks.py 中添加方法并用@benchmark_clickhouse装饰 |
| 回填历史结果 | 克隆 benchmark-results 至ee/benchmarks/results,asv run ... --date-period 4d master~500..,再asv publish |
| 单条 SQL 深度剖析 | 使用 measure.sh,配合--explain与火焰图 |
总结
PostHog 的 ClickHouse 基准测试套件是一套“框架 + 用例 + 指标采集 + CI 联动 + 独立剖析工具”的完整性能治理方案:ASV 负责环境管理与长期回归检测,@benchmark_clickhouse装饰器基于system.query_log稳定采集耗时、读取行数与内存指标,QuerySuite通过物化列对照用例量化特性收益,performance标签把性能反馈嵌入 PR 评审,measure.sh则为单查询提供执行计划与火焰图级剖析能力。无论是排查线上查询性能问题、评估自己的优化改动,还是为 PostHog 贡献新的查询路径,这套套件都提供了可复制、可对比、可追溯的实践范式。
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考