字节跳动广告场景使用的“ClickHouse”介绍

2,878 阅读21分钟

背景

随着互联网的快速发展和互联网+物联网的场景不断增加,信息数据量正在呈几何式的爆发。这些海量数据决定着企业的未来发展。这些数据中,有用的价值数据就需要数据分析师或分析系统去分析。

但是使用一般的工具己满足不了和承受不住庞大的数据带来的压力。近两年针对大数据的开发工具开启了开源大潮,为大数据开发者提供了多个选择,但是也增加了开发者选择合适的工具的难度。

尤其是新入行的开发者,会增加很大的学习成本,框架的多样化和复杂度成了很大的难题。有时候会需要各种框架、工具、中间件、平台等整合到一起才能完成数据分析。

因此,大数据分析平台简单化和统一化成了开发者刚需。

什么是ClickHouse?

ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。我们可能都听过clickhouse速度很快,也听说过它不能处理高并发等各种特性,下面我们就从它的工作原理来分析下这些结论到底对不对,以及为什么会是这样。 要想弄清楚ClickHouse做查询分析那么快的原因,咱们可以反客为主,先想想自己设计一款OLAP数据库的核心技术应该有哪些?然后我们再来看看ClickHouse是如何实现如何工作的。  我们先看两条查询语句场景一:select username, number from user where id = 1;场景二:select department, avg(age) from student group by department;第一种场景: 如果数据量小,并且数据是结构化的,使用MySQL去存储即可; 如果数据量大,不管是不是结构化的,可以转成key-value的存储,使用 HBase,Cassandra等来解决。    第二种场景:  如果数据量小,并且数据是结构化的,使用MySQL去存储即可; 如果数据量大,不管是不是结构化的,设计一个专门用来做分析的存储计算引擎解决分析的低效率问题。

一、如何设计一个oltp数据库?

  1. 内存 + 磁盘:保证处理效率,也保证数据安全  
  2. 内存:必须经过设计,内存具备优秀的数据结构,保证基本的读写高效,甚至为了不同的需求,可以让读写效率倾斜。
  3. 磁盘:数据必须存放在磁盘,保证数据安全。磁盘数据文件必须经过精心设计,保证扫描磁盘数据文件的高效率  
  4. 数据排序:在海量数据中要想保证低延时的随机读写操作,数据最好是排序的  
  5. 范围分区:当数据排序之后,可以进行范围分区,来平摊负载,让多台服务器联合起来对外提供服务  
  6. 跳表:基于数据排序+范围分区构建索引表,形成跳表的拓扑结构,方便用户操作时快速定位数据分区的位置
  7. LSM-Tree存储引擎:把随机写变成顺序追加,在通过定期合并的方式来合并数据,去除无效数据,从而实现数据的删除和修改。

image.png 海量数据中,如果进行高效率的查询的核心思想:设计一种架构,能够快速把待搜寻的数据范围降低到原来的1/n,然后再结合索引或者热点数据放在内存等思路,就能实现高效率的查询了。  那么一个专门用来做OLAP分析的存储引擎该如何设计呢?如何在海量数据中,针对大量数据进行查询分析呢?一些常见的方案和手段如下:  

  1. 列式存储 + 字段类型统一  
  2. 列裁剪  
  3. 数据排序 
  4. 数据分区分片 + 分布式查询  
  5. 预聚合 
  6. 利用CPU特性:向量化引擎,操作系统必须支持  
  7. 主键索引+二级索引+位图索引+布隆索等 
  8. 支持近似计算pv 
  9. 定制引擎:多样化的存储引擎满足不同场景的特定需要  
  10. 多样化算法选择:Volnitsky高效字符串搜索算法和HyperLogLog基于概率高效去重算法  

总结一下:单条记录的增删改等操作,通过数据的横向划分,做到数据操作的快速定位,在海量数据查询分析中,一般就是针对某些列做分析,既然并不是全部列,那么把数据做纵向切分把表中的数据按照列来单独存储,那么在做分析的时候,同样可以快速把待查询分析的数据总量降低到原来表的1/n,同样提高效率。

二、总体介绍:

ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。来自于2011 年在纳斯达克上市的俄罗斯本土搜索引擎企业Yandex公司,诞生之初就是为了服务Yandex公司自家的Web流量分析产品Yandex.Metrica,后来经过演变,逐渐形成为现在的 ClickHouse,全称是:Click Stream,Data WareHouse ClickHouse.

官网:clickhouse.tech/

ClickHouse具有ROLAP、在线实时查询、完整的DBMS功能支持、列式存储、不需要任何数据预处理、支持批量更新、拥有非常完善的SQL支持和函数、支持高可用、不依赖 Hadoop复杂生态、开箱即用等许多特点。

在1亿数据集体量的情况下,ClickHouse的平均响应速度是Vertica的2.63倍、InfiniDB的17倍、MonetDB的27倍、Hive的126倍、MySQL的429倍以及Greenplum的10倍。

详细的测试结果可以查阅:clickhouse.tech/benchmark/d…

ClickHouse非常适用于商业智能领域(也就是我们所说的BI领域),除此之外,它也能够被广泛应用于广告流量、Web、App流量、电信、金融、电子商务、信息安全、网络游戏、物联网等众多其他领域。

ClickHouse是近年来备受关注的开源列式数据库,主要用于数据分析(OLAP)领域。目前国内社区火热,各个大厂纷纷跟进大规模使用:

  1. 今日头条内部用ClickHouse来做用户行为分析,内部一共几千个ClickHouse节点,单集群最大1200节点,总数据量几十PB,日增原始数据300TB左右。  
  2. 腾讯内部用ClickHouse做游戏数据分析,并且为之建立了一整套监控运维体系。  
  3. 携程内部从18年7月份开始接入试用,目前80%的业务都跑在ClickHouse上。每天数据增量十多亿,近百万次查询请求。 
  4. 快手内部也在使用ClickHouse,存储总量大约10PB,每天新增200TB,90%查询小于3S。

三:ClickHouse表引擎介绍

表引擎在ClickHouse中的作用十分关键,直接决定了数据如何存储和读取、是否支持并发读写、是否支持index、支持的query种类、是否支持主备复制等。

  1. 数据的存储方式和位置,写到哪里以及从哪里读取数据  
  2. 支持哪些查询以及如何支持。  
  3. 并发数据访问。  
  4. 索引的使用(如果存在)。  
  5. 是否可以执行多线程请求。 
  6. 数据复制参数

具体可看官网:clickhouse.tech/docs/zh/eng…

关于ClickHouse的底层引擎,其实可以分为数据库引擎和表引擎两种。在此,我们重点关注表引擎。 

关于库引擎,简单总结一下:ClickHouse也支持在创建库的时候,指定库引擎,目前支持5种,分别是:Ordinary,Dictionary,Memory,Lazy,MySQL,其实Ordinary 是默认库引擎,在此类型库引擎下,可以使用任意类型的表引擎。5种库引擎说明:

  1. Ordinary引擎:默认引擎,如果不指定数据库引擎创建的就是Ordinary数据库  
  2. Dictionary引擎:此数据库会自动为所有数据字典创建表  
  3. Memory引擎:所有数据只会保存在内存中,服务重启数据消失,该数据库引擎只能够创建Memory引擎表 
  4. MySQL引擎:改引擎会自动拉取远端MySQL中的数据,并在该库下创建 MySQL表引擎的数据表  
  5. Lazy延时引擎:在距最近一次访问间隔expiration_time_in_seconds时间段内,将表保存在内存中,仅适用于Log引擎表

ClickHouse的表引擎提供了四个系列(Log、MergeTree、Integration、Special)大约28种表引擎,各有各的用途。

比如Log系列用来做小表数据分析,MergeTree系列用来做大数据量分析,而Integration系列则多用于外表数据集成。Log、Special、Integration系列的表引擎相对来说,应用场景有限,功能简单,应用特殊用途,MergeTree系列表引擎又和两种特殊表引擎(Replicated,Distributed)正交形成多种具备不同功能的MergeTree表引擎。

这是ClickHouse的表引擎系列家谱:

image.png

MergeTree作为家族中最基础的表引擎,提供了主键索引、数据分区、数据副本和数据采样等基本能力,而家族中其他的表引擎则在 MergeTree 的基础之上各有所长:

image.png

四:MergeTree 引擎工作机制详解

MergeTree系列是官方主推的存储引擎,支持几乎所有ClickHouse核心功能,该系列中,常用的表引擎有:MergeTree、ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree、SummingMergeTree、AggregatingMergeTree等。学习好MergeTree表引擎的工作机制,是应用好ClickHouse的最基本基础。

4.1 关于表引擎类型

第一:MergeTree表引擎主要用于海量数据分析,支持数据分区、存储有序、主键索引、稀疏索引、数据TTL等。MergeTree支持所有ClickHouse SQL语法,但是有些功能与 MySQL并不一致,比如在MergeTree中主键并不用于去重。

第二:为了解决MergeTree相同主键无法去重的问题,ClickHouse提供了ReplacingMergeTree引擎,用来做去重。ReplacingMergeTree确保数据最终被去重,但是无法保证查询过程中主键不重复。因为相同主键的数据可能被shard到不同的节点,但是 compaction只能在一个节点中进行,而且optimize的时机也不确定。

第三:CollapsingMergeTree引擎要求在建表语句中指定一个标记列Sign(插入的时候指定为1,删除的时候指定为-1),后台Compaction时会将主键相同、Sign相反的行进行折叠,也即删除。来消除ReplacingMergeTree的限制。

第四:为了解决CollapsingMergeTree乱序写入情况下无法正常折叠问题,VersionedCollapsingMergeTree表引擎在建表语句中新增了一列Version,用于在乱序情况下记录状态行与取消行的对应关系。主键相同,且Version相同、Sign相反的行,在 Compaction时会被删除。

第五:ClickHouse通过SummingMergeTree来支持对主键列进行预先聚合。在后台 Compaction时,会将主键相同的多行进行sum求和,然后使用一行数据取而代之,从而大幅度降低存储空间占用,提升聚合计算性能。 第六:AggregatingMergeTree也是预先聚合引擎的一种,用于提升聚合计算的性能。与SummingMergeTree的区别在于:SummingMergeTree对非主键列进行sum聚合,而 AggregatingMergeTree则可以指定各种聚合函数。

4.2 MergeTree的建表语法:

CREATE TABLE [IF NOT EXISTS] [db_name.]table_name (
name1 [type] [DEFAULT|MATERIALIZED|ALIAS expr],
name2 [type] [DEFAUErEMAMLERLALLIZED|ALIAS expr],
省略...
) ENGINE = MergeTree()
[PARTITION BY expr]
[ORDER BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[SETTINGS name=value, 省略...]

介绍一下其中的几个关键选项:

  1. PARTITION BY:分区键。指定表数据以何种标准进行分区。分区键既可以是单个列字段,也可以通过元组的形式使用多个列字段,同时它也支持使用列表达式。
  2. ORDER BY:排序键,用于指定在一个数据片段内,数据以何种标准排序。默认情况下主键(PRIMARY KEY)与排序键相同。  
  3. PRIMARY KEY:主键。声明后会依照主键字段生成一级索引。默认情况下,主键与排序键(ORDER BY)相同,所以通常直接使用ORDER BY代为指定主键。 
  4. SETTINGS:index_granularity选项表示索引的粒度,默认值为8192。MergeTree 索引在默认情况下,每间隔8192行数据才生成一条索引。 
  5. SAMPLE BY:抽样表达式,用于声明数据以何种标准进行采样。

注意settings中的重要参数:  

  1. index_granularity 默认是8192 
  2. index_granularity_bytes 默认10M,需要通过enable_mixed_granularity_parts=1来开启

##五:ClickHouse 工作原理

MergeTree表引擎的内部工作细节!最终就是告诉你:为什么clickhouse做查询分析,那么快? 

ClickHouse从OLAP场景需求出发,定制开发了一套全新的高效列式存储引擎,并且实现了数据有序存储、主键索引、稀疏索引、数据Sharding、数据Partitioning、TTL、主备复制等丰富功能。这些功能共同为ClickHouse极速的分析性能奠定了基础。

5.1 数据分区

CREATE TABLE user(
id String,Name String,age UInt8,create_time DateTime
) ENGINE = MergeTree 
PARTITION BY toYYYYMMDD(create_time)
ORDER BY age SETTINGS index_granularity = 8192    

关于表分区目录结构:MergeTree 表的分区目录物理结构:

image.png

关于这些文件的解释:

  1. 分区目录:20210427_1_1_0,一个分区可能会有多个不同的目录,该目录下存储该分区的数据及其他各种形式的数据。后台会执行合并,把相同分区的多个目录合并到一个分区。 
  2. checksums.txt:校验文件。使用二进制格式存储。它保存了余下各类文件(primary.idx、count.txt等)的size大小及size的哈希值,用于快速校验文件的完整性和正确性。  
  3. columns.txt:列信息文件,使用明文格式存储。用于保存此数据分区下的列字段信息。  
  4. count.txt:计数文件,使用明文格式存储。用于记录当前数据分区目录下数据的总行数。  
  5. primary.idx:一级索引文件,主键索引文件。  
  6. xxx.bin:数据文件,使用压缩格式存储,默认为LZ4压缩格式,用于存储某一列的数据,每一列都对应一个该文件,如列date为date.bin。  
  7. xxx.mrk2:列字段标记文件,如果使用了自适应大小的索引间隔,则标记文件以.mrk2命名,否则以.mrk命名。它建立primary.idx稀疏索引与xxx.bin数据文件之间的映射关系,先通过主键索引找到数据的偏移量,然后去 xxx.bin数据文件中找到真实数据。 
  8. ...还有二级索引和分区键相关信息文件等等。

5.2 分区合并一个批次一个目录文件(后期合并),这也是为什么不适合高频单条插入适合大批量插入。

image.png

image.png

5.3 列式存储

image.png

相比于行式存储,列式存储在分析场景下有着许多优良的特性。  

  1. 分析场景中往往需要读大量行但是少数几个列。在行存模式下,数据按行连续存储,所有列的数据都存储在一个block中,不参与计算的列在IO时也要全部读出,读取操作被严重放大。而列存模式下,只需要读取参与计算的列即可,极大的减低了IO cost,加速了查询。  
  2. 同一列中的数据属于同一类型,压缩效果显著。列存往往有着高达十倍甚至更高的压缩比,节省了大量的存储空间,降低了存储成本。 
  3. 更高的压缩比意味着更小的data size,从磁盘中读取相应数据耗时更短。  
  4. 自由的压缩算法选择。不同列的数据具有不同的数据类型,适用的压缩算法也就不尽相同。可以针对不同列类型,选择最合适的压缩算法。  
  5. 高压缩比,意味着同等大小的内存能够存放更多数据,系统cache效果更好。

5.4 一级索引

关于一级索引:MergeTree的主键使用PRIMARY KEY定义,待主键定义之后,MergeTree会依据index_granularity 间隔(默认 8192 行),为数据表生成一级索引并保存至 primary.idx 文件内。   一级索引是稀疏索引,意思就是说:每一段数据生成一条索引记录,而不是每一条数据都生成索引,如果是每一条数据都生成索引,则是稠密索引。稀疏索引的好处,就是少量的索引标记,就能记录大量的数据区间位置信息,比如不到24414条标记信息,就能为2E条数据提供索引(算法:200000000/8192)。  

在 ClickHouse中,一级索引常驻内存。总的来说:一级索引和标记文件一一对齐,两个索引标记之间的数据,就是一个数据区间,在数据文件中,这个数据区间的所有数据,生成一个压缩数据块。

image.png

image.png

image.png

需要注意的是:ClickHouse的主键索引与MySQL等数据库不同,它并不用于去重,即便primary key相同的行,也可以同时存在于数据库中。要想实现去重效果,需要结合具体的表引擎ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree实现。

5.5.二级索引

关于二级索引:又称之为跳数索引。目的和一级索引一样,是为了减少待搜寻的数据的范围。 

跳数索引的默认是关闭的,需要通过SET allow_experimental_data_skipping_indices = 1 来开启,索引生成粒度由granularity控制,如果生成了二级索引,则会在分区目录下生成额外的:skp_idx_[Column].idx与skp_idx_[Column].mrk文件。

跳数索引的生成规则:按照特定规则每隔granularity个index_granularity条数据,就会生成一条跳数索引。 比如minmax跳数索引,生成的是:granularity个index_granularity条数据内的最大值最小值生成一条索引,如果将来需要针对构建二级索引的这个字段求最大值最小值,则可以帮助提高效率。

 跳数索引一共支持四种类型:minmax(最大最小)、set(去重集合)、ngrambf_v1(ngram分词布隆索引)和tokenbf_v1(标点符号分词布隆索引),一张数据表支持同时声明多个跳数索引。比如: 

GRANULARITY = 你在创建二级索引索引的指定的  
INDEX_GRANULARITY = 8192  GRANULARITY * INDEX_GRANULARITY
CREATE TABLE skip_test(
  ID String,
  URL String,
Code String,
EventTime Date,
INDEX a ID TYPE minmax GRANULARITY 5,
  INDEX b (length(ID) * 8) TYPE set(2) GRANULARITY 5,
  INDEX c (ID, Code) TYPE ngrambf_v1(3, 256, 2, O) GRANULARITY 5,
  INDEX d ID TYPE tokenbf_v1(256, 2, 0) GRANULARITY 5
) ENGINE= MergeTree()
order by id; 

关于跳数索引支持的多种类型的区别:

  1. minmax:以index_granularity为单位,存储指定表达式计算后的min、max值;在等值和范围查询中能够帮助快速跳过不满足要求的块,减少IO。  
  2. set(max_rows):以index granularity为单位,存储指定表达式的distinct value集合,用于快速判断等值查询是否命中该块,减少IO。 
  3. ngrambf_v1(n,size_of_bloom_filter_in_bytes,number_of_hash_functions, random_seed):将string进行ngram分词后,构建bloom filter,能够优化等值、like、in等查询条件。  
  4. tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed):与ngrambf_v1类似,区别是不使用ngram进行分词,而是通过标点符号进行词语分割。  
  5. bloom_filter([false_positive]):对指定列构建bloom filter,用于加速等值、like、in等查询条件的执行。

5.6 数据压缩

关于数据压缩:ClickHouse的数据存储文件column.bin中存储是一列的数据,由于一列是相同类型的数据,所以方便高效压缩。

在进行压缩的时候,请注意:一个压缩数据块由头信息和压缩数据两部分组成,头信息固定使用9位字节表示,具体由1个UInt8(1字节)整型和 2个UInt32(4字节)整型组成,分别代表使用的压缩算法类型、压缩后的数据大小和压缩前的数据大小。

单个压缩数据块的体积,按照其压缩前的数据字节大小,都被严格控制在64KB~1MB,其上下限分别由min_compress_block_size(默认65536=64KB)与 max_compress_block_size(默认1048576=1M)参数指定。

具体压缩规则: 原理的说法:每8192条记录,其实就是一条一级索引一个索引区间 压缩成一个数据块。自适应压缩

  1. 单个批次数据size < 64KB:如果单个批次数据小于64KB,则继续获取下一批数据,直至累积到size >= 64KB时,生成下一个压缩数据块。如果平均每条记录小于8byte,多个数据批次压缩成一个数据块。
  2. 单个批次数据64KB<= size <=1MB:如果单个批次数据大小恰好在64KB 与1MB之间,则直接生成下一个压缩数据块。
  3. 单个批次数据size > 1MB:如果单个批次数据直接超过1MB,则首先按照1MB大小截断并生成下一个压缩数据块。剩余数据继续依照上述规则执行。此时,会出现一个批次数据生成多个压缩数据块的情况。如果平均每条记录的大小超过 128byte,则会把当前这一个批次的数据压缩成多个数据块。

image.png

image.png

5.7 数据标记

关于数据标记:数据标记文件也与.bin文件一一对应。即每一个列字段[Column].bin文件都有一个与之对应的 [Column].mrk2 数据标记文件,用于记录数据在 .bin 文件中的偏移量信息。  

一行标记数据使用一个元组表示,元组内包含两个整型数值的偏移量信息。它们分别表示在此段数据区间内,在对应的.bin压缩文件中,压缩数据块的起始偏移量;以及将该数据压缩块解压后,其未压缩数据的起始偏移量。每一行标记数据都表示了一个片段的数据(默认8192行)在.bin压缩文件中的读取位置信息。标记数据与一级索引数据不同,它并不能常驻内存,而是使用LRU(最近最少使用)缓存策略加快其取用速度。  

总结数据读取流程:先根据一级索引,找到标记文件中的对应数据压缩块信息(压缩块在.bin文件中的起始偏移量和未压缩之前该条数据的是偏移量)然后从.bin文件中,把压缩块加载到内存,解压缩之后,执行读取。建立了主键索引到数据文件的映射!

5.8 查询数据

指定分区 ==> 指定字段(xxx.bin)==> 根据一级索引(primary.idx)定位到 标记文件(name.mrk2)中的那一条记录 ==> 扫描对应字段的 mark 标记文件 获取两个偏移量信息(当前要查找的数据,处于这个.bin数据文件中的那个压缩数据块,这个压缩数据块在 .bin 文件的偏移量,这个压缩数据块解压缩 出来之后,要找的数据在当前这个压缩数据快的偏移量)==> 根据第一个偏移量去.bin文件中定位到一个压缩数据快 ==> 读取数据到内存执行解压缩 ==> 根据第二个偏移量去内存解压缩数据中找到对应的数据。  

提高数据查询效率的核心原则只有一个:谁做的辅助动作能快速的帮助我们去快速降低待搜寻的数据范围。

六:总结

在大数据分析领域中,传统的大数据分析需要不同框架和技术组合才能达到最终的效果,在人力成本,技术能力和硬件成本上以及维护成本让大数据分析变得成为昂贵的事情。让很多中小型企业非常苦恼,不得不被迫租赁第三方大型公司的数据分析服务。ClickHouse开源的出现让许多想做大数据并且想做大数据分析的很多公司和企业耳目一新。ClickHouse 正是以不依赖Hadoop 生态、安装和维护简单、查询速度快、可以支持SQL等特点在大数据分析领域越走越远。