垃圾回收優化
master基本不會遇到垃圾回收的問題。
由於memstore的刷寫機制是不連續的,所以java虛擬機的堆內存會出現孔洞。
快速刷寫到磁盤的數據會被劃分到新生代,這種空間會被優先回收
數據停留的時間太長,會被劃分到老生代甚至終生代。而且老生代和終生代一般占據了好幾個G,而新生代一般就幾百M而已
新生代空間
由此得出新生代的空間一般的分配如下
-XX:MaxNewSize=128m -XX:NewSize=128m
可以縮寫為
-Xmn128m
設定好之後觀察是否合理如果不合理你會發現服務器的CPU使用量急劇上升,因為新生代的回收很占CPU
新生代的設定如果調大,會帶來的好處:則生存期較長的對象不會過快的劃分為老生代。
如果太大,回收會產生較長時間停頓
gc日志
如果JRE中孔洞太多,空間不夠的時候,就需要壓縮堆內存碎片,如果壓縮內存碎片失敗會出現失敗日志。所以要通過以下參數開啟jvm的gc日志
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:$HBASE_HOME/logs/gc-${hostname}-hbase.log
日志中會出現 "concurrent mode failure" 或者 "promotion failed" 信息注意:不過這個日志不會自動滾動,會越來越大你的手動用linux的每日滾動去做手動清理
回收策略
垃圾回收策略是可以切換的,建議用以下策略
-XX:+UseParNewGC and -XX:+UseConcMarkSweepGC
第一個選項是設置年輕代用 Parallel New Collector 回收策略:停止jvm去清空年輕代。因為年輕代很小,所以這個過程很快,一般不到一秒,所以這個暫停是可以接受的
CMS策略
但是老生代不能用這個策略,因為老生代很大,暫停會很久,如果大於zk的會話超時,就會引起朱麗葉暫停問題。所以老生代用 並行標記回收器(Concurrent Mark-Sweep Collector, CMS)來緩解。這種策略盡量異步實現垃圾回收,但是cpu占用率高。不過如果回收失敗的話,還是會讓jvm暫停來進行內存整理。使用了CMS策略有一個額外的參數設定什麼時候開始進行並發標記
-XX:CMSInitiatingOccupancyFraction=70
這個值設定了一個百分比。70%是一個比較好的值,因為它比region的堆占用率60%略大(20%塊緩存+40%memstore)
這樣在堆空間被占完之前就開始並行回收
不會太小而導致回收頻繁進行
優化原則
塊緩存+memstore 不能 大於 100%
要留空間給其他操作,所以 塊緩存+memstore = 60% 比較合理
本地memstore分配緩沖區(MSLAB)
MSLAB=Memstore-Local Allocation Buffers 本地memstore分配緩沖區jvm孔洞(碎片)如果太多會觸發 stop-the-world 垃圾回收,就是把整個jvm停掉回收垃圾。所以MSLAB致力於減少碎片。方法是:每次分配固定大小的對象,當這些對象被回收的時候,會留下固定大小的孔洞,之後如果新對象的大小也相同就可以直接用這些孔洞了,就不會引發 promotion fail,就不會觸發 stop-the-world 過程MSLAB默認是開啟的,如果沒有就設置 hbase.hregion.memstore.mslab.enabled來開啟
hbase.hregion.memstore.mslab.chunksize 可以設定之前所說的固定大小孔洞的大小,默認是2MB。如果你存儲的東西都很大,那就調大這個值
如果你要存的東西大於存儲緩沖區的上邊界 hbase.hregion.memstore.mslab.max.allocation 默認值是256K。任何大於該值的單元格不會使用mslab特性,而是直接向jvm申請空間。
MSLAB的代價是空間的浪費,就算你沒用到緩沖區的最後一個字節,緩沖區依然是那麼大。所以你必須權衡利弊(我個人建議是浪費就浪費,總比引起jvm暫停好)
使用緩沖區需要額外的內存復制工作,所以會比直接使用KeyValue實例要慢一點
壓縮
推薦使用snappy 。不過要在一開始就使用,中間切換不好搞
優化拆分和合並
管理拆分
拆分/合並風暴
當用戶的region大小以恆定的速度保持增長時,region拆分會在同一時間發生,因為同時需要壓縮region中的存儲文件,這個過程會重寫拆分後的region,這將會引起IO上升。建議:關閉自動拆分,然後手動調用split和major_compact 命令
如何關閉自動拆分?
將 hbase.hregion.max.filesize 調的非常大,但是不要大過 Long.MAX_VALUE (即 9223372036854775807),建議為100G手動運行還有一個好處,可以在不同時間段不同的region上執行,分散壓力。
用戶可以做成cron的job定時執行這些操作
手動拆分可以避免:當你做troubleshooting 的時候自動拆分有可能會把你正在看的region拆掉,這樣就不好了
region熱點
注意不要用類似時間戳這樣的遞增的東西做主鍵,防止出現region熱點
預拆分region
在建立表的時候通過 SPLITS 屬性可以直接定義各個region的范圍,進行region的預拆分
負載均衡
master有一個內置的均衡器。默認情況下,均衡器每五分鐘運行一次,這是通過 hbase.balancer.period 屬性設置的。它會嘗試均勻分配region到所有region服務器。啟動均衡器,均衡器首先會確定一個region分配計劃,該計劃用於描述region如何移動。然後通過迭代調用管理API中的 unassign() 方法開始移動region。均衡器有一個可以限制自身運行時間的上限,通過 hbase.balancer.max.balancing 屬性來配置,默認設置為均衡器運行時間間隔周期的一半,即兩分半鐘。
合並region
用 hbase org.apache.hadoop.hbase.util.Merge testtable 可以合並多個region
刪除大量數據的時候,可以合並region,讓region不會那麼多
客戶端API:最佳實踐
禁止自動刷寫
如果有大量的寫入操作時,使用setAutoFlush(false) ,否則 Put 實例會被逐個傳送到region服務器。禁止了自動刷寫就可以等到寫緩沖區被填滿的時候一次性批量的發送。
你可以可以使用 flushCommits() 方法顯式刷寫數據
用 HTable 的 close 方法也會隱式的調用刷寫
使用掃描緩存
如果HBase被作為一個MapReduce 作業的輸入源,就可以用 setCache() 設置一個比1大的多的數值,可以開啟掃描緩存。這樣可以一次從region取多條(比如500條)到客戶端來處理。
不過傳輸數據的開銷和內存開銷都會增大。所以不是越大越好
限定掃描范圍
當Scan被用來處理大量行時(比如作為MapReduce輸入源時)最好只設定指定的列,如果用addFamily() 會把整個family的所有列都加載進來。(其實就是跟傳統SQL建議大家不要 SELECT * 一回事)
關閉ResultScanner
一定要記得及時關閉 ResultScanner (其實跟傳統數據庫要記得關閉連接一回事)
在finally 裡面關閉 ResultScanner
塊緩存用法
Scan可以通過設置 setCacheBlocks() 來設置使用region服務器中的塊緩存。
如果在MapReduce中,這個應該被設置成false
如果某些行被頻繁訪問,這個應該被設置成true
優化獲取行鍵的方式
如果你只是進行某些簡單的行統計之類不需要獲取所有列的操作,記得在 FilterList中添加 FirstKeyFilter 或者 KeyOnlyFilter ,這樣就可以只返回第一個KeyValue行鍵,極大的減少了網絡傳輸
關閉Put上的WAL
Put 的 writeToWAL(false) 可以關閉WAL,可以大幅提高吞吐量,但是副作用就是region如果出問題就會丟失數據。
其實如果數據是在集群間分布均勻後,其實關閉日志不會提升多少性能
所以最好不要關閉WAL。要是真的要提高吞吐量的話就用 批量導入 (bulk load) 技術。這個在 12.2.3 中會進行介紹
配置
減少ZooKeeper超時的發生
默認的region和zk之間的超時時間是3分鐘。推薦設置為1分鐘,這樣可以更快的發現這一個故障
默認時間那麼長是為了避免大數據到導入時出問題,如果沒有大數據的導入情況就可以把超時設置短一點
12.5.3中“穩定性問題”會介紹一個方法來檢測這種停頓
個人認為沒有太大必要,要掛就是一直掛,快那麼2分鐘也沒什麼用
增加處理線程
hbase.regionserver.handler.count 定義了響應外部用戶訪問數據表請求的線程數,默認是10,有點偏小。這是為了防止用戶在客戶端高並發使用較大的緩沖區的情況下服務器端過載。
但是當單次請求開銷較小時,可以設定的高一點
設置太高,會對region的內存造成壓力,甚至會導致 OutOfMemoryError。而且可用內存過低的話又會觸發垃圾回收,造成全面暫停
增加堆大小
在 hbase-env.sh 中 調整 HBASE_HEAPSIZE ,增大為8G
不過最好用 HBASE_REGIONSERVER_OPTS 而不是 HBASE_HEAPSIZE 可以單獨調大region的堆大小,master不需要太大的堆大小,1G就夠用了
啟用數據壓縮
推薦snappy,如果沒有snappy就用LZO壓縮
增加region大小
更大的region可以減少region數量
少region可以讓集群運行更平穩
如果一個region變熱點就手動拆分它
默認的region是256M,可以配置成1G或者更大
region太大的話高負載下的合並會停頓很長時間
通過 hbase.hregion.max.filesize 設置region的大小
調整塊緩存大小
默認塊緩存是20%(即0.2)
通過 perf.hfile.block.cache.size 屬性可以設置這個百分比
如果根據10.2.3節提到的 evicted(LV) 參數發現有許多塊被換出。這樣就需要增加塊緩存大小來容納更多的塊
如果用戶負載基本都是讀請求,也可以增加塊緩存
塊緩存+memstore上限不能超過100%。默認他們的和是60%,只有當用戶確認有必要並且不會造成副作用時才調整
調整memstore限制
通過 hbase.regionserver.global.memstore.upperLimit 設置上限。默認是0.4
hbase.regionserver.global.memstore.lowerLimit 設置下限。默認是0.35
把上下限設置的接近一點來避免過度刷寫
如果主要處理讀請求,可以考慮同時減少memstore的上下限來增加塊緩存的空間。
如果刷寫的數據量很小,比如只有5MB,就可以增加存儲限制來降低IO操作
增加阻塞是存儲文件數目
hbase.hstore.blockingStoreFiles 設置,決定了當存儲文件的數據達到閥值時,所有更新操作(put,delete)等會被阻塞。然後執行合並操作。默認值是7
如果文件數一直很高,就不要提高該配置項,因為這樣只會延遲問題的發生,而不能避免
增加阻塞倍率
hbase.hregion.memstore.block.multiplier 的默認值為2 。當memstore達到屬性 multiplier 乘以 flush 的大小限制時會阻止進一步的更新
當有足夠的存儲空間時,用戶可以增加這個值來增加平滑的處理寫入突發流量
減少最大日志
設置 hbase.regionserver.maxlogs 屬性是的用戶基於磁盤的WAL文件數目,控制刷寫頻率。默認值是32
對於寫壓力比較大的應用來說要把這個值調低,可以讓數據更頻繁的寫到硬盤上,這樣已經刷寫到硬盤上的日志就可以被丟棄
負載測試
PE
HBase有自帶的壓力測試工具名叫 PE(Performance Evaluation)
YCSB
Yahoo 推出的雲服務基准測試工具。比PE更好用,可以對hbase進行壓力測試
YCSB提供了更多的選項,並且能夠將讀寫負載混合在一起
HBase 性能優化筆記
1 hbase.hregion.max.filesize應該設置多少合適
默認值:256M
說明:Maximum HStoreFile size. If any one of a column families' HStoreFiles has grown to exceed this value, the hosting HRegion is split in two.
HStoreFile的最大值。如果任何一個Column Family(或者說HStore)的HStoreFiles的大小超過這個值,那麼,其所屬的HRegion就會Split成兩個。
調優:
hbase中hfile的默認最大值(hbase.hregion.max.filesize)是256MB,而google的bigtable論文中對tablet的最大值也推薦為100-200MB,這個大小有什麼秘密呢?
眾所周知hbase中數據一開始會寫入memstore,當memstore滿64MB以後,會flush到disk上而成為storefile。當storefile數量超過3時,會啟動compaction過程將它們合並為一個storefile。這個過程中會刪除一些timestamp過期的數據,比如update的數據。而當合並後的storefile大小大於hfile默認最大值時,會觸發split動作,將它切分成兩個region。
lz進行了持續insert壓力測試,並設置了不同的hbase.hregion.max.filesize,根據結果得到如下結論:值越小,平均吞吐量越大,但吞吐量越不穩定;值越大,平均吞吐量越小,吞吐量不穩定的時間相對更小。
為什麼會這樣呢?推論如下:
a 當hbase.hregion.max.filesize比較小時,觸發split的機率更大,而split的時候會將region offline,因此在split結束的時間前,訪問該region的請求將被block住,客戶端自我block的時間默認為1s。當大量的region同時發生split時,系統的整體訪問服務將大受影響。因此容易出現吞吐量及響應時間的不穩定現象
b 當hbase.hregion.max.filesize比較大時,單個region中觸發split的機率較小,大量region同時觸發split的機率也較小,因此吞吐量較之小hfile尺寸更加穩定些。但是由於長期得不到split,因此同一個region內發生多次compaction的機會增加了。compaction的原理是將原有數據讀一遍並重寫一遍到hdfs上,然後再刪除原有數據。無疑這種行為會降低以io為瓶頸的系統的速度,因此平均吞吐量會受到一些影響而下降。
綜合以上兩種情況,hbase.hregion.max.filesize不宜過大或過小,256MB或許是一個更理想的經驗參數。對於離線型的應用,調整為128MB會更加合適一些,而在線應用除非對split機制進行改造,否則不應該低於256MB
2 autoflush=false的影響
無論是官方還是很多blog都提倡為了提高hbase的寫入速度而在應用代碼中設置autoflush=false,然後lz認為在在線應用中應該謹慎進行該設置。原因如下:
a autoflush=false的原理是當客戶端提交delete或put請求時,將該請求在客戶端緩存,直到數據超過2M(hbase.client.write.buffer決定)或用戶執行了hbase.flushcommits()時才向regionserver提交請求。因此即使htable.put()執行返回成功,也並非說明請求真的成功了。假如還沒有達到該緩存而client崩潰,該部分數據將由於未發送到regionserver而丟失。這對於零容忍的在線服務是不可接受的。
b autoflush=true雖然會讓寫入速度下降2-3倍,但是對於很多在線應用來說這都是必須打開的,也正是hbase為什麼讓它默認值為true的原因。當該值為true時,每次請求都會發往regionserver,而regionserver接收到請求後第一件事就是寫hlog,因此對io的要求是非常高的,為了提高hbase的寫入速度,應該盡可能高地提高io吞吐量,比如增加磁盤、使用raid卡、減少replication因子數等
3 從性能的角度談table中family和qualifier的設置
對於傳統關系型數據庫中的一張table,在業務轉換到hbase上建模時,從性能的角度應該如何設置family和qualifier呢?
最極端的,①每一列都設置成一個family,②一個表僅有一個family,所有列都是其中的一個qualifier,那麼有什麼區別呢?
從讀的方面考慮:
family越多,那麼獲取每一個cell數據的優勢越明顯,因為io和網絡都減少了。
如果只有一個family,那麼每一次讀都會讀取當前rowkey的所有數據,網絡和io上會有一些損失。
當然如果要獲取的是固定的幾列數據,那麼把這幾列寫到一個family中比分別設置family要更好,因為只需一次請求就能拿回所有數據。
從寫的角度考慮:
首先,內存方面來說,對於一個Region,會為每一個表的每一個Family分配一個Store,而每一個Store,都會分配一個MemStore,所以更多的family會消耗更多的內存。
其次,從flush和compaction方面說,目前版本的hbase,在flush和compaction都是以region為單位的,也就是說當一個family達到flush條件時,該region的所有family所屬的memstore都會flush一次,即使memstore中只有很少的數據也會觸發flush而生成小文件。這樣就增加了compaction發生的機率,而compaction也是以region為單位的,這樣就很容易發生compaction風暴從而降低系統的整體吞吐量。
第三,從split方面考慮,由於hfile是以family為單位的,因此對於多個family來說,數據被分散到了更多的hfile中,減小了split發生的機率。這是把雙刃劍。更少的split會導致該region的體積比較大,由於balance是以region的數目而不是大小為單位來進行的,因此可能會導致balance失效。而從好的方面來說,更少的split會讓系統提供更加穩定的在線服務。而壞處我們可以通過在請求的低谷時間進行人工的split和balance來避免掉。
因此對於寫比較多的系統,如果是離線應該,我們盡量只用一個family好了,但如果是在線應用,那還是應該根據應用的情況合理地分配family。
4 hbase.regionserver.handler.count
RegionServer端開啟的RPC監聽器實例個數,也即RegionServer能夠處理的IO請求線程數。默認是10.
此參數與內存息息相關。該值設置的時候,以監控內存為主要參考。
對於 單次請求內存消耗較高的Big PUT場景(大容量單次PUT或設置了較大cache的scan,均屬於Big PUT)或ReigonServer的內存比較緊張的場景,可以設置的相對較小。
對於 單次請求內存消耗低,TPS(TransactionPerSecond,每秒事務處理量)要求非常高的場景,可以設置的相對大些。