背景
架构分类
OLAP名为联机分析,又可以称为多维分析,是由关系型数据库之父埃德加·科德(Edgar Frank Codd)于1993年提出的概念。顾名思义,它指的是通过多种不同的维度审视数据,进行深层次分析。为了实现上述这些操作,将常见的OLAP架构大致分成两类。
第一类架构称为ROLAP(Relational OLAP,关系型OLAP)。顾名思义,它直接使用关系模型构建,数据模型常使用星型模型或者雪花模型,多维分析的操作,可以直接转换成SQL查询,但是这种架构对数据的实时处理能力要求很高,在大数据量下执行的sql查询会异常缓慢。
第二类架构称为MOLAP(Multidimensional OLAP,多维型OLAP)。它的出现是为了缓解ROLAP性能问题。MOLAP使用多维数组的形式保存数据,其核心思想是借助预先聚合结果,使用空间换取时间的形式最终提升查询性能。也就是说,用更多的存储空间换得查询时间的减少。但是这种架构也并不完美。维度预处理可能会导致数据的膨胀。由于使用了预处理的形式,数据立方体会有一定的滞后性,不能实时进行数据分析。而且,立方体只保留了聚合后的结果数据,导致无法查询明细数据。
技术演进
OLAP领域技术发展至今方兴未艾,分析型数据库百花齐放,众观OLAP的发展历程,可以分为两个阶段:
第一个可以称为传统关系型数据库阶段。在这个阶段中,OLAP主要基于以Oracle、MySQL为代表的一众关系型数据实现。在ROLAP架构下,直接使用这些数据库作为存储与计算的载体;在MOLAP架构下,则借助物化视图的形式实现数据立方体。在这个时期,不论是ROLAP还是MOLAP,在数据体量大、维度数目多的情况下都存在严重的性能问题,甚至存在根本查询不出结果的情况。
第二个可以称为大数据技术阶段。由于大数据处理技术的普及,人们开始使用大数据技术重构ROLAP和MOLAP。以ROLAP架构为例,传统关系型数据库就被Hive和SparkSQL这类新兴技术所取代。虽然,以Spark为代表的分布式计算系统,相比Oracle这类传统数据库而言,在面向海量数据的处理性能方面已经优秀很多,但是直接把它们作为面向终端用户的在线查询系统还是太慢了。我们的用户普遍缺乏耐心,如果一个查询响应需要几十秒甚至数分钟才能返回,那么这套方案就完全行不通。再看MOLAP架构,MOLAP背后也转为依托MapReduce或Spark这类新兴技术,将其作为立方体的计算引擎,加速立方体的构建过程。其预聚合结果的存储载体也转向HBase这类高性能分布式数据库。大数据技术阶段,主流MOLAP架构已经能够在亿万级数据的体量下,实现毫秒级的查询响应时间。尽管如此,MOLAP架构依然存在维度爆炸、数据同步实时性不高的问题。
不难发现,虽然OLAP在经历了大数据技术的洗礼之后,其各方面性能已经有了脱胎换骨式的改观,但不论是ROLAP还是MOLAP,仍然存在各自的痛点。
clickhouse
ClickHouse背后的研发团队是来自俄罗斯的Yandex公司,Yandex占据了本国47%以上的搜索市场,是现今世界上最大的俄语搜索引擎,而一般搜索引擎公司为了更好地帮助自身及用户分析网络流量,都会推出自家的在线流量分析产品,例如Google的Google Analytics、百度的百度统计。Yandex也不例外,Yandex.Metrica就是这样一款用于在线流量分析的产品。伴随着Yandex.Metrica业务的发展,其底层架构经历过数次调整,才逐渐形成了现在的clickhouse,clickhouse其具有ROLAP、在线实时查询、完整的DBMS、列式存储、不需要任何数据预处理、支持批量更新、拥有非常完善的SQL支持和函数、支持高可用、不依赖Hadoop复杂生态、开箱即用等许多特点。它解决了ROLAP、MOLAP各自的痛点,实现了大数据量下实时查询快,同步实时性高,数据量占用小的目标,满足了一款高性能、高可用OLAP数据库的一切诉求。
优势劣势
优势
- 按列存储,非常适合聚合计算
- 数据压缩空间大,网络io传输快,存储空间占用小
- 写入速度快,单次写入可到50m-200m每秒
- 充分调用cpu资源,查询速度极快
- 提供了标准协议的SQL查询接口,上手快
劣势
- 不支持事务
- 不太支持高并发,官方建议100qps,简单短查询的的情况下能达到几百qps
- 不擅长根据主键按行粒度进行查询(虽然支持)
特性
列式存储与数据压缩
对于一款高性能数据库来说是必不可少的特性。一个非常流行的观点认为,如果你想让查询变得更快,最简单且有效的方法是减少数据扫描范围和数据传输时的大小。
- 按列存储与按行存储相比,前者可以有效减少查询时所需扫描的数据量,而分析统计数据本身就是只要少量的列,因此列式存储数据库也十分适合做统计分析。
- 相应的,列式存储由于每列的数据格式都一致,因此重复性会更高,压缩率也更高,数据默认使用LZ4算法压缩,在Yandex.Metrica的生产环境中,数据总体的压缩比可以达到8:1(未压缩前17PB,压缩后2PB)
向量化执行引擎
向量化执行,可以简单地看作一项消除程序中循环的优化。为了实现向量化执行,需要利用CPU的SIMD指令。SIMD的全称是Single Instruction Multiple Data,即用单条指令操作多条数据。现代计算机系统概念中,它是通过数据并行以提高性能的一种实现方式(其他的还有指令级并行和线程级并行),它的原理是在CPU寄存器层面实现数据的并行操作,而cpu从寄存器访问数据的速度极快,所以利用CPU向量化执行的特性,可以显著提升程序性能。
关系模型与SQL查询
相比HBase和Redis这类NoSQL数据库,ClickHouse使用关系模型描述数据并提供了传统数据库的概念(数据库、表、视图和函数等)。与此同时,ClickHouse完全使用SQL作为查询语言(支持GROUP BY、ORDER BY、JOIN、IN等大部分标准SQL),这使得它平易近人,容易理解和学习。同时由于ClickHouse使用了关系模型,所以将构建在传统关系型数据库或数据仓库之上的系统迁移到ClickHouse的成本会变得更低。
多样化的表引擎
与MySQL类似,ClickHouse也将存储部分进行了抽象,把存储引擎作为一层独立的接口。ClickHouse共拥有合并树、内存、文件、接口和其他6大类20多种表引擎。其中每一种表引擎都有着各自的特点,用户可以根据实际业务场景的要求,选择合适的表引擎使用。
多线程与分布式
如果说向量化执行是通过数据级并行的方式提升了性能,那么多线程处理就是通过线程级并行的方式实现了性能的提升。相比基于底层硬件实现的向量化执行SIMD,线程级并行通常由更高层次的软件层面控制。现代计算机系统早已普及了多处理器架构,所以现今市面上的服务器都具备良好的多核心多线程处理能力。由于SIMD不适合用于带有较多分支判断的场景,ClickHouse也大量使用了多线程技术以实现提速,以此和向量化执行形成互补。
多主架构
ClickHouse则采用Multi-Master多主架构,集群中的每个节点角色对等,客户端访问任意一个节点都能得到相同的效果。这种多主的架构有许多优势,例如对等的角色使系统架构变得更加简单,不用再区分主控节点、数据节点和计算节点,集群中的所有节点功能相同。所以它天然规避了单点故障的问题,非常适合用于多数据中心、异地多活的场景。
在线查询
与其他数据库相比,ClickHouse拥有明显的优势。例如,Vertica这类商用软件价格高昂;SparkSQL与Hive这类系统无法保障90%的查询在1秒内返回,在大数据量下的复杂查询可能会需要分钟级的响应时间;而Elasticsearch这类搜索引擎在处理亿级数据聚合查询时则显得捉襟见肘。而ClickHouse即便是在复杂查询的场景下,它也能够做到极快响应,且无须对数据进行任何预处理加工。
数据分片与分布式查询
ClickHouse支持分片,而分片则依赖集群。每个集群由1到多个分片组成,而每个分片则对应了ClickHouse的1个服务节点。分片的数量上限取决于节点数量(1个分片只能对应1个服务节点)。
ClickHouse并不像其他分布式系统那样,拥有高度自动化的分片功能。ClickHouse提供了本地表(Local Table)与分布式表(Distributed Table)的概念。一张本地表等同于一份数据的分片。而分布式表本身不存储任何数据,它是本地表的访问代理,其作用类似分库中间件。借助分布式表,能够代理访问多个数据分片,从而实现分布式查询。
这种设计类似数据库的分库和分表,十分灵活,当数据量小时,使用单个节点的本地表(单个数据分片)即可满足业务需求;当数据量大时,再通过新增数据分片的方式分流数据,并通过分布式表实现分布式查询。
性能表现
读取性能
根据官方的数据,在1亿数据集体量的情况下,ClickHouse的平均响应速度(第1、2、3次平均)是Vertica的4.16倍、InfiniDB的24.38倍、Hive的208.43倍、MySQL的635.99倍。
写入性能
建议在至少 1000 行的数据包中插入数据,或者每秒不超过一个请求。从制表符分隔的转储插入 MergeTree 表时,插入速度可以为 50 到 200 MB/s。如果插入的行大小约为 1 KB,则速度将为每秒 50,000 到 200,000 行。如果行数很小,则每秒行数的性能会更高。为了提高性能,您可以并行进行多个 INSERT 查询,这些查询可以线性扩展。
吞吐量
吞吐量可以以每秒行数或每秒兆字节数来衡量。如果数据放在页面缓存中,一个不太复杂的查询在现代硬件上以大约 2-10 GB/s 的速度在单个服务器上处理未压缩数据(对于最简单的情况,速度可能达到 30 GB/秒)。如果数据没有放在页面缓存中,速度取决于磁盘子系统和数据压缩率。例如,如果磁盘子系统允许以 400 MB/s 的速度读取数据,并且数据压缩率为 3,则速度预计在 1.2 GB/s 左右。要获得以每秒行数为单位的速度,请将速度(以每秒字节数为单位)除以查询中使用的列的总大小。例如,如果提取 10 字节的列,则速度预计在每秒 100-2 亿行左右。
并发情况
在相同的条件下,ClickHouse 可以在单台服务器上每秒处理数百个查询(最好情况下可达数千个)。由于这种情况对于分析型 DBMS 来说并不典型,因此我们建议预计每秒最多 100 个查询。
其他性能测试参考:
集群
副本Replication & 分片Sharding
ClickHouse依靠ReplicatedMergeTree引擎族与ZooKeeper实现了复制表机制, 成为其高可用的基础.
ClickHouse像ElasticSearch一样具有数据分片(shard)的概念, 这也是分布式存储的特点之一, 即通过并行读写提高效率. ClickHouse依靠Distributed引擎实现了分布式表机制, 在所有分片(本地表)上建立视图进行分布式查询.
副本
Replicated Table & ReplicatedMergeTree Engines
不同于HDFS的副本机制(基于集群实现), Clickhouse的副本机制是基于表实现的. 用户在创建每张表的时候, 可以决定该表是否高可用.
创建本地表Local_table
CREATE TABLE IF NOT EXISTS {local_table} ({columns})
ENGINE = ReplicatedMergeTree('/clickhouse/tables/#_tenant_id_#/#__appname__#/#_at_date_#/{shard}/hits', '{replica}')
partition by toString(_at_date_) sample by intHash64(toInt64(toDateTime(_at_timestamp_)))
order by (_at_date_, _at_timestamp_, intHash64(toInt64(toDateTime(_at_timestamp_))))
支持复制表的引擎都是ReplicatedMergeTree引擎族, 具体可以查看官网:
ReplicatedMergeTree引擎族接收两个参数:
ZK中该表相关数据的存储路径, ClickHouse官方建议规范化, 例如: /clickhouse/tables/{shard}/[database_name]/[table_name].副本名称, 一般用{replica}即可.
ReplicatedMergeTree引擎族非常依赖于zookeeper, 它在zookeeper中存储了大量的数据,如表结构信息、元数据、操作日志、副本状态、数据块校验值、数据part merge过程中的选主信息.
同时, zookeeper又在复制表之下扮演了三种角色:元数据存储、日志框架、分布式协调服务
可以说当使用了ReplicatedMergeTree时, zookeeper压力特别重, 一定要保证zookeeper集群的高可用和资源。
数据同步的流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5KNz1nEy-1657353463896)(https://raw.githubusercontent.com/lydon-GH/cloudimg/main/img/image-20220705163914561.png)]
- 写入到一个节点
- 通过interserver HTTP port端口同步到其他实例上
- 更新zookeeper集群记录的信息
分片
Distributed Table & Distributed Engine
ClickHouse分布式表的本质并不是一张表, 而是一些本地物理表(分片)的分布式视图,本身并不存储数据. 分布式表建表的引擎为Distributed.
创建Distrbuted_table的示例:
CREATE TABLE IF NOT EXISTS {distributed_table} as {local_table}
ENGINE = Distributed({cluster}, '{local_database}', '{local_table}', rand())
Distributed引擎需要以下几个参数:
Distributed引擎需要以下几个参数:
- 集群标识符
- 本地表所在的数据库名称
- 本地表名称
- 分片键(sharding key) - 可选
该键与config.xml中配置的分片权重(weight)一同决定写入分布式表时的路由, 即数据最终落到哪个物理表上. 它可以是表中一列的原始数据(如site_id), 也可以是函数调用的结果, 如上面的SQL语句采用了随机值rand(). 注意该键要尽量保证数据均匀分布, 另外一个常用的操作是采用区分度较高的列的哈希值, 如intHash64(user_id).
4.1. 数据查询的流程

- 各个实例之间会交换自己持有的分片的表数据
- 汇总到同一个实例上返回给用户
扩展
支持kafka,mysql,pg,s3,dbc
mysql
-
支持mysql数据同步至clickhouse
-
支持clickhoulse 查询,插入路数据到mysql数据库中
对象性存储
Amazon S3 或 Amazon Simple Storage Service 是 Amazon Web Services (AWS) 提供的一项服务,它通过 Web 服务接口提供对象存储。用户可以将基于 S3 的数据插入 ClickHouse 并使用 S3 作为导出目的地,从而允许与“数据湖”架构进行交互。此外,s3 可以提供“冷”存储层并协助分离存储和计算。
与s3对接
使用表函数远程操作
支持使用clickhouse作为查询入库查询s3数据,但直接从 s3 读取数据不太可能在更大的数据集上执行。
SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_*.gz', 'TabSeparatedWithNames') LIMIT 10;
支持从 s3 存储桶中读取数据并使用远程函数插入到 clickhouse metritree
INSERT INTO trips SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_*.gz', 'TabSeparatedWithNames') LIMIT 1000000;
支持从clickhouse流式传输到s3存储桶
INSERT INTO FUNCTION s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/csv/trips.csv.lz4', 's3_key', 's3_secret', 'CSV') SELECT * FROM trips LIMIT 10000;
表引擎
虽然这些s3函数允许对存储在 S3 中的数据执行临时查询,但对于更复杂的查询,它们在语法上是冗长的。用户可能自然希望将 S3 存储桶视为表,在现有查询中使用它。为了解决这个问题,ClickHouse 提供了 S3 表引擎。
支持查询s3数据
CREATE TABLE trips_raw
(
`trip_id` UInt32,
`vendor_id` Enum8('1' = 1, '2' = 2, '3' = 3, '4' = 4, 'CMT' = 5, 'VTS' = 6, 'DDS' = 7, 'B02512' = 10, 'B02598' = 11, 'B02617' = 12, 'B02682' = 13, 'B02764' = 14, '' = 15),
`pickup_date` Date,
`pickup_datetime` DateTime,
`dropoff_date` Date,
`dropoff_datetime` DateTime,
`store_and_fwd_flag` UInt8,
`rate_code_id` UInt8,
`pickup_longitude` Float64,
`pickup_latitude` Float64,
`dropoff_longitude` Float64,
`dropoff_latitude` Float64,
`passenger_count` UInt8,
`trip_distance` Float64,
`fare_amount` Float32,
`extra` Float32,
`mta_tax` Float32,
`tip_amount` Float32,
`tolls_amount` Float32,
`ehail_fee` Float32,
`improvement_surcharge` Float32,
`total_amount` Float32,
`payment_type_` Enum8('UNK' = 0, 'CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4),
`trip_type` UInt8,
`pickup` FixedString(25),
`dropoff` FixedString(25),
`cab_type` Enum8('yellow' = 1, 'green' = 2, 'uber' = 3),
`pickup_nyct2010_gid` Int8,
`pickup_ctlabel` Float32,
`pickup_borocode` Int8,
`pickup_ct2010` String,
`pickup_boroct2010` FixedString(7),
`pickup_cdeligibil` String,
`pickup_ntacode` FixedString(4),
`pickup_ntaname` String,
`pickup_puma` UInt16,
`dropoff_nyct2010_gid` UInt8,
`dropoff_ctlabel` Float32,
`dropoff_borocode` UInt8,
`dropoff_ct2010` String,
`dropoff_boroct2010` FixedString(7),
`dropoff_cdeligibil` String,
`dropoff_ntacode` FixedString(4),
`dropoff_ntaname` String,
`dropoff_puma` UInt16
) ENGINE = S3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_{0..9}.tsv.gz', 'TabSeparatedWithNames', 'gzip');
SELECT payment_type, max(tip_amount) as max_tip FROM trips_raw GROUP BY payment_type;
支持插入
创建插入的clickhouse表
CREATE TABLE trips_dest
(
`trip_id` UInt32,
`pickup_date` Date,
`pickup_datetime` DateTime,
`dropoff_datetime` DateTime,
`tip_amount` Float32,
`total_amount` Float32
) ENGINE = S3('<bucket path>/trips.bin', 'Native');
INSERT INTO trips_dest SELECT trip_id, pickup_date, pickup_datetime, dropoff_datetime, tip_amount, total_amount FROM trips LIMIT 10;
需要注意
- 不支持 ALTER 查询
- 不支持 SAMPLE 操作
- 没有索引的概念,即主要或跳过。
s3作为存储桶
ClickHouse 存储卷允许从 MergeTree 表引擎中抽象出物理磁盘。任何单个卷都可以由一组有序的磁盘组成。虽然原则上允许多个块设备潜在地用于数据存储,但这种抽象还允许其他存储类型,包括 S3。ClickHouse 数据部分可以根据存储策略在卷和填充率之间移动,从而创建存储层的概念。
存储层解锁了冷热架构,其中最新数据(通常也是查询最多的数据)仅需要高性能存储(例如 NVMe SSD)上的少量空间。随着数据的老化,查询时间的 SLA 会增加,查询频率也会增加。这种肥尾数据可以存储在速度较慢、性能较低的存储设备(如 HDD)或对象存储设备(如 S3)上。
配置配置文件
<clickhouse>
<storage_configuration>
...
<disks>
<s3>
<type>s3</type>
<endpoint>https://sample-bucket.s3.us-east-2.amazonaws.com/tables/</endpoint>
<access_key_id>your_access_key_id</access_key_id>
<secret_access_key>your_secret_access_key</secret_access_key>
<region></region>
<metadata_path>/var/lib/clickhouse/disks/s3/</metadata_path>
<cache_enabled>true</cache_enabled>
<data_cache_enabled>true</data_cache_enabled>
<cache_path>/var/lib/clickhouse/disks/s3/cache/</cache_path>
</s3>
</disks>
<policies>
<s3_main>
<volumes>
<main>
<disk>s3</disk>
</main>
</volumes>
</s3_main>
</policies>
...
</storage_configuration>
</clickhouse>
创建mergeTree表
CREATE TABLE trips_s3
(
`trip_id` UInt32,
`pickup_date` Date,
`pickup_datetime` DateTime,
`dropoff_datetime` DateTime,
`pickup_longitude` Float64,
`pickup_latitude` Float64,
`dropoff_longitude` Float64,
`dropoff_latitude` Float64,
`passenger_count` UInt8,
`trip_distance` Float64,
`tip_amount` Float32,
`total_amount` Float32,
`payment_type` Enum8('UNK' = 0, 'CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(pickup_date)
ORDER BY pickup_datetime
SETTINGS index_granularity = 8192, storage_policy='s3_main'
导入数据
INSERT INTO trips_s3 SELECT trip_id, pickup_date, pickup_datetime, dropoff_datetime, pickup_longitude, pickup_latitude, dropoff_longitude, dropoff_latitude, passenger_count, trip_distance, tip_amount, total_amount, payment_type FROM s3('https://ch-nyc-taxi.s3.eu-west-3.amazonaws.com/tsv/trips_{0..9}.tsv.gz', 'TabSeparatedWithNames') LIMIT 1000000;
查询统计数据
SELECT passenger_count, avg(tip_amount) as avg_tip, avg(total_amount) as avg_amount FROM trips_s3 GROUP BY passenger_count;
在对象存储这一块,AWS 的 S3、华为云的 OBS、腾讯云的 COS 以及 阿里云的 OSS,ClickHouse 现在已经全面适配。
数据统计查询场景clickhouse实现
统计总数等聚合场景
使用count、sum等常见聚合函数来实现功能
普通分组
使用group by函数分组统计
按时间间隔分组
使用分时统计函数 toStartOfInterval(t, INTERVAL 5 minute)

toStartOfInterval(t, INTERVAL 3 hour)

toStartOfInterval(t, INTERVAL 1 day)

数据聚合
在es等不擅长聚合的存储库中,需要预先通过手动编写聚合逻辑并写入库中,而在clickhouse可通过将AggregatingMergeTree引擎作为物化视图的表引擎,架在明细表上对外提供查询,明细表作为写入接口来完成数据聚合的实现,如下图所示:

- 在新增数据时,面向的对象是底表MergeTree,在底表新增的数据会自动同步到物化视图,并按照AggregatingMergeTree引擎的规则处理。
- 在查询数据时,面向的对象是物化视图AggregatingMergeTree
实例说明:
1、首先,建立明细数据表,也就是俗称的底表:
CREATE TABLE agg_table_basic(
id String,
city String,
code String,
value UInt32
)ENGINE = MergeTree()
PARTITION BY city
ORDER BY (id,city)
2、接着,新建一张物化视图:
CREATE MATERIALIZED VIEW agg_view
ENGINE = AggregatingMergeTree()
PARTITION BY city
ORDER BY (id,city)
AS SELECT
id,
city,
uniqState(code) AS code,
sumState(value) AS value
FROM agg_table_basic
GROUP BY id, city
上例中列字段id和city是聚合条件,等同于下面的语义:
GROUP BY id,city
而code和value是聚合字段,其语义等同于:
UNIQ(code), SUM(value)
3、然后新增数据
INSERT INTO TABLE agg_table_basic
VALUES('A000','wuhan','code1',100),('A000','wuhan','code2',200),('A000','zhuhai', 'code1',200)
4、在查询数据
ELECT id, sumMerge(value), uniqMerge(code) FROM agg_view GROUP BY id, city
┌─id──┬─sumMerge(value)──┬──uniqMerge(code)─┐
│ A000 │ 200 │ 1 │
│ A000 │ 300 │ 2 │
└─────┴────────────┴────────────┘
别人使用clickhouse的实践示例
支撑700亿数据量的ClickHouse高可用架构实践
腾讯云Clickhouse 实践
参考资料
- 《clickhouse原理解析与应用实践》
- 大厂使用clickhouse的实践:
ClickHouse 在唯品会 OLAP 系统的时间
腾讯云CDW-ClickHouse云原生实践
腾讯云从 Clickhouse 到 Snowflake云原生
腾讯:看云上 ClickHouse 如何做计算存储分离
华为:clickhouse存算分离在华为云实践
唯品会:ClickHouse 在唯品会 OLAP 系统的实践
b站:基于ClickHouse的交互式OLAP技术架构
字节跳动:从 ClickHouse 到 ByteHouse:字节跳动实时数据分析场景下的优化实践
字节跳动:ClickHouse为啥在字节跳动能这么火
京东:ClickHouse和Doris打配合,京东万亿级OLAP稳得一批
1039

被折叠的 条评论
为什么被折叠?



