第一種情況:無ttl_only_drop_parts配置
總體示例以及說明
如果沒有ttl_only_drop_parts的配置,過期數據的刪除(這里是刪除,是將過期的數據從這個part刪除,并將過期的數據構成一個part,這個過期的part標記為inactive,沒過期的變為新part)會在TTL Merge任務中進行。
示例如下,ttl_only_drop_parts = 0:
CREATE TABLE my_table1
(
? ? `event_time` DateTime,
? ? `id` UInt64,
? ? `message` String
)
ENGINE = MergeTree
PARTITION BY toStartOfHour(event_time)
ORDER BY (event_time, id)
TTL event_time + toIntervalMinute(3)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 0
?插入數據:
INSERT INTO my_table1
SELECT
? ? now() - INTERVAL intDiv(number, 1000) SECOND AS event_time, ?-- 控制時間分布在最近3分鐘
? ? number AS id,
? ? concat('message_', toString(number)) AS message
FROM numbers(1000000);
這條 SQL 語句的作用是:向 my_table1
表插入 1,000,000 條數據,這些數據的 event_time
字段分布在最近 3 分鐘內。
此時100W的數據中,會有兩個分區生成:2025-06-30 12:00:00,2025-06-30 13:00:00。
過了一段時間后,由于2025-06-30 12:00:00中已經有部分數據達到過期時間,所以會生成一個1751284800_2_2_1的part,它的active字段為1。
而之前的那個老分區直接設置active字段為0即可,表示為不活躍。
用圖解可以表示為:
過一段時間之后:
此時可以看到1751284800_2_2_0中的部分數據也達到了過期時間,之后生成一個名字為1751284800_2_2_1的分區,剩余數據為168000行,相當于過期了1000行。
此部分可以用圖解表示為:
再過一段時間可以看到:
此時cleanup后臺線程進行了清理后,剩下了最后兩個part:?
這里大家可能會有疑問?為什么最后兩個part遲遲沒有刪除呢,它們明明已經到達了最大過期時間了啊?
這個結論放在 <分區到達過期時間卻未刪除> 這一小節說明。
梳理part的轉變
我們也可以從part_log中梳理關于這個表TTL merge:
1??最初插入的數據,ClickHouse 生成了以下兩個原始 part:
Time | Event | Part Name | Rows | 分區 |
---|---|---|---|---|
13:02:48 | NewPart | 1751284800_2_2_0 | 831000 | 2025-06-30 13:00:00 |
13:02:48 | NewPart | 1751288400_1_1_0 | 169000 | 2025-06-30 12:00:00 |
這兩個 part 加起來就是插入的 1,000,000 行 ?
2?? ClickHouse 執行 TTL 刪除合并1:
Time | Event | Part Name | Rows | 類型 |
---|---|---|---|---|
13:02:48 | MergePartsStart | 1751284800_2_2_1 | 0 | 開始 TTL 合并 |
13:02:48 | MergeParts | 1751284800_2_2_1 | 11000 | TTL 合并后剩下的數據 |
3?? ClickHouse 執行 TTL 刪除合并2:
Time | Event | Part Name | Rows | 類型 |
---|---|---|---|---|
13:03:00 | MergePartsStart | 1751288400_1_1_1 | 0 | 開始 TTL 合并 |
13:03:00 | MergeParts | 1751288400_1_1_1 | 168000 | TTL 合并后剩下的數據 |
4??清理不活躍的分區
Time | Event | Part Name | Rows | 說明 |
---|---|---|---|---|
13:12:12 | RemovePart | 1751284800_2_2_0 | 831000 | 被后臺clean線程刪掉 |
13:12:12 | RemovePart | 1751288400_1_1_0 | 169000 | 被后臺clean線程刪掉 |
分區到達過期時間卻未刪除
在上面,我們看到了分區達到過期時間,但是卻未刪除的場景,這是為什么呢?
此時os time:
$ date
Mon Jun 30 01:41:14 PM UTC 2025
這與一個參數有關系:merge_with_ttl_timeout
官網鏈接如下:Manage Data with TTL (Time-to-live) | ClickHouse Docs
對于后臺merge線程選擇一個TTL Merge任務(也就是類型為TTLDelete的merge任務)后,它會推遲這個分區向后merge_with_ttl_timeout 毫秒,才能再次選擇這個分區。
例如依據上面的示例來說,假如2025-06-30 13:00:00在13:03:00執行,那么下次能選擇到分區為2025-06-30 13:00:00的part進行merge,至少在13:03:00 + 4h =?15:03:00 才能發生。
4h為merge_with_ttl_timeout的默認值(轉化為小時)。
關鍵代碼位置,更新分區的選擇時機:
void MergeTreeDataMergerMutator::updateTTLMergeTimes(const MergeSelectorChoice & merge_choice, const MergeTreeSettingsPtr & settings, time_t current_time)
{chassert(!merge_choice.range.empty());const String & partition_id = merge_choice.range.front().info.getPartitionId();switch (merge_choice.merge_type){case MergeType::Regular:/// Do not update anything for regular merge.return;case MergeType::TTLDelete:next_delete_ttl_merge_times_by_partition[partition_id] = current_time + (*settings)[MergeTreeSetting::merge_with_ttl_timeout];return;case MergeType::TTLRecompress:next_recompress_ttl_merge_times_by_partition[partition_id] = current_time + (*settings)[MergeTreeSetting::merge_with_recompression_ttl_timeout];return;}
}
選擇part去merge的調用棧:
chooseMergeFrom()? ? ? ? ->
????????MergeSelectorApplier::chooseMergeFrom()? ? ? ? ->
? ? ? ? ? ? ? ? if 如果有TTL...? then?tryChooseTTLMerge()? ? ? ? ->
????????????????????????ITTLMergeSelector::select()? ? ? ?->
????????????????????????????????ITTLMergeSelector::findCenter()
判斷位置,是否推遲的邏輯關鍵函數為needToPostponePartition:
std::optional<ITTLMergeSelector::CenterPosition> ITTLMergeSelector::findCenter(const PartsRanges & parts_ranges) const
{
? ? assert(!parts_ranges.empty());
? ? std::optional<CenterPosition> position = std::nullopt;
? ? for (auto range = parts_ranges.begin(); range != parts_ranges.end(); ++range)
? ? {
? ? ? ? assert(!range->empty());
? ? ? ? const auto & range_partition = range->front().info.getPartitionId();
? ? ? ? if (needToPostponePartition(range_partition))
? ? ? ? ? ? continue;
? ? ? ? for (auto part = range->begin(); part != range->end(); ++part)
? ? ? ? {
? ? ? ? ? ? if (!canConsiderPart(*part))
? ? ? ? ? ? ? ? continue;
? ? ? ? ? ? time_t ttl = getTTLForPart(*part);
? ? ? ? ? ? if (!ttl || ttl > current_time)
? ? ? ? ? ? ? ? continue;
? ? ? ? ? ? if (!position || ttl < getTTLForPart(*position->center))
? ? ? ? ? ? ? ? position.emplace(range, part);
? ? ? ? }
? ? }
? ? return position;
}
needToPostponePartition內部邏輯為:
bool ITTLMergeSelector::needToPostponePartition(const std::string & partition_id) const
{
? ? if (auto it = merge_due_times.find(partition_id); it != merge_due_times.end())
? ? ? ? return it->second > current_time;
? ?
? ? return false;
}
而這個merge_due_times就是每次merge完之后的調整的next_delete_ttl_merge_times_by_partition。
最后附上TTL執行的部分調用棧:
TODO:調小merge_with_ttl_timeout,驗證。
第二種情況: 有ttl_only_drop_parts配置
如果配置了這個參數,ck并不會在某個part中的部分數據達到過期時間時,進行過期數據的刪除。而是整個part的數據都達到過期時間時才會進行刪除(這里的刪除并不是物理刪除,而是邏輯上的刪除,即將這個part標記為inactive,對應system.parts關于這個part的記錄的active字段為0)。
之后MergeTree / ReplicatedMergeTree 的cleanup線程會進行刪除(這是物理刪除,即刪除磁盤中的文件)的動作。
由此可見,不管配沒配置這個參數,ck在TTL Merge任務的時候并不會做真正刪除過期數據的操作,只是邏輯刪除,真正的刪除交給后臺cleanup線程。
核心代碼位置:
執行階段:
直接跳過:
具體的執行邏輯可以看日志:
部分日志是我手動添加日志的,例如:merge_type = XXX. 這部分。
TODO: 設置system.parts的active字段為0的位置。