12
头图

开始文章前先扯个淡

最近在思考统计功能的时候,一开始是寻思着用程序配合mysql来进行存储,但是如果用mysql来存一些比较细致化的数据然后再做统计,数据量会非常庞大,然后又思考用mysql存储数据,然后用定时脚本或队列来按照具体要求更新到redis中,这样查询就很快了。

后来想起我们本身用的plausible来记录数据,然后寻思着用plausibleevent来记录自定义的goals,发现的确可行,模拟了500万数据之后,表现良好,感觉自己棒棒哒。谁知道数据量起来之后,2000万的数据就能让服务炸了。期间,也遇到了一些其他问题,也给官方提了一些建议,逼不得已不得不学了elixir,当然这是后话了。这里提一嘴这个plausible主要是给后来者提个醒,如果也想用plausible来做自定义的一些统计功能,最好是放弃掉,改为直接用ClickHouse来处理。

最后没有办法,我观察了plausible的数据存储结构,发现根本原因就是event的存储类型导致的,性能误解,从而萌生了去研究一下ClickHouse,看看自己实现能不能得到很好的性能,于是便有了这篇文章,如果对大家有用,欢迎探讨,如果理解有错的话,欢迎各位给我斧正。

行式数据库 vs 列式数据库结构对比

以下先以官方文档的表格来抛砖引玉,记录一下这段时间对于clickhouse的学习成果。

行式数据库

RowWatchIDJavaEnableTitleGoodEventEventTime
#0893543506621Investor Relations12016-05-18 05:19:20
#1903295099580Contact us12016-05-18 08:10:20
#2899537060541Mission12016-05-18 07:38:00
#N

列式数据库

Row:#0#1#2#N
WatchID:893543506629032950995889953706054
JavaEnable:101
Title:Investor RelationsContact usMission
GoodEvent:111
EventTime:2016-05-18 05:19:202016-05-18 08:10:202016-05-18 07:38:00

从表格上看,一下子比较难以理解,行式数据库和我们电子表格看到的表现一样,但是存储是一行一行的存储,列式数据库是以一列一列的存储。以下是以文件存储结构来做一个示例:

也就是说,对于行数据库,一行数据是形成一个完整的文件(实际底层具体存储不是这样子,这里是方便说明整个思想),这个文件包含了这一整行的每个数据字段,取到这一行就能拿到完整的数据。

而对于列式存储来说,一列数据为一个文件,如以上所展示的,WatchId 的数据全部存在WatchId文件里面。

然后我做了个脑图再次对这个结构做个对比。
数据表对比.png

以下用json来描述两种数据结构的区别:

  1. 行式数据库存储结构

    [
     {
         "id": 1,
         "title": "t1",
         "content": "c1"
     },
     {
         "id": 2,
         "title": "t2",
         "content": "c2"
     },
     {
         "id": 2,
         "title": "t2",
         "content": "c2"
     },
     {
         "id": 3,
         "title": "t3",
         "content": "c3"
     }
    ]
  2. 列式数据库结构

    {
     "id": [
         1,
         2,
         3,
         4
     ],
     "title": [
         "t1",
         "t2",
         "t3",
         "t4"
     ],
     "content": [
         "c1",
         "c2",
         "c3",
         "c4"
     ]
    }

列式数据库主要应用场景

列式数据库主要运用场景为OLAP(联机分析),也就是说主要应用于数据分析这一块,通常不用来处理具体的业务数据存储。行式数据库主要用于处理业务数据的存储,业务数据的特点是对单条数据经常会产生更新,会对数据一致性有非常严格的要求。对于OLAP来说,数据很少会有变动,所以通常来说,列式数据库主要目的是为了更快的写入以及更快的数据查询和统计。

以下我列出一些OLAP场景的关键特征:

  • 绝大多数是读请求
  • 数据以相当大的批次(> 1000行)更新,而不是单行更新;或者根本没有更新。
  • 已添加到数据库的数据不能修改。
  • 对于读取,从数据库中提取相当多的行,但只提取列的一小部分。
  • 宽表,即每个表包含着大量的列
  • 查询相对较少(通常每台服务器每秒查询数百次或更少)
  • 对于简单查询,允许延迟大约50毫秒
  • 列中的数据相对较小:数字和短字符串(例如,每个URL 60个字节)
  • 处理单个查询时需要高吞吐量(每台服务器每秒可达数十亿行)
  • 事务不是必须的
  • 对数据一致性要求低
  • 每个查询有一个大表。除了他以外,其他的都很小。
  • 查询结果明显小于源数据。换句话说,数据经过过滤或聚合,因此结果适合于单个服务器的RAM中

MergeTree(合并树) 数据表引擎

主要特点

MergeTreeClickHouse最基础的数据存储引擎,绝大部分时候都是用这个数据引擎。

MergeTree 系列的引擎被设计用于插入极大量的数据到一张表当中。数据可以以数据片段的形式一个接着一个的快速写入,数据片段在后台按照一定的规则进行合并。相比在插入时不断修改(重写)已存储的数据,这种策略会高效很多。

主要特点:

  • 存储的数据按主键排序。

    这使得您能够创建一个小型的稀疏索引来加快数据检索。

  • 如果指定了分区键的话,可以使用分区。

    在相同数据集和相同结果集的情况下 ClickHouse 中某些带分区的操作会比普通操作更快。查询中指定了分区键时 ClickHouse 会自动截取分区数据。这也有效增加了查询性能。

    通常来说,都是以时间来进行分区,从表象来看的话,就是一个分区是一个文件夹,这样可以避免一个文件存储大量的数据。

  • 支持数据副本。

    ReplicatedMergeTree 系列的表提供了数据副本功能。

  • 支持数据采样。

    需要的话,您可以给表设置一个采样方法。

结构说明

table_name.png

这段时间为了研究plausible的统计性能问题,主要也是围绕业务展开的,对其他几个数据引擎没有做深入的了解,其他引擎也是继承于MergeTree引擎,如果大家感兴趣可以了解一下,以下我大致列出其他几个数据引擎:

  • VersionedCollapsingMergeTree

    • 允许快速写入不断变化的对象状态。
    • 删除后台中的旧对象状态。 这显着降低了存储体积。
  • GraphiteMergeTree

    • 该引擎用来对 Graphite数据进行瘦身及汇总。对于想使用CH来存储Graphite数据的开发者来说可能有用。
    • 如果不需要对Graphite数据做汇总,那么可以使用任意的CH表引擎;但若需要,那就采用 GraphiteMergeTree 引擎。它能减少存储空间,同时能提高Graphite数据的查询效率。
  • AggregatingMergeTree

    • 该引擎继承自 MergeTree,并改变了数据片段的合并逻辑。 ClickHouse 会将一个数据片段内所有具有相同主键(准确的说是 排序键)的行替换成一行,这一行会存储一系列聚合函数的状态。
  • CollapsingMergeTree

    • 该引擎继承于 MergeTree,并在数据块合并算法中添加了折叠行的逻辑。
  • ReplacingMergeTree

    • 该引擎和 MergeTree 的不同之处在于它会删除排序键值相同的重复项
  • SummingMergeTree

    • 当合并 SummingMergeTree 表的数据片段时,ClickHouse 会把所有具有相同主键的行合并为一行,该行包含了被合并的行中具有数值数据类型的列的汇总值。如果主键的组合方式使得单个键值对应于大量的行,则可以显著的减少存储空间并加快数据查询的速度。

ClickHouse 数据库目前梳理的一些注意点

  • 不适合处理事务,没有完整的事务支持。
  • 缺少高频率,低延迟的修改或删除已存在数据的能力。仅能用于批量删除或修改数据,数据更新是异步的
  • 大部分数据查询适用标准的SQL语句,如果一直使用MySQL这种行式数据库,没太多切换成本
  • 定义排序字段或者说是定义索引,存储时候就会按照排序存储,范围查询速度非常快
  • ClickHouse提供各种各样在允许牺牲数据精度的情况下对查询进行加速的方法(目前我没有用过,暂时不知道应用场景)
  • 稀疏索引使得ClickHouse不适合通过其键检索单行的点查询。
  • 数据类型丰富,支持Map(key,value),Tuple(T1,T2,...),Array(T), Geo,Nested嵌套数据结构(类似于嵌套表)
  • Datetime 类型group bytoStartOf*开头的转换函数,如toStartOfDay,相比用DATE函数转换,能提升几十倍性能

kumfo
6.7k 声望4.1k 粉丝

程序生存法则: