clickhouse(八、特有存储结构和分布式表)

存储结构

以下实例我们都以clickhouse最常用的*MergeTree(合并树)子类引擎来做介绍。

逻辑划分

以分布式表为例,那么ck数据存放于该集群下多个shard分片中。如果shard不在一个节点上,也就是数据会分散到多台机下。每个分片中的数据会根据建表时指定的partition在进行划分,而单个partition中,如果数据容量超过一定阈值又会重新拆分。

# 表结构:
${ck_data}/metadata/path_to_table/*.sql
# 实际数据存放目录:
${ck_data}/data/path_to_table/${partition_*}/**
# 装卸数据目录
${ck_data}/data/path_to_table/detached

列式存储

clickhouse是真正的列式数据库管理系统,除了数据本身外基本不存在其他额外的数据。
下面我们看下clickhouse特有的计算优势。

  • 多服务器分布式处理
    在ClickHouse中,数据可以保存在不同的shard上,每一个shard都由一组用于容错的replica组成,查询可以多服务器并行地在所有shard上进行处理。
  • 向量引擎
    为了高效的使用CPU,计算时ck能按向量(列的一部分)进行处理,相对于实际的数据处理成本,向量化处理具有更低的转发成本。这样可以更加高效地使用CPU。

稀疏索引

Clickhouse 中最强大的表引擎当属 MergeTree (合并树)引擎及该系列(*MergeTree)中的其他引擎。MergeTree 引擎系列的基本理念如下。当你有巨量数据要插入到表中,你要高效地一批批写入数据片段,并希望这些数据片段在后台按照一定规则合并。相比在插入时不断修改(重写)数据进存储,这种策略会高效很多。主要优势:

  • 存储的数据按主键排序。
    这让你可以创建一个用于快速检索数据的小稀疏索引。
  • 允许使用分区,如果指定了 分区键 的话。
    在相同数据集和相同结果集的情况下 ClickHouse 中某些带分区的操作会比普通操作更快。查询中指定了分区键时 ClickHouse 会自动截取分区数据。这也有效增加了查询性能。
  • 支持数据副本。
    ReplicatedMergeTree 系列的表便是用于此。

以官网用例来看。我们以 (CounterID, Date) 以主键。排序好的索引的图示会是下面这样:

全部数据  :      [-------------------------------------------------------------------------]
CounterID:      [aaaaaaaaaaaaaaaaaabbbbcdeeeeeeeeeeeeefgggggggghhhhhhhhhiiiiiiiiikllllllll]
Date:           [1111111222222233331233211111222222333211111112122222223111112223311122333]
标记:            |      |      |      |      |      |      |      |      |      |      |
                a,1    a,2    a,3    b,3    e,2    e,3    g,1    h,2    i,1    i,3    l,3
标记号:          0      1      2      3      4      5      6      7      8      9      10

如果指定查询如下:
CounterID in ('a', 'h'),服务器会读取标记号在 [0, 3) 和 [6, 8) 区间中的数据。
CounterID IN ('a', 'h') AND Date = 3,服务器会读取标记号在 [1, 3) 和 [7, 8) 区间中的数据。
Date = 3,服务器会读取标记号在 [1, 10] 区间中的数据。上面例子可以看出使用索引通常会比全表描述要高效。
稀疏索引会引起额外的数据读取。当读取主键单个区间范围的数据时,每个数据块中最多会多读 index_granularity * 2 行额外的数据。大部分情况下,当 index_granularity = 8192 时,ClickHouse的性能并不会降级。
稀疏索引让你能操作有巨量行的表。因为这些索引是常驻内存(RAM)的。ClickHouse 不要求主键惟一。所以,你可以插入多条具有相同主键的行。下面看在实际语法:

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
)
ENGINE MergeTree() 
PARTITION BY toYYYYMM(EventDate) 
ORDER BY (CounterID, EventDate, intHash32(UserID)) 
SETTINGS index_granularity=8192
  • ENGINE - 引擎名和参数。
    ENGINE = MergeTree(). MergeTree 引擎没有参数。
  • PARTITION BY — 分区键 。
    要按月分区,可以使用表达式 toYYYYMM(date_column) ,这里的 date_column 是一个 Date 类型的列。这里该分区名格式会是 “YYYYMM” 这样。
  • ORDER BY — 表的排序键。
    可以是一组列的元组或任意的表达式。 例如: ORDER BY (CounterID, EventDate) 。
  • PRIMARY KEY - 主键,如果要设成 跟排序键不相同。
    默认情况下主键跟排序键(由 ORDER BY 子句指定)相同。因此,大部分情况下不需要再专门指定一个 PRIMARY KEY 子句。

存储源码实现

存储部分相关大部分逻辑放在/src/Storages下。

表引擎

表的顶层抽象是IStorage,对此接口不同的实现成为不同的表引擎. 例如 StorageMergeTree, StorageMemory等,这些类的实例是表。
该接口中包含很多通用的方法,常用的增删改查read、write、alter、drop等

  • read
    表的readStreams方法能够返回多个IBlockInputStream 对象允许并行处理数据. 这些多个数据块输入流能够从一个表中并行读取数据. 然后你能够用不同的转换来封装这些数据流(例如表达式评估,数据过滤) 能够被单独计算。
	virtual Pipes read(
        const Names & /*column_names*/,
        const SelectQueryInfo & /*query_info*/,
        const Context & /*context*/,
        QueryProcessingStage::Enum /*processed_stage*/,
        size_t /*max_block_size*/,
        unsigned /*num_streams*/)
    {
    
    
        throw Exception("Method read is not supported by storage " + getName(), ErrorCodes::NOT_IMPLEMENTED);
    }

    /** The same as read, but returns BlockInputStreams.
     */
    BlockInputStreams readStreams(
            const Names & /*column_names*/,
            const SelectQueryInfo & /*query_info*/,
            const Context & /*context*/,
            QueryProcessingStage::Enum /*processed_stage*/,
            size_t /*max_block_size*/,
            unsigned /*num_streams*/);

  • write
virtual BlockOutputStreamPtr write(
        const ASTPtr & /*query*/,
        const Context & /*context*/)
    {
    
    
        throw Exception("Method write is not supported by storage " + getName(), ErrorCodes::NOT_IMPLEMENTED);
    }

数据流

  • 数据块流
    用于处理数据。我们使用数据块的数据流从某处读取数据,执行数据转换或者写入数据到某处。IBlockInputStream 有一个read方法获取下一个数据块。IBlockOutputStream 有一个write方法发送数据块到某处。
    例如,当你从AggregatingBlockInputStream拉取数据时,它将从数据源上读取所有的数据,聚合它,然后为你返回一个汇总数据流。另一个示例:UnionBlockInputStream接收很多输入数据源和一些线程。它启动了多个线程,从多个数据源中并行读取数据。
  • 数据块
    是一个容器,代表了内存中一个表的子集。它也是三元组的集合:(IColumn,IDataType,columnname)

存储HA

高可用存储对应生产来说是必不可少的。这里看下ck的分布式存储,和hive的区别还是较大,首先是在查询上需要借助分布式表才能实现。值得注意的是,ck的分布式表并不直接存储数据,而是类似于视图的存在。读是自动并行的。读取时,远程服务器表的索引(如果有的话)会被使用。

官网配置

假设4个节点example01-01-1、example01-01-2、example01-02-1、example01-02-2。集群名称为logs。

<remote_servers>
    <logs>
        <shard>
            <!-- Optional. Shard weight when writing data. Default: 1. -->
            <weight>1</weight>
            <!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->
            <internal_replication>false</internal_replication>
            <replica>
                <host>example01-01-1</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>example01-01-2</host>
                <port>9000</port>
            </replica>
        </shard>
        <shard>
            <weight>2</weight>
            <internal_replication>false</internal_replication>
            <replica>
                <host>example01-02-1</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>example01-02-2</host>
                <secure>1</secure>
                <port>9440</port>
            </replica>
        </shard>
    </logs>
</remote_servers>

我们在logs集群4个节点中都创建test1表,根据totalDate分区。

CREATE TABLE default.test1 on cluster logs 
(`uid` Int32, `totalDate` String ) 
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/test1', '{replica}') 
PARTITION BY totalDate ORDER BY totalDate SETTINGS index_granularity = 8192;

之后在集群中创建分布式表test1_all


-- 建分布式表指向test1
CREATE TABLE default.test1_all on cluster logs 
as test1
ENGINE = Distributed(logs, default, test1, rand())

然后可以向分布式表写入些测试数据,之后到具体的节点查原表进行校验。因为系列六中有类似的实例就不在此赘述。

高可用配置

ck推荐采用复制表+内部同步实现。我们先看下上述配置中internal_replication属性。当设置false时,插入到分布式表中的数据被插入到两个本地表中,因为不会检查副本的一致性,并且随着时间的推移,副本数据可能会有些不一样。
复制表 ,ck数据副本是提供的表级的,而非服务器级别的,所以,服务器里可以同时有复制表和非复制表。这又是ck和hive的一个很大不同。
下面我们来看下四种复制模式

  1. 非复制表,internal_replication=false
    如果在插入期间没有问题,则两个本地表上的数据保持同步。我们称之为“穷人的复制”,因为复制在网络出现问题的情况下容易发生分歧,没有一个简单的方法来确定哪一个是正确的复制。
  2. 非复制表,internal_replication=true
    数据只被插入到一个本地表中,但没有任何机制可以将它转移到另一个表中。因此,在不同主机上的本地表看到了不同的数据,查询分布式表时会出现非预期的数据。 显然,这是配置ClickHouse集群的一种不正确的方法
  3. 复制表,internal_replication=true
    插入到分布式表中的数据仅插入到其中一个本地表中,但通过复制机制传输到另一个主机上的表中。因此两个本地表上的数据保持同步。这是推荐的配置
  4. 复制表,internal_replication=false
    数据被插入到两个本地表中,但同时复制表的机制保证重复数据会被删除。数据会从插入的第一个节点复制到其它的节点。其它节点拿到数据后如果发现数据重复,数据会被丢弃。这种情况下,虽然复制保持同步,没有错误发生。但由于不断的重复复制流,会导致写入性能明显的下降。所以这种配置实际应该是避免的,应该使用配置3。

猜你喜欢

转载自blog.csdn.net/yyoc97/article/details/106128124