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。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章