閱讀157 返回首頁    go 技術社區[雲棲]


日誌係統之HBase日誌存儲設計優化

本人博客文章如未特別注明皆為原創!如有轉載請注明出處:https://blog.csdn.net/yanghua_kobe/article/details/46482319

繼續談論最近接手的日誌係統,上篇關於日誌收集相關的內容,這篇我們談談日誌存儲相關的話題。

簡介

我們首先來總結一下日誌這種數據的業務特點:它幾乎沒有更新的需求,一個組件或一個係統通常有一個固定的日誌格式,但就多個組件或係統而言它會存在各種五花八門的自定義的tag,這些tag建立的目的通常是為了後期查詢/排查線上問題的需要,因此日誌的檢索字段也靈活多變。

我們的日誌存儲選擇是HBase,這主要是因為我們認為HBase的如下特點非常適合日誌數據:

(1)HBase的qualifier相當靈活,可以動態創建,非常適合日誌這種tag不固定的半結構化數據(這裏的靈活性主要針對tag的存儲)

(2)HBase歸屬於Hadoop生態體係,方便後麵做離線分析、數據挖掘

結合上麵我們提到的日誌數據的特點,由於tag靈活多變,因此對基於tag的查詢HBase顯得有些力不從心。這主要是因為HBase本身並不提供二級索引,你無法基於Column進行搜索。如果無法確定rowKey或rowKey的範圍並且也沒有輔助索引,那麼將不得不進行全表掃描。從這一點上來看,你可以將其看作是一個Key-Value形式的數據庫(比如redis)。

基於HBase自建索引的缺陷

索引的設計

因為HBase自身不提供二級索引機製,所以很常見的做法是在外部自己構建索引,我在接手日誌係統時的實現就是這麼做的。基本思路是日誌存儲在日誌表,人為構建基於tag的索引信息存入索引元數據表,元數據表中一條索引信息對應一個索引表,在索引表中利用Column-Family的橫向擴展來存儲日誌的rowKey。總結如下:

(1)log表:存儲日誌記錄

(2)meta表:存儲索引元數據(其中包含動態索引表的表名稱)

(3)動態index表:存儲索引的具體信息,一個索引對應一張表

下麵我們來看一下這幾張表的Schema設計:


這裏我可以談談原先建立動態索引表的大致邏輯,它需要三個參數:

(1)indexName:索引名稱

(2)tags:需要為其建立索引的tag數組

(3)span:時間間隔

先將tags數組轉換為(fast-json#toJSONBytes)byte[]並將其作為rowKey,在meta表中檢查是否已存在為該tag組合構建的索引名。(HBase識別的rowKey格式是byte[]形式的,meta表的rowKey即為tags的json數組序列化為byte[]後的表示形式)。

如果該索引的元數據不存在,則創建動態索引表,該索引表的表名為indexName。

而索引表的rowKey對象的設計包含了兩個屬性:

(1)time:日誌的“準”記錄時間,注意此處不是真實的記錄時間,而是間接真實事件的一個時間點(timestamp / span *span)

(2)tags:tag的字符串數組

然後對log(日誌表)進行全表掃描,對每一條日誌記錄進行如下操作:

(1)獲取日誌產生的時間

(2)然後內部存在一個遍曆tags的循環,對每一個tag:判斷該條日誌是否存在該tag,如果不存在則直接跳出該循環,如果tag都能匹配上tags裏的每一條,則才為其建立索引

(3)如果需要建立索引,則往索引表內添加一條數據

總得來說,這裏建立的索引就是匹配tags集合以及時間分片,將滿足條件的日誌向最靠近它的時間點聚攏。

索引設計存在的問題

(1)索引表的建立效率很低,需要一個兩層的嵌套循環,最外層做的事情是全表掃描,如果數據量龐大後,這種處理方式很難被接受。其實,這種方式類似於數據已在,事後補償的機製。而通常的做法是:索引表建立時隻是個空表,數據入庫時動態分析其是否有必要構建索引(這句的後半句原來也有實現,是通過storm實現的)

(2)通過索引進行查找的時候,還需要兩層循環,外層是查找動態索引表裏的行集合,內層是獲取列簇裏所有的日誌表裏相關記錄的rowKey。如果查詢的時間範圍比較長或者時間分片的間隔比較端,那麼時間點會非常多,而時間點一多,外層循環次數將會非常多,因此為了避免這一點,實現時做了時間片段限製,也就是片段不能大於一定的範圍;如果該時間片的日誌非常密集,那麼這些日誌就都會落到該時間點上,那麼內層循環次數將會非常多。

(3)查詢的效率將非常依賴於索引建立的健全程度,這種情況下建立索引的tag集合必須小而全,如果大而廣,那麼構建索引的條件匹配度就會變低。如果沒有針對要查詢的tag的索引信息,將不得不進行全表掃描。

(4)日誌表ID采用的是分布式自增ID,其他表用的是json對象的字符串形式,沒有注意rowKey對HBase查詢的重要性。

HBase存儲日誌的查詢優化

HBase查詢的基礎概念

產生這些問題的原因是自建索引的實現方式,我們必須對日誌係統的查詢進行優化,在此之前我們首先要對HBase的查詢有一些基本的了解。訪問HBase的行記錄有以下三種方式:

(1)通過rowKey作唯一匹配

(2)通過rowKey的range匹配一個範圍,然後通過多種filter在範圍內篩選

(3)全表掃描

從編程角度來看,HBase的查詢實現隻支持兩種方式:

(1)get:指定rowkey獲取唯一一條記錄

(2)scan:按指定的條件獲得一批記錄(它包含了上麵的2,3兩種方式)

通常情況下,全表掃描很少是我們期望的做法。因此我們如果我們想提升查詢效率,必須精心設計rowKey。

從上麵自建索引產生的問題以及我們對HBase查詢的基本了解。問題主要有兩個方麵:

(1)自建索引的實現方式不夠高效

(2)沒有對rowKey進行良好的設計(日誌記錄的ID采用分布式自增ID)

下麵我們針對這兩點來談談優化策略。

rowKey的優化

rowKey在這裏絕對不能像傳統的RDBMS處理主鍵那樣,簡單地用UUID或自增ID來處理。HBase的rowKey是基於字典排序的,具體來說是基於key的ASCII碼來排序,我們的思路是要往rowKey中加入我們想要查詢的條件因子,通過多個因子相互組合,來一步步確定查找範圍。比如時間肯定是我們應該加到rowKey裏的一個查詢因子,一個開始時間跟一個截止時間就形成了一個時間段範圍,就能固定一個結果集範圍。

你很容易看出來rowkey裏加入的查詢因子越多,查詢範圍定位的精確度越高。但查詢因子其實是從眾多日誌中抽象而來(比如host,level,timestamp等),這要求它們是每條日誌記錄中共性的東西,就我們目前的日誌係統而言,大致劃分為兩種日誌類型:

(1)定格式的業務係統/框架日誌(比如業務框架/web app等)

(2)不定格式的技術係統/組件/框架日誌(比如nginx、redis、RabbitMQ等)

針對定格式的日誌,我們的rowKey的規則是:


針對不定格式的日誌,我們的rowKey規則是:


因為各種技術組件的日誌格式多樣,導致我們無法從中解析出時間,所以這裏我們選擇日誌的收集時間作為鑒別時間戳。這裏我們隻能假設:整個日誌係統一直都良好運轉,也就是說日誌產生時間給收集時間相近。但毫無疑問這樣的假設有時是不準確的,但我們不會以真實的時間作為基準,因為這種類型的日誌是通過離線批處理進行解析後重新轉存的,因此最終還是會得出精確的日誌時間戳。

rowkey最好被設計為定長的,而且最好將rowkey的每個分段都轉化成純數字或純字母這種很容易轉化為ASCII碼並且容易人為設置最大值與最小值的形式。舉例來說:假如前麵幾位都固定,最後三位是不定的,如果是數字,那麼區間的範圍會在XXXXX000-XXXXX999之間。

通常我們想加入rowKey的查詢因子,其值不為數字或者字母是很正常的,這時我們可以通過碼表來映射,比如上麵我們針對AppLog的logLevel因子就是通過碼表來進行映射的,目前我們用兩位數來映射可能存在的level。

篩選器-filter

在通過rowKey的範圍確定對結果集的掃描範圍之後,下一步就是通過內置的filter來進行更精確的篩選,HBase默認提供多種filter供使用者針對rowKey、column-family、qualifier等進行篩選。當然如果rowKey的篩選條件取值跨度比較大,還會產生接近類似於全表掃描的影響。我們能做的事情就隻剩下對查詢條件進行限製了,比如:

(1)查詢時間區間的跨度隻能限製在一定的範圍

(2)分頁給出查詢結果

再談自建索引

既然索引是優化查詢非常關鍵的一環,所以建索引的思路是沒有問題的。但是,無論如何自建索引還是需要精心設計rowKey,不管是數據表的rowKey還是索引表的rowKey。有時為了查詢效率,甚至會固定某段rowKey的前幾位,並讓其代表的數據落在同一個region中。精心設計rowKey的原因,還是因為HBase的查詢特征:你獲得的rowKey範圍越精確,查找的速度越快。

協處理器-coprocessor

通常情況下,索引表建立時不應該進行全表掃描,但我們應該對日誌表的每條數據進行處理來生成最終的索引數據。在我們現在的係統中,是通過storm進行分析、插入的。這裏我們上storm的目的也不是為了做這件事,最主要的目的是實時過去logLevel為Error的日誌,並做到準實時通知。那麼問題來了,如果我們不存在這個需求,我們是不是為了要計算索引而要上一個storm集群?答案是:大可不必。

其實這裏主要是在往HBase裏插入數據時,獲得一個hock(鉤子)或者說callback來攔截每條數據,分析是否應該將其rowKey加入索引表中去。HBase在0.92版本之後提供了一個稱之為協處理器(coprocessor)的技術,允許裏編寫運行在HBase Server上的代碼攔截數據,協處理器大致分為兩類:

(1)Observer(類比於RDBMS中的觸發器)

(2)EndPoint(類比於RDBMS中的存儲過程)

我們可以通過Observer來攔截日誌記錄,並加入代碼處理邏輯來為其構建索引。由於介紹HBase技術細節不是本文的重點,所以這裏就提及一下,如果後麵有機會,再來繼續探討。

回到自建索引這個話題,上麵談及了自建索引依賴的技術點,下麵推薦一個自建索引的設計思路。這裏有一篇不錯的文章,通過巧妙地設計索引表的rowKey來滿足多條件查詢的需求。這是一個二級多列索引的設計。通過對多個查詢條件鍵以及條件值映射到rowKey來縮小索引表的rowKey區間到最後確定唯一目標rowKey,並從cell裏獲得數據表的rowKey。但這樣的索引設計,依賴於表結構已知且前提條件固定。很明顯,日誌表中存在各種無法預知的tag,沒有辦法參照這樣的索引設計。而這樣的場景最好通過一些專門針對全文檢索的搜索引擎來建立索引。

第三方專業索引機製

從上麵的討論可以看出,近似於全文檢索的需求在表中的數據非常多的情況下,HBase很難實現非常高效的索引。這時我們可以借助於全文檢索引擎提供的索引的能力來給HBase的rowKey建立索引,而HBase隻負責存儲基礎數據。業界已經有很多基於此思路(索引+存儲)的實踐總結。這裏,全文索引的選擇可以是Solr,或者是更適用於日誌搜索的ElasticSearch(它自身也具備存儲機製)。解決方案可參照這個Slide

這裏有一張整體架構模式圖:


如果有時間和機會,後麵我會談談ElasticSearch+HBase組合的日誌全文檢索實踐。


最後更新:2017-09-20 17:03:56

  上一篇:go  數據可視化難在哪裏?該如何入門
  下一篇:go  人工智能正麵臨克隆風險!