存儲結構
以下實例我們都以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的一個很大不同。
下面我們來看下四種複製模式
- 非複製表,internal_replication=false
如果在插入期間沒有問題,則兩個本地表上的數據保持同步。我們稱之爲“窮人的複製”,因爲複製在網絡出現問題的情況下容易發生分歧,沒有一個簡單的方法來確定哪一個是正確的複製。 - 非複製表,internal_replication=true
數據只被插入到一個本地表中,但沒有任何機制可以將它轉移到另一個表中。因此,在不同主機上的本地表看到了不同的數據,查詢分佈式表時會出現非預期的數據。 顯然,這是配置ClickHouse集羣的一種不正確的方法。 - 複製表,internal_replication=true
插入到分佈式表中的數據僅插入到其中一個本地表中,但通過複製機制傳輸到另一個主機上的表中。因此兩個本地表上的數據保持同步。這是推薦的配置。 - 複製表,internal_replication=false
數據被插入到兩個本地表中,但同時複製表的機制保證重複數據會被刪除。數據會從插入的第一個節點複製到其它的節點。其它節點拿到數據後如果發現數據重複,數據會被丟棄。這種情況下,雖然複製保持同步,沒有錯誤發生。但由於不斷的重複複製流,會導致寫入性能明顯的下降。所以這種配置實際應該是避免的,應該使用配置3。