一、ClickHouse的核心特性
1. 完備的DBMS功能
ClickHouse擁有完備的管理功能,所以它稱得上是一個DBMS ( Database Management System,數據庫管理系統 ),而不僅是一個數據庫。作為一個DBMS,它具備了一些基本功能,如下所示。
DDL ( 數據定義語言 ):可以動態地創建、修改或刪除數據庫、表和視圖,而無須重啟服務。
DML ( 數據操作語言 ):可以動態查詢、插入、修改或刪除數據。
權限控制:可以按照用戶粒度設置數據庫或者表的操作權限,保障數據的安全性。
數據備份與恢復:提供了數據備份導出與導入恢復機制,滿足生產環境的要求。
分布式管理:提供集群模式,能夠自動管理多個數據庫節點。
這里只列舉了一些最具代表性的功能,但已然足以表明為什么Click House稱得上是DBMS了。
2. 列式存儲與數據壓縮
列式存儲和數據壓縮,對於一款高性能數據庫來說是必不可少的特性。一個非常流行的觀點認為,如果你想讓查詢變得更快,最簡單且有效的方法是減少數據掃描范圍和數據傳輸時的大小,而列式存儲和數據壓縮就可以幫助我們實現上述兩點。列式存儲和數據壓縮通常是伴生的,因為一般來說列式存儲是數據壓縮的前提。
按列存儲與按行存儲相比,前者可以有效減少查詢時所需掃描的數據量,這一點可以用一個示例簡單說明。假設一張數據表A擁有50個字段A1~A50,以及100行數據。現在需要查詢前5個字段並進行數據分析,則可以用如下SQL實現:
SELECT A1,A2,A3,A4,A5 FROM A
如果數據按行存儲,數據庫首先會逐行掃描,並獲取每行數據的所有50個字段,再從每一行數據中返回A1~A5這5個字段。不難發現,盡管只需要前面的5個字段,但由於數據是按行進行組織的,實際上還是掃描了所有的字段。如果數據按列存儲,就不會發生這樣的問題。由於數據按列組織,數據庫可以直接獲取A1~A5這5列的數據,從而避免了多余的數據掃描。
按列存儲相比按行存儲的另一個優勢是對數據壓縮的友好性。同樣可以用一個示例簡單說明壓縮的本質是什么。假設有兩個字符串abcdefghi和bcdefghi,現在對它們進行壓縮,如下所示:
壓縮前:abcdefghi_bcdefghi壓縮后:abcdefghi_(9,8)
可以看到,壓縮的本質是按照一定步長對數據進行匹配掃描,當發現重復部分的時候就進行編碼轉換。例如上述示例中的 (9,8),表示如果從下划線開始向前移動9個字節,會匹配到8個字節長度的重復項,即這里的bcdefghi。
真實的壓縮算法自然比這個示例更為復雜,但壓縮的實質就是如此。數據中的重復項越多,則壓縮率越高;壓縮率越高,則數據體量越小;而數據體量越小,則數據在網絡中的傳輸越快,對網絡帶寬和磁盤IO的壓力也就越小。既然如此,那怎樣的數據最可能具備重復的特性呢?答案是屬於同一個列字段的數據,因為它們擁有相同的數據類型和現實語義,重復項的可能性自然就更高。
ClickHouse就是一款使用列式存儲的數據庫,數據按列進行組織,屬於同一列的數據會被保存在一起,列與列之間也會由不同的文件分別保存 ( 這里主要指MergeTree表引擎 )。數據默認使用LZ4算法壓縮,在Yandex.Metrica的生產環境中,數據總體的壓縮比可以達到8:1 ( 未壓縮前17PB,壓縮后2PB )。列式存儲除了降低IO和存儲的壓力之外,還為向量化執行做好了鋪墊。
3. 向量化執行引擎
坊間有句玩笑,即'能用錢解決的問題,千萬別花時間'。而業界也有種調侃如出一轍,即'能升級硬件解決的問題,千萬別優化程序'。有時候,你千辛萬苦優化程序邏輯帶來的性能提升,還不如直接升級硬件來得簡單直接。這雖然只是一句玩笑不能當真,但硬件層面的優化確實是最直接、最高效的提升途徑之一。向量化執行就是這種方式的典型代表,這項寄存器硬件層面的特性,為上層應用程序的性能帶來了指數級的提升。
向量化執行,可以簡單地看作一項消除程序中循環的優化。這里用一個形象的例子比喻。小胡經營了一家果汁店,雖然店里的鮮榨蘋果汁深受大家喜愛,但客戶總是抱怨制作果汁的速度太慢。小胡的店里只有一台榨汁機,每次他都會從籃子里拿出一個蘋果,放到榨汁機內等待出汁。如果有8個客戶,每個客戶都點了一杯蘋果汁,那么小胡需要重復循環8次上述的榨汁流程,才能榨出8杯蘋果汁。如果制作一杯果汁需要5分鍾,那么全部制作完畢則需要40分鍾。為了提升果汁的制作速度,小胡想出了一個辦法。他將榨汁機的數量從1台增加到了8台,這么一來,他就可以從籃子里一次性拿出8個蘋果,分別放入8台榨汁機同時榨汁。此時,小胡只需要5分鍾就能夠制作出8杯蘋果汁。為了制作n杯果汁,非向量化執行的方式是用1台榨汁機重復循環制作n次,而向量化執行的方式是用n台榨汁機只執行1次。
為了實現向量化執行,需要利用CPU的SIMD指令。SIMD的全稱是Single Instruction Multiple Data,即用單條指令操作多條數據。現代計算機系統概念中,它是通過數據並行以提高性能的一種實現方式 ( 其他的還有指令級並行和線程級並行 ),它的原理是在CPU寄存器層面實現數據的並行操作。
在計算機系統的體系結構中,存儲系統是一種層次結構。典型服務器計算機的存儲層次結構如圖1所示。一個實用的經驗告訴我們,存儲媒介距離CPU越近,則訪問數據的速度越快。
圖1 距離CPU越遠,數據的訪問速度越慢
從上圖中可以看到,從左向右,距離CPU越遠,則數據的訪問速度越慢。從寄存器中訪問數據的速度,是從內存訪問數據速度的300倍,是從磁盤中訪問數據速度的3000萬倍。所以利用CPU向量化執行的特性,對於程序的性能提升意義非凡。
ClickHouse目前利用SSE4.2指令集實現向量化執行。
4. 關系模型與SQL查詢
相比HBase和Redis這類NoSQL數據庫,ClickHouse使用關系模型描述數據並提供了傳統數據庫的概念 ( 數據庫、表、視圖和函數等 )。與此同時,ClickHouse完全使用SQL作為查詢語言 ( 支持GROUP BY、ORDER BY、JOIN、IN等大部分標准SQL ),這使得它平易近人,容易理解和學習。因為關系型數據庫和SQL語言,可以說是軟件領域發展至今應用最為廣泛的技術之一,擁有極高的'群眾基礎'。也正因為ClickHouse提供了標准協議的SQL查詢接口,使得現有的第三方分析可視化系統可以輕松與它集成對接。在SQL解析方面,ClickHouse是大小寫敏感的,這意味着SELECT a 和 SELECT A所代表的語義是不同的。
關系模型相比文檔和鍵值對等其他模型,擁有更好的描述能力,也能夠更加清晰地表述實體間的關系。更重要的是,在OLAP領域,已有的大量數據建模工作都是基於關系模型展開的 ( 星型模型、雪花模型乃至寬表模型 )。ClickHouse使用了關系模型,所以將構建在傳統關系型數據庫或數據倉庫之上的系統遷移到ClickHouse的成本會變得更低,可以直接沿用之前的經驗成果。
5. 多樣化的表引擎
也許因為Yandex.Metrica的最初架構是基於MySQL實現的,所以在ClickHouse的設計中,能夠察覺到一些MySQL的影子,表引擎的設計就是其中之一。與MySQL類似,ClickHouse也將存儲部分進行了抽象,把存儲引擎作為一層獨立的接口。截至本書完稿時,ClickHouse共擁有合並樹、內存、文件、接口和其他6大類20多種表引擎。其中每一種表引擎都有着各自的特點,用戶可以根據實際業務場景的要求,選擇合適的表引擎使用。
通常而言,一個通用系統意味着更廣泛的適用性,能夠適應更多的場景。但通用的另一種解釋是平庸,因為它無法在所有場景內都做到極致。
在軟件的世界中,並不會存在一個能夠適用任何場景的通用系統,為了突出某項特性,勢必會在別處有所取舍。其實世間萬物都遵循着這樣的道理,就像信天翁和蜂鳥,雖然都屬於鳥類,但它們各自的特點卻鑄就了完全不同的體貌特征。信天翁擅長遠距離飛行,環繞地球一周只需要1至2個月的時間。因為它能夠長時間處於滑行狀態,5天才需要扇動一次翅膀,心率能夠保持在每分鍾100至200次之間。而蜂鳥能夠垂直懸停飛行,每秒可以揮動翅膀70~100次,飛行時的心率能夠達到每分鍾1000次。如果用數據庫的場景類比信天翁和蜂鳥的特點,那么信天翁代表的可能是使用普通硬件就能實現高性能的設計思路,數據按粗粒度處理,通過批處理的方式執行;而蜂鳥代表的可能是按細粒度處理數據的設計思路,需要高性能硬件的支持。
將表引擎獨立設計的好處是顯而易見的,通過特定的表引擎支撐特定的場景,十分靈活。對於簡單的場景,可直接使用簡單的引擎降低成本,而復雜的場景也有合適的選擇。
6. 多線程與分布式
ClickHouse幾乎具備現代化高性能數據庫的所有典型特征,對於可以提升性能的手段可謂是一一用盡,對於多線程和分布式這類被廣泛使用的技術,自然更是不在話下。
如果說向量化執行是通過數據級並行的方式提升了性能,那么多線程處理就是通過線程級並行的方式實現了性能的提升。相比基於底層硬件實現的向量化執行SIMD,線程級並行通常由更高層次的軟件層面控制。現代計算機系統早已普及了多處理器架構,所以現今市面上的服務器都具備良好的多核心多線程處理能力。由於SIMD不適合用於帶有較多分支判斷的場景,ClickHouse也大量使用了多線程技術以實現提速,以此和向量化執行形成互補。
如果一個籃子裝不下所有的雞蛋,那么就多用幾個籃子來裝,這就是分布式設計中分而治之的基本思想。同理,如果一台服務器性能吃緊,那么就利用多台服務的資源協同處理。為了實現這一目標,首先需要在數據層面實現數據的分布式。因為在分布式領域,存在一條金科玉律—計算移動比數據移動更加划算。在各服務器之間,通過網絡傳輸數據的成本是高昂的,所以相比移動數據,更為聰明的做法是預先將數據分布到各台服務器,將數據的計算查詢直接下推到數據所在的服務器。ClickHouse在數據存取方面,既支持分區 ( 縱向擴展,利用多線程原理 ),也支持分片 ( 橫向擴展,利用分布式原理 ),可以說是將多線程和分布式的技術應用到了極致。
7. 多主架構(對等架構)
HDFS、Spark、HBase和Elasticsearch這類分布式系統,都采用了Master-Slave主從架構,由一個管控節點作為Leader統籌全局。而ClickHouse則采用Multi-Master多主架構,集群中的每個節點角色對等,客戶端訪問任意一個節點都能得到相同的效果。這種多主的架構有許多優勢,例如對等的角色使系統架構變得更加簡單,不用再區分主控節點、數據節點和計算節點,集群中的所有節點功能相同。所以它天然規避了單點故障的問題,非常適合用於多數據中心、異地多活的場景。
8. 在線查詢
ClickHouse經常會被拿來與其他的分析型數據庫作對比,比如Vertica、SparkSQL、Hive和Elasticsearch等,它與這些數據庫確實存在許多相似之處。例如,它們都可以支撐海量數據的查詢場景,都擁有分布式架構,都支持列存、數據分片、計算下推等特性。這其實也側面說明了ClickHouse在設計上確實吸取了各路奇技淫巧。與其他數據庫相比,ClickHouse也擁有明顯的優勢。例如,Vertica這類商用軟件價格高昂;SparkSQL與Hive這類系統無法保障90%的查詢在1秒內返回,在大數據量下的復雜查詢可能會需要分鍾級的響應時間;而Elasticsearch這類搜索引擎在處理億級數據聚合查詢時則顯得捉襟見肘。
正如ClickHouse的'廣告詞'所言,其他的開源系統太慢,商用的系統太貴,只有Clickouse在成本與性能之間做到了良好平衡,即又快又開源。ClickHouse當之無愧地闡釋了'在線'二字的含義,即便是在復雜查詢的場景下,它也能夠做到極快響應,且無須對數據進行任何預處理加工。
9. 數據分片與分布式查詢
數據分片是將數據進行橫向切分,這是一種在面對海量數據的場景下,解決存儲和查詢瓶頸的有效手段,是一種分治思想的體現。ClickHouse支持分片,而分片則依賴集群。每個集群由1到多個分片組成,而每個分片則對應了ClickHouse的1個服務節點。分片的數量上限取決於節點數量 ( 1個分片只能對應1個服務節點 )。
ClickHouse並不像其他分布式系統那樣,擁有高度自動化的分片功能。ClickHouse提供了本地表 ( Local Table ) 與分布式表 ( Distributed Table ) 的概念。一張本地表等同於一份數據的分片。而分布式表本身不存儲任何數據,它是本地表的訪問代理,其作用類似分庫中間件。借助分布式表,能夠代理訪問多個數據分片,從而實現分布式查詢。
這種設計類似數據庫的分庫和分表,十分靈活。例如在業務系統上線的初期,數據體量並不高,此時數據表並不需要多個分片。所以使用單個節點的本地表 ( 單個數據分片 ) 即可滿足業務需求,待到業務增長、數據量增大的時候,再通過新增數據分片的方式分流數據,並通過分布式表實現分布式查詢。這就好比一輛手動擋賽車,它將所有的選擇權都交到了使用者的手中。
二、ClickHouse為何如此之快
很多用戶心中一直會有這樣的疑問,為什么ClickHouse這么快?前面的介紹對這個問題已經做出了科學合理的解釋。比方說,因為ClickHouse是列式存儲數據庫,所以快;也因為ClickHouse使用了向量化引擎,所以快。這些解釋都站得住腳,但是依然不能消除全部的疑問。因為這些技術並不是秘密,世面上有很多數據庫同樣使用了這些技術,但是依然沒有ClickHouse這么快。所以我想從另外一個角度來探討一番ClickHouse的秘訣到底是什么。
首先向各位讀者拋出一個疑問:在設計軟件架構的時候,做設計的原則應該是自頂向下地去設計,還是應該自下而上地去設計呢?在傳統觀念中,或者說在我的觀念中,自然是自頂向下的設計,通常我們都被教導要做好頂層設計。而ClickHouse的設計則采用了自下而上的方式。ClickHouse的原型系統早在2008年就誕生了,在誕生之初它並沒有宏偉的規划。相反它的目的很單純,就是希望能以最快的速度進行GROUP BY查詢和過濾。他們是如何實踐自下而上設計的呢?
1. 着眼硬件,先想后做
首先從硬件功能層面着手設計,在設計伊始就至少需要想清楚如下幾個問題。
我們將要使用的硬件水平是怎樣的?包括CPU、內存、硬盤、網絡等。
在這樣的硬件上,我們需要達到怎樣的性能?包括延遲、吞吐量等。
我們准備使用怎樣的數據結構?包括String、HashTable、Vector等。
選擇的這些數據結構,在我們的硬件上會如何工作?
如果能想清楚上面這些問題,那么在動手實現功能之前,就已經能夠計算出粗略的性能了。所以,基於將硬件功效最大化的目的,ClickHouse會在內存中進行GROUP BY,並且使用HashTable裝載數據。與此同時,他們非常在意CPU L3級別的緩存,因為一次L3的緩存失效會帶來70~100ns的延遲。這意味着在單核CPU上,它會浪費4000萬次/秒的運算;而在一個32線程的CPU上,則可能會浪費5億次/秒的運算。所以別小看這些細節,一點一滴地將它們累加起來,數據是非常可觀的。正因為注意了這些細節,所以ClickHouse在基准查詢中能做到1.75億次/秒的數據掃描性能。
2. 算法在前,抽象在后
常有人念叨:'有時候,選擇比努力更重要。'確實,路線選錯了再努力也是白搭。在ClickHouse的底層實現中,經常會面對一些重復的場景,例如字符串子串查詢、數組排序、使用HashTable等。如何才能實現性能的最大化呢?算法的選擇是重中之重。以字符串為例,有一本專門講解字符串搜索的書,名為'Handbook of Exact String Matching Algorithms',列舉了35種常見的字符串搜索算法。各位猜一猜ClickHouse使用了其中的哪一種?答案是一種都沒有。這是為什么呢?因為性能不夠快。在字符串搜索方面,針對不同的場景,ClickHouse最終選擇了這些算法:對於常量,使用Volnitsky算法;對於非常量,使用CPU的向量化執行SIMD,暴力優化;正則匹配使用re2和hyperscan算法。性能是算法選擇的首要考量指標。
3. 勇於嘗鮮,不行就換
除了字符串之外,其余的場景也與它類似,ClickHouse會使用最合適、最快的算法。如果世面上出現了號稱性能強大的新算法,ClickHouse團隊會立即將其納入並進行驗證。如果效果不錯,就保留使用;如果性能不盡人意,就將其拋棄。
4. 特定場景,特殊優化
針對同一個場景的不同狀況,選擇使用不同的實現方式,盡可能將性能最大化。關於這一點,其實在前面介紹字符串查詢時,針對不同場景選擇不同算法的思路就有體現了。類似的例子還有很多,例如去重計數uniqCombined函數,會根據數據量的不同選擇不同的算法:當數據量較小的時候,會選擇Array保存;當數據量中等的時候,會選擇HashSet;而當數據量很大的時候,則使用HyperLogLog算法。
對於數據結構比較清晰的場景,會通過代碼生成技術實現循環展開,以減少循環次數。接着就是大家熟知的大殺器—向量化執行了。SIMD被廣泛地應用於文本轉換、數據過濾、數據解壓和JSON轉換等場景。相較於單純地使用CPU,利用寄存器暴力優化也算是一種降維打擊了。
5. 持續測試,持續改進
如果只是單純地在上述細節上下功夫,還不足以構建出如此強大的ClickHouse,還需要擁有一個能夠持續驗證、持續改進的機制。由於Yandex的天然優勢,ClickHouse經常會使用真實的數據進行測試,這一點很好地保證了測試場景的真實性。與此同時,ClickHouse也是我見過的發版速度最快的開源軟件了,差不多每個月都能發布一個版本。沒有一個可靠的持續集成環境,這一點是做不到的。正因為擁有這樣的發版頻率,ClickHouse才能夠快速迭代、快速改進。
所以ClickHouse的黑魔法並不是一項單一的技術,而是一種自底向上的、追求極致性能的設計思路。這就是它如此之快的秘訣。
——本文摘自機械工業出版社華章圖書
《ClickHouse原理解析與實踐》
作者介紹:
朱凱,ClickHouse貢獻者之一,ClickHouse布道者,資深架構師,騰訊雲最具價值專家TVP,開源愛好者,十多年IT從業經驗,對大數據領域主流技術與解決方案有深入研究,擅長分布式系統的架構設計與整合。曾主導過多款大數據平台級產品的規划、設計與研發工作,一線實戰經驗豐富。現就職於遠光軟件股份有限公司,任大數據事業部平台開發部總經理。著有《企業級大數據平台構建:架構與實現》、《ClickHouse原理解析與實踐》。