第二章曾经向读者阐明了,影响 olap 查问速度的瓶颈其实是在磁盘。并且也给出了两种具备代表性的优化计划,别离是分布式和列存。之后大部分的 olap 数据库都采纳了相似的架构,那么凭什么 clickhouse 能从这些 olap 数据库中怀才不遇摘得桂冠呢?从本章开始,本系列将逐步向读者展现 clickhouse 的精妙设计。
Block + LSM
其实本节的题目也能够换成批处理 + 预排序。clickhouse 通过 block 的设计来实现批处理,通过 lsm 算法来实现预排序。咱们别离来剖析一下,这个组合对查问速度的影响。
首先,咱们剖析有序存储和无序存储对查问速度的影响。咱们个别在做查问时,大抵能够分为按值查问和按范畴查问两种。
有序存储 | 无序存储 | |
---|---|---|
按值查问 | 1 次读取 | 1 次读取 |
按范畴查问 | 1 次读取 | n 次读取 |
两种查问对的磁盘拜访
从表中能够看出,在都应用了索引的状况下,如果是按值查问那么有序存储和无序存储根本都能做到一次磁盘 IO 就能实现数据读取。但按范畴读取,因为是有序存储,因而只须要一次对磁盘的拜访即可读取所有数据。而对于无序存储的数据来说,最坏的状况可能须要读取 n 次磁盘。
还是以一个小例子来做下阐明:
SELECT avg(price) FROM orders where age between 20 and 30;
计算订单中年龄在 20 到 30 岁用户的均匀订单金额。假如数仓内有 1 亿条记录,每条数据约 1k,其中 20-30 岁之间的用户订单大概有 10%。
在数据依照 age 有序存储的状况下,读取的数据量为 1 亿 10%1KB≈10G。
若数据未依照 age 有序存储,这种状况下,读取的数据量为 1 亿 10%4K*(1-27.1%)≈29.2G。两者相差靠近 3 倍。
由此可见,整体上来说,有序的数据在查问时更占优势。因而,clickhouse 在设计时应用了写入前预排序,以保障查问时能取得更快的速度。不过这也必然带来了数据写入的延时,因而 clickhouse 不适宜用在写多读少的场景。
说完了预排序,再来说下批处理对性能的影响。clickhouse 能解决的最小单位是 block,block 就是一群行的汇合,默认最大 8192 行组成一个 block。
其实做了预排序后再做批处理很好了解,毕竟存储到 clickhouse 中的数据都是有序的,而 clickhouse 设计进去是为了解决上百亿条记录的大数据数仓,因而个别的范畴查问返回的数据量都十分大,如果每次解决 1 行数据的话,就会大大增加磁盘 IO 次的次数。当然,到目前为止,只是减少了 IO 次数,并没有缩小数据量,因而到此时,依照 block 读取的优化如同显得没有必要,毕竟一次 IO 的工夫和读取数据的工夫相比,根本能够忽略不计。读者们不必焦急,真正 block 的省时的点就在下一段。
block 真正施展威力的点其实是在压缩!对,没错,就是毫不起眼的压缩!那么压缩能节俭多少数据量呢?咱们还是拿 clickhouse 存储引擎中理论存储的数据谈话。以 clickhouse 官网提供的 hits_v1 库为例,我筛选了其中的 UserID 列为例,应用 clickhouse 提供的 compressor 工具读取该列的数据文件,能够看到这个文件中每一个 block 的压缩前和压缩后的大小。
我大抵看了一下,压缩率最大的一个 block 压缩前是 130272 间接,压缩后只有 639 字节,压缩率高达 203 倍!当然,这是特例,那咱们统计下整个文件的 block 的压缩前和压缩后的大小,还是这个列为例,UserID 列压缩前是 70991184 字节,压缩后是 11596909 字节,压缩比约为 6.2 倍!
能达到这么高压缩比,其实是列存的功绩,对于列存数据库,因为每一列独自存储,因而每个数据文件相比行存数据库来说更有法则,因而能够达到十分高的压缩率。
到这里,批处理的威力就进去了,通过压缩,再次升高了 6 倍的文件大小, 也就是说再次缩小 6 倍的磁盘 IO 工夫。
这里就是 clickhouse 最重要的存储引擎上的优化,通过批处理 + 预排序,相比拟于无此性能的列式数据库来说,缩小了范畴查问量在 10% 左右时大概 18 倍的磁盘读取工夫。而若在百亿数据库中,查问量 1% 左右时能节俭 24 倍的磁盘读取工夫。
当然,任何架构都有两面性,在节俭磁盘读取工夫的状况下,也带来了如下毛病:
- 适宜数据的大批量写入,如果写入频繁,会影响写入性能
- 如果范畴查问的数据量大,那么性能晋升会低。因而数据量太小时无奈施展最大劣势。
- 因为依照 block 作为最小解决单位,因而删除单条数据性能不高。
- 批改的性能很差,尤其是批改了用于排序的列。因而不适宜做事务型数据库。
附
–
可能有读者会问,为什么无序存储要乘以 4K。这个起因是因为操作系统在读取磁盘时,根据数据局部性原理,会依照页为单位读取,每页的大小默认是 4k。在 unistd.h 头文件中的 getpagesize() 能够获取本机的页面大小,这里依照默认大小进行计算。
式子中的 27.1% 是指的缓存命中率,命中率由须要查问的数据占所有数据的百分比 r 决定。在本例中依照 4k 的页面大小和 1k 的记录大小,命中率和数据占比之间的关系如下图所示:
缓存命中率和数据量占比之间的关系 y=1-r^3
不难发现,两者成负相关的相关性。