雲數據庫ClickHouse資源隔離-彈性資源隊列

引言

ClickHouse內核分析系列文章,本文將爲你們深度解讀ClickHouse當前的MPP計算模型、用戶資源隔離、查詢限流機制,在此基礎上爲你們介紹阿里巴巴雲數據庫ClickHouse在八月份即將推出的自研彈性資源隊列功能。ClickHouse開源版本當前尚未資源隊列相關的規劃,自研彈性資源隊列的初衷是更好地解決隔離和資源利用率的問題。下文將從ClickHouse的MPP計算模型、現有的資源隔離方案展開來看ClickHouse當前在資源隔離上的痛點,最後爲你們介紹咱們的自研彈性資源隊列功能。node

MPP計算模型

在深刻到資源隔離以前,這裏有必要簡單介紹一下ClickHouse社區純自研的MPP計算模型,由於ClickHouse的MPP計算模型和成熟的開源MPP計算引擎(例如:Presto、HAWQ、Impala)存在着較大的差別(que xian),這使得ClickHouse的資源隔離也有一些獨特的要求,同時但願這部份內容能指導用戶更好地對ClickHouse查詢進行調優。linux

ClickHouse的MPP計算模型最大的特色是:它壓根沒有分佈式執行計劃,只能經過遞歸子查詢和廣播表來解決多表關聯查詢,這給分佈式多表關聯查詢帶來的問題是數據shuffle爆炸。另外ClickHouse的執行計劃生成過程當中,僅有一些簡單的filter push down,column prune規則,徹底沒有join reorder能力。對用戶來講就是"所寫即所得"的模式,要求人人都是DBA,下面將結合簡單的查詢例子來介紹一下ClickHouse計算模型最大的幾個原則。c++

遞歸子查詢

在閱讀源碼的過程當中,我能夠感覺到ClickHouse前期是一個徹底受母公司Yandex搜索分析業務驅動成長起來的數據庫。而搜索業務場景下的Metric分析(uv / pv ...),對分佈式多表關聯分析的並無很高的需求,絕大部分業務場景均可以經過簡單的數據分表分析而後聚合結果(數據建模比較簡單),因此從一開始ClickHouse就註定不擅長處理複雜的分佈式多表關聯查詢,ClickHouse的內核只是把單機(單表)分析作到了性能極致。可是任何一個業務場景下都不能徹底避免分佈式關聯分析的需求,ClickHouse採用了一套簡單的Rule來處理多表關聯分析的查詢。sql

對ClickHouse有所瞭解的同窗應該知道ClickHouse採用的是簡單的節點對等架構,同時不提供任何分佈式的語義保證,ClickHouse的節點中存在着兩種類型的表:本地表(真實存放數據的表引擎),分佈式表(代理了多個節點上的本地表,至關於"分庫分表"的Proxy)。當ClickHouse的節點收到兩表的Join關聯分析時,問題比較收斂,無非是如下幾種狀況:本地表 Join 分佈式表 、本地表 Join 本地表、 分佈式表 Join 分佈式表、分佈式表 Join 本地表,這四種狀況會如何執行這裏先放一下,等下一小節再介紹。數據庫

接下來問題複雜化,如何解決多個Join的關聯查詢?ClickHouse採用遞歸子查詢來解決這個問題,以下面的簡單例子所示ClickHouse會自動把多個Join的關聯查詢改寫成子查詢進行嵌套, 規則很是簡單:1)Join的左右表必須是本地表、分佈式表或者子查詢;2)傾向把Join的左側變成子查詢;3)從最後一個Join開始遞歸改寫成子查詢;4)對Join order不作任何改動;5)能夠自動根據where條件改寫Cross Join到Inner Join。下面是兩個具體的例子幫助你們理解:網絡

例1架構

select from local_tabA join (select from dist_tabB join local_tabC on dist_tabB.key2 = local_tabC.key2) as sub_Q1 on local_tabA.key1 = sub_Q1.key1 join dist_tabD on local_tabA.key1 = dist_tabD.key1; =============> select from (select from local_tabA join (select * from dist_tabB join local_tabC on dist_tabB.key2 = local_tabC.key2) as sub_Q1 on local_tabA.key1 = sub_Q1.key1) as sub_Q2 join dist_tabD on sub_Q2.key1 = dist_tabD.key1;併發

例2異步

select from local_tabA join (select from dist_tabB join local_tabC on dist_tabB.key2 = local_tabC.key2) as sub_Q1 on local_tabA.key1 = sub_Q1.key1 join dist_tabD on local_tabA.key1 = dist_tabD.key1; =============> select from (select from local_tabA join (select * from dist_tabB join local_tabC on dist_tabB.key2 = local_tabC.key2) as sub_Q1 on local_tabA.key1 = sub_Q1.key1) as sub_Q2 join dist_tabD on sub_Q2.key1 = dist_tabD.key1;async

Join關聯中的子查詢在計算引擎裏就相關因而一個本地的"臨時表",只不過這個臨時表的Input Stream對接的是一個子查詢的Output Stream。因此在處理多個Join的關聯查詢時,ClickHouse會把查詢拆成遞歸的子查詢,每一次遞歸只處理一個Join關聯,單個Join關聯中,左右表輸入有多是本地表、分佈式表、子查詢,這樣問題就簡化了。

這種簡單的遞歸子查詢解決方案純在最致命的缺陷是:

(1)系統沒有自動優化能力,Join reorder是優化器的重要課題,可是ClickHouse徹底不提供這個能力,對內核不夠了解的用戶基本沒法寫出性能最佳的關聯查詢,可是對經驗老道的工程師來講這是另外一種體驗:能夠徹底掌控SQL的執行計劃。

(2)沒法徹底發揮分佈式計算的能力,ClickHouse在兩表的Join關聯中可否利用分佈式算力進行join計算取決於左表是不是分佈式表,只有當左表是分佈式表時纔有可能利用上Cluster的計算能力,也就是左表是本地表或者子查詢時Join計算過程只在一個節點進行。

(3)多個大表的Join關聯容易引發節點的OOM,ClickHouse中的Hash Join算子目前不支持spill(落盤),遞歸子查詢須要節點在內存中同時維護多個完整的Hash Table來完成最後的Join關聯。

兩表Join規則

上一節介紹了ClickHouse如何利用遞歸子查詢來解決多個Join的關聯分析,最終系統只會focus在單個Join的關聯分析上。除了常規的Join方式修飾詞之外,ClickHouse還引入了另一個Join流程修飾詞"Global",它會影響整個Join的執行計劃。節點真正採用Global Join進行關聯的前提條件是左表必須是分佈式表,Global Join會構建一個內存臨時表來保存Join右測的數據,而後把左表的Join計算任務分發給全部代理的存儲節點,收到Join計算任務的存儲節點會跨節點拷貝內存臨時表的數據,用以構建Hash Table。

下面依次介紹全部可能出現的單個Join關聯分析場景:

(1)(本地表/子查詢)Join(本地表/子查詢):常規本地Join,Global Join不生效

(2)(本地表/子查詢)Join(分佈式表):分佈式表數據所有讀到當前節點進行Hash Table構建,Global Join不生效

(3)(分佈式表)Join(本地表/子查詢):Join計算任務分發到分佈式表的全部存儲節點上,存儲節點上收到的Join右表取決因而否採用Global Join策略,若是不是Global Join則把右測的(本地表名/子查詢)直接轉給全部存儲節點。若是是Global Join則當前節點會構建Join右測數據的內存表,收到Join計算任務的節點會來拉取這個內存表數據。

(4)(分佈式表)Join(分佈式表):Join計算任務分發到分佈式表的全部存儲節點上,存儲節點上收到的Join右表取決因而否採用Global Join策略,若是不是Global Join則把右測的分佈式表名直接轉給全部存儲節點。若是是Global Join則當前節點會把右測分佈式表的數據所有收集起來構建內存表,收到Join計算任務的節點會來拉取這個內存表數據。

從上面能夠看出只有分佈式表的Join關聯是能夠進行分佈式計算的,Global Join能夠提早計算Join右測的結果數據構建內存表,當Join右測是帶過濾條件的分佈式表或者子查詢時,下降了Join右測數據重複計算的次數,還有一種場景是Join右表只在當前節點存在則此時必須使用Global Join把它替換成內存臨時表,由於直接把右表名轉給其餘節點必定會報錯。

ClickHouse中還有一個開關和Join關聯分析的行爲有關:distributed_product_mode,它只是一個簡單的查詢改寫Rule用來改寫兩個分佈式表的Join行爲。當set distributed_product_mode = 'LOCAL'時,它會把右表改寫成代理的存儲表名,這要求左右表的數據分區對齊,不然Join結果就出錯了,當set distributed_product_mode = 'GLOBAL'時,它會把自動改寫Join到Global Join。可是這個改寫Rule只針對左右表都是分佈式表的case,複雜的多表關聯分析場景下對SQL的優化做用比較小,仍是不要去依賴這個自動改寫的能力。

ClickHouse的分佈式Join關聯分析中還有另一個特色是它並不會對左表的數據進行re-sharding,每個收到Join任務的節點都會要全量的右表數據來構建Hash Table。在一些場景下,若是用戶肯定Join左右表的數據是都是按照某個Join key分區的,則可使用(分佈式表)Join(本地表)的方式來緩解一下這個問題。可是ClickHouse的分佈式表Sharding設計並不保證Cluster在調整節點後數據能徹底分區對齊,這是用戶須要注意的。

小結

總結一下上面兩節的分析,ClickHouse當前的MPP計算模型並不擅長作多表關聯分析,主要存在的問題:1)節點間數據shuffle膨脹,Join關聯時沒有數據re-sharding能力,每一個計算節點都須要shuffle全量右表數據;2)Join內存膨脹,緣由同上;3)非Global Join下可能引發計算風暴,計算節點重複執行子查詢;4)沒有Join reorder優化。其中的1和3還會隨着節點數量增加變得更加明顯。在多表關聯分析的場景下,用戶應該儘量爲小表構建Dictionary,並使用dictGet內置函數來代替Join,針對沒法避免的多表關聯分析應該直接寫成嵌套子查詢的方式,並根據真實的查詢執行狀況嘗試調整Join order尋找最優的執行計劃。當前ClickHouse的MPP計算模型下,仍然存在很多查詢優化的小"bug"可能致使性能不如預期,例如列裁剪沒有下推,過濾條件沒有下推,partial agg沒有下推等等,不過這些小問題都是能夠修復。

資源隔離現狀

當前的ClickHouse開源版本在系統的資源管理方面已經作了不少的feature,我把它們總結爲三個方面:全鏈路(線程-》查詢-》用戶)的資源使用追蹤、查詢&用戶級別資源隔離、資源使用限流。對於ClickHouse的資深DBA來講,這些資源追蹤、隔離、限流功能已經能夠解決很是多的問題。接下來我將展開介紹一下ClickHouse在這三個方面的功能設計實現。

trace & profile

ClickHouse的資源使用都是從查詢thread級別就開始進行追蹤,主要的相關代碼在 ThreadStatus 類中。每一個查詢線程都會有一個thread local的ThreadStatus對象,ThreadStatus對象中包含了對內存使用追蹤的 MemoryTracker、profile cpu time的埋點對象 ProfileEvents、以及監控thread 熱點線程棧的 QueryProfiler。

1.MemoryTracker

ClickHouse中有不少不一樣level的MemoryTracker,包括線程級別、查詢級別、用戶級別、server級別,這些MemoryTracker會經過parent指針組織成一個樹形結構,把內存申請釋放信息層層反饋上去。

MemoryTrack中還有額外的峯值信息(peak)統計,內存上限檢查,一旦某個查詢線程的申請內存請求在上層(查詢級別、用戶級別、server級別)MemoryTracker遇到超過限制錯誤,查詢線程就會拋出OOM異常致使查詢退出。同時查詢線程的MemoryTracker每申請必定量的內存都會統計出當前的工做棧,很是方便排查內存OOM的緣由。

ClickHouse的MPP計算引擎中每一個查詢的主線程都會有一個ThreadGroup對象,每一個MPP引擎worker線程在啓動時必需要attach到ThreadGroup上,在線程退出時detach,這保證了整個資源追蹤鏈路的完整傳遞。最後一個問題是如何把CurrentThread::MemoryTracker hook到系統的內存申請釋放上去?ClickHouse首先是重載了c++的new_delete operator,其次針對須要使用malloc的一些場景封裝了特殊的Allocator同步內存申請釋放。爲了解決內存追蹤的性能問題,每一個線程的內存申請釋放會在thread local變量上進行積攢,最後以大塊內存的形式同步給MemoryTracker。

class MemoryTracker { std::atomic<Int64> amount {0}; std::atomic<Int64> peak {0}; std::atomic<Int64> hard_limit {0}; std::atomic<Int64> profiler_limit {0}; Int64 profiler_step = 0; /// Singly-linked list. All information will be passed to subsequent memory trackers also (it allows to implement trackers hierarchy). /// In terms of tree nodes it is the list of parents. Lifetime of these trackers should "include" lifetime of current tracker. std::atomic<MemoryTracker *> parent {}; /// You could specify custom metric to track memory usage. CurrentMetrics::Metric metric = CurrentMetrics::end(); ... }

2.ProfileEvents:

ProfileEvents顧名思義,是監控系統的profile信息,覆蓋的信息很是廣,全部信息都是經過代碼埋點進行收集統計。它的追蹤鏈路和MemoryTracker同樣,也是經過樹狀結構組織層層追蹤。其中和cpu time相關的核心指標包括如下:

///Total (wall clock) time spent in processing thread. RealTimeMicroseconds; ///Total time spent in processing thread executing CPU instructions in user space. ///This include time CPU pipeline was stalled due to cache misses, branch mispredictions, hyper-threading, etc. UserTimeMicroseconds; ///Total time spent in processing thread executing CPU instructions in OS kernel space. ///This include time CPU pipeline was stalled due to cache misses, branch mispredictions, hyper-threading, etc. SystemTimeMicroseconds; SoftPageFaults; HardPageFaults; ///Total time a thread spent waiting for a result of IO operation, from the OS point of view. ///This is real IO that doesn't include page cache. OSIOWaitMicroseconds; ///Total time a thread was ready for execution but waiting to be scheduled by OS, from the OS point of view. OSCPUWaitMicroseconds; ///CPU time spent seen by OS. Does not include involuntary waits due to virtualization. OSCPUVirtualTimeMicroseconds; ///Number of bytes read from disks or block devices. ///Doesn't include bytes read from page cache. May include excessive data due to block size, readahead, etc. OSReadBytes; ///Number of bytes written to disks or block devices. ///Doesn't include bytes that are in page cache dirty pages. May not include data that was written by OS asynchronously OSWriteBytes; ///Number of bytes read from filesystem, including page cache OSReadChars; ///Number of bytes written to filesystem, including page cache OSWriteChars;

以上這些信息都是從linux系統中直接採集,參考 sys/resource.h 和 linux/taskstats.h。採集沒有固定的頻率,系統在查詢計算的過程當中每處理完一個Block的數據就會依據距離上次採集的時間間隔決定是否採集最新數據。

3.QueryProfiler:

QueryProfiler的核心功能是抓取查詢線程的熱點棧,ClickHouse經過對線程設置timer_create和自定義的signal_handler讓worker線程定時收到SIGUSR信號量記錄本身當前所處的棧,這種方法是能夠抓到全部被lock block或者sleep的線程棧的。

除了以上三種線程級別的trace&profile機制,ClickHouse還有一套server級別的Metrics統計,也是經過代碼埋點記錄系統中全部Metrics的瞬時值。ClickHouse底層的這套trace&profile手段保障了用戶能夠很方便地從系統硬件層面去定位查詢的性能瓶頸點或者OOM緣由,全部的metrics, trace, profile信息都有對象的system_log系統表能夠追溯歷史。

資源隔離

資源隔離須要關注的點包括內存、CPU、IO,目前ClickHouse在這三個方面都作了不一樣程度功能:

1.內存隔離

當前用戶能夠經過max_memory_usage(查詢內存限制),max_memory_usage_for_user(用戶的內存限制),max_memory_usage_for_all_queries(server的內存限制),max_concurrent_queries_for_user(用戶併發限制),max_concurrent_queries(server併發限制)這一套參數去規劃系統的內存資源使用作到用戶級別的隔離。可是當用戶進行多表關聯分析時,系統派發的子查詢會突破用戶的資源規劃,全部的子查詢都屬於default用戶,可能引發用戶查詢的內存超用。

2.CPU隔離

ClickHouse提供了Query級別的CPU優先級設置,固然也能夠爲不一樣用戶的查詢設置不一樣的優先級,有如下兩種優先級參數:

///Priority of the query. ///1 - higher value - lower priority; 0 - do not use priorities. ///Allows to freeze query execution if at least one query of higher priority is executed. priority; ///If non zero - set corresponding 'nice' value for query processing threads. ///Can be used to adjust query priority for OS scheduler. os_thread_priority;

3.IO隔離

ClickHouse目前在IO上沒有作任何隔離限制,可是針對異步merge和查詢都作了各自的IO限制,儘可能避免IO打滿。隨着異步merge task數量增多,系統會開始限制後續單個merge task涉及到的Data Parts的disk size。在查詢並行讀取MergeTree data的時候,系統也會統計每一個線程當前的IO吞吐,若是吞吐不達標則會反壓讀取線程,下降讀取線程數緩解系統的IO壓力,以上這些限制措施都是從局部來緩解問題的一個手段。

Quota限流

除了靜態的資源隔離限制,ClickHouse內部還有一套時序資源使用限流機制--Quota。用戶能夠根據查詢的用戶或者Client IP對查詢進行分組限流。限流和資源隔離不一樣,它是約束查詢執行的"速率",當前主要包括如下幾種"速率":

QUERIES; /// Number of queries. ERRORS; /// Number of queries with exceptions. RESULT_ROWS; /// Number of rows returned as result. RESULT_BYTES; /// Number of bytes returned as result. READ_ROWS; /// Number of rows read from tables. READ_BYTES; /// Number of bytes read from tables. EXECUTION_TIME; /// Total amount of query execution time in nanoseconds.

用戶能夠自定義規劃本身的限流策略,防止系統的負載(IO、網絡、CPU)被打爆,Quota限流能夠認爲是系統自我保護的手段。系統會根據查詢的用戶名、IP地址或者Quota Key Hint來爲查詢綁定對應的限流策略。計算引擎在算子之間傳遞Block時會檢查當前Quota組內的流速是否過載,進而經過sleep查詢線程來下降系統負載。

小結

總結一下ClickHouse在資源隔離/trace層面的優缺點:ClickHouse爲用戶提供了很是多的工具組件,可是欠缺總體性的解決方案。以trace & profile爲例,ClickHouse在自身系統裏集成了很是完善的trace / profile / metrics日誌和瞬時狀態系統表,在排查性能問題的過程當中它的鏈路是完備的。但問題是這個鏈路太複雜了,對通常用戶來講排查很是困難,尤爲是碰上遞歸子查詢的多表關聯分析時,須要從用戶查詢到一層子查詢到二層子查詢步步深刻分析。當前的資源隔離方案呈現給用戶的更加是一堆配置,根本不是一個完整的功能。Quota限流雖然是一個完整的功能,可是卻不容易使用,由於用戶不知道如何量化合理的"速率"。

彈性資源隊列

第一章爲你們介紹了ClickHouse的MPP計算模型,核心想闡述的點是ClickHouse這種簡單的遞歸子查詢計算模型在資源利用上是很是粗暴的,若是沒有很好的資源隔離和系統過載保護,節點很容易就會由於bad sql變得不穩定。第二章介紹ClickHouse當前的資源使用trace profile功能、資源隔離功能、Quota過載保護。可是ClickHouse目前在這三個方面作得都不夠完美,還須要深度打磨來提高系統的穩定性和資源利用率。我認爲主要從三個方面進行增強:性能診斷鏈路自動化使用戶能夠一鍵診斷,資源隊列功能增強,Quota(負載限流)作成自動化並拉通來看查詢、寫入、異步merge任務對系統的負載,防止過載。

阿里雲數據庫ClickHouse在ClickHouse開源版本上即將推出用戶自定義的彈性資源隊列功能,資源隊列DDL定義以下:

CREATE RESOURCE QUEUE [IF NOT EXISTS | OR REPLACE] test_queue [ON CLUSTER cluster] memory=10240000000, ///資源隊列的總內存限制 concurrency=8, ///資源隊列的查詢併發控制 isolate=0, ///資源隊列的內存搶佔隔離級別 priority=high ///資源隊列的cpu優先級和內存搶佔優先級 TO {role [,...] | ALL | ALL EXCEPT role [,...]};

我認爲資源隊列的核心問題是要在保障用戶查詢穩定性的基礎上最大化系統的資源利用率和查詢吞吐。傳統的MPP數據庫相似GreenPlum的資源隊列設計思想是隊列之間的內存資源徹底隔離,經過優化器去評估每個查詢的複雜度加上隊列的默人併發度來決定查詢在隊列中可佔用的內存大小,在查詢真實開始執行以前已經限定了它可以使用的內存,加上GreenPlum強大的計算引擎全部算子均可以落盤,使得資源隊列能夠保障系統內的查詢穩定運行,可是吞吐並不必定是最大化的。由於GreenPlum資源隊列之間的內存不是彈性的,有隊列空閒下來它的內存資源也不能給其餘隊列使用。拋開資源隊列間的彈性問題,其要想作到單個資源隊列內的查詢穩定高效運行離不開Greenplum的兩個核心能力:CBO優化器智能評估出查詢須要佔用的內存,全算子可落盤的計算引擎。

ClickHouse目前的現狀是:1)沒有優化器幫助評估查詢的複雜度,2)整個計算引擎的落盤能力比較弱,在限定內存的狀況下沒法保障query順利執行。所以咱們結合ClickHouse計算引擎的特點,設計了一套彈性資源隊列模型,其中核心的彈性內存搶佔原則包括如下幾個:

  1. 對資源隊列內的查詢不設內存限制
  2. 隊列中的查詢在申請內存時若是遇到內存不足,則嘗試從優先級更低的隊列中搶佔式申請內存
  3. 在2)中內存搶佔過程當中,若是搶佔申請失敗,則檢查本身所屬的資源隊列是否被其餘查詢搶佔內存,嘗試kill搶佔內存最多的查詢回收內存資源
  4. 若是在3)中嘗試回收被搶佔內存資源失敗,則當前查詢報OOM Exception
  5. 每一個資源隊列預留必定比例的內存不可搶佔,當資源隊列中的查詢負載到達必定水位時,內存就變成徹底不可被搶佔。同時用戶在定義資源隊列時,isolate=0的隊列是容許被搶佔的,isolate=1的隊列不容許被搶佔,isolate=2的隊列不容許被搶佔也不容許搶佔其餘隊列
  6. 當資源隊列中有查詢OOM失敗,或者由於搶佔內存被kill,則把當前資源隊列的併發數臨時下調,等系統恢復後再逐步上調。

ClickHouse彈性資源隊列的設計原則就是容許內存資源搶佔來達到資源利用率的最大化,同時動態調整資源隊列的併發限制來防止bad query出現時致使用戶的查詢大面積失敗。因爲計算引擎的約束限制,目前沒法保障查詢徹底沒有OOM,可是用戶端能夠經過錯誤信息來判斷查詢是否屬於bad sql,同時對誤殺的查詢進行retry。

原文連接 本文爲阿里雲原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索