前言
ClickHouse?是俄羅斯最大的搜索引擎Yandex在2016年開源的數據庫管理系統(DBMS),主要用于聯機分析處理(OLAP)。其采用了面向列的存儲方式,性能遠超傳統面向行的DBMS,近幾年受到廣泛關注。
本文綜合介紹(東拼西湊)了?ClickHouse MergeTree系列表引擎的相關知識,并通過示例分析MergeTree存儲引擎的數據存儲結構。
MergeTree 引擎簡介
為什么叫 MergeTree ?
ClickHouse MergeTree 的表存儲引擎,在寫入一批數據時,數據總會以數據片段的形式寫入磁盤,且數據片段不可修改。為了避免片段過多,ClickHouse會通過后臺線程定期合并這些數據片段,屬于相同分區的數據片段會被合成一個新的片段。這種數據片段往復合并的特點也正是合并樹的名稱由來。
MergeTree 核心引擎如下:
ReplacingMergeTree:在后臺數據合并期間,對具有相同排序鍵的數據進行去重操作。
SummingMergeTree:當合并數據時,會把具有相同主鍵的記錄合并為一條記錄。根據聚合字段設置,該字段的值為聚合后的匯總值,非聚合字段使用第一條記錄的值,聚合字段類型必須為數值類型。
AggregatingMergeTree:在同一數據分區下,可以將具有相同主鍵的數據進行聚合。
CollapsingMergeTree:在同一數據分區下,對具有相同主鍵的數據進行折疊合并。
VersionedCollapsingMergeTree:基于CollapsingMergeTree引擎,增添了數據版本信息字段配置選項。在數據依據ORDER BY設置對數據進行排序的基礎上,如果數據的版本信息列不在排序字段中,那么版本信息會被隱式的作為ORDER BY的最后一列從而影響數據排序。
GraphiteMergeTree:用來存儲時序數據庫Graphites的數據。
MergeTree是該系列引擎中最核心的引擎,其他引擎均以MergeTree為基礎,并在數據合并過程中實現了不同的特性,從而構成了MergeTree表引擎家族。下面我們通過MergeTree來具體了解MergeTree表系列引擎。
ClickHouse 建表語句
建表DDL 語法
創建MergeTree的DDL如下所示:
CREATETABLE[IFNOTEXISTS][db.]table_name[ONCLUSTERcluster](name1[type1][DEFAULT|MATERIALIZED|ALIASexpr1][TTLexpr1],name2[type2][DEFAULT|MATERIALIZED|ALIASexpr2][TTLexpr2],...)ENGINE=MergeTree()ORDERBYexpr[PARTITIONBYexpr][PRIMARYKEYexpr][SAMPLEBYexpr][TTLexpr[DELETE|TODISK'xxx'|TOVOLUME'xxx'],...][SETTINGSname=value,...
這里說明一下MergeTree引擎的主要參數:
[必填選項]
ENGINE:引擎名字,MergeTree引擎無參數。
ORDER BY:排序鍵,可以由一列或多列組成,決定了數據以何種方式進行排序,例如ORDER BY(CounterID, EventDate)。如果沒有顯示指定PRIMARY KEY,那么將使用ORDER BY作為PRIMARY KEY。通常只指定ORDER BY即可。
[選填選項]
PARTITION BY:分區鍵,指明表中的數據以何種規則進行分區。分區是在一個表中通過指定的規則劃分而成的邏輯數據集。分區可以按任意標準進行,如按月、按日或按事件類型。為了減少需要操作的數據,每個分區都是分開存儲的。
PRIMARY KEY:主鍵,設置后會按照主鍵生成一級索引(primary.idx),數據會依據索引的設置進行排序,從而加速查詢性能。默認情況下,PRIMARY KEY與ORDER BY設置相同,所以通常情況下直接使用ORDER BY設置來替代主鍵設置。
SAMPLE BY:數據采樣設置,如果顯示配置了該選項,那么主鍵配置中也應該包括此配置。例如 ORDER BY CounterID / EventDate / intHash32(UserID)、SAMPLE BY intHash32(UserID)。
TTL:數據存活時間,可以為某一字段列或者一整張表設置TTL,設置中必須包含Date或DateTime字段類型。如果設置在列上,那么會刪除字段中過期的數據。如果設置的是表級的TTL,那么會刪除表中過期的數據。如果設置了兩種類型,那么按先到期的為準。例如,TTL createtime + INTERVAL 1 DAY,即一天后過期。使用場景包括定期刪除數據,或者定期將數據進行歸檔。
index_granularity:索引間隔粒度。MergeTree索引為稀疏索引,每index_granularity個數據產生一條索引。index_granularity默認設置為8092。
enable_mixed_granularity_parts:是否啟動index_granularity_bytes來控制索引粒度大小。
index_granularity_bytes:索引粒度,以字節為單位,默認10Mb。
merge_max_block_size:數據塊合并最大記錄個數,默認8192。
merge_with_ttl_timeout:合并頻率最小時間間隔,默認1天。
建表 SQL 實例
CREATE TABLE IF NOT EXISTS mergetree_sample_table
(
name? ? String,
price? ? UInt64,
shop_id? UInt64,
quantity UInt64,
p_date? DateTime
)
ENGINE =MergeTree()
partition by p_date
? ? ? ? order by (name,shop_id,p_date)
SETTINGS index_granularity =2;
插入數據:
INSERT INTO mergetree_sample_table
VALUES ('Apple',2,1,40,now()) ('Apple',2,3,35,now()) ('Apple',3,2,45,now()) ('Apple',3,4,35,now()) ('Orange',1,2,40,now()) ('Orange',3,3,50,now()) ('Banana',2,1,25,now()) ('Banana',2,2,55,now());
查詢數據:
SELECT t.*
? ? ? FROM mydb.mergetree_sample_table t
? ? ? LIMIT 501
底層文件存儲
MergeTree 表引擎底層的物理存儲文件目錄如下:
MergeTree 表引擎的物理文件存儲目錄結構:
├── 1638121099_1_1_0
│?? ├── checksums.txt
│?? ├── columns.txt
│?? ├── count.txt
│?? ├── data.bin
│?? ├── data.mrk3
│?? ├── default_compression_codec.txt
│?? ├── minmax_p_date.idx
│?? ├── partition.dat
│?? └── primary.idx
├── detached
└── format_version.txt
2 directories, 10 files
其中,
$cat default_compression_codec.txt?
CODEC(LZ4)%?
$cat columns.txt?
columns format version: 1
5 columns:
`name` String
`price` UInt64
`shop_id` UInt64
`quantity` UInt64
`p_date` DateTime
$cat count.txt?
8%? ? ? ? ?
數據分區目錄命名規則
目錄命名規則如下:
PartitionId_MinBlockNum_MaxBlockNum_Level
PartitionID:分區id,例如20210301。
MinBlockNum:最小分區塊編號,自增類型,從1開始向上遞增。每產生一個新的目錄分區就向上遞增一個數字。
MaxBlockNum:最大分區塊編號,新創建的分區MinBlockNum等于MaxBlockNum的編號。
Level:合并的層級,被合并的次數。合并次數越多,層級值越大。
level為0,表示此分區沒有合并過。
索引文件:稀疏索引
MergeTree索引為稀疏索引,它并不索引單條數據,而是索引一定范圍的數據。也就是從已排序的全量數據中,間隔性的選取一些數據記錄主鍵字段的值來生成primary.idx索引文件,從而加快表查詢效率。間隔設置參數為index_granularity。
標記文件
mrk標記文件在primary.idx索引文件和bin數據文件之間起到了橋梁作用。primary.idx文件中的每條索引在mrk文件中都有對應的一條記錄。
一條記錄的組成包括:
offset-compressed bin file:表示指向的壓縮數據塊在bin文件中的偏移量。
offset-decompressed data block:表示指向的數據在解壓數據塊中的偏移量。
row counts:代表數據記錄行數,小于等于index_granularity所設置的值。
索引、標記和數據文件下圖所示:
MergeTree表引擎家族詳解
在ClickHouse的整個體系里面,MergeTree表引擎絕對是一等公民,使用ClickHouse就是在使用MergeTree,這種說法一點也不為過。MergeTree表引擎是一個家族系列,目前整個系列一共包含了14種不同類型的MergeTree。
MergeTree(合并樹)系列表引擎是ClickHouse提供的最具特色的存儲引擎。MergeTree引擎支持數據按主鍵、數據分區、數據副本以及數據采樣等特性。官方提供了包括MergeTree、ReplacingMergeTree、SummingMergeTree、AggregatingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree、GraphiteMergeTree等7種不同類型的MergeTree引擎的實現,以及與其相對應的支持數據副本的MergeTree引擎(Replicated*)。
這么多表引擎,它們之間是什么關系?
我們可以使用兩種關系,來理解整個MergeTree系列:
繼承關系
首先,為了便于理解,可以使用繼承關系來看待MergeTree。通過最基礎的MergeTree表引擎,向下派生出6個變種表引擎,如下圖所示
在ClickHouse底層具體的實現方法中,上述7種表引擎的區別主要體現在Merge合并的邏輯部分。如下圖所示:
在具體的實現邏輯部分,7種MergeTree共用一個主體,在觸發Merge動作時,調用了各自獨有的合并邏輯。特殊功能只會在Merge合并時才會觸發。
組合關系
剛才已經介紹了7種MergeTree的關系,余下的7種是ReplicatedMergeTree系列。
ReplicatedMergeTree與普通的MergeTree又有什么區別呢?? 我們接著看下面這張圖:
圖中的虛線框部分是MergeTree的能力邊界,而ReplicatedMergeTree在它的基礎之上增加了分布式協同的能力(HA)。ClickHouse 集群借助ZooKeeper的消息日志廣播,實現了副本實例之間的數據同步功能。
ReplicatedMergeTree系列可以用組合關系來理解,如下圖所示:
當我們為7種MergeTree加上Replicated前綴后,又能組合出7種新的表引擎,這些ReplicatedMergeTree 擁有副本協同的能力。
我們到底應該使用哪一種表引擎?
現在回答第二個問題,按照使用的場景劃分,可以將上述14種表引擎大致分成以下6類應用場景:
默認情況
在沒有特殊要求的場合,使用基礎的MergeTree表引擎即可,它不僅擁有高效的性能,也提供了所有MergeTree共有的基礎功能,包括列存、數據分區、分區索引、一級索引、二級索引、TTL、多路徑存儲等等。
與此同時,它也定義了整個MergeTree家族的基調,例如:
ORDER BY 決定了每個分區中數據的排序規則;
PRIMARY KEY 決定了一級索引(primary.idx);
ORDER BY 可以指代PRIMARY KEY, 通常只用聲明ORDER BY 即可。
接下來將要介紹的其他表引擎,除開ReplicatedMergeTree系列外,都是在Merge合并動作時添加了各自獨有的邏輯。
數據去重
ReplacingMergeTree 使用示例
1.建表
CREATE TABLE IF NOT EXISTS replacingmergetree_test
(
ID? ? ? ? ? String,
Name? ? ? ? String,
DateOfBirth Date
)
ENGINE =ReplacingMergeTree()
PARTITION BY ID
? ? ? ? ORDER BY (ID,DateOfBirth)
SETTINGS
? ? ? ? ? ? index_granularity =1024;
2.插入數據
INSERT INTO replacingmergetree_test
VALUES ('a1','Jim','1995-05-01'),
('a1','Jim','1995-05-01'),
('a1','Jim','1995-05-02'),
('a2','Jil','1995-06-01'),
('a2','Jil','1995-06-01'),
('a2','Jil','1995-06-02');
查詢數據,看看效果:
?desc replacingmergetree_test
DESCRIBE TABLE? replacingmergetree_test
Query id: 59ab6932-9912-4d86-b97d-7782a8e11f65
┌─name────────┬─type───┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ ID? ? ? ? ? │ String │? ? ? ? ? ? ? │? ? ? ? ? ? ? ? ? ? │? ? ? ? │? ? ? ? ? ? ? ? ? │? ? ? ? ? ? ? ? │
│ Name? ? ? ? │ String │? ? ? ? ? ? ? │? ? ? ? ? ? ? ? ? ? │? ? ? ? │? ? ? ? ? ? ? ? ? │? ? ? ? ? ? ? ? │
│ DateOfBirth │ Date? │? ? ? ? ? ? ? │? ? ? ? ? ? ? ? ? ? │? ? ? ? │? ? ? ? ? ? ? ? ? │? ? ? ? ? ? ? ? │
└─────────────┴────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘
3 rows in set. Elapsed: 0.001 sec.?
SELECT *
FROM replacingmergetree_test
Query id: 082de655-db24-4679-ab10-8d431320fae2
┌─ID─┬─Name─┬─DateOfBirth─┐
│ a2 │ Jil? │? 1995-06-01 │
│ a2 │ Jil? │? 1995-06-02 │
└────┴──────┴─────────────┘
┌─ID─┬─Name─┬─DateOfBirth─┐
│ a1 │ Jim? │? 1995-05-01 │
│ a1 │ Jim? │? 1995-05-02 │
└────┴──────┴─────────────┘
4 rows in set. Elapsed: 0.003 sec.?
3.執行merge
可以手動執行:
OPTIMIZE TABLE replacingmergetree_test FINAL;
一般ClickHouse會由后臺程序自動執行 merge 操作。
4.查詢數據
通過剛才的說明,大家應該明白,MergeTree的主鍵(PRIMARY KEY)只是用來生成一級索引(primary.idx)的,并沒有唯一性約束這樣的語義。
一些朋友在使用MergeTree的時候,用傳統數據庫的思維來理解MergeTree就會出現問題。
如果業務上不允許數據重復,遇到這類場景就可以使用ReplacingMergeTree,如下圖所示:
ReplacingMergeTree通過ORDER BY,表示判斷唯一約束的條件。當分區合并之時,根據ORDER BY排序后,相鄰重復的數據會被排除。
由此,可以得出幾點結論:
第一,使用ORDER BY作為特殊判斷標識,而不是PRIMARY KEY。關于這一點網上有一些誤傳,但是如果理解了ORDER BY與PRIMARY KEY的作用,以及合并邏輯之后,都能夠推理出應該是由ORDER BY決定。
ORDER BY的作用, 負責分區內數據排序;
PRIMARY KEY的作用, 負責一級索引生成;
Merge的邏輯, 分區內數據排序后,找到相鄰的數據,做特殊處理。
第二,只有在觸發合并之后,才能觸發特殊邏輯。以去重為例,在沒有合并的時候,還是會出現重復數據。
第三,只對同一分區內的數據有效。以去重為例,只有屬于相同分區的數據才能去重,跨越不同分區的重復數據不能去重。
上述幾點結論,適用于包含ReplacingMergeTree在內的6種MergeTree,所以后面不在贅述。
小結:ReplacingMergeTree?引擎會把相同索引的數據進行替換,但僅限本地單臺機器。如果使用分布式表,就要確保相同索引的數據入到同一臺機器,否則每臺機器可能會有一條相同索引的數據。
該索引只有在 merge 的時候才會執行替換,因為 merge 是不定時的,如果沒有 merge 的情況下,會出現多條數據的情況。因此必要的話,可以進行手動進行 merge。手動 merge 命令:optimize table db.table;
該索引的建表語句如果沒有用某個字段標定版本,該字段可以是 int、double、date 類型,數據庫就一定會把后入庫的覆蓋新入庫 (如果有區分版本的字段,則會留下數值大的那條記錄)。
預聚合(數據立方體)
有這么一類場景,它的查詢主題是非常明確的,也就是說聚合查詢的維度字段是固定,并且沒有明細數據的查詢需求,這類場合就可以使用SummingMergeTree或是AggregatingMergeTree,如下圖所示:
可以看到,在新分區合并后,在同一分區內,ORDER BY條件相同的數據會進行合并。如此一來,首先表內的數據行實現了有效的減少,其次度量值被預先聚合,進一步減少了后續計算開銷。聚合類MergeTree通常可以和MergeTree表引擎協同使用,如下圖所示:
可以將物化視圖設置成聚合類MergeTree,將其作為固定主題的查詢表使用。
值得一提的是,通常只有在使用SummingMergeTree或AggregatingMergeTree的時候,才需要同時設置ORDER BY與PRIMARY KEY。
顯式的設置PRIMARY KEY,是為了將主鍵和排序鍵設置成不同的值,是進一步優化的體現。
例如某個場景的查詢需求如下:
聚合條件,GROUP BY A,B,C
過濾條件,WHERE A
此時,如下設置將會是一種較優的選擇:
GROUP BY?A,B,C
PRIMARY KEY?A
BTW,如果ORDER BY與PRIMARY KEY不同,PRIMARY KEY必須是ORDER BY的前綴(為了保證分區內數據和主鍵的有序性)。
SummingMergeTree 引擎測試
該引擎會把索引以為的所有 number 型字段(包含 int 和 double)自動進行聚合。
該引擎在分布式情況下并不是完全聚合,而是每臺機器有一條同緯度的數據。SummingMergeTree 是按 part 緯度來聚合,數據剛導入 clickhouse 可能會產生多個 part,但是 clickhouse 會定期把 part merge,從而實現一臺機器只有一條同緯度的數據。
如果將字段設為索引,則不會繼續聚合,對于非設為索引的字段,如果是 int 類型會進行聚合,非 int 類型,會隨機選取一個字段進行覆蓋。
數據更新
數據的更新在ClickHouse中有多種實現手段,例如按照分區Partition重新寫入、使用Mutation的DELETE和UPDATE查詢。
使用CollapsingMergeTree或VersionedCollapsingMergeTree也能實現數據更新,這是一種使用標記位,以增代刪的數據更新方法,如下圖所示:
通過增加一個 sign 標志字段(例如圖中的sign字段),作為數據有效性的判斷依據。
可以看到,在新分區合并后,在同一分區內,ORDER BY條件相同的數據,其標志值為1和-1的數據行會進行抵消。
下圖是另外一種便于理解的視角,就如同擠壓瓦楞紙一般,數據被抵消了:
VersionedCollapsingMergeTree:?帶版本的CollapsingMergeTree
CollapsingMergeTree 和 VersionedCollapsingMergeTree的區別又是什么呢?
CollapsingMergeTree?對數據寫入的順序是敏感的,它要求標志位需要按照正確的順序排序。例如按照1,-1的寫入順序是正確的; 而如果按照-1,1的錯誤順序寫入,CollapsingMergeTree就無法正確抵消。
試想,如果在一個多線程并行的寫入場景,我們是無法保證這種順序寫入的,此時就需要使用VersionedCollapsingMergeTree了。
VersionedCollapsingMergeTree?在 CollapsingMergeTree基礎之上,額外要求指定一個version字段,在分區Merge合并時,它會自動將version字段追加到ORERY BY的末尾,從而保證了標志位的有序性。
ENGINE=VersionedCollapsingMergeTree(sign,ver) ORDER BY id //等效于ORDER BY id,ver
監控集成
GraphiteMergeTree可以與Graphite集成,如果你使用了Graphite作為系統的運行監控系統, 則可以通過GraphiteMergeTree存儲指標數據,加速查詢性能、降低存儲成本。
高可用
Replicated* 擁有數據副本的能力,如下圖所示:
結合剛才的5類場景,如果進一步需要高可用的需求,選擇一種MergeTree和Replicated組合即可,例如 ReplicatedMergeTree、ReplicatedReplacingMergeTree 等等。
合并算法概述?Overview of the merge algorithm
每個合并按塊順序執行。
Each merge is executed sequentially block by block.
合并算法的主要思想是,確保合并操作不是一個在線程池中執行的子例程(因為它可能會占用一段時間的線程),而是使合并操作在一個協程中完成。它可以在某些點暫停執行,然后從該點恢復執行。
The main idea is to make a merge not a subroutine which is executed in a thread pool and may occupy a thread for a period of time,? ?but to make a merge a coroutine which can suspend the execution? in some points and then resume the execution from this point.
掛起執行的最佳點是在一個塊上的工作完成之后。
任務本身將通過? BackgroundJobExecutor 執行。
任務的接口很簡單。主要方法是' execute() ',如果任務想要再次執行,它將返回true,否則返回false。
A perfect point where to suspend the execution is after the work over a block is finished.
The task itself will be executed via BackgroundJobExecutor.
The interface of the task is simple. The main method is `execute()` which will return true, if the task wants to be executed again and false otherwise.
對于這種任務,我們可以給合并一個優先級。 優先級很簡單:
合并的大小越小,優先級越高。?
By default priority queue will have max element at top。
所以,如果ClickHouse想要將一些真正大的部分合并成一個更大的部分,那么它將被執行很長一段時間,因為合并的結果并不是真正需要立即。 最好盡快合并小部分。
With this kind of task we can give a merge a priority. A priority is simple :
?the lower the size of the merge, the higher priority.?
So, if ClickHouse wants to merge some really big parts into a bigger part, then it will be executed for a long time, because the result of the merge is not really needed immediately. It is better to merge small parts as soon as possible.
合并任務后臺執行器:MergeTreeBackgroundExecutor
一個 MergeTreeBackgroundExecutor 任務有兩個隊列:Pending 掛起隊列(所有任務的主隊列)和 Active 活動隊列(當前正在執行)。
Pending 掛起隊列是需要的,因為任務的數量將超過線程執行。
MergeTreeBackgroundExecutor.h? 代碼:
#pragma once
#include <deque>
#include <functional>
#include <atomic>
#include <mutex>
#include <future>
#include <condition_variable>
#include <set>
#include <iostream>
#include <boost/circular_buffer.hpp>
#include <base/shared_ptr_helper.h>
#include <base/logger_useful.h>
#include <Common/ThreadPool.h>
#include <Common/Stopwatch.h>
#include <Storages/MergeTree/IExecutableTask.h>
namespace DB
{
namespace ErrorCodes
{
? ? extern const int LOGICAL_ERROR;
}
struct TaskRuntimeData;
using TaskRuntimeDataPtr = std::shared_ptr<TaskRuntimeData>;
/**
* Has RAII class to determine how many tasks are waiting for the execution and executing at the moment.
* Also has some flags and primitives to wait for current task to be executed.
*/
struct TaskRuntimeData
{
? ? TaskRuntimeData(ExecutableTaskPtr && task_, CurrentMetrics::Metric metric_)
? ? ? ? : task(std::move(task_))
? ? ? ? , increment(std::move(metric_))
? ? {}
? ? ExecutableTaskPtr task;
? ? CurrentMetrics::Increment increment;
? ? std::atomic_bool is_currently_deleting{false};
? ? /// Actually autoreset=false is needed only for unit test
? ? /// where multiple threads could remove tasks corresponding to the same storage
? ? /// This scenario in not possible in reality.
? ? Poco::Event is_done{/*autoreset=*/false};
? ? /// This is equal to task->getPriority() not to do useless virtual calls in comparator
? ? UInt64 priority{0};
? ? /// By default priority queue will have max element at top
? ? static bool comparePtrByPriority(const TaskRuntimeDataPtr & lhs, const TaskRuntimeDataPtr & rhs)
? ? {
? ? ? ? return lhs->priority > rhs->priority;
? ? }
};
class OrdinaryRuntimeQueue
{
public:
? ? TaskRuntimeDataPtr pop()
? ? {
? ? ? ? auto result = std::move(queue.front());
? ? ? ? queue.pop_front();
? ? ? ? return result;
? ? }
? ? void push(TaskRuntimeDataPtr item) { queue.push_back(std::move(item));}
? ? void remove(StorageID id)
? ? {
? ? ? ? auto it = std::remove_if(queue.begin(), queue.end(),
? ? ? ? ? ? [&] (auto item) -> bool { return item->task->getStorageID() == id; });
? ? ? ? queue.erase(it, queue.end());
? ? }
? ? void setCapacity(size_t count) { queue.set_capacity(count); }
? ? bool empty() { return queue.empty(); }
private:
? ? boost::circular_buffer<TaskRuntimeDataPtr> queue{0};
};
/// Uses a heap to pop a task with minimal priority
class MergeMutateRuntimeQueue
{
public:
? ? TaskRuntimeDataPtr pop()
? ? {
? ? ? ? std::pop_heap(buffer.begin(), buffer.end(), TaskRuntimeData::comparePtrByPriority);
? ? ? ? auto result = std::move(buffer.back());
? ? ? ? buffer.pop_back();
? ? ? ? return result;
? ? }
? ? void push(TaskRuntimeDataPtr item)
? ? {
? ? ? ? item->priority = item->task->getPriority();
? ? ? ? buffer.push_back(std::move(item));
? ? ? ? std::push_heap(buffer.begin(), buffer.end(), TaskRuntimeData::comparePtrByPriority);
? ? }
? ? void remove(StorageID id)
? ? {
? ? ? ? auto it = std::remove_if(buffer.begin(), buffer.end(),
? ? ? ? ? ? [&] (auto item) -> bool { return item->task->getStorageID() == id; });
? ? ? ? buffer.erase(it, buffer.end());
? ? ? ? std::make_heap(buffer.begin(), buffer.end(), TaskRuntimeData::comparePtrByPriority);
? ? }
? ? void setCapacity(size_t count) { buffer.reserve(count); }
? ? bool empty() { return buffer.empty(); }
private:
? ? std::vector<TaskRuntimeDataPtr> buffer{};
};
/**
*? Executor for a background MergeTree related operations such as merges, mutations, fetches an so on.
*? It can execute only successors of ExecutableTask interface.
*? Which is a self-written coroutine. It suspends, when returns true from executeStep() method.
*
*? There are two queues of a tasks: pending (main queue for all the tasks) and active (currently executing).
*? Pending queue is needed since the number of tasks will be more than thread to execute.
*? Pending tasks are tasks that successfully scheduled to an executor or tasks that have some extra steps to execute.
*? There is an invariant, that task may occur only in one of these queue. It can occur in both queues only in critical sections.
*
*? Pending:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Active:
*
*? |s| |s| |s| |s| |s| |s| |s| |s| |s| |s|? ? ? ? ? ? ? |s|
*? |s| |s| |s| |s| |s| |s| |s| |s| |s|? ? ? ? ? ? ? ? ? |s|
*? |s| |s|? ? |s|? ? |s| |s|? ? |s|? ? ? ? ? ? ? ? ? |s|
*? ? ? |s|? ? ? ? ? ? |s| |s|? ? ? ? ? ? ? ? ? ? ? ? ? |s|
*? ? ? |s|? ? ? ? ? ? ? ? |s|
*? ? ? ? ? ? ? ? ? ? ? ? ? |s|
*
*? Each task is simply a sequence of steps. Heavier tasks have longer sequences.
*? When a step of a task is executed, we move tasks to pending queue. And take another from the queue's head.
*? With these architecture all small merges / mutations will be executed faster, than bigger ones.
*
*? We use boost::circular_buffer as a container for queues not to do any allocations.
*
*? Another nuisance that we faces with is than background operations always interact with an associated Storage.
*? So, when a Storage want to shutdown, it must wait until all its background operaions are finished.
*/
template <class Queue>
class MergeTreeBackgroundExecutor final : public shared_ptr_helper<MergeTreeBackgroundExecutor<Queue>>
{
public:
? ? MergeTreeBackgroundExecutor(
? ? ? ? String name_,
? ? ? ? size_t threads_count_,
? ? ? ? size_t max_tasks_count_,
? ? ? ? CurrentMetrics::Metric metric_)
? ? ? ? : name(name_)
? ? ? ? , threads_count(threads_count_)
? ? ? ? , max_tasks_count(max_tasks_count_)
? ? ? ? , metric(metric_)
? ? {
? ? ? ? if (max_tasks_count == 0)
? ? ? ? ? ? throw Exception(ErrorCodes::LOGICAL_ERROR, "Task count for MergeTreeBackgroundExecutor must not be zero");
? ? ? ? pending.setCapacity(max_tasks_count);
? ? ? ? active.set_capacity(max_tasks_count);
? ? ? ? pool.setMaxThreads(std::max(1UL, threads_count));
? ? ? ? pool.setMaxFreeThreads(std::max(1UL, threads_count));
? ? ? ? pool.setQueueSize(std::max(1UL, threads_count));
? ? ? ? for (size_t number = 0; number < threads_count; ++number)
? ? ? ? ? ? pool.scheduleOrThrowOnError([this] { threadFunction(); });
? ? }
? ? ~MergeTreeBackgroundExecutor()
? ? {
? ? ? ? wait();
? ? }
? ? bool trySchedule(ExecutableTaskPtr task);
? ? void removeTasksCorrespondingToStorage(StorageID id);
? ? void wait();
private:
? ? String name;
? ? size_t threads_count{0};
? ? size_t max_tasks_count{0};
? ? CurrentMetrics::Metric metric;
? ? void routine(TaskRuntimeDataPtr item);
? ? void threadFunction();
? ? /// Initially it will be empty
? ? Queue pending{}; // 等待隊列
? ? boost::circular_buffer<TaskRuntimeDataPtr> active{0}; // 執行隊列
? ? std::mutex mutex; // 互斥鎖
? ? std::condition_variable has_tasks;
? ? std::atomic_bool shutdown{false};
? ? ThreadPool pool;
};
extern template class MergeTreeBackgroundExecutor<MergeMutateRuntimeQueue>;
extern template class MergeTreeBackgroundExecutor<OrdinaryRuntimeQueue>;
using MergeMutateBackgroundExecutor = MergeTreeBackgroundExecutor<MergeMutateRuntimeQueue>;
using OrdinaryBackgroundExecutor = MergeTreeBackgroundExecutor<OrdinaryRuntimeQueue>;
}
MergeTreeBackgroundExecutor.cpp 源代碼:
#include <Storages/MergeTree/MergeTreeBackgroundExecutor.h>
#include <algorithm>
#include <Common/setThreadName.h>
#include <Storages/MergeTree/BackgroundJobsAssignee.h>
namespace DB
{
template <class Queue>
void MergeTreeBackgroundExecutor<Queue>::wait() // 等待執行
{
? ? {
? ? ? ? std::lock_guard lock(mutex);
? ? ? ? shutdown = true;
? ? ? ? has_tasks.notify_all();
? ? }
? ? pool.wait();
}
template <class Queue>
// 調度任務執行
bool MergeTreeBackgroundExecutor<Queue>::trySchedule(ExecutableTaskPtr task)
{
? ? std::lock_guard lock(mutex); // 上鎖
? ? if (shutdown)
? ? ? ? return false;
? ? auto & value = CurrentMetrics::values[metric];
? ? if (value.load() >= static_cast<int64_t>(max_tasks_count))
? ? ? ? return false;
? ? pending.push(std::make_shared<TaskRuntimeData>(std::move(task), metric));
? ? has_tasks.notify_one();
? ? return true;
}
template <class Queue>
void MergeTreeBackgroundExecutor<Queue>::removeTasksCorrespondingToStorage(StorageID id)
{
? ? std::vector<TaskRuntimeDataPtr> tasks_to_wait;
? ? {
? ? ? ? std::lock_guard lock(mutex);?// 上鎖
? ? ? ? /// Erase storage related tasks from pending and select active tasks to wait for
? ? ? ? pending.remove(id);
? ? ? ? /// Copy items to wait for their completion
? ? ? ? std::copy_if(active.begin(), active.end(), std::back_inserter(tasks_to_wait),
? ? ? ? ? ? [&] (auto item) -> bool { return item->task->getStorageID() == id; });
? ? ? ? for (auto & item : tasks_to_wait)
? ? ? ? ? ? item->is_currently_deleting = true;
? ? }
? ? /// Wait for each task to be executed
? ? for (auto & item : tasks_to_wait)
? ? {
? ? ? ? item->is_done.wait();
? ? ? ? item.reset();
? ? }
}
template <class Queue>
void MergeTreeBackgroundExecutor<Queue>::routine(TaskRuntimeDataPtr item)
{
? ? DENY_ALLOCATIONS_IN_SCOPE;
? ? /// All operations with queues are considered no to do any allocations
? ? auto erase_from_active = [this, item]
? ? {
? ? ? ? active.erase(std::remove(active.begin(), active.end(), item), active.end());
? ? };
? ? bool need_execute_again = false;
? ? try
? ? {
? ? ? ? ALLOW_ALLOCATIONS_IN_SCOPE;
? ? ? ? need_execute_again = item->task->executeStep();
? ? }
? ? catch (...)
? ? {
? ? ? ? tryLogCurrentException(__PRETTY_FUNCTION__);
? ? }
? ? if (need_execute_again)
? ? {
? ? ? ? std::lock_guard guard(mutex);? // 上鎖
? ? ? ? if (item->is_currently_deleting)
? ? ? ? {
? ? ? ? ? ? erase_from_active();
? ? ? ? ? ? /// This is significant to order the destructors.
? ? ? ? ? ? item->task.reset();
? ? ? ? ? ? item->is_done.set();
? ? ? ? ? ? item = nullptr;
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ? /// After the `guard` destruction `item` has to be in moved from state
? ? ? ? /// Not to own the object it points to.
? ? ? ? /// Otherwise the destruction of the task won't be ordered with the destruction of the
? ? ? ? /// storage.
? ? ? ? pending.push(std::move(item));
? ? ? ? erase_from_active();
? ? ? ? has_tasks.notify_one();
? ? ? ? item = nullptr;
? ? ? ? return;
? ? }
? ? {
? ? ? ? std::lock_guard guard(mutex);??// 上鎖
? ? ? ? erase_from_active();
? ? ? ? has_tasks.notify_one();
? ? ? ? try
? ? ? ? {
? ? ? ? ? ? ALLOW_ALLOCATIONS_IN_SCOPE;
? ? ? ? ? ? /// In a situation of a lack of memory this method can throw an exception,
? ? ? ? ? ? /// because it may interact somehow with BackgroundSchedulePool, which may allocate memory
? ? ? ? ? ? /// But it is rather safe, because we have try...catch block here, and another one in ThreadPool.
? ? ? ? ? ? item->task->onCompleted();
? ? ? ? }
? ? ? ? catch (...)
? ? ? ? {
? ? ? ? ? ? tryLogCurrentException(__PRETTY_FUNCTION__);
? ? ? ? }
? ? ? ? /// We have to call reset() under a lock, otherwise a race is possible.
? ? ? ? /// Imagine, that task is finally completed (last execution returned false),
? ? ? ? /// we removed the task from both queues, but still have pointer.
? ? ? ? /// The thread that shutdowns storage will scan queues in order to find some tasks to wait for, but will find nothing.
? ? ? ? /// So, the destructor of a task and the destructor of a storage will be executed concurrently.
? ? ? ? item->task.reset();
? ? ? ? item->is_done.set();
? ? ? ? item = nullptr;
? ? }
}
template <class Queue>
void MergeTreeBackgroundExecutor<Queue>::threadFunction()
{
? ? setThreadName(name.c_str());
? ? DENY_ALLOCATIONS_IN_SCOPE;
? ? while (true) // MergeTreeBackgroundExecutor 常駐線程池?
? ? {
? ? ? ? try
? ? ? ? {
? ? ? ? ? ? TaskRuntimeDataPtr item;
? ? ? ? ? ? {
? ? ? ? ? ? ? ? std::unique_lock lock(mutex);???// 上鎖
? ? ? ? ? ? ? ? has_tasks.wait(lock, [this](){ return !pending.empty() || shutdown; });
? ? ? ? ? ? ? ? if (shutdown)
? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? ? ? item = std::move(pending.pop());
? ? ? ? ? ? ? ? active.push_back(item);
? ? ? ? ? ? }
? ? ? ? ? ? routine(std::move(item));
? ? ? ? }
? ? ? ? catch (...)
? ? ? ? {
? ? ? ? ? ? tryLogCurrentException(__PRETTY_FUNCTION__);
? ? ? ? }
? ? }
}
template class MergeTreeBackgroundExecutor<MergeMutateRuntimeQueue>;
template class MergeTreeBackgroundExecutor<OrdinaryRuntimeQueue>;
}
附錄:ClickHouse SQL 表達式語法
參考:ASTSelectQuery.h
#pragma once#include
#include
namespace DB
{
struct ASTTablesInSelectQueryElement;
struct StorageID;
/** SELECT query
*/
class ASTSelectQuery :public IAST
{
public:
enum class Expression : uint8_t
{
WITH,
SELECT,
TABLES,
PREWHERE,
WHERE,
GROUP_BY,
HAVING,
WINDOW,
ORDER_BY,
LIMIT_BY_OFFSET,
LIMIT_BY_LENGTH,
LIMIT_BY,
LIMIT_OFFSET,
LIMIT_LENGTH,
SETTINGS
? ? };
static String expressionToString(Expression expr)
{
switch (expr)
{
case Expression::WITH:
return "WITH";
case Expression::SELECT:
return "SELECT";
case Expression::TABLES:
return "TABLES";
case Expression::PREWHERE:
return "PREWHERE";
case Expression::WHERE:
return "WHERE";
case Expression::GROUP_BY:
return "GROUP BY";
case Expression::HAVING:
return "HAVING";
case Expression::WINDOW:
return "WINDOW";
case Expression::ORDER_BY:
return "ORDER BY";
case Expression::LIMIT_BY_OFFSET:
return "LIMIT BY OFFSET";
case Expression::LIMIT_BY_LENGTH:
return "LIMIT BY LENGTH";
case Expression::LIMIT_BY:
return "LIMIT BY";
case Expression::LIMIT_OFFSET:
return "LIMIT OFFSET";
case Expression::LIMIT_LENGTH:
return "LIMIT LENGTH";
case Expression::SETTINGS:
return "SETTINGS";
}
return "";
}
/** Get the text that identifies this element. */
? ? String getID(char)const override {return "SelectQuery"; }
ASTPtr clone()const override;
bool distinct =false;
bool group_by_with_totals =false;
bool group_by_with_rollup =false;
bool group_by_with_cube =false;
bool group_by_with_constant_keys =false;
bool limit_with_ties =false;
ASTPtr & refSelect()? ? {return getExpression(Expression::SELECT); }
ASTPtr & refTables()? ? {return getExpression(Expression::TABLES); }
ASTPtr & refPrewhere()? {return getExpression(Expression::PREWHERE); }
ASTPtr & refWhere()? ? {return getExpression(Expression::WHERE); }
ASTPtr & refHaving()? ? {return getExpression(Expression::HAVING); }
const ASTPtr with()const {return getExpression(Expression::WITH); }
const ASTPtr select()const {return getExpression(Expression::SELECT); }
const ASTPtr tables()const {return getExpression(Expression::TABLES); }
const ASTPtr prewhere()const {return getExpression(Expression::PREWHERE); }
const ASTPtr where()const {return getExpression(Expression::WHERE); }
const ASTPtr groupBy()const {return getExpression(Expression::GROUP_BY); }
const ASTPtr having()const {return getExpression(Expression::HAVING); }
const ASTPtr window()const {return getExpression(Expression::WINDOW); }
const ASTPtr orderBy()const {return getExpression(Expression::ORDER_BY); }
const ASTPtr limitByOffset()const {return getExpression(Expression::LIMIT_BY_OFFSET); }
const ASTPtr limitByLength()const {return getExpression(Expression::LIMIT_BY_LENGTH); }
const ASTPtr limitBy()const {return getExpression(Expression::LIMIT_BY); }
const ASTPtr limitOffset()const {return getExpression(Expression::LIMIT_OFFSET); }
const ASTPtr limitLength()const {return getExpression(Expression::LIMIT_LENGTH); }
const ASTPtr settings()const {return getExpression(Expression::SETTINGS); }
bool hasFiltration()const {return where() || prewhere() || having(); }
/// Set/Reset/Remove expression.
? ? void setExpression(Expression expr, ASTPtr && ast);
ASTPtr getExpression(Expression expr,bool clone =false)const
? ? {
auto it = positions.find(expr);
if (it != positions.end())
return clone ? children[it->second]->clone() : children[it->second];
return {};
}
/// Compatibility with old parser of tables list. TODO remove
? ? ASTPtr sampleSize()const;
ASTPtr sampleOffset()const;
std::pair arrayJoinExpressionList()const;
const ASTTablesInSelectQueryElement * join()const;
bool final()const;
bool withFill()const;
void replaceDatabaseAndTable(const String & database_name,const String & table_name);
void replaceDatabaseAndTable(const StorageID & table_id);
void addTableFunction(ASTPtr & table_function_ptr);
void updateTreeHashImpl(SipHash & hash_state)const override;
void setFinal();
const char * getQueryKindString()const override {return "Select"; }
protected:
void formatImpl(const FormatSettings & settings, FormatState & state, FormatStateStacked frame)const override;
private:
std::unordered_mappositions;
ASTPtr & getExpression(Expression expr);
};
}
參考資料
https://cloud.tencent.com/developer/article/1604965
https://zhuanlan.zhihu.com/p/361622782
https://clickhouse.com/docs/en/