Categories
程式開發

趣頭條基於ClickHouse玩轉每天1000億數據量


本文由 dbaplus 社群授權轉載。

業務背景

隨著公司規模越來越大,業務線越來越多,公司的指標規模也在急速增長,現有的基於storm實時計算的指標計算架構的缺點越來越凸顯,所以我們急需對現有的架構進行調整。

1、基於storm的指標平台存在的問題

  • 指標口徑不夠直觀
  • 數據無法回溯
  • 穩定性不夠

2、什麼是我們需要的?

我們需要一個穩定的、基於SQL、方便進行數據回溯、並且要足夠快速的引擎,支持我們的實時指標平台。

1)穩定性 是最主要的,基於storm的架構數據都是存儲在內存中的,如果指標配置有問題,很容易導致OOM,需要清理全部的數據才能夠恢復。

2)基於SQL 是避免像storm架構下離線SQL到storm topology轉換的尷尬經歷。

3)方便回溯 是數據出現問題以後,我們可以簡單的從新刷一下就可以恢復正常,在storm架構下有些場景無法完成。

4)快速 那是必須的,指標數越來越多,如果不能再5分鐘週期內完成所有的指標計算是不能接受的。

clickhouse

1、為什麼選擇clickhouse?

足夠快,在選擇clickhouse以前我們也有調研過presto、druid等方案,presto的速度不夠快,無法在5分鐘內完成這麼多次的查詢。

druid的預計算挺好的,但是維度固定,我們的指標的維度下鑽都是很靈活的,並且druid的角色太多維護成本也太高,所以也被pass了。

最終我們選擇了clickhouse,在我們使用之前,部門內部其實已經有使用單機版對離線數據的查詢進行加速了,所以選擇clickhouse也算是順理成章。

2、clickhouse和presto查詢速度比較

clickhouse集群現狀:32核128G內存機器60台,使用ReplicatedMergeTree引擎,每個shard有兩個replica。

presto集群的現狀:32核128G內存機器100台。

1)最簡單的count()的case

趣頭條基於ClickHouse玩轉每天1000億數據量 1

趣頭條基於ClickHouse玩轉每天1000億數據量 2

從上圖可以看到clickhouse在count一個1100億數據表只需要2s不到的時間, 由於數據冗餘存儲的關係,clickhouse實際響應該次查詢的機器數只有30台(60 / 2),presto在count一個400億的數據表耗時80秒左右的時候,100台機器同時在處理這個count的查詢。

2)常規指標維度下鑽計算count() + group by + order by + limit

趣頭條基於ClickHouse玩轉每天1000億數據量 3

趣頭條基於ClickHouse玩轉每天1000億數據量 4

同樣在1100億數據表中clickhouse在該case上面的執行時間也是非常不錯的耗時5s左右,presto在400億的數據集上完成該查詢需要100s左右的時間。

從上面兩個常規的case的執行時間我們可以看出,clickhouse的查詢速度比presto的查詢速度還是要快非常多的。

3、clickhouse為什麼如此快

1)優秀的代碼,對性能的極致追求

clickhouse是CPP編寫的,代碼中大量使用了CPP最新的特性來對查詢進行加速。

2)優秀的執行引擎以及存儲引擎

clickhouse是基於列式存儲的,使用了向量化的執行引擎,利用SIMD指令進行處理加速,同時使用LLVM加快函數編譯執行,當然了Presto也大量的使用了這樣的特性。

3)稀疏索引

相比於傳統基於HDFS的OLAP引擎,clickhouse不僅有基於分區的過濾,還有基於列級別的稀疏索引,這樣在進行條件查詢的時候可以過濾到很多不需要掃描的塊,這樣對提升查詢速度是很有幫助的。

4)存儲執行耦合

存儲和執行分離是一個趨勢,但是存儲和執行耦合也是有優勢的,避免了網絡的開銷,CPU的極致壓榨加上SSD的加持,每秒的數據傳輸對於網絡帶寬的壓力是非常大的,耦合部署可以避免該問題。

5)數據存儲在SSD,極高的iops。

4、clickhouse的insert和select

1)clickhouse如何完成一次完整的select

這裡有個概念需要澄清一下,clickhouse的表分為兩種,一種是本地表另一種是分佈式表。本地表是實際存儲數據的而分佈式表是一個邏輯上的表,不存儲數據的只是做一個路由使用,一般在查詢的時候都是直接使用分佈式表,分佈式表引擎會將我們的查詢請求路由本地表進行查詢,然後進行匯總最終返回給用戶。

2)索引在查詢中的使用

索引是clickhouse查詢速度比較快的一個重要原因,正是因為有索引可以避免不必要的數據的掃描和處理。傳統基於hdfs的olap引擎都是不支持索引的,基本的數據過濾只能支持分區進行過濾,這樣會掃描處理很多不必要的數據。

clickhouse不僅支持分區的過濾也支持列級別的稀疏索引。 clickhouse的基礎索引是使用了和kafka一樣的稀疏索引,索引粒度默認是8192,即每8192條數據進行一次記錄,這樣對於1億的數據只需要記錄12207條記錄,這樣可以很好的節約空間。

二分查找+遍歷也可以快速的索引到指定的數據,當然相對於稠密索引,肯定會有一定的性能損失,但是在大數據量的場景下,使用稠密索引對存儲也是有壓力的。

趣頭條基於ClickHouse玩轉每天1000億數據量 5

下面我們通過舉例看下索引在clickhouse的一次select中的應用,該表的排序情況為order by CounterID, Date 第一排序字段為CounterID,第二排序字段為Date,即先按照CounterID進行排序,如果CounterID相同再按照Date進行排序。

  • 場景1 where CounterId=’a’

CounterID是第一索引列,可以直接定位到CounterId=’a’的數據是在(0,3)數據塊中。

  • 場景2 where Date=’3’

Date為第二索引列,索引起來有點費勁,過濾效果還不是特別的好,Date=’3’的數據定位在(2,10)數據塊中。

  • 場景3 where CounterId=’a’ and Date=’3’

第一索引 + 第二索引同時過濾,(0,3) 和 (2,10)的交集,所以為(2,3)數據塊中。

  • 場景4 where noIndexColumn=’xxx’

對於這樣沒有索引字段的查詢就需要直接掃描全部的數據塊(0,10)。

3)clickhouse如何完成一次插入

clickhouse的插入是基於Batch的,它不能夠像傳統的mysql那樣頻繁的單條記錄插入,批次的大小從幾千到幾十萬不等,需要和列的數量以及數據的特性一起考慮,clickhouse的寫入和Hbase的寫入有點”像”(類LSM-Tree),主要區別有:

  • 沒有內存表;
  • 不進行日誌的記錄。

clickhouse寫入的時候是直接落盤的, 在落盤之前會對數據進行排序以及必要的拆分(如不同分區的數據會拆分成多個文件夾),如果使用的是ReplicatedMergeTree引擎還需要與zookeeper進行交互,最終會有線程在後台把數據(文件夾)進行合併(merge),將小文件夾合併生成大文件夾方便查詢的時候進行讀取(小文件會影響查詢性能)。

5、關於集群的搭建

1)單副本

缺點:

  • 集群中任何一台機器出現故障集群不可用;
  • 如果磁盤出現問題不可恢復數據永久丟失;
  • 集群升級期間不可用(clickhouse版本更新快)。

2)多副本

多副本可以完美的解決單副本的所有的問題,多副本有2個解決方案:

  • RAID磁盤陣列;
  • 使用ReplicatedMergeTree引擎,clickhouse原生支持同步的引擎(基於zookeeper)。

兩種方案的優缺點:

  • 基於RAID磁盤陣列的解決方案,在版本升級,機器down機的情況下無法解決單副本的缺陷;
  • 基於zookeeper的同步,需要雙倍的機器(費錢),同時對zookeeper依賴太重,zookeeper會成為集群的瓶頸,當zookeeper有問題的時候集群不可寫入(ready only mode);
  • 副本不僅僅讓數據更安全,查詢的請求也可以路由到副本所在的機器,這樣對查詢並發度的提升也是有幫助的,如果查詢性能跟不上添加副本的數量也是一個解決方案。

6、常見的引擎(MergeTree家族)

1)(Replicated)MergeTree

該引擎為最簡單的引擎,存儲最原始數據不做任何的預計算,任何在該引擎上的select語句都是在原始數據上進行操作的,常規場景使用最為廣泛,其他引擎都是該引擎的一個變種。

2)(Replicated)SummingMergeTree

該引擎擁有“預計算(加法)”的功能。

實現原理:在merge階段把數據加起來(對於需要加的列需要在建表的時候進行指定),對於不可加的列,會取一個最先出現的值。

3)(Replicated)ReplacingMergeTree

該引擎擁有“處理重複數據”的功能。

使用場景:“最新值”,“實時數據”。

4)(Replicated)AggregatingMergeTree

該引擎擁有“預聚合”的功能。

使用場景:配合”物化視圖”來一起使用,擁有毫秒級計算UV和PV的能力。

5)(Replicated)CollapsingMergeTree

該引擎和ReplacingMergeTree的功能有點類似,就是通過一個sign位去除重複數據的。

需要注意的是,上述所有擁有”預聚合”能力的引擎都在”Merge”過程中實現的,所以在表上進行查詢的時候SQL是需要進行特殊處理的。

如SummingMergeTree引擎需要自己sum(), ReplacingMergeTree引擎需要使用時間+版本進行order by + limit來取到最新的值,由於數據做了預處理,數據量已經減少了很多,所以查詢速度相對會快非常多。

7、最佳實踐

1)實時寫入使用本地表,不要使用分佈式表

分佈式表引擎會幫我們將數據自動路由到健康的數據表進行數據的存儲,所以使用分佈式表相對來說比較簡單,對於Producer不需要有太多的考慮,但是分佈式表有些致命的缺點。

  • 數據的一致性問題,先在分佈式表所在的機器進行落盤,然後異步的發送到本地表所在機器進行存儲,中間沒有一致性的校驗,而且在分佈式表所在機器時如果機器出現down機,會存在數據丟失風險;
  • 據說對zookeeper的壓力比較大(待驗證)。

2)推薦使用(*)MergeTree引擎,該引擎是clickhouse最核心的組件,也是社區優化的重點

數據有保障,查詢有保障,升級無感知。

3)謹慎使用on cluster的SQL

使用該類型SQL hang住的案例不少,我們也有遇到,可以直接寫個腳本直接操作集群的每台進行處理。

8、常見參數配置推薦

1)max_concurrent_queries

最大並發處理的請求數(包含select,insert等),默認值100,推薦150(不夠再加),在我們的集群中出現過”max concurrent queries”的問題。

2)max_bytes_before_external_sort

當order by已使用max_bytes_before_external_sort內存就進行溢寫磁盤(基於磁盤排序),如果不設置該值,那麼當內存不夠時直接拋錯,設置了該值order by可以正常完成,但是速度相對存內存來說肯定要慢點(實測慢的非常多,無法接受)。

3)background_pool_size

後台線程池的大小,merge線程就是在該線程池中執行,當然該線程池不僅僅是給merge線程用的,默認值16,推薦32提升merge的速度(CPU允許的前提下)。

4)max_memory_usage

單個SQL在單台機器最大內存使用量,該值可以設置的比較大,這樣可以提升集群查詢的上限。

5)max_memory_usage_for_all_queries

單機最大的內存使用量可以設置略小於機器的物理內存(留一點內操作系統)。

6)max_bytes_before_external_group_by

在進行group by的時候,內存使用量已經達到了max_bytes_before_external_group_by的時候就進行寫磁盤(基於磁盤的group by相對於基於磁盤的order by性能損耗要好很多的),一般max_bytes_before_external_group_by設置為max_memory_usage / 2,原因是在clickhouse中聚合分兩個階段:

  • 查詢並且建立中間數據;
  • 合併中間數據 寫磁盤在第一個階段,如果無須寫磁盤,clickhouse在第一個和第二個階段需要使用相同的內存。

這些內存參數強烈推薦配置上,增強集群的穩定性避免在使用過程中出現莫名其妙的異常。

9、那些年我們遇到過的問題

1)Too many parts(304). Merges are processing significantly slower than inserts

相信很多同學在剛開始使用clickhouse的時候都有遇到過該異常,出現異常的原因是因為MergeTree的merge的速度跟不上目錄生成的速度, 數據目錄越來越多就會拋出這個異常,所以一般情況下遇到這個異常,降低一下插入頻次就ok了,單純調整background_pool_size的大小是治標不治本的。

我們的場景

我們的插入速度是嚴格按照官方文檔上面的推薦”每秒不超過1次的insert request”,但是有個插入程序在運行一段時間以後拋出了該異常,很奇怪。

問題排查

排查發現失敗的這個表的數據有一個特性,它雖然是實時數據但是數據的eventTime是最近一周內的任何時間點,我們的表又是按照day + hour組合分區的那麼在極限情況下,我們的一個插入請求會涉及7*24分區的數據,也就是我們一次插入會在磁盤上生成168個數據目錄(文件夾),文件夾的生成速度太快,merge速度跟不上了,所以官方文檔的上每秒不超過1個插入請求,更準確的說是每秒不超過1個數據目錄。

case study

分區字段的設置要慎重考慮,如果每次插入涉及的分區太多,那麼不僅容易出現上面的異常,同時在插入的時候也比較耗時,原因是每個數據目錄都需要和zookeeper進行交互。

2)DB::NetException: Connection reset by peer, while reading from socket xxx

查詢過程中clickhouse-server進程掛掉。

問題排查

排查發現在這個異常拋出的時間點有出現clickhouse-server的重啟,通過監控系統看到機器的內存使用在該時間點出現高峰,在初期集群”裸奔”的時期,很多內存參數都沒有進行限制,導致clickhouse-server內存使用量太高被OS KILL掉。

case study

上面推薦的內存參數強烈推薦全部加上,max_memory_usage_for_all_queries該參數沒有正確設置是導致該case觸發的主要原因。

3)Memory limit (for query) exceeded:would use 9.37 GiB (attempt to allocate chunk of 301989888 bytes), maximum: 9.31 GiB

該異常很直接,就是我們限制了SQL的查詢內存(max_memory_usage)使用的上線,當內存使用量大於該值的時候,查詢被強制KILL。

對於常規的如下簡單的SQL, 查詢的空間複雜度為O(1) 。

select count(1) from table where condition1 and condition2
select c1, c2 from table where condition1 and condition2

對於group by, order by , count distinct,join這樣的複雜的SQL,查詢的空間複雜度就不是O(1)了,需要使用大量的內存。

  • 如果是group by內存不夠,推薦配置上max_bytes_before_external_group_by參數,當使用內存到達該閾值,進行磁盤group by
  • 如果是order by內存不夠,推薦配置上max_bytes_before_external_sort參數,當使用內存到達該閾值,進行磁盤order by
  • 如果是count distinct內存不夠,推薦使用一些預估函數(如果業務場景允許),這樣不僅可以減少內存的使用同時還會提示查詢速度
  • 對於JOIN場景,我們需要注意的是clickhouse在進行JOIN的時候都是將”右表”進行多節點的傳輸的(右表廣播),如果你已經遵循了該原則還是無法跑出來,那麼好像也沒有什麼好辦法了

4)zookeeper的snapshot文件太大,follower從leader同步文件時超時

上面有說過clickhouse對zookeeper的依賴非常的重,表的元數據信息,每個數據塊的信息,每次插入的時候,數據同步的時候,都需要和zookeeper進行交互,上面存儲的數據非常的多。

就拿我們自己的集群舉例,我們集群有60台機器30張左右的表,數據一般只存儲2天,我們zookeeper集群的壓力已經非常的大了,zookeeper的節點數據已經到達500w左右,一個snapshot文件已經有2G+左右的大小了,zookeeper節點之間的數據同步已經經常性的出現超時。

問題解決

  • zookeeper的snapshot文件存儲盤不低於1T,注意清理策略,不然磁盤報警報到你懷疑人生,如果磁盤爆了那集群就處於“殘廢”狀態;
  • zookeeper集群的znode最好能在400w以下;
  • 建表的時候添加use_minimalistic_part_header_in_zookeeper參數,對元數據進行壓縮存儲,對於高版本的clickhouse可以直接在原表上面修改該setting信息,注意修改完了以後無法再回滾的。

5)zookeeper壓力太大,clickhouse表處於”read only mode”,插入失敗

  • zookeeper機器的snapshot文件和log文件最好分盤存儲(推薦SSD)提高ZK的響應;
  • 做好zookeeper集群和clickhouse集群的規劃,可以多套zookeeper集群服務一套clickhouse集群。

直播回放

作者介紹

王海勝,趣頭條數據中心大數據開發工程師

8年互聯網工作經驗,曾在eBay、唯品會、趣頭條等公司從事大數據開發相關工作,有豐富的大數據落地經驗。

原文鏈接

https://mp.weixin.qq.com/s/egzFxUOAGen_yrKclZGVag