HBase 在統(tǒng)一內(nèi)容平臺業(yè)務的優(yōu)化實踐
一、業(yè)務簡介
統(tǒng)一內(nèi)容平臺主要承擔vivo內(nèi)容生態(tài)的內(nèi)容審核、內(nèi)容理解、內(nèi)容智作和內(nèi)容分發(fā)等核心功能,通過聚合全網(wǎng)內(nèi)容,建設行業(yè)級的業(yè)務中臺和內(nèi)容生態(tài),為上下游提供優(yōu)質(zhì)可靠的一站式服務,目前服務的業(yè)務方包括視頻業(yè)務、泛信息流業(yè)務等。
作為一個內(nèi)容中臺,每天要新增存儲大量圖文和視頻內(nèi)容來滿足分發(fā)的需要。與此同時,對這些內(nèi)容加工處理的數(shù)據(jù)也都需要存儲,包括基礎信息,分類標簽信息,審核信息等等。而且不僅僅是需要存儲的數(shù)據(jù)量級很大,對數(shù)據(jù)的讀取和寫入操作也非常頻繁,當前對數(shù)據(jù)庫的讀寫主要集中在兩個方面:
- 核心鏈路的內(nèi)容處理包含了大量對內(nèi)容特征信息的讀取和寫入操作;
- 對外提供的查詢服務會產(chǎn)生很多回源查詢數(shù)據(jù)庫的操作。
在經(jīng)過多年累積后,當前存儲的數(shù)據(jù)量越變越大,并且可以預見數(shù)據(jù)量級會不斷膨脹下去,如何選擇一種可靠的存儲選型,來保證服務穩(wěn)定性和擴展性成了當前項目架構的重中之重。
二、存在的問題
在選用HBase之前,內(nèi)容平臺核心數(shù)據(jù)的存儲以Mongodb為主要存儲選型,但是在日常的使用中,發(fā)現(xiàn)存儲側存在如下一些痛點問題:
- 核心數(shù)據(jù)量大,大表有20TB 以上,總存儲 60TB 以上,Mongodb 的存儲架構,無法滿足良好擴展性要求;
- 訪問查詢流量大,需要承載智慧push、泛信息流、視頻推薦側的大回源查詢流量,保持查詢接口高性能;
- 為了維護Mongodb的穩(wěn)定性,需要定期切換 Mongodb 數(shù)據(jù)庫主從節(jié)點,重做實例,需要運維長期投入,維護成本高。
所以我們迫切需要尋找一個更適合當前場景的數(shù)據(jù)庫來滿足業(yè)務請求量和存儲大小日益增長的需求,并且要求具備高性能、高穩(wěn)定、可擴展、低維護成本的特性。
三、存儲選型
經(jīng)過一些調(diào)研后發(fā)現(xiàn)HBase的一些特性能很好地滿足當前場景的要求。
(1)高性能
HBase采用的是Key/Value的列式存儲方式(對比Mongodb是行式數(shù)據(jù)庫),同一個列族的數(shù)據(jù)存放在一個文件中,隨著文件的增長會進行分裂,分散到其他機器上,所以即使隨著數(shù)據(jù)量增大,也不會導致讀寫性能的下降。HBase具備毫秒級的讀寫性能,如果寫入數(shù)據(jù)量大,還可以使用bulkload導入數(shù)據(jù)的方式進行高效入庫。
(2)高擴展性、高容錯性
HBase的存儲是基于Hadoop的,Hadoop實現(xiàn)了一個分布式文件系統(tǒng)(HDFS),HDFS的副本機制使得其具有高容錯性的特點,并且HDFS的Federation機制使得其具有高擴展性?;贖adoop意味著HBase與生俱來的超強擴展性和高容錯性。
(3)強一致性
HBase的數(shù)據(jù)是強一致性的,從CAP理論來看,HBase是屬于CP的。CAP 定理表明,在存在網(wǎng)絡分區(qū)的情況下,一致性和可用性必須二選一。HBase在寫入數(shù)據(jù)時,先把操作的記錄寫入到預寫日志中(Write-ahead log,WAL),然后再被加載到Memstore的。就算某個節(jié)點機器宕掉了,由于WAL的數(shù)據(jù)是存儲在HDFS上的,所以數(shù)據(jù)并不會丟失,后續(xù)可以通過讀取預寫日志恢復內(nèi)容。
(4)列值支持多版本
HBase的多版本特性可以針對某個列族控制列值的版本數(shù),默認是1,即每個key保存一個版本,同一個rowkey的情況下,后面的列值會覆蓋前面的列值??梢詣討B(tài)修改列族的版本數(shù),每個版本使用時間戳進行標記,默認是寫入時間作為該版本的時間戳,也可以在寫入時指定時間戳。
綜合以上特性,HBase是非常適合當前項目對數(shù)據(jù)庫選型的要求。
四、HBase 優(yōu)化實踐
隨著HBase在整個項目中逐步擴大使用,也發(fā)現(xiàn)了一些使用規(guī)范問題以及一些查詢的性能問題。比如查詢毛刺比較多、夜間Compact期間耗時比較高、流量高峰期的時候少量請求會有延遲。針對這些問題,我們從下面四個方面,對HBase的使用進行了優(yōu)化。
4.1 集群升級
剛開始使用HBase的時候,我們使用的HBase集群版本是1.2版本的,此版本存在諸多弊端,如:RIT(Region-In-Transition)問題頻發(fā)、請求延時突刺、建刪表速度慢、meta 表穩(wěn)定性差、節(jié)點故障恢復速度慢等問題。我們在使用過程發(fā)現(xiàn)的主要問題是響應時間突刺問題,該問題會導致我們實時查詢接口在回源時超時較多,導致接口的響應時間有突刺被下游業(yè)務方熔斷,影響業(yè)務查詢。與HBase團隊討論與評估后,決定將業(yè)務使用的集群升級到HBase 2.4.8 版本。該版本在公司較多的業(yè)務場景中已經(jīng)得到驗證,可以解決大部分1.2.0版本存在的痛點問題,可以大幅提升讀寫性能,有效降低讀毛刺,單機處理性能的提升可減少20%左右機器成本。
下面是集群升級后的讀寫平均耗時對比圖。可以看到在升級之前,平均響應時間經(jīng)常會有一些突刺,最高能達到超過10s,升級后幾乎不存在這么高的平均響應時間突刺,能保持在10ms以下,偶爾較高也是幾十毫秒級別。
升級前
升級后
4.2 連接池使用和連接預熱
HBase Connection 創(chuàng)建對象并不是簡單對應一個socket連接,需要與Zookeeper以及HMaster、RegionServer都建立連接,所以該過程是一個非常耗資源的過程,一般只創(chuàng)建一個 Connection 實例,其它地方共享該實例。在Connection初始化之后,用connection下的getTable方法實現(xiàn)對表格的連接。為了減少與表格連接帶來的網(wǎng)絡開銷,我們建立了對不同表格的連接池來管理客戶端和服務端的連接。大致流程圖如下圖所示。
通過建立連接池,帶來了以下三點優(yōu)勢:
(1)對表和表之間進行了連接資源隔離,避免互相影響;
(2)對連接實現(xiàn)了復用,減少了創(chuàng)建連接的網(wǎng)絡開銷;
(3)防止突增的流量帶來的影響,實現(xiàn)平滑處理流量。
此外,圖中可以看到,在程序啟動階段,可以實現(xiàn)對HBase表連接的預熱,提前建立對表格的連接,可以有效避免在程序啟動階段由于大量建立連接導致讀寫的響應時間變長,影響整體性能。
連接池通過使用Apache Commons Pool提供的GenericObjectPool通用對象池來實現(xiàn),GenericObjectPool包含豐富的配置選項,能夠定期回收空閑對象,并且支持對象驗證,具有強大的線程安全性和可擴展性。然后將不同表格的連接池對象放到本地緩存LoadingCache中,LoadingCache底層通過LRU算法實現(xiàn)對最久遠且沒有使用的數(shù)據(jù)的淘汰,保證沒有使用的表格連接能及時釋放。通過使用第三方的對象池和本地緩存,建立了對HBase表格的連接池,并且實現(xiàn)了預加載,減少了一些讀寫HBase的開銷,降低了讀寫耗時,對于剛啟動服務時的讀寫突刺帶來了一些改善。
4.3 按列讀取
HBase建表的時候是不需要確定列的,因為列是可變的,它非常靈活,唯一需要確定的就是列族。一張表的很多屬性比如過期時間、數(shù)據(jù)塊緩存以及是否壓縮等都是定義在列族上,而不是定義在表上或者列上,這一點做法跟以往的數(shù)據(jù)庫有很大的區(qū)別。同一個表里的不同列族可以有完全不同的屬性配置,但是同一個列族內(nèi)的所有列都會有相同的屬性。一個沒有列族的表是沒有意義的,因為列必須依賴列族而存在,所以在HBase中一個列的名稱前面總是帶著它所屬的列族。列族存在使得HBase會把相同列族的列盡量放在同一臺機器上,不同列族的列分布在不同的機器上。
一般情況下,從客戶端發(fā)起請求讀取數(shù)據(jù),到數(shù)據(jù)返回大致有如下幾步:
- 客戶端從ZooKeeper中獲取meta表所在regionServer節(jié)點信息。
- 客戶端訪問meta表所在的regionServer節(jié)點,獲取region所在節(jié)點信息。
- 客戶端訪問具體region所在regionServer,找到對應的region。
- 首先從blockCache中讀取數(shù)據(jù),存在則返回,不存在則去memstore中讀取數(shù)據(jù),存在則返回,不存在去storeFile(HFile)中讀取數(shù)據(jù),存在會先將數(shù)據(jù)寫入到blockCache中,然后返回數(shù)據(jù),不存在則返回空。
簡單的示意圖如下所示:
整個過程中如果讀取字段過多,或者字段長度過大,那么返回所有列的數(shù)據(jù)會導致大量無效的數(shù)據(jù)傳輸,進而導致集群網(wǎng)絡帶寬等系統(tǒng)資源被大量占用,必然導致讀取性能降低,所以需要減少一些不必要字段的查詢。
Get類是HBase官方提供的查詢類,在該類中主要有以下幾個方法提供來實現(xiàn)減少字段讀取:
- addFamily:添加要取出的列族;
- addColumn:添加要取出的列;
- setTimeRange:設置要取出的版本范圍;
- setMaxVersions:設置取出版本數(shù)量。
當前項目中沒有使用到HBase的版本范圍和版本數(shù)量的特性,但是主要場景使用的表字段都比較多(如內(nèi)容的基本屬性能達到上百個字段),或者字段的大小都比較大(如內(nèi)容解析的一些向量字段),原本在查詢時,都是直接讀取所有字段,導致很多字段其實不需要使用也被一直讀取,浪費性能。通過改用按列讀取的方式來實現(xiàn)不同場景下不同字段的查詢,避免了超過一半無用字段的返回,平均響應時間也下降了一些。
4.4 compact優(yōu)化
HBase是基于LSM樹存儲模型的分布式NoSQL數(shù)據(jù)庫。LSM樹相比于普遍使用在各種數(shù)據(jù)庫的B+樹來說,能夠獲得較高隨機寫性能的同時,也能保持可靠的隨機讀性能。在進行讀請求的時候,LSM樹要把多個子樹(類似B+樹結構)進行歸并查詢, 因此歸并查詢的子樹數(shù)越少,查詢的性能就越高。當MemStore超過閥值的時候,就要flush到HDFS上生成一個HFile。因此隨著不斷寫入,HFile的數(shù)量將會越來越多,根據(jù)前面所述,HFile數(shù)量過多會降低讀性能。為了避免對讀性能的影響,可以對這些HFile進行compact操作,把多個HFile合并成一個HFile。compact操作需要對HBase的數(shù)據(jù)進行多次的重新讀寫,因此這個過程會產(chǎn)生大量的IO。可以看到compact操作的本質(zhì)就是以IO操作換取后續(xù)的讀性能的提高。
HBase的compact是針對HRegion的HFile文件進行操作的。compact操作分為major和minor兩種。major compaction會把所有的HFile都compact為一個HFile,并同時忽略標記為delete的KeyValue(被刪除的KeyValue只有在compact過程中才真正被"刪除"),可以想象major compaction會產(chǎn)生大量的IO操作,對HBase的讀寫性能產(chǎn)生影響。minor則只會選擇數(shù)個HFile文件compact為一個HFile,minor的過程一般較快,而且IO相對較低。在業(yè)務高峰期間,都會禁止major操作,只在業(yè)務空閑的時段定時執(zhí)行。
hbase.hstore.compaction.throughput.higher.bound是HBase中控制HFile文件合并(compaction)速度的參數(shù)之一。它指定了一個HFile文件每秒最大合并數(shù)據(jù)大小的上限,以字節(jié)為單位。如果一個HFile文件的大小超過了這個上限,HBase就會嘗試將其分裂成較小的文件來加快合并速度。通過調(diào)整該參數(shù),可以控制HBase在什么條件下開始嘗試合并HFile文件。較小的值會導致更頻繁的文件合并,也會降低HBase的性能。較大的值則可能導致HFile文件的大小增長過快,從而影響讀取性能。
hbase.hstore.compaction.throughput.lower.bound也是HBase中控制HFile文件合并速度的參數(shù)之一。它指定了一個HFile文件每秒最小合并數(shù)據(jù)大小的下限,以字節(jié)為單位。當合并速度達到這個下限時,HBase會停止合并更小的HFile文件,而等待更多的數(shù)據(jù)到達之后再進行合并操作。與higher.bound參數(shù)相比,lower.bound參數(shù)更加影響文件合并頻率和性能。過高的值會導致較少的文件合并和較大的HFile文件,這會影響讀取性能和寫入并發(fā)性。反之,過低的值會導致過于頻繁的文件合并,從而占用過多的CPU和磁盤I/O資源,影響整個HBase集群的性能。針對Compact對業(yè)務耗時的影響,我們對Compact 操作進行了限流,并且通過多次測試調(diào)整Compact上文提到的兩個限流的閾值,取得了非常好的效果。Compact期間的耗時下降了70%y以上。下圖展示了采取限流前后的耗時對比。
4.5 字段級版本管理
除了上述提到的優(yōu)化點,我們也探索了一些HBase的其它特性,以備將來用來優(yōu)化其他方面。上文提到,通過對HBase進行按列讀取數(shù)據(jù),可以減少get查詢的時間,通常意義來講,列(也就是每個字段)已經(jīng)是每條數(shù)據(jù)的最基本單位了,但是HBase中的數(shù)據(jù)粒度比傳統(tǒng)數(shù)據(jù)結構更細一級,同一個位置的數(shù)據(jù)還細分成多個版本,一個列上可以存儲多個版本的值,多個版本的值被存儲在多個單元格里面,多個版本之間用版本號( version)來區(qū)分。所以,唯一確定一條結果的表達式應該是行鍵:列族:列:版本號(rowkey:column family:column:version)。不過,版本號通常是可以省略的,如果寫入時不寫版本號,每個列或者單元格的值都被賦予一個時間戳,這個時間戳默認是由系統(tǒng)制定的,當然寫入時也可以由用戶顯式指定具體的版本號。在查詢時如果不指定版本號,HBase默認獲取最后一個版本的數(shù)據(jù)返回給你。當然也可以指定版本號返回需要的其他版本的數(shù)據(jù)。簡單的示意圖如下所示:
同時HBase為了避免數(shù)據(jù)存在過多的版本造成不必要的負擔,HBase提供了兩種數(shù)據(jù)版本的回收方式,一是按照數(shù)量維度,保存最后的n個版本,二是按照時間維度,保存最近一段時間的版本數(shù)據(jù),比如保存一個月。通過多版本同時存儲,對于一些有時序要求的場景非常友好,通過指定版本的時間戳,可以避免在已經(jīng)更新了新數(shù)據(jù)的情況下,被舊數(shù)據(jù)覆蓋。當前我們建表是都是只指定了一個版本,使用也都是用的以時間戳為版本號的默認版本,沒有采取版本管理的措施,不同單元格可以記錄多版本的特性可以考慮應用于字段更新時記錄下多個版本的數(shù)據(jù),在不影響讀寫效率的情況下,方便后續(xù)在沒有相關日志的情況下,回溯最近幾次更新的值,并且可以防止誤操作或數(shù)據(jù)損壞,因為用戶可以恢復到之前的版本數(shù)據(jù)。此外我們的系統(tǒng)中存在一些通過消息隊列異步更新場景,此時可以使用消息體中的時間戳作為當前版本號,這樣可以在多線程消費時,也能保證消費的時序性,因為低版本的版本號無法更新高版本的版本號。
五、總結
本文在對統(tǒng)一內(nèi)容平臺在數(shù)據(jù)庫選型分析和優(yōu)化的基礎上,簡要介紹了HBase在實際使用中的一些優(yōu)化方案,經(jīng)優(yōu)化后,項目整體讀取和寫入性能都有比較明顯的提升,較好的保障了統(tǒng)一內(nèi)容平臺業(yè)務的穩(wěn)定性,并且大大降低了業(yè)務側的運維成本。Hbase本身就具備強大的功能,在大數(shù)據(jù)領域有獨有的優(yōu)勢,但是在不同的業(yè)務場景,對于HBase的要求也是不一樣的,可以結合具體的實際情況,從使用的數(shù)據(jù)庫版本、從HBase底層機制的調(diào)參、從客戶端調(diào)用機制的優(yōu)化等多方面挖掘,探索更適合業(yè)務的方式,希望本文中提到的一些優(yōu)化方案能給讀者帶來一些啟發(fā)。