ClickHouse为什么那么快

为啥他能这么快呢?设计如此=-=

概述

ClickHouse设计的初衷是to filter and aggregate data as fast as possible

再优秀的系统的设计也只能对各个指标进行取舍,无法兼得,而ClickHouse的最重要的指标和任务就是做好这样的一件事:尽可能快的过滤和聚合数据。实际上,这也是传统的OLAP系统所追求的指标。体现到语义上,就是实现GROUP BY查询的实现。

为了实现这一目的,ClickHouse在上层设计上做出了如下的优化:

  • Column-oriented storage
  • Indexes
  • Block
  • Pre-sort
  • Data compression
  • Vectorized query execution
  • Scalability

然而许多其他的数据库使用的相似的优化策略,为什么ClickHouse能够脱颖而出呢?另一个值得关注的点是其底层实现细节。一个观点是:ClickHouse并没有太多的新的方法或者理论,而是将前人的研究成果应用到了OLAP这个领域。因此可以说,ClickHouse是一个非常优秀及经典的工程化项目,是人类计算机工程的结晶。

Column-oriented storage

列式存储能够显著的降低磁盘IO。对于OLAP场景中,每次只关心一个大宽表中的某几列的场景,相比于行式存储,列式存储仅需获取需要的数据,磁盘的IO理论上能够降低为所关心的列与所有列之比。因此,越是宽的表,行式存储的优势越明显。

同样,使用ClickHouse时,也一定要使用列式存储数据库的方式来查询数据,查询时指定具体的列,否则性能提升会不明显。

Block

ClickHouse使用Block实现批处理。Block 是ClickHouse中的数据最小处理单元,表示内存中表的子集(chunk)的容器,是由三元组: (IColumn, IDataType, 列名) 构成的集合。在查询执行期间,数据是按 Block 进行处理的。如果我们有一个 Block ,那么就有了数据(在 IColumn 对象中),有了数据的类型信息告诉我们如何处理该列,同时也有了列名(来自表的原始列名,或人为指定的用于临时计算结果的名字)。
当我们遍历一个块中的列进行某些函数计算时,会把结果列加入到块中,但不会更改函数参数中的列,因为操作是不可变的。之后,不需要的列可以从块中删除,但不是修改。这对于消除公共子表达式非常方便。

Pre-sort

ClickHouse会对插入的数据进行预排序(基于LSM算法)。这里设计的原因是针对大数据量场景,为了处理上百亿条记录的数据,一般的查询返回的数据量都非常大,如果数据是无序的,对于按字段聚合或者范围查询的场景,会大大增加磁盘IO次的次数。

to be discussed
这里预排序是对每个字段都预排序吗?如果按照字段A进行范围筛选,获取相应的字段B进行计算,按照ClickHouse的列式存储和预排序,这里的流程是如何实现的?ClickHouse是如何根据排序后的A,找到对应行的B的呢?

Data compression

数据压缩是提升ClickHouse性能的一个关键。我们发现,前面介绍的列存、分块和预排序,实际上都对压缩有着一定的好处,

  • 列式的存储使得数据更有规律,可以更好地进行数据压缩(相同类型的数据放在一起,对压缩更加友好);同时,能够最小化数据扫描的范围。
  • 排序后的数据可以使用更有效的压缩方式来进行处理。
  • 按照block作为最小处理单元的原因是,虽然数据被压缩后能够有效减少数据大小,降低存储空间并加速数据传输效率,但数据的压缩和解压动作,其本身也会带来额外的性能损耗。所以需要控制被压缩数据的大小,以求在性能损耗和压缩率之间寻求一种平衡。在具体读取某一列数据时(.bin文件),首先需要将压缩数据加载到内存并解压,这样才能进行后续的数据处理。通过压缩数据块,可以在不读取整个.bin文件的情况下将读取粒度降低到压缩数据块级别,从而进一步缩小数据读取的范围。

Indexes

由于ClickHouse实现了在插入时进行了预排序,因此其索引做的非常简单,无需像MySQL需要设置单独的索引文件索引数据的位置,因为其数据本身就是有序的。

ClickHouse的索引包括一级索引标记二级索引一级索引记录每一个block第一个值。例如一组一亿行的数据,主键范围从1~100,000,000。存储到ClickHouse后按照8192行为一个block,那么一共有12208个block。索引为1,8193,16635……在查询时只需要就可以根据值确定到需要读取哪几个block了。但是仅仅靠定位到具体哪个block还是不够,因为我们仍不清楚这个block在文件的哪个位置,因此就有了标记来记录block在文件中的偏移量。由于一级索引非常小,1亿条数据只需要1万多行的索引,因此一级索引可以常驻内存,加速查找。

Vectorized query execution

ClickHouse使用C++进行开发,通过向量化的实现进行数据的高效查询。主要包括提升CPU 缓存利用率,以及使用 SIMD CPU 指令。
Clickhouse在所有能够提高CPU计算效率的地方,都大量的使用了SIMD,频繁调用的基础函数,大量的进行可并行计算,将工程上的优化做到了极致。

以下的代码是一个例子,对于一个简单的额大小写转换的方法,也调用了SIMD命令来进行优化。

template <char not_case_lower_bound, char not_case_upper_bound>struct LowerUpperImpl{
public:   
	static void array( char * src, char * src_end, char * dst)   {
   //32
		const auto flip_case_mask = 'A' ^ 'a';
#ifdef __SSE2__
		const auto bytes_sse = sizeof(__m128i);
		const auto src_end_sse = src_end - (src_end - src) % bytes_sse;
		const auto v_not_case_lower_bound = _mm_set1_epi8(not_case_lower_bound - 1);
		const auto v_not_case_upper_bound = _mm_set1_epi8(not_case_upper_bound + 1);
		const auto v_flip_case_mask = _mm_set1_epi8(flip_case_mask);
		for (; src < src_end_sse; src += bytes_sse, dst += bytes_sse){
		   	//_mm_loadu_si128表示:Loads 128-bit value;即加载128位值。
		   	//一次性加载16个连续的8-bit字符
			const auto chars = _mm_loadu_si128(reinterpret_cast<const __m128i *>(src));
		   	//_mm_and_si128(a,b)表示:将a和b进行与运算,即r=a&b
		   	//_mm_cmpgt_epi8(a,b)表示:分别比较a的每个8bits整数是否大于b的对应位置的8bits整数,若大于,则返回0xff,否则返回0x00。
		   	//_mm_cmplt_epi8(a,b)表示:分别比较a的每个8bits整数是否小于b的对应位置的8bits整数,若小于,则返回0xff,否则返回0x00。
		   	//下面的一行代码对这128位的寄存器并行操作了3遍,最后得到一个128位数,对应位置上是0xff的,表示
		   	//那个8-bit数在 [case_lower_bound, case_upper_bound]范围之内的,其余的被0占据的位置,是不在操作范围内的数。
			const auto is_not_case = _mm_and_si128(_mm_cmpgt_epi8(chars, v_not_case_lower_bound), _mm_cmplt_epi8(chars, v_not_case_upper_bound));
   			//每个0xff位置与32进行与操作,原来的oxff位置变成32,也就是说,每个在 [case_lower_bound, case_upper_bound]范围区间的数,现在变成了32,其他的位置是0
			const auto xor_mask = _mm_and_si128(v_flip_case_mask, is_not_case);
   			//将源chars内容与xor_mask进行异或,符合条件的字节可能从uppercase转为lowercase,也可能从lowercase转为uppercase,不符合区间的仍保留原样。
			const auto cased_chars = _mm_xor_si128(chars, xor_mask);
   			//将结果集存到dst中
			_mm_storeu_si128(reinterpret_cast<__m128i *>(dst), cased_chars);
		}
#endif
#ifndef __SSE2__
		for (; src < src_end; ++src, ++dst)
			if (*src >= not_case_lower_bound &amp;&amp; *src <= not_case_upper_bound)
				*dst = *src ^ flip_case_mask;
			else
				*dst = *src;
#endif   
	}
};

Scalability

ClickHouse 可以利用所有可用的 CPU 内核和磁盘来执行单个查询。 不仅在单个服务器上,而且还包括集群的所有 CPU 内核和磁盘。

ClickHouse将数据划分为多个partition,每个partition再进一步划分为多个index granularity,然后通过多个CPU核心分别处理其中的一部分来实现并行数据处理。

在这种设计下,单条Query就能利用整机所有CPU。极致的并行处理能力,极大的降低了查询延时。

除了优秀的单机并行处理能力,ClickHouse还提供了可线性拓展的分布式计算能力。ClickHouse会自动将查询拆解为多个task下发到集群中,然后进行多机并行处理,最后把结果汇聚到一起。

在存在多副本的情况下,ClickHouse还提供了多种query下发策略。

当前的架构,会带来什么问题

ClickHouse的compaction

这个问题实际上是LSM算法的特点,为了实现高效的追加写和预排序,必然要在其他方面有一些牺牲。

LSM算法的几个核心步骤:

在于数据写入存储系统前首先记录日志,防止系统崩溃。
记录完日志后在内存中以供使用,当内存达到极限后写入磁盘,记录合并次数Level为0(L=0)。已经写入磁盘的文件不可变。
每过一段时间将磁盘上L和L+1的文件合并
我们用一个示例来展示下整个过程
T=0时刻,数据库为空。
T=1时刻,clickhouse收到一条500条insert的插入请求,这500条数据时乱序的。此时,clickhouse开始插入操作。首先将500条插入请求一次性写入日志。接着在内存中进行排序,排序完成后将有序的结果写入磁盘,此时L=0;
T=2时刻,clickhouse收到一条800条insert的插入请求,这800条数据时乱序的。此时,clickhouse开始插入操作。首先将800条插入请求一次性写入日志。接着在内存中进行排序,排序完成后将有序的结果写入磁盘,此时L=0;
T=3时刻,clickhouse开始合并,此时此刻,磁盘上存在两个L=0的文件。这两个文件每个文件内部有序,但可能存在重合。(例如第一批500条的范围是300-400,第二批800条数据的范围是350-700)。因此需要合并。clickhouse在后台完成合并后,产生了一个新的L=1的文件。将两个L=0的文件标记为删除。
T=4时刻,clickhouse开始清理,将两个被标记为删除的文件真正地物理删除。
T=5时刻,clickhouse收到一条100条insert的插入请求,这100条数据时乱序的。此时,clickhouse开始插入操作。首先将100条插入请求一次性写入日志。接着在内存中进行排序,排序完成后将有序的结果写入磁盘,此时L=0;
T=6时刻,clickhouse开始合并,此时此刻,磁盘上存在1个L=0的文件和1个L=1的文件。这两个文件每个文件内部有序,但不存在重合。(例如L0文件的范围是100-200,L1文件的范围是300-700)。因此不需要合并。clickhouse在后台将L=0的文件升级成L=1,此时数据库内存在两个L=1的互不重合的文件。

以上过程来自Clickhouse系列-番外-LSM算法 - 知乎 (zhihu.com)

如上述过程,这里整个系统必然会频繁的进行Compaction(合并),写入量越大,Compaction的过程越频繁。而compaction是一个compare & merge的过程,非常消耗CPU和存储IO,在高吞吐的写入情形下,大量的compaction操作占用大量系统资源,必然带来整个系统性能断崖式下跌,对应用系统产生巨大影响,当然我们可以禁用自动Major Compaction,在每天系统低峰期定期触发合并,来避免这个问题。

文章wxlog如何使用clickhouse支撑百万亿行日志存储与检索 - WXG技术能力提升 - KM平台 (woa.com)就提到了对合并过程的调优。

Block作为最小处理单元
  • 由于按照block作为最小处理单位,因此删除单条数据性能不高。

  • 修改的性能很差,尤其是修改了用于排序的列。

  • 不支持事务

多核的使用

因为采用了并行处理机制,即使一个查询,也会用服务器一半的CPU去执行,所以ClickHouse不能支持高并发的使用场景,默认单查询使用CPU核数为服务器核数的一半,安装时会自动识别服务器核数,可以通过配置文件修改该参数。

你可能感兴趣的:(中间件,clickhouse,数据库,java)