为什么 Phoenix 与 ClickHouse 的选型之争永无止境
来源标题:Phoenix vs. ClickHouse: Why the Fight Never Ends
一名工程师用同一份一千万条文档的数据在 Phoenix 和 ClickHouse 上做对比测试,试图解释两者的选型争论为何反复出现。他指出 Phoenix 属 OLTP、ClickHouse 属 OLAP 这类标签并不能说明具体查询的成本,因此争论无法平息。文章从两个引擎实际读写的数据入手进行拆解分析。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
84 hbase clickhouse cassandra databases data-engineering 之争 我参与的系统把销售文档存放在两个数据库中,每隔几周,同样的争论就会在我们的站会上重演。 一方说:“数据已经在 Phoenix 里了。月报就在那儿做,何必再复制一份?”这份月报是对几个月文档做 GROUP BY。它要跑上几分钟,region server 忙得满头大汗,而仪表盘总是最先放弃。 另一方说:“就是一份状态不对的文档。直接在 ClickHouse 里改掉就行。”改法是 ALTER TABLE ... UPDATE。它只改一行,却重写了远不止一行,一天下来这样的修改会在身后留下一串 mutation 排队。 我两边都辩过。有时就在同一周里。 解决这种争论的惯常办法是贴标签。Phoenix 是 OLTP,ClickHouse 是 OLAP;一个按行存储,一个按列存储;一个是 LSM 树,另一个不是。这些标签并不算错,但它们都没有说明某条具体查询要付出多少代价,所以争论还会回来。 这篇文章把两个引擎拆到读写的最底层,用的测试台上各装着同样的 1000 万份文档。底下有四个旋钮。每个旋钮都配一个小公式,公式能提前告诉你站会上哪一方会输。 第一个意外出现在任何公式之前:Phoenix 根本不在擂台上。 Phoenix 是 HBase 之上的一层 SQL。它自己不存任何东西:它把 SQL 编译成 HBase already 已有的操作。 region 1 scan, partial sums region 2 scan, partial sums region 3 scan, partial sums region 4 scan, partial sums region server 1 region server 2 Phoenix client: merge four sets of partial sums into one result Phoenix 决定工作在哪里运行。每个 region 从磁盘读什么,由 HBase 决定。 在数据旁边做汇总,是 Phoenix 的主要技巧:只有部分和跨网络传输,而不是整行数据。它也能增加工作,比如每次 upsert 都写一次索引。它无法改变的是 region server 为回答查询而从磁盘读取的内容。那是 HBase,一块一块地读。 所以这场仗以 Phoenix 命名,但物理本质是 HBase 对 ClickHouse 的 MergeTree。把 Phoenix 的开销算到 HBase 头上,或者把 HBase 的物理特性记到 Phoenix 账上,都会让每一次比较失真。从这里开始,测试台直接通过 HBase 的原生客户端(Get、Put、Scan)与 HBase 对话,Phoenix 只在它增加或减少开销的地方重新登场。 教科书式的框架说,OLTP 存储是就地更新页面的 B 树,OLAP 存储则完全是另一回事。这里两个引擎都不就地更新任何东西。它们的写入路径形状相同: WAL + memstore sorted in memory HFile flushed at 128 MB compaction minor and major ClickHouse INSERT a block of rows block sorted in memory part one per insert merge in the background Cassandra write one row's cells commit log
- memtable SSTable flushed memtable compaction a strategy per table 三条写入路径,一种形状:缓冲,写出不可变的有序文件,之后再合并这些文件。 一次 HBase Put 写入预写日志并进入 memstore——内存中的有序缓冲区;memstore 达到 128 MB 时被刷写成 HFile,之后的 compaction 再把 HFile 合并成更大的文件。ClickHouse 的 INSERT block 在内存中排序后写成一个新的 part——一个由列文件组成的目录;后台 merge 之后再把 part 合并成更大的。 缓冲、不可变有序文件、稍后合并。两者都是日志结构合并设计。“LSM 对 B 树”不是这场仗。 形状相同,故障也相同。测试台上的第一次加载按 key 顺序把一千万行写入 HBase,于是任何时刻每次写入都落到正在被填充的那一个 region 上。刷写产生文件的速度超过了 compaction 合并它们的速度。当 region 里有 17 个文件、超过 hbase.hstore.blockingStoreFiles 一个时,HBase 开始把每次刷写延迟最多 90 秒。memstore 继续增长到它的阻塞大小——刷写大小乘以块倍数,128 MB x 4 = 512 MB——然后该 region 拒绝写入: RegionTooBusyException: Over memstore limit=512.0 M 直到 compaction 追上来。ClickHouse 在另一个名字下有同样的故障。当 insert 创建 part 的速度超过 merge 合并它们的速度时,它先放慢 insert(parts_to_delay_insert),然后以“Too many parts”拒绝(parts_to_throw_insert)。一次写入停滞,两个名字。 测试台上的第三个引擎 Cassandra 属于同一家族:commit log 和 memtable,刷写成 SSTable,由 compaction 合并,这是 Google Bigtable 的写入路径,HBase 也照抄了它。它的存储选择与 HBase 接近,所以在前三个旋钮上不再单独过一遍。它在第四个旋钮上分道扬镳——谁拥有一个 key——那也是它重新登场的地方。 如果两个引擎都是追加再合并,那是什么让一个擅长月报,另一个擅长修一份文档?四个选择,每一个都是每个日志结构引擎都必须在某处设定的旋钮: 每个设置在一边便宜,在另一边昂贵。这些边有个名字,叫 RUM 三角:读取(Reading)的代价、更新(Updating)的代价、以及数据占用的内存或空间(Memory)的代价。接下来四节测量每个旋钮的代价,最后的三角把它们放在一起。 两个引擎都不为每一行建索引。两者都维护稀疏索引:行按 key 排序并切成块,索引记住每块从哪里开始。要找到一行,引擎找到它所在的块,然后把整块读出来。 块有多大,是第一个旋钮。称它为 g,即一个索引项背后的行数: ClickHouse 设 g = 8,192 行,称这个块为 granule。测试台上的表有 1000 万行,主索引有 1,224 个 mark:4.9 KB,始终在内存中(system.parts 里的 marks 和 primary_key_bytes_in_memory)。十亿行大约需要 12.2 万个 mark,仍然不算什么。代价在公式的另一边:要返回一行,ClickHouse 要处理整个 granule,即查询所要求的每一列的 8,192 行。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。