ClickBench Playground:一个能跑 110 个数据库系统的在线试验场

查看原文 HN 讨论

文章摘要

ClickBench Playground 是一个自助式的在线试验场:你可以选择 ClickBench 收录的一百多个数据库系统中的任意一个,对它执行任意 SQL——不只是查询,还可以建表建库、插入数据、删表。每个数据库都预装了一份 1 亿条记录的数据集,方便你直接跑示例查询。它不只有关系型数据库,还有一些相当不寻常的系统,甚至包括来自「完全另一个宇宙」的 BQN。它还提供一个「竞赛」模式,可以同时选中多个系统让它们赛跑,或者比对它们的结果是否一致。作者 Alexey Milovidov 认为这大概是交互式试验场里规模最大的数据库系统合集——类似的站点如 sqlfiddle、db-fiddle、codapi 都存在,但没有一个接近一百个系统的量级。他在 HN 的自述里说:「我主要是为了测试和探索才做它的,但主要原因是——在之前的工作之后,它变得可能了。」

这里的「之前的工作」指的是 ClickBench 项目本身的演进史,而作者那篇长达 22 分钟阅读量的技术博客把这段历史和实现细节讲得非常完整。ClickBench 的基准最早在 2013 年为 ClickHouse 的对比测试而创建,2022 年扩展为面向分析型数据库的开放基准。为了尽可能多地测试数据库,重点一直放在「让添加新数据库变得容易」上:每个数据库就是一个目录加几个 shell 脚本,主脚本负责安装系统、下载数据、导入、跑基准、输出结果,脚本在一台新创建的 EC2 机器上手工运行;如果要测云上的 SaaS 数据库,就记录一份分步说明而不是脚本。这种做法极其灵活——某个系统在 Ubuntu EC2 镜像上跑不起来?加一条用 Docker 跑的说明;需要配置某个冷门 JVM 版本?把每一步写进脚本。它以极少的代码换来了极高的可贡献性,也让 ClickBench 成为测试分析型数据库最流行的开放基准。

但支撑一百个 shell 脚本、让它们保持最新,是个真实的负担。跑一次容易,反复跑就很不愉快:有些结果会过期需要重跑,而只要你想改动基准里的任何东西——比如试新查询、换数据下载地址——就得把所有工作重来一遍。2025 年夏天作者加了 cloud-init 脚本实现无人值守运行,脚本打印带可识别格式的日志再上传到 ClickHouse 服务,另一个脚本发现新结果并更新仓库;随后就是一项「巨大而无聊」的任务:更新所有脚本确保它们还能跑。更大的问题是:有些参赛者在基准上作弊——「忘记」在查询之间清空页缓存,或者不把某些优化工作计入加载时间。多亏了贡献者互相监督(一个作弊,另一个抓出来),但既然靠缓存作弊如此容易,他们决定在测量每个冷查询结果之前重启每个系统。可怎么给一百个略有差异的 shell 脚本加上重启逻辑?答案是重构:让每个系统提供一套实现共同接口的脚本,比如每个目录都要有跑单条查询的 query 脚本、stopstartcheck 等等。作者说重构一百个 shell 脚本「远超人类能力」,他先手动试、后来用 AI 试,几次之后终于做成了。这里的难点在于 ClickBench 不只收录真正的数据库,还有无服务端的数据库引擎(Datafusion、DuckDB 这类可作为自定义 SQL 引擎积木的东西),以及 Pandas、Polars 这样的数据分析工具/库——为了统一,这些嵌入式系统被包进一个 Python HTTP 服务器里,好让基准能逐条运行查询。

统一接口打开了一堆新可能,作者急切地试了不少:把数据集放大十倍会怎样?他发现有一个参赛者根本不允许大数据集——它把非商业版的数据集大小阈值设得刚好装得下这个基准,「我们不予置评」。把 43 条查询换成 100 条、加入更多 JOIN、窗口函数和相关子查询会怎样?他发现一些顶尖参赛者对默认查询过度优化,面对更宽范围的查询就失去了竞争力。做一个并发跑所有查询、测量 QPS 的测试会怎样?结果同样令人意外——ClickHouse 居中榜首,而有些系统在任何合理的并行度下就直接不工作了。在极小或极大的 AWS 实例上跑会怎样?他加入了小到 2GB 内存、大到 192 CPU 的机器,AMD64 和 ARM 都有;ClickHouse 在最小的机器上一次通过(因为它是生产级系统),而其他系统失败了(很多新系统在没有多少生产使用的情况下针对特定基准过度优化)。而最后一个问题——「如果我把所有系统都保持运行、让你随便查呢?」——就是 Playground 的由来。

博客最精彩的部分是「如何在不太贵也相对安全的前提下托管一百个不同的数据库」这个工程问题的推演,作者逐一排除了各种方案。用 AWS EC2 机器:ClickBench 最常用的 c6a.4xlarge(32GB 内存)每小时 0.612 美元,全部系统常开一年约 53.6 万美元,「我不想为我的小实验花掉五十万」。而且就算掏钱也不现实——大多数系统会崩,你没法把每个数据库都配置成不崩溃、不进死循环、不 OOM、不吃光磁盘(他说唯一能这样配置的系统是 ClickHouse,别的他不知道)。按需创建机器:要么给 Playground 加登录限制,要么就会被访问网站的各种机器人拉起虚拟机;而 EC2 启动要半分钟到几分钟,对交互式服务太慢;机器快照也帮不上,因为 AWS 上从快照恢复很慢(页面内部从 S3 抓取)。至于有人会推荐 spot 实例加自动伸缩组,作者的评价是「但那个人可能是个 AWS 顾问」。AWS Lambda:镜像大小限制太小(zip 和容器镜像都是),大多数参赛者超标,数据集更大;Lambda 能挂 EFS 但不能挂 S3(FUSE 在 Lambda 里不工作),而 EFS 不在同一可用区时和 S3 一样慢;此外「用 Lambda 就像在瓶子里造船——你把东西塞进容器然后得到晦涩的错误信息」,而且成本可能失控。ECS/EKS:很多参赛者本身就要求 Docker,这意味着要么再重构一次要么套娃 Docker-in-Docker;另外 Docker 的快照(实验性的 CRIU 实现)在 ECS 和 EKS 上都不支持,而快照是必需的——加载数据集要很久,有些数据库光启动都要很久。单台大机器托管全部:会是一团糟,怎么隔离?一个数据库崩溃、另一个内存膨胀、第三个陷入死循环烧 CPU;相比 ClickHouse,大多数数据库不是内存安全的、有大量漏洞,很短时间内就会有人黑进去开个反向 shell 接管机器。Docker 隔离不够(虽然能限 CPU、内存、磁盘,也能用 gVisor 加固),但 Docker-in-Docker 会破坏它——要么需要 privileged 模式(与 gvisor 不兼容),要么要把宿主的 dockerd 暴露进容器(立刻不安全)。Kubernetes 也被排除,作者的原话很有个人风格:「我认识一些热爱 Kubernetes 的朋友,因为他们和它的复杂度处在一段虐恋关系里。」

最终的答案是真正的虚拟化:Firecracker(KVM 之上提供配置与管理 API 的封装)。由于 EC2 本身就是虚拟机,这需要嵌套虚拟化,而 AWS 长期不支持、如今也只在一种罕见机型上支持——但每个最大规格的机型都有「metal」变体(比如 r6i.metal),可以在上面跑小虚拟机。「简而言之,我们在云里造了一朵云。」

资源分配上的三个技巧尤其有意思。CPU 最简单:每台机器限 4 核,都在用时就超售、由宿主 OS 调度器公平分配,另加一个看门狗杀掉长期占 CPU 的机器。内存更复杂,因为有些系统连 16GB 都装不下,而他还想托管 Pandas、Polars 这类把整个数据集载入内存的「数据框」系统。这里有几个有趣的观察:Firecracker 创建内存映射,但宿主系统是惰性分配物理内存的,所以虚拟机看到 16GB 物理内存、实际用得少时宿主用得也少;对需要超过 16GB 的系统则启用交换空间——听起来奇怪(swap 很慢),但技巧在于 swap 建在客户机的虚拟磁盘上,而虚拟磁盘映射到宿主的物理磁盘,且 fsync 调用被配置成让客户机想刷出的页留在宿主内存里而不写盘。「所以客户机的交换空间被映射到了宿主机的页缓存,只要宿主总内存不紧张,客户机就能像有更大内存一样快地工作。这让内存资源也像 CPU 一样有弹性,我觉得这个技巧相当好笑。」当然,所有机器同时要大量内存时,宿主的 OOM killer 会杀掉一些。

磁盘空间是最棘手的:要在加载完数据后给所有系统做快照,包括数据集大小(每个从十几 GB 到几百 GB)加上内存镜像(16GB 起,取决于 swap 使用),运行时还要给每个系统预留数 GB,总计约 100TB。他先挂了一块大 EBS 卷,但慢到「我没法开发这个服务」的程度;EBS 可以配更高 IOPS 和吞吐但很贵,于是改用带真实物理盘的机器(r6id.metal),可它的盘只有 7.5TB。第一步是用 ext4 加稀疏文件(零页不占空间,对压缩内存快照很有效),这里有一串实用细节:要确保客户机的物理内存是零初始化的(典型 Linux 机器的物理内存里是什么就是什么,只有虚拟内存在首次访问时被清零),他们用 init_on_free=1 之类的选项配置了客户机内核;客户机的文件系统镜像也创建为稀疏文件;注意稀疏文件在复制时会失去稀疏性,要用 cp --sparse=always 重新造洞;创建快照前还要先 sync、drop_caches、fstrim,免得快照里带上不必要的东西。第二步是发现了 reflink(一个文件的某些范围可以链接到另一个文件,修改时写时复制),用它避免快照与可运行镜像之间的重复,这样机器启动时不需要物理复制任何东西;但 ext4 不支持 reflink,于是换成 XFS。可 7.5TB 还是不够,他决定用压缩文件系统重建一切——哪个 Linux 文件系统同时支持 reflink 和压缩?XFS 不支持压缩,只剩 BtrFS 和大概 ZFS。「一开始我不信任 BtrFS,因为我以为它是某种拙劣模仿 ZFS 的尝试。但它同时支持压缩和 reflink,所以我没得选,而且我完全不担心 Playground 里的数据。」第一次尝试时压缩没生效(BtrFS 根据文件开头几个字节判断是否可压缩,而这些字节对客户机文件系统快照来说不具代表性),改用 compress-force=zstd:6 挂载并做一次带 zstd 的递归碎片整理之后,结果是:一百个系统全部装进 7.5TB,而且从快照启动很快。

网络部分同样精巧。创建镜像时需要联网(下载系统、apt-get、docker pull),这有一定危险但可控——安装说明都是经过 review 提交到 GitHub 上 ClickBench 仓库的,镜像创建也只做一次。所以他决定在安装阶段给网络,查询阶段撤掉。但 ClickBench 里有少数条目需要远程数据(「data lake」类处理 S3 上的 Parquet、「web」类处理 HTTP 服务器上的数据集),他希望这些在 Playground 里也能用,于是必须给虚拟机联网——而这很危险,因为联网的同时它们可能访问到宿主 localhost 上运行的服务,从而操纵宿主「越狱」;联网还会让它们能访问 IMDS(AWS 元数据服务,可以查到包括认证令牌在内的各种信息)。他没有给这台 EC2 任何 IAM 角色、宿主上也没有认证令牌,但仍然不希望客户数据库去查宿主的 IMDS。方案是:每台机器分到私有网段的静态 IP 和一个虚拟网络设备,宿主侧是 tap 设备,宿主用 iptables 配置路由把 tap 设备的包送到互联网再送回来——「可以说我们在自己的机器里实现了一个互联网网关和 NAT」。运行时的访问控制则通过一个小代理实现:每个包都路由到代理,由它决定放行与否,允许 DNS、HTTP 和 HTTPS 并带一份主机白名单。怎么在不破坏加密和证书、不做中间人的前提下代理和过滤 HTTPS?答案是利用 SNI——HTTPS 流量里的服务器名称指示字段是明文的,只读这个字段、原封不动地转发包,效果就等同于请求源自宿主机再被代理到客户虚拟机。此外还有一些小坑:回收非活跃镜像的 tap 设备有些问题;客户机里的 sudo 在没有 DNS 时不工作,除非把配置的主机名写进 /etc/hosts 供反向解析;以及 Docker 是个大麻烦——客户机里的 Docker 想改自己的 iptables,必须小心禁用,还需要在内核里提供更多模块(overlay、veth、br_netfilter、iptable_nat)。作者的评论很直白:「老实说,我不信任任何需要 Docker 才能运行的数据库。好的数据库,比如 ClickHouse,在任何系统上都跑得很好。但我还是想支持所有那些没那么好的数据库。」

由于系统快照里已经包含了启动完成的数据库进程,加上前述优化,即使是几百 GB 的镜像,冷启动时间也在 5 秒以内。这带来了一个方便的设计:任何查询出错后都直接从快照重建系统——所以你可以去 Playground 执行 DROP TABLE hits 甚至 SYSTEM SHUTDOWN,也不会「毒害」下一个用户的体验,系统会被重置。

GitHub 上的 playground README 补充了运行时的架构细节:数据集(ClickBench 用到的所有格式)在宿主上只下载一次到单个目录,作为 virtio-blk 设备以只读方式暴露给每台虚拟机;每个系统的 Firecracker microVM 首次启动时带互联网访问以运行该系统的 install、start、load 脚本;随后拍下内存加磁盘的快照并持久化,后续恢复不带互联网,唯一的进出通路是宿主与虚拟机之间的控制链路;虚拟机内有一个小 agent 通过 HTTP 暴露 POST /query,宿主的 API 服务器把用户查询代理给它,把原始输出以 application/octet-stream 返回并把计时放进响应头;一个 monitor 循环监视每台虚拟机的 CPU/磁盘/内存和宿主总量,杀掉行为异常或体积过大的虚拟机;每个请求和每次重启都会被追加写入 ClickHouse Cloud 的一张表。请求的生命周期是:客户端请求某个系统 → ensure_ready 检查(已运行且 /health 正常就继续;未运行就从快照恢复;无响应就杀掉、恢复、重试一次)→ agent 运行 ./query、捕获 stdout/stderr 并返回带 X-Query-TimeX-Output-TruncatedX-Output-Bytes 等响应头的输出 → 记录日志 → 返回客户端。每台虚拟机在专属 tap 上有自己的 /30 子网,宿主侧地址形如 10.200.x.1、虚拟机侧 10.200.x.2;安装阶段为 tap 打开 iptables FORWARD 和 MASQUERADE,快照拍完后移除转发规则,宿主与虚拟机的链路仍在但外部流量被黑洞。

博客最后的「有趣之处」一节展示了这个 Playground 的一个副产品:它让你能对比不同的查询语言。同一个「按广告引擎 ID 分组计数并降序排列」的需求,在 SQL 里是标准的 SELECT ... GROUP BY ... ORDER BY;在 DataFrame 里是 Pandas 的链式布尔索引加 groupby;在 BQN 里是一串密集的符号;在 Elastic 里是一个嵌套的 JSON 查询体;在 Mongo 里是一个 $match/$group/$sort 的聚合管道;在 LogsQL 里则是管道风格的表达式。当你选中一个示例查询并在系统之间切换时,它会自动切换到各自对应的示例。

作者对这件事的总结相当个人化:他做这个服务是为自己——他喜欢收集各种数据库系统,生产的和实验的、流行的和冷门的,甚至古怪的和民科的,现在他终于有地方放这个收藏了;顺便他学到的关于虚拟化和操作系统的知识远超预期。他希望这个服务能帮助人们对比不同系统的行为与标准符合度,并支持 ClickHouse 与 ClickBench 的开发。最后一句是:「我找到了一个同样喜欢收集数据库的朋友——现在他和我一起在 ClickHouse 工作。」

HN 评论精华

需要如实说明:这条 Show HN 的讨论非常稀少,只有 21 分和 6 条评论,其中两条来自作者本人(HN 用户名 zX41ZdbW)。因此这里没有多少可供挑选的「精华」,下面是全部有实质内容的发言。

换句话说,这个项目在 HN 上并没有引发讨论,它更像是被少数几个人认出了价值然后安静地收藏了。真正值得读的内容全在作者那篇技术博客里,尤其是把「一百个数据库塞进 7.5TB 磁盘」和「用 SNI 做透明代理来限制客户虚拟机的出网」这两段——它们和数据库本身没什么关系,是扎实的系统工程。