ClickHouse 的基本介绍(一)

楔子

最近公司决定采用 ClickHouse 来做数据的大规模处理,关于 ClickHouse 虽然早有耳闻,但因为时间原因并没有专门去学习。而公司也考虑到目前内部具有 ClickHouse 使用经验的人还不是很多,因此给了相对比较充足的时间去了解。虽然 ClickHouse 诞生于 2016 年,但相对于 Hadoop 生态圈而言,普及度显然还没有那么广,因此除了官网之外还没有看到比较合适的教程。不过幸运的是在京东上面发现了一本关于 ClickHouse 的书,叫《ClickHouse 原理解析与应用实践》,由朱凯老师编写,据悉这是第一本讲解 ClickHouse 的书。看了一下目录,感觉内容还是比较充实的,于是果断买下来,用于学习。

因此本文很多内容均来自于此书,只不过书中的 ClickHouse 版本有些低了(ClickHouse 的发布频率还是挺快的),这里采用了一个比较新的版本,因此安装时的细节会有些不同。那么下面就开始 ClickHouse 的学习之旅吧,看看 ClickHouse 究竟是何方神圣,为何能够异军突起。

Google 于 2003~2006 年相继发表了三篇论文:”Google File System”、”Google MapReduce”、”Google Bigtable”,将大数据的处理技术带进了大众视野,而 2006 年开源项目 Hadoop 的出现,则标志着大数据处理技术普及的开始,大数据技术真正开始走向大众。Hadoop 最初指的是分布式文件系统 HDFS 和 MapReduce 计算框架,但是它一路高歌猛进,在此基础之上像搭积木一样快速发展成为一个庞大的生态(被称为 Hadoop 生态圈),其中包括 Hive、HBase、Spark 等数十种框架。而在大数据分析场景的解决方案中,传统的关系型数据库很快就被 Hadoop 生态圈所取代,BI 领域就是其中之一。像传统关系型数据库所构建的数据仓库,就被以 Hive 为代表的大数据技术所取代,数据查询分析的手段更是层出不穷,Spark、Impala、Kylin 等框架百花齐放。Hadoop 发展至今,早已上升成为大数据的代名词,仿佛一提到海量数据分析场景下的技术选型,就非 Hadoop 生态莫属。

然而世间并没有银弹(万全之策),Hadoop 也跳不出这个规则。虽然 Hadoop 生态圈已经相当完善了,不同的组件也可以相互对接,例如分布式文件系统 HDFS 可以直接作为其他组件的底层存储(像 HBase、Hive 等),生态内部的组件之间不用重复造轮子,只需相互借力、组合就能形成新的方案。但生态化的另一面则可以看做臃肿和复杂,Hadoop 生态下每种组件都自成一体、相互独立,这种强强组合的技术组件有些时候则显得过于笨重了。与此同时,随着现代化终端系统对实时性的要求越来越高,Hadoop 生态在海量数据和高时效性的双重压力下,也显得有些力不从心了。

而这个时候,ClickHouse出现了,它是俄罗斯的 Yandex 公司于 2016 年开源的列式存储数据库,使用 C++ 语言编写,专门用于 OLAP(联机分析处理),其惊人的性能可以瞬间让你跪倒在它的石榴裙下。

什么是 OLAP?

这里先回顾一下什么是 OLAP,以及实现 OLAP 的几种常见思路。

前面说了,OLAP 名为联机分析处理,又可以称之为多维分析处理,是由关系型数据之父于 1993 年提出的概念。顾名思义,它指的是通过多种不同的维度审视数据,进行深层次分析。维度可以看成是观察数据的一种视角,例如人类能看到的世界是三维的,它包含长、宽、高三个维度。直接一点理解,维度就好比是一张数据表的字段,而多维分析则是基于这些字段进行聚合查询。那么多维分析通常都包含哪些基本操作呢?为了更好地理解多维分析的概念,可以通过对一个立方体的图像进行具象化来描述。以如下一张销售明细表为例:

img

那么数据立方体可以进行如下操作:

下钻:从高层次向低层次明细数据进行穿透。例如从 “省” 下钻到 “市”,从 “湖北省” 穿透到 “武汉” 和 “宜昌” 。

img

上卷:和下钻相反,从低层次向高层次汇聚。例如从 “市” 汇聚到 “省”,将 “武汉” 和 “宜昌” 汇聚成 “湖北”。

img

切片:观察立方体的一层,将一个或多个温度设为单个固定的值,然后观察剩余的维度,例如将商品维度固定为 “足球”。

img

切块:和切片类似,只是将单个固定值变成多个固定值。例如将商品维度固定为”足球”、”篮球” 和 “乒乓球”。

img

旋转:旋转立方体的一面,如果要将数据映射到一张二维表,那么就要进行旋转,等同于行列转换。

img

OLAP 架构分类

为了实现上述操作,将常见的 OLAP 架构大致分为三类。

第一类架构称为 ROLAP(Relational OLAP,关系型 OLAP),顾名思义,它直接使用关系模型构建,数据模型常使用星型模型或者雪花模型,这是最先能够想到、也是最为直接的实现方法。

星型模型:事实表周围的维度表只能有一层;雪花模型:事实表周围的维度表可以有多层。

因为 OLAP 概念在最初提出的时候,就是建立在关系型数据库之上的,多维分析的操作可以直接转成 SQL 查询。例如,通过上卷操作查看省份的销售额,就可以转成类似下面的 SQL 语句:

1
SELECT SUM(价格) FROM 销售数据表 GROUP BY 省;

但是这种架构对数据的实时处理能力要求很高,试想一下,如果对一张存有上亿条记录的表同时执行数十个字段的GROUP BY查询,将会发生什么事情?

第二类架构称为 MOLAP(Multidimensional OLAP,多维型 OLAP),它的出现就是为了缓解 ROLAP 性能问题。

MOLAP 使用多维数组的形式保存数据,其核心思想是借助预先聚合结果(说白了就是提前先算好,然后将结果保存起来),使用空间换取时间的形式从而提升查询性能。也就是说,用更多的存储空间换得查询时间的减少,其具体的实现方式是依托立方体模型的概念。首先,对需要分析的数据进行建模,框定需要分析的维度字段;然后,通过预处理的形式,对各种维度进行组合并事先聚合;最后,将聚合结果以某种索引或者缓存的形式保存起来(通常只保留聚合后的结果,不存储明细数据),这样一来,在随后的查询过程中,可以直接利用结果返回数据。

但是这种架构显然并不完美,原因如下:

  • 预聚合只能支持固定的分析场景,所以它无法满足自定义分析的需求。
  • 维度的预处理会导致数据膨胀,这里可以做一次简单的计算,以上面的销售明细表为例,如果数据立方体包含了 5 个维度(字段),那么维度的组合方式就有 2525 种。维度组合爆炸会导致数据膨胀,这样会造成不必要的计算和存储开销。此外,用户并不一定会用到所有维度的组合,那么没有被用到的组合将会浪费。
  • 另外,由于使用了预聚合的方式,数据立方体会有一定的滞后性,不能实时地进行数据分析。所以对于在线实时接收的流量数据,预聚合还需要考虑如何及时更新数据。而且立方体只保留了聚合后的结果数据,因此明细数据是无法查询的。

第三类架构称为 HOLAP(Hybrid OLAP,混合架构的OLAP),这种思路可以理解成 ROLAP 和 MOLAP 两者的组合,这里不再展开,我们重点关注 ROLAP 和 MOLAP。

OLAP 实现技术的演进

在介绍了 OLAP 几种主要的架构之后,再来看看它们背后技术的演进过程,我们把这个演进过程划分为两个阶段。

第一个可以称为传统关系型数据库阶段。在这个阶段中,OLAP 主要基于以 Oracle、MySQL 为代表的一种关系型数据库实现。在 ROLA P架构下,直接使用这些数据作为存储与计算的载体;在 MOLAP 架构下,则借助物化视图的方式实现数据立方体。在这个时期,不论是 ROLAP 还是 MOLAP,在数据体量大、维度数目多的情况下都存在严重的性能问题,甚至存在根本查询不出结果的情况。

第二个阶段可以称为大数据技术阶段。由于大数据技术的普及,人们开始使用大数据技术重构 ROLAP 和 MOLAP。以 ROLAP 架构为例,传统关系型数据库就被 Hive 和 SparkSQL 这类新兴技术所取代。虽然,以 Spark 为代表的分布式计算系统,相比 Oracle 这类传统数据库而言,在面向海量数据的处理性能方面已经优秀很多,但是直接把它们作为面向终端用户的在线查询系统则还是太慢了。我们的用户普遍缺乏耐心,如果一个查询响应需要几十秒甚至数分钟才能返回,那么这套方案就完全行不通。再看 MOLAP 架构,MOLAP 背后也转为依托 MapReduce 或 Spark 这类新兴技术,将其作为立方体的计算引擎,进行立方体的构建,其预聚合结构的存储载体也转向 HBase 这类高性能分布式数据库。大数据技术阶段,主流 MOLAP 架构已经能够在亿万级数据的体量下,实现毫秒级的查询,但我们说 MOLAP 架构会存在维度爆炸、数据同步实时性不高的问题。

不难发现,虽然 OLAP 在经历了大数据技术的洗礼之后,其各方面性能已经有了脱胎换骨式的改观,但不论是 ROLAP 还是 MOLAP,仍然存在各自的痛点。可如果单纯从模型角度考虑,很明显 ROLAP 要更胜一筹,因为关系型数据库的存在,所以关系模型拥有最好的 “群众基础”,也更简单且容易理解。它直接面向明细数据查询,由于不需要预处理,也就自然没有预处理带来的负面影响(维度组合爆炸、数据实时性、更新问题)。那是否存在这样一种技术,它使用 ROLAP 模型,但同时又拥有比肩 MOLAP 的性能呢?

显然是存在的,就是我们今天的主角 ClickHouse,有一篇性能报告,在 10 亿条测试数据的体量下,Spark 被 ClickHouse 打的落花流水。ClickHouse 具有 ROLAP、在线实时查询、完整的 DBMS、列式存储、不需要任何数据预处理、支持批量更新、拥有非常完善的 SQL 支持和函数、支持高可用、不依赖 Hadoop 复杂生态、开箱即用等许多特点。特别是它那夸张的查询性能,我想大多数刚接触 ClickHouse 的人也一定会因为它的性能指标而动容。

在一系列官方公布的基准测试对比中,ClickHouse 都遥遥领先对手,这其中也不乏一些我们耳熟能详的名字。所有用于对比的数据库都使用了相同配置的服务器,在单个节点的情况下,对一张拥有 133 个字段的数据表分别在 1000 万、1 亿和 10 亿三种数据体量下执行基准测试,基准测试的范围涵盖 43 项SQL查询。在 1 亿数据级体量的情况下,ClickHouse 的平均响应速度是 Vertica 的2.63倍、InfiniDB 的 17 倍、MonetDB 的 27 倍、Hive 的 126 倍、MySQL 的 429 倍以及 Greenplum 的 10 倍。

此外 ClickHouse 是一款开源软件,遵循 Apache License 2.0 协议,所以它可以被免费使用。

ClickHouse 的名字由来:ClickHouse 最初的设计目标是为了服务于自家公司的一款名叫 Metrica 流量分析工具。Metrica 在采集数据的过程中,一次页面点击(Click),就会产生一个事件(Event),就是基于页面的点击事件流(Stream),然后面向数据仓库进行 OLAP 分析。所以 ClickHouse 的全称是 Click Stream、Data WareHouse,简称 ClickHouse。

初识 ClickHouse

随着业务的迅猛增长,Yandex 公司的 Metrica 目前已经成为世界第三大 Web 流量分析平台,每天处理超过两百亿个跟踪事件,而之所以能够有如此惊人的体量,在背后为其提供支撑的 ClickHouse 功不可没。ClickHouse 已经为 Metrica 存储了超过 20 万亿行的数据,百分之 90 的查询能够在 1 秒内返回,其集群规模也已经超过了 400 台服务器。虽然 ClickHouse 起初只是为了 Metrica 而研发的,但由于它出众的性能,目前也被广泛应用于 Yandex 公司内部的其它数十个产品中。

除了 Yandex 自己以外,ClickHouse 还被众多商业公司或研究组织应用到了其生产环境。欧洲核子研究中心(CERN,想起了石头门)将它用于保存强对撞机试验后所记录的数十亿事件的测量数据,并成功将先前的数据查询时间从几个小时缩短到几秒;著名的 CDN 服务厂商 CloudFlare 将 ClickHouse 用于 HTTP 的流量分析;国内的头条、阿里巴巴、腾讯和新浪等一众互联网公司都在应用 ClickHouse。

可以说 ClickHouse 具备了人们对一款高性能 OLAP 数据库的美好向往,所以它基本能够胜任各种数据分析类的场景,并且随着数据体量的增大,它的优势也会变得越为明显。ClickHouse 非常适用于商业智能领域,也就是我们所说的 BI 领域。除此之外,它也能够被广泛应用于广告流量、Web、App流量、电信、金融、电子商务、信息安全、网络游戏、物联网等众多其它领域。

想必到了这里,你是不是产生了这样的一种错觉呢?ClickHouse 仿佛违背了物理定律,没有任何缺点,是一个不真实的存在,一款高性能、高可用 OLAP 数据库的一切诉求,ClickHouse 都能满足。但是估计很多人之前都是 Hadoop 生态圈的,那么你在转向 ClickHouse 的时候应该会有一些不适应,因为它和我们平时使用的技术的 “性格” 迥然不同,这是正常现象。如果把数据库比作汽车,那么 ClickHouse 俨然就是一辆手动挡的赛车,它在很多方面不像其它系统那样高度自动化。ClickHouse 的一些概念也和我们平常理解的有所不同,特别是在分片和副本方面,有些时候数据的分片甚至需要手动完成。但在进一步深入使用 ClickHouse 之后,我们就会渐渐理解这些设计的目的,某些看似不自动化的设计,在实际使用中反而带来了极大的灵活性。与 Hadoop 生态的其它数据库相比,ClickHouse 更像是一款传统 MPP 架构的列式存储数据库,它没有采用 Hadoop 生态中常用的主从架构,而是使用了多主对等网络结构,同时它也是基于关系模型的 ROLAP 方案。

那么下面就让我们抽丝剥茧,看看 ClickHouse 都有哪些核心特性。

完备的 DBMS 功能

ClickHouse 拥有完备的管理功能,所以它称得上是一个 DBMS(DataBase Management System,数据库管理系统),而作为一个 DBMS,它具备如下功能:

  • DDL(数据定义语言):可以动态地创建、修改或者删除数据库、表和视图,而无需重启服务
  • DML(数据操作语言):可以动态地查询、插入、修改或删除数据
  • 权限控制:可以按照用户粒度设置数据库或者表的操作权限,保障数据的安全性
  • 数据备份与恢复:提供了数据备份导出与导入恢复机制,满足生产环境的要求
  • 分布式管理:提供集群模式,能够自动管理多个数据库节点

上面只列举了一些最具代表性的功能,但这已然足以表明为什么 ClickHouse 称得上是 DBMS 了。

但是注意,ClickHouse 虽然很优秀,但它毕竟是一款面向 OLAP 的数据库,我们不能把它用于任何 OLTP 事务性操作的场景,因为它有以下几点不足:

  • 不支持事务
  • 不擅长根据主键按行粒度进行查询(虽然支持),所以不应该把 ClickHouse 当做键值对数据库使用
  • 不擅长按行删除数据(虽然支持)

不过这些不足并不能算是 ClickHouse 的缺点,事实上其它的同类高性能面向 OLAP 的数据库一样不擅长上面这些。因为对于 OLAP 数据库而言,上述这些能力不是重点,只能说这是为了极致的查询性能所做的权衡。

列式存储与数据压缩

列式存储与数据压缩,对于一款高性能数据库来说是必不可少的特性。一个非常流行的观点认为:如果你想让查询变得更快,最简单且有效的方法就是减少数据扫描范围和数据传输时的大小,而列式存储和数据压缩就可以实现上面两点。列式存储和数据压缩通常是伴生的,因为一般来说列式存储是数据压缩的前提。

那什么是列式存储呢?

首先列式存储,或者说按列存储,相比按行存储,前者可以有效减少查询时需要扫描的数据量,我们可以举个栗子说明一下。假设一张数据表 A,里面有 50 个字段 A1 ~ A50,如果我们需要查询前 5 个字段的数据的话,那么可以使用如下 SQL 实现:

1
SELECT A1, A2, A3, A4, A5 from A;

但是这样问题来了,数据库每次都会逐行扫描、并获取每行数据的全部字段,这里就是 50 个,然后再从中返回前 5 个字段。因此不难发现,尽管只需要前 5 个字段,但由于数据是按行进行组织的,实际上还是扫描了所有的字段。但如果数据是按列进行存储,则不会出现这样的问题,由于数据按列进行组织,数据库可以直接选择 A1 ~ A5 这 5 列的数据并返回,从而避免多余的数据扫描。为了更好的说明这两者的区别,我们画一张图:

img

如果是按行存储的话,那么假设我们要计算 age 这一列的平均值,就需要一行一行扫描,所以最终会至少扫描 11 个值( 3 + 3 + 3 + 2 )才能找到 age 这一列所存储的 4 个值。这意味着我们要花费更多的时间等待 IO 完成,而且读完之后还要扔掉很多(因为我们只需要部分字段)。但如果是按列存储的话,我们只需要获取 age 这一列的连续快,即可得到我们想要的 4 个值,所以这种操作速度更快、效率更高。

按列存储相比按行存储的另一个优势是对数据压缩的友好性,同样可以举一个栗子简单说明一下压缩的本质是什么。假设有个字符串 abcdefghi_bcdefghi,现在对它进行压缩,如下所示:

1
2
压缩前:abcdefghi_bcdefghi
压缩后:abcdefghi_(9,8)

可以看到,压缩的本质就是按照一定步长对数据进行匹配扫描,当发现重复部分的时候就会编码转换。例如上面的 (9, 8),表示从下划线开始向前移动 9 个字节,会匹配到 8 个字节长度的重复项,即 bcdefghi。

尽管真实的压缩算法要比这个复杂许多,但压缩的本质就是如此。数据中的重复项越多,则压缩率越高;压缩率越高,则数据体量越小;而数据体量越小,在网络中传输的速度则越快,并且对网络带宽和磁盘 IO 的压力也就越小。可怎样的数据最可能具备重复的特性呢?答案是属于同一个列字段的数据,因为它们具有相同的数据类型和现实语义,重复项的可能性自然就更高。

ClickHouse 就是一款使用列式存储的数据库,数据按列进行组织,属于同一列的数据会被保存在一起,并进行压缩,而列与列之间的数据也会由不同的文件分别保存(这里主要是指 MergeTree 引擎,后续会详细介绍)。数据默认使用 LZ4 算法压缩,在 Yandex 公司的 Metrica 生产环境中,数据整体的压缩比可以达到 8 比 1(未压缩前 17 PB,压缩后 8 PB)。另外列式存储除了降低 IO 和存储的压力之外,还为向量化执行做好了铺垫。

向量化执行引擎

坊间有句玩笑:”能用 money 解决的问题,千万别花时间”。而业界也有种调侃与之如出一辙:”能升级硬件解决的问题,千万别优化程序”。有时候,你千辛万苦优化程序逻辑带来的性能提升,还不如直接升级硬件来的简单直接。这虽然是一句玩笑话不能当真,但硬件层面的优化确实是最简单粗暴、并且有效的途径之一。向量化执行就是典型代表,这项寄存器硬件层面的特性,为上层应用的程序在性能上带来了指数级的提升。

向量化执行可以简单地看做成一种消除程序中的循环所做的优化,举个栗子。假设小明在卖果汁,而榨一杯果汁假设需要 2 分钟,有客户抱怨太慢了,那么为了增加速度要怎么办呢?显然多准备几台榨汁机就好了,因此非向量化执行的方式是利用 1 台榨汁机重复 n 次,而向量化执行的方式是利用 n 台榨汁机只执行 1 次。

而为了实现向量化执行,需要利用 CPU 的 SIMD 指令。SIMD 的全称是:Single Instruction Multiple Data,即用单条指令操作多条数据。现代计算机系统概念中,它是通过数据并行以提高性能的一种实现方式(其它的还有指令级并行和线程级并行),它的原理是在 CPU 寄存器层面实现数据的并行计算。

在计算机系统的体系结构中,存储系统是一种层次结构,典型服务器计算机的存储层次结构如下所示:

img

它们的大小会根据 CPU 的不同而不同,以我之前的个人笔记本电脑为例:

img

从左向右距离 CPU 越近,访问速度就越快,显然能够容纳的数据大小也就越小;别忘了,CPU 寄存器也是可以存储数据的,CPU 从寄存器中获取数据的速度是最快的,是内存的 300 倍,磁盘的 3000 万倍。所以利用 CPU 向量化执行的特性,对于程序的性能提升有着非凡的意义。

ClickHouse 目前使用 SSE 4.2 指令集实现向量化执行。

关系模型与 SQL 查询

相比 HBase 和 Redis 这类 NoSQL 数据库,ClickHouse 使用关系模型描述数据并提供了传统数据库的概念(数据库、表、视图和函数等)。与此同时,ClickHouse 完全使用 SQL 作为查询语言(支持 GROUP BY、ORDER BY、JOIN、IN 等大多数标准 SQL),这使得它平易近人,容易理解和学习。因为关系型数据库和 SQL 语言,可以说是软件领域发展至今应用最为广泛的技术之一,也正因为 ClickHouse 提供了标准协议的 SQL 查询接口,使得现有的第三方分析可视化系统可以轻松地与它集成对接。而在 SQL 解析方面,ClickHouse 是大小写敏感的,这意味着 SELECT a 和 SELECT A 所代表的语义是不同的(关键字非大小写敏感)。

关系模型相比文档模型、键值对模型等等,拥有更好的描述能力,也能更加清楚地表示实体之间的关系。更重要的是,在 OLAP 领域,已有的大量数据建模工作都是基于关系模型展开的(星座模型、雪花模型和宽表模型)。ClickHouse 使用了关系模型,所以将构建在传统关系型数据库或者数仓之上的系统迁移到 ClickHouse 的成本会变得更低,可以直接沿用之前的经验成果。

多样化的表引擎

ClickHouse 并不是直接就一蹴而就的,Metrica 产品的最初架构是基于MySQL实现的,所以在 ClickHouse 的设计中,能够察觉到一些 MySQL 的影子,表引擎的设计就是其中之一。与 MySQL 类似,ClickHouse 也将存储部分进行了抽象,把存储引擎作为一层独立的接口,并且拥有合并树、内存、文件、接口等 20 多种引擎。其中每一种引擎都有着各自的特点,用户可以根据实际业务场景的需求,选择合适的引擎。

通常而言,一个通用系统意味着更广泛的实用性,能够适应更多的场景。但通用的另一种解释是平庸,因为它无法在所有场景中都做到极致。

在软件的世界中,并不会存在一个能够适用任何场景的通用系统,为了突出某项特性势必会在别处有所取舍。所以将表引擎独立设计的好处是显而易见的,通过特定的表引擎支撑特定的场景,十分灵活。对于简单的场景,可直接使用简单的引擎降低成本,而复杂的场景也有合适的选择。

多线程与分布式

ClickHouse 几乎具备现代化高性能数据库的所有典型特征,对于可以提升性能的手段可谓是全部用尽,对于多线程和分布式这类被广泛应用的技术自然更是不在话下。

如果说向量化执行是通过数据级别的并行方式提升了性能,那么多线程处理就是通过线程级并行的方式实现了性能提升。相比基于底层硬件实现的向量化执行 SIMD,线程级并行通常由更高层次的软件层面控制。现代计算机系统早已普及了多处理器架构,所以现今市面上的服务器都具备良好的多核心多线程处理能力。由于 SIMD 不适合用于带有较多分支判断的场景,ClickHouse 也大量使用了多线程技术以实现提速,以此和向量化执行形成互补。

如果一个篮子装不下所以的鸡蛋,就多用几个篮子来装,这就是分布式设计中分而治之的思想。同理,如果一台服务器性能吃紧,那么就利用多台服务的资源协同处理。为了实现这一目标,首先就要在数据层面实现数据的分布式,因为在分布式领域,存在着一条公认的理论:移动计算优于移动数据。在各服务器之间,通过网络传输数据的成本是很高的,所以相比移动数据,更为聪明的做法是预先将数据分布到各台服务器,将数据的计算查询直接下推到数据所在的服务器。而 ClickHouse 在数据存储方面,既支持分区(纵向扩展,利用多线程资源),也支持分片(横向扩展,利用分布式原理),可以说是将多线程和分布式的技术应用到了极致。

多主架构

HDFS、Spark、HBase 和 Elasticsearch 这类分布式系统,都采用了 master-slave 主从架构,由一个管控节点作为 Leader 统筹全局,而 ClickHouse 则采用 multi-master 多主架构,集群中的每个节点角色对等,客户端访问任意一个节点都能得到相同的效果。这种多主架构有很多优势,例如对等的角色使系统架构变得更加简单,不用再区分主控节点、数据节点和计算节点,集群中的所有节点功能相同。所以它天然规避了单点故障的问题,非常适合用于多数据中心、异地多活的场景。

在线查询

ClickHouse 经常会被拿来与其它的分析性数据库做对比,比如 Vertica、SparkSQL、Hive 和 Elasticsearch 等,它与这些数据库确实存在许多相似之处。例如,它们都可以支撑海量数据的查询场景,都拥有分布式架构,都支持列存储、数据分片、计算下推等功能。这其实也说明了 ClickHouse 在设计上吸取了各路奇技淫巧,而与其它数据库相比,ClickHouse 也拥有明显的优势。例如,Vertica 这类商用软件价格高昂;SparkSQL 与 Hive 这类系统无法保障 90% 的查询都能在 1 秒内返回,在大数据量下的复杂查询可能会需要分钟级的响应时间;而 Elasticsearch 这类搜索引擎在处理亿级数据聚合查询时则已经显得捉襟见肘。

正如 ClickHouse 的 “广告词” 所言,其它的开源系统太慢,商用的系统太贵,只有 ClickHouse 在成本与性能之间选择了良好平衡,又快又开源。ClickHouse 当之无愧地阐释了 “在线” 二字的含义,即便是在复杂的查询场景下,它也能够做到极其快速的响应,且无需对数据进行任何预处理加工。

数据分片与分布式查询

数据分片是将数据进行横向切分,这是一种在面对海量数据的场景下,解决存储和查询瓶颈的有效手段,是一种分之思想的体现。ClickHouse 支持分片,而分片则依赖集群,每个集群可以有一到多个分片,但是注意:一个服务节点只能有一个分片,所以分片的数量取决于节点数量。那么什么是分片呢?往下看。

ClickHouse 并不像其他分布式系统那样,拥有高度自动化的分片功能,ClickHouse 提供了本地表(Local Table)和分布式表(Distributed Table)的概念。一张本地表等同于一份数据的分片,而分布式表本身不存储任何数据,它是本地表的访问代理,其作用类似分库中间件。借助分布式表,能够代理访问多个数据分片,从而实现分布式查询。

这类设计类似于数据库的分库和分表,十分灵活。例如在业务上线的初期,数据体量并不高,此时数据表并不需要多个分片。所以使用单个节点的本地表(单个数据分片)即可满足业务需求,待到业务增长、数据量增大的时候,再通过新增数据分片的方式分流数据,并通过分布式表实现分布式查询。这就好比一辆手动挡赛车,它将所有的选择权都交给了使用者。

ClickHouse 具有无与伦比的查询速度

ClickHouse 查询速度快我们已经说过了,这里来探讨一下速度快的原因,虽然前面对这个问题已经做出了科学合理的解释,比方说:因为 ClickHouse 是列式存储数据库;ClickHouse 使用了向量化引擎等等。这些解释虽然都站得住脚,但是依然不能消除全部的疑问,因为这些技术不是什么秘密,市面有很多数据库同样使用了这些技术,但是依然没有 ClickHouse 这么快,因此我们需要从另外一个角度来探讨一番 ClickHouse 的秘诀到底是什么?

首先抛出一个疑问:在设计软件架构的时候,做设计的原则应该是自顶向下地去设计,还是自底向上地去设计呢?在传统的观念中,自然是自顶向下的设计,因为在做什么事情的一定先有一个大纲,然后再慢慢实现细节。而 ClickHouse 的设计则采用了自底向上的方式,因为它的原型早在 2008 年就诞生了,在诞生之处它并没有宏伟的规划。相反它的目的很单纯,就是希望能以最快的速度进行 GROUP BY 查询和过滤,那么战斗民族的攻城狮们是如何实现自底向上的设计呢?

着眼硬件,先想后做

首先从硬件功能层面着手设计,在设计伊始就至少需要想清楚如下几个问题:

  • 将要使用的硬件水平是怎样的?包括 CPU、内存、硬盘、网络等等
  • 在这样的硬件上,需要达到怎样的性能?包括延迟、吞吐量等等
  • 准备使用怎样的数据结构?包括 String、HashTable、Vetcor 等等
  • 选择这样的数据结构,在硬件上会如何工作?

如果能想清楚上面这些问题,那么在动手实现功能之前,就已经能够计算出粗略的性能了。所以,基于将硬件功效最大化的目的,ClickHouse 会在内存中进行 GROUP BY,并且使用 HashTable 装载数据。与此同时,攻城狮们非常在意 CPU L3 级别的缓存,因为一次 L3 的缓存失效将会带来 700~100ns 的延迟,这意味着在单核 CPU 上,它会浪费每秒 4000 万次的运算;而在一个 32 线程的 CPU 上,将会浪费每秒 5 亿次的运算。所以别小看这些细节,一点一滴地将它们累加起来,数据是非常可观的。正是因为注意这些细节,ClickHouse 在基准查询中才能做到每秒 1.75 亿次的数据扫描性能。

算法在前,抽象在后

常有人念叨:”有时候选择比努力更重要”,确实,路线选错了的话再努力也是白搭。在 ClickHouse 的底层实现中,经常会面对一些重复的场景,例如字符串子串查询、数据排序、使用 HashTable 等,如何才能实现性能的最大化呢?算法的选择就变成了重中之重。以字符串为例,有一本专门讲解字符串搜索的书,里面列举了 35 种常见的字符串搜索算法,那么你猜猜 ClickHouse 使用了里面的哪种算法呢?答案是一种都没有,因为性能不够快,在字符串搜索方面,针对不同的场景,ClickHouse 最终选择了这些算法:对于常量,使用 Volnitsky 算法;对于非常量,使用 CPU 的向量化执行 SIMD,暴力优化;正则匹配使用 re2 和 hyperscan 算法。

性能是算法选择的首要考量指标。

用于尝鲜,不行就换

除了字符串之外,其余的场景也与它类似,ClickHouse 会使用最合适、最快的算法。如果市面上出现了号称性能最强大的新算法,那么 ClickHouse 团队就会立即将其纳入并进行验证。如果效果不错就保留使用,不尽人意的话就将其丢弃。

特定场景,特殊优化

针对同一个场景的不同状况,选择使用不同的实现方式,尽可能将性能最大化。关于这一点,其实在前面介绍字符串查询的时候,针对不同场景选择不同算法的思路就有所体现了。

类似的例子还有很多,例如去重计数 uniqCombined 函数,会根据数据量的不同选择不同的算法;当数据量很小的时候,会选择使用 Array 保存;当数据量中等的时候,会选择 HashSet;而当数据量很大的时候,则使用 HyperLogLog 算法。

对于数据结构化比较清晰的场景,会通过代码生成技术实现循环展开,以减少循环次数。接着就是大家熟知的大杀器 “向量化执行”。SIMD 被广泛地应用于文本转换、数据过滤、数据解压和 JSON 转换等场景。相较于单纯地使用 CPU,利用寄存器暴力优化也算是一种降维打击了。

持续测试,持续改进

如果只是单纯地在上述细节上下功夫,还不足以构建出如此强大的 ClickHouse,因此还需要一个能够持续验证、持续改进的机制。由于 Yandex 公司的天然优势,ClickHouse 经常使用真实的数据进行测试,这一点很好地保证了测试场景的真实性。与此同时,ClickHouse 算是更新速度最快的开源软件了,差不多每个月都能发布一个版本,没有一个可靠的持续集成环境,这一点是做不到的。正因为拥有这样的频率,ClickHouse 才能快速迭代、快速改进。

所以 ClickHouse 的黑魔法并不是一项单一的技术,而是一种自底向上的、追求极限性能的设计思路,这就是它如此块的秘诀。