主頁 >  其他 > 如何基于HBase構建容納大規模資料、支撐高并發、毫秒回應、穩定高效的OLTP實時系統

如何基于HBase構建容納大規模資料、支撐高并發、毫秒回應、穩定高效的OLTP實時系統

2020-10-05 19:14:32 其他

前言

本文致力于從架構原理、集群部署、性能優化與使用技巧等方面,闡述在如何基于HBase構建容納大規模資料、支撐高并發、毫秒回應、穩定高效的OLTP實時系統 ,

一、架構原理

1.1 基本架構


從上層往下可以看到HBase架構中的角色分配為:

Client——>Zookeeper——>HMaster——>RegionServer——>HDFS

Client

Client是執行查詢、寫入等對HBase表資料進行增刪改查的使用方,可以是使用HBase Client API撰寫的程式,也可以是其他開發好的HBase客戶端應用,

Zookeeper

Zookeeper同HDFS一樣,HBase使用Zookeeper作為集群協調與管理系統

在HBase中其主要的功能與職責為:

  • 存盤整個集群HMasterRegionServer的運行狀態
  • 實作HMaster的故障恢復自動切換
  • 為Client提供元資料表存盤資訊
  1. HMaster、RegionServer啟動之后,將會在Zookeeper上注冊并創建節點(/hbasae/master/hbase/rs/*),同時 Zookeeper 通過Heartbeat的心跳機制維護與監控節點狀態,一旦節點丟失心跳,則認為該節點宕機或者下線,將清除該節點在Zookeeper中的注冊資訊,
  2. 當Zookeeper中任一RegionServer節點狀態發生變化時,HMaster都會收到通知,并作出相應處理,例如RegionServer宕機,HMaster重新分配Regions至其他RegionServer,以保證集群整體可用性
  3. 當HMaster宕機時(Zookeeper監測到心跳超時),Zookeeper中的 /hbasae/master 節點將會消失,同時Zookeeper通知其他備用HMaster節點重新創建 /hbasae/master 并轉化為active master,

協調程序示意圖如下:


除了作為集群中的協調者,Zookeeper還為Client提供了 hbase:meta 表的存盤資訊,

客戶端要訪問HBase中的資料,只需要知道Zookeeper集群的連接資訊,訪問步驟如下:

  1. 客戶端將從Zookeeper(/hbase/meta-region-server)獲得 hbase:meta 表存盤在哪個RegionServer快取該位置資訊
  2. 查詢該RegionServer上的 hbase:meta 表資料,查找要操作的 rowkey所在的Region存盤在哪個RegionServer中,快取該位置資訊
  3. 在具體的RegionServer上,根據rowkey檢索該Region資料

可以看到,客戶端操作資料程序并不需要HMaster的參與,通過Zookeeper間接訪問RegionServer來操作資料

第一次請求將會產生3次RPC,之后使用相同的rowkey時,客戶端將直接使用快取下來的位置資訊,直接訪問RegionServer,直至快取失效(Region失效遷移等原因),

通過Zookeeper的讀寫流程如下:


hbase:meta 表存盤了集群中所有Region位置資訊,
表結構如下:

rowkey規則:${表名},${起始鍵},${region時間戳}.${encode編碼}.

列簇:info

state:Region狀態,正常情況下為 OPEN
serverstartcode:RegionServer啟動的13位時間戳
server:所在RegionServer 地址和埠,如cdh85-47:16020
sn:server和serverstartcode組成,如cdh85-47:16020,1549491783878
seqnumDuringOpen:Region在線時長的二進制串
regioninfo:region的詳細資訊,如:ENCODED、NAME、STARTKEY、ENDKEY等
ENCODED:基于${表名},${起始鍵},${region時間戳}生成的32位md5字串,
region資料存盤在hdfs上時使用的唯一編號,可以從meta表中根據該值定位到hdfs中的具體路徑,
rowkey中最后的${encode編碼}就是 ENCODED 的值,其是rowkey組成的一部分,
NAME:與ROWKEY值相同
STARTKEY:該region的起始鍵
ENDKEY:該region的結束鍵

簡單總結Zookeeper在HBase集群中的作用如下:

  • 對于服務端,是實作集群協調控制的重要依賴,
  • 對于客戶端,是查詢操作資料必不可少的一部分,

HMaster

  1. HBase整體架構中HMaster的功能與職責如下:
  2. 管理RegionServer,監聽其狀態,保證集群負載均衡且高可用,
  3. 管理Region,如新Region的分配RegionServer宕機時該節點Region的分配與遷移
  4. 接收客戶端的DDL操作,如創建與洗掉表、列簇等資訊
  5. 權限控制

如我們前面所說的,HMaster 通過 Zookeeper 實作對集群中,各個 RegionServer 的監控與管理,在RegionServer 發生故障時,可以發現節點宕機,并轉移 Region 至其他節點以保證服務的可用性,

但是HBase的故障轉移并不是無感知的,相反故障轉移程序中,可能會直接影響到線上請求的穩定性,造成段時間內的大量延遲

在分布式系統的 CAP定理中(Consistency一致性、Availability可用性、Partition tolerance磁區容錯性),分布式資料庫基本特性都會實作P,但是不同的資料庫對于A和C各有取舍,

如HBase選擇了C而通過Zookeeper這種方式來輔助實作A(雖然會有一定缺陷),而Cassandra選擇了A,通過其他輔助措施實作了C,各有優劣,

對于HBase集群來說,HMaster是一個內部管理者,除了DDL操作并不對外(客戶端)開放,因而HMaster的負載是比較低的,

造成HMaster壓力大的情況,可能是集群中存在多個(兩個或者三個以上)HMaster,備用的Master會定期與Active Master通信,以獲取最新的狀態資訊,以保證故障切換時自身的資料狀態是最新的,因而Active Master可能會收到大量來自備用Master的資料請求

RegionServer

RegionServer在HBase集群中的功能與職責:

  • 根據HMaster的region分配請求,存放和管理Region
  • 接受客戶端的讀寫請求,檢索與寫入資料,產生大量IO
  • 一個RegionServer中存盤并管理者多個Region,是HBase集群中真正 存盤資料、接受讀寫請求 的地方,是HBase架構中最核心、同時也是最復雜的部分,

RegionServer內部結構圖如下:


BlockCache

BlockCache為RegionServer中的讀快取一個RegionServer共用一個BlockCache,

RegionServer處理客戶端讀請求的程序:

  1. 在BlockCache中查詢是否命中快取
  2. 快取未命中,則定位到存盤該資料的Region
  3. 檢索Region Memstore中,是否有所需要的資料
  4. Memstore中未查得,則檢索Hfiles
  5. 任一程序查詢成功,則將資料回傳給客戶端,并快取至BlockCache,

BlockCache有兩種實作方式,有不同的應用場景,各有優劣

  • On-Heap的LRUBlockCache
  1. 優點:直接從Java堆內記憶體獲取,回應速度快,
  2. 缺陷:容易受GC影響,回應延遲不穩定,特別是在堆記憶體巨大的情況下
  3. 適用于:寫多讀少型、小記憶體等場景
  • Off-Heap的BucketCache
  1. 優點:無GC影響延遲穩定
  2. 缺陷:從堆外記憶體獲取資料性能略差于堆內記憶體
  3. 適用于:讀多寫少型、大記憶體等場景

我們將在「性能優化」一節中具體討論如何判斷應該使用哪種記憶體模式,

WAL

全稱 Write Ahead Log ,是 RegionServer 中的預寫日志,

所有寫入資料,默認情況下,都會先寫入WAL中,以保證RegionServer宕機重啟之后,可以通過WAL來恢復資料,一個RegionServer中共用一個WAL,

RegionServer的寫流程如下:

  1. 將資料寫入WAL中
  2. 根據TableName、Rowkey和ColumnFamily將資料寫入對應的Memstore
  3. Memstore通過特定演算法記憶體中的資料刷寫成Storefile寫入磁盤,并標記WAL sequence值
  4. Storefile定期合小檔案

WAL會通過日志滾動的操作,定期對日志檔案進行清理已寫入HFile中的資料可以清除),對應HDFS上的存盤路徑為 /hbase/WALs/${HRegionServer_Name} ,

Region

一個Table由一個或者多個Region組成,一個Region中可以看成是Table按行切分有序的資料塊,每個Region都有自身的StartKey、EndKey,

一個Region由一個或者多個Store組成,每個Store存盤該Table對應Region中一個列簇的資料,相同列簇的列,存盤在同一個Store中,

同一個Table的Region,會分布在集群中不同的RegionServer上,以實作讀寫請求的負載均衡,故,一個RegionServer中,將會存盤來自不同Table的N多個Region

Store、Region與Table的關系可以表述如下:多個Store(列簇)組成Region多個Region(行資料塊)組成完整的Table

其中,Store由Memstore(記憶體)StoreFile(磁盤)兩部分組成

在RegionServer中,Memstore可以看成指定Table、Region、Store的寫快取(正如BlockCache小節中所述,Memstore還承載了一些讀快取的功能),以RowKey、Column Family、Column、Timestamp進行排序,如下圖所示:


寫請求到RegionServer之后,并沒有立刻寫入磁盤中,而是先寫入記憶體中的Memstore(記憶體中資料丟失問題,可以通過回放WAL解決)以提升寫入性能,

Region中的Memstore,會根據特定演算法,將記憶體中的資料,將會刷寫到磁盤,形成Storefile檔案,因為資料在Memstore中為已排序,順序寫入磁盤性能高、速度快,

在這種 Log-Structured Merge Tree架構模式下,隨機寫入HBase擁有相當高的性能,

Memstore刷磁盤形成的StoreFile,以HFile格式,存盤HBase的KV資料于HDFS之上,

HDFS

HDFS為HBase提供底層存盤系統,通過HDFS的高可用、高可靠等特性,保障了HBase的資料安全容災與備份

1.2寫資料 與 Memstore Flush

對于客戶端來說,將請求發送到需要寫入的RegionServer,等待RegionServer寫入WAL、Memstore之后,即回傳寫入成功的ack信號,
對于RegionServer來說,寫入的資料,還需要經過一系列的處理步驟,

首先我們知道Memstore是在記憶體中的,將資料放在記憶體中,可以得到優異的讀寫性能,但是同樣也會帶來麻煩:

  1. 記憶體中的資料如何防止斷電丟失
  2. 資料存盤于記憶體中的代價是高昂的,空間總是有限的

對于第一個問題,雖然可以通過WAL機制在重啟的時候,進行資料回放,但是對于第二個問題,則必須將記憶體中的資料持久化到磁盤中

在不同情況下,RegionServer通過不同級別的刷寫策略,對Memstore中的資料進行持久化,根據觸發刷寫動作的時機,以及影響范圍,可以分為不同的幾個級別:

  1. Memstore級別:Region中任意一個MemStore達到了 hbase.hregion.memstore.flush.size 控制的上限(默認128MB),會觸發Memstore的flush,
  2. Region級別:Region中Memstore大小之和達到了 hbase.hregion.memstore.block.multiplier *, hbase.hregion.memstore.flush.size 控制的上限(默認 2 * 128M = 256M),會觸發Memstore的flush
  3. RegionServer級別:Region Server中所有Region的Memstore大小總和達到了 hbase.regionserver.global.memstore.upperLimit * hbase_heapsize 控制的上限(默認0.4,即RegionServer 40%的JVM記憶體),將會按Memstore由大到小進行flush,直至總體Memstore記憶體使用量低于 hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize 控制的下限(默認0.38, 即RegionServer 38%的JVM記憶體),
  4. RegionServer中HLog數量達到上限:將會選取最早的 HLog對應的一個或多個Region進行flush通過引數hbase.regionserver.maxlogs配置),
  5. HBase定期flush:確保Memstore不會長時間沒有持久化,默認周期為1小時,為避免所有的MemStore,在同一時間都進行flush導致的問題,定期的flush操作,有20000左右的隨機延時
  6. 手動執行flush:用戶可以通過shell命令 flush ‘tablename’或者flush ‘region name’,分別一個表或者一個Region進行flush,

Memstore刷寫時,會阻塞線上的請求回應,由此可以看到,不同級別的刷寫,對線上的請求,會造成不同程度影響的延遲:

  1. 對于MemstoreRegion級別的刷寫,速度是比較快的,并不會對線上造成太大影響
  2. 對于RegionServer級別的刷寫,將會阻塞發送到該RegionServer上的所有請求,直至Memstore刷寫完畢,會產生較大影響

所以在Memstore的刷寫方面,需要盡量避免出現RegionServer級別的刷寫動作,

資料在經過Memstore刷寫到磁盤時,對應的會寫入WAL sequence的相關資訊,已經持久化到磁盤的資料,就沒有必要通過WAL記錄的必要,

RegionServer會根據這個sequence值,對WAL日志進行滾動清理,防止WAL日志數量太多,RegionServer啟動時,加載太多資料資訊
同樣,在Memstore的刷寫策略中,可以看到,為了防止WAL日志數量太多,達到指定閾值之后,將會選擇WAL記錄中,最早的一個或者多個Region進行刷寫,

1.3讀資料 與 Bloom Filter

經過前文的了解,我們現在可以知道HBase中一條資料完整的讀取操作流程中Client會和Zookeeper、RegionServer等發生多次互動請求,

基于HBase的架構,一條資料可能存在RegionServer中的三個不同位置:

  1. 對于剛讀取過的資料將會被快取到BlockCache中
  2. 對于剛寫入的資料,其存在Memstore中
  3. 對于之前已經從Memstore刷寫磁盤的,其存在于HFiles中

RegionServer接收到一條資料查詢請求,只需要從以上三個地方,檢索到資料即可,

在HBase中的檢索順序依次是:BlockCache -> Memstore -> HFiles

其中,BlockCache、Memstore都是直接在記憶體中進行高性能的資料檢索,

HFiles則是真正存盤在HDFS上的資料

  1. 檢索HFiles時會產生真實磁盤的IO操作
  2. Memstore不停刷寫的程序中,將會產生大量的HFile

如何在大量的HFile中快速找到所需要的資料呢?

為了提高檢索HFiles的性能,HBase支持使用 Bloom FliterHFiles進行快讀定位

Bloom Filter(布隆過濾器)是一種資料結構,常用于大規模資料查詢場景,其能夠快速判斷一個元素一定不在集合中,或者可能在集合中,
Bloom Filter由 一個長度為m位陣列 k個哈希函式 組成,

其作業原理如下:

  • 原始集合寫入一個元素時,Bloom Filter同時將該元素 經過k個哈希函式映射成k個數字,并以這些數字為下標,將 位陣列 中對應下標的元素標記為1
  • 當需要判斷一個元素是否存在于原始集合中,只需要將該元素經過同樣的 k個哈希函式得到k個數字
  1. 位陣列 中對應下標的元素,如果都為1,則表示元素可能存在
  2. 如果存在其中一個元素為0,則該元素不可能存在于原始集合中
  • 因為哈希碰撞問題不同的元素經過相同的哈希函式之后可能得到相同的值
  1. 對于集合外的一個元素,如果經過 k個函式得到的k個數字,對應位陣列中的元素都為1,可能是該元素存在于集合中
  2. 也有可能是集合中的其他元素”碰巧“讓這些下標對應的元素標記為1,所以只能說其可能存在
  • 對于集合中的不同元素,如果 經過k個函式得到的k個數字中,任意一個重復
  1. 位陣列 中對應下標的元素會被覆寫,此時該下標的元素不能被洗掉即歸零
  2. 洗掉可能會導致其他多個元素在Bloom Filter表示不「存在」

由此可見,Bloom Filter中:

  • 位陣列的長度m越大,誤差率越小,而存盤代價越大
  • 哈希函式的個數k越多,誤差率越小,而性能越低

HBase中支持使用以下兩種Bloom Filter:

  1. ROW:基于 Rowkey 創建的Bloom Filter
  2. ROWCOL基于 Rowkey+Column 創建的Bloom Filter

兩者的區別僅僅是:是否使用列資訊作為Bloom Filter的條件,

  1. 使用ROWCOL時,可以讓指定列的查詢更快,因為其通過Rowkey列資訊來過濾不存在資料的HFile,但是相應的,產生的Bloom Filter資料會更加龐大,
  2. 而只通過Rowkey進行檢索的查詢,即使指定了ROWCOL也不會有其他效果,因為沒有攜帶列資訊
  3. 通過Bloom Filter(如果有的話)快速定位當前的Rowkey資料存盤于哪個HFile之后(或者不存在直接回傳),通過HFile攜帶的 Data Block Index 等元資料資訊,可快速定位到具體的資料塊起始位置,讀取并回傳(加載到快取中),

這就是Bloom Filter在HBase檢索資料的應用場景:

  1. 高效判斷key是否存在
  2. 高效定位key所在的HFile

當然,如果沒有指定創建Bloom Filter,RegionServer將會花費比較多的力氣,一個個檢索HFile,來判斷資料是否存在,

1.4 HFile存盤格式

通過Bloom Filter快速定位需要檢索的資料,所在的HFile之后的操作,自然是從HFile中讀出資料,并回傳,

據我們所知,HFile是HDFS上的檔案(或大或小都有可能),現在HBase面臨的一個問題,就是如何在HFile中 快速檢索獲得指定資料?

HBase隨機查詢的高性能,很大程度上取決于底層HFile的存盤格式,所以這個問題可以轉化為 HFile的存盤格式,該如何設計,才能滿足HBase 快速檢索 的需求,

生成一個HFile

Memstore記憶體中的資料,在刷寫到磁盤時,將會進行以下操作:

  1. 會先現在記憶體中創建 空的Data Block資料塊 包含 預留的Header空間,而后,將Memstore中的KVs一個個順序寫滿該Block一般默認大小為64KB),
  2. 如果指定了壓縮或者加密演算法Block資料寫滿之后,將會對整個資料區相應的壓縮或者加密處理,
  3. 隨后在預留的Header區,寫入該Block的元資料資訊,如 壓縮前后大小上一個block的offsetchecksum 等,
  4. 記憶體中的準備作業完成之后,通過HFile Writer輸出流將資料寫入到HDFS中,形成磁盤中的Data Block
  5. 為輸出的Data Block生成一條索引資料,包括 {startkeyoffsetsize} 資訊,該索引資料會被暫時記錄在記憶體中的Block Index Chunk中,

至此,已經完成了第一個 Data Block 的寫入作業,Memstore中的 KVs 資料,將會按照這個程序,不斷進行 寫入記憶體 中的Data Block ——> 輸出到HDFS——> 生成索引資料保存到記憶體中Block Index Chunk 流程

值得一提的是,如果啟用了Bloom Filter,那么 Bloom Filter Data(位圖資料Bloom元資料(哈希函式與個數等) 將會和 KVs 資料一樣被處理:寫入記憶體中的Block ——> 輸出到HDFS Bloom Data Block ——>生成索引資料保存到相對應的記憶體區域中,

由此我們可以知道,HFile寫入程序中,Data BlockBloom Data Block交叉存在的

隨著輸出的Data Block越來越多,記憶體中的索引資料Block Index Chunk也會越來越大,

達到一定大小之后(默認128KB)將會經過類似Data Block的輸出流程,寫入到HDFS中,形成 Leaf Index Block (和Data Block一樣,Leaf Index Block也有對應的Header區,保留該Block的元資料資訊),

同樣的,也會生成一條該 Leaf Index Block 對應的索引記錄,保存在記憶體中的 Root Block Index Chunk

Root Index ——> Leaf Data Block ——> Data Block 的索引關系,類似 B+樹 的結構,得益于多層索引,HBase可以在不讀取整個檔案的情況下查找資料,

隨著記憶體中最后一個 Data Block、Leaf Index Block 寫入到HDFS,形成 HFile 的 Scanned Block Section,

Root Block Index Chunk 也會從記憶體中寫入HDFS,形成 HFile 的 Load-On-Open Section 的一部分,
至此,一個完整的HFile已經生成,如下圖所示:


檢索HFile

生成HFile之后該如何使用呢?

HFile的索引資料(包括 Bloom Filter索引資料索引資訊)會在 Region Open 的時候被加載到讀快取中,之后資料檢索經過以下程序:

  1. 所有的讀請求,如果讀快取Memstore中不存在,那么將會檢索HFile索引
  2. 通過Bloom Filter索引(如果有設定Bloom Filter的話)檢索Bloom Data快速定位HFile是否存在 所需資料
  3. 定位到資料可能存在HFile之后,讀取該HFile的 三層索引資料,檢索資料是否存在
  4. 存在,則根據索引中的 元資料 ,找到具體的 Data Block 讀入記憶體,取出所需的KV資料

可以看到,在HFile的資料檢索程序中,一次讀請求,只有 真正確認資料存在,需要讀取硬碟資料的時候,才會 執行硬碟查詢操作
同時,得益于 分層索引分塊存盤,在Region Open加載索引資料的時候,再也不必和老版本(0.9甚至更早,HFile只有一層資料索引并且統一存盤)一樣加載所有索引資料到記憶體中,導致啟動緩慢甚至卡機等問題

1.5 HFile Compaction

Bloom Filter解決了如何在大量的HFile中快速定位資料,所在的HFile檔案,雖然有了Bloom Filter的幫助,大大提升了檢索效率,但是對于RegionServer來說,要檢索的HFile數量并沒有減少,

為了再次提高HFile的檢索效率,同時避免大量小檔案的產生,造成性能低下,RegionServer會通過Compaction機制,HFile進行合并操作,

常見的Compaction觸發方式有:

  1. Memstore Flush檢測條件執行
  2. RegionServer定期檢查執行
  3. 用戶手動觸發執行

Minor Compaction

Minor Compaction 只執行簡單的檔案合并操作,選取較小的HFiles,將其中的資料順序寫入新的HFile后替換老的HFiles

但是如何在眾多HFiles中,選擇本次Minor Compaction,要合并的檔案卻有不少講究:

  1. 首先排除掉檔案大小 大于 hbase.hstore.compaction.max.size 值的HFile
  2. 將HFiles按照檔案年齡排序(older to younger),并從older file開始選擇
  3. 如果該檔案大小 小于 hbase.hstore.compaction.min ,則加入Minor Compaction中
  4. 如果該檔案大小 小于 后續hbase.hstore.compaction.max HFile大小之和 * hbase.hstore.compaction.ratio,則將該檔案加入Minor Compaction
  5. 掃描程序中,如果需要合并的HFile檔案數 達到 hbase.hstore.compaction.max(默認為10)開始合并程序
  6. 掃描結束后,如果需要合并的HFile的檔案數 大于 hbase.hstore.compaction.min(默認為3) 則開始合并程序
  7. 通過 hbase.offpeak.start.hourhbase.offpeak.end.hour 設定高峰、非高峰時期,使 hbase.hstore.compaction.ratio的值在不同時期靈活變化高峰值1.2非高峰值5

可以看到,Minor Compaction不會合并過大的HFile合并的HFile數量也有嚴格的限制,以避免產生太大的IO操作,Minor Compaction經常在Memstore Flush后觸發,但不會線上讀寫請求造成太大延遲影響

Major Compaction

相對于Minor Compaction 只合并選擇的一部分HFile合并合并時只簡單合并資料檔案的特點Major Compaction則將會把Store中的所有HFile合并成一個大檔案,將會產生較大的IO操作

同時將會清理三類無意義資料被洗掉的資料TTL過期資料版本號超過設定版本號的資料,Region Split程序中產生的Reference檔案也會在此時被清理
Major Compaction定期執行的條件由以下兩個引數控制:

  • hbase.hregion.majorcompaction:默認7天
  • hbase.hregion.majorcompaction.jitter:默認為0.2

集群中各個RegionServer將會在 hbase.hregion.majorcompaction +- hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter 區間浮動進行Major Compaction,以避免過多RegionServer同時進行,造成較大影響,

Major Compaction 執行時機觸發之后,簡單來說,如果當前Store中HFile的最早更新時間,早于某個時間值,就會執行Major Compaction,該時間值為 hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter

手動觸發的情況下將會直接執行Compaction,

Compaction的優缺點

HBase通過Compaction機制,使底層HFile檔案數,保持在一個穩定的范圍,減少一次請求產生的IO次數檔案Seek次數,確保HFiles檔案檢索效率,從而實作高效處理線上請求,

如果沒有Compaction機制,隨著Memstore刷寫資料越來越多,HFile檔案數量將會持續上漲一次讀請求生產的IO操作Seek檔案的次數將會越來越多,反饋到線上,就是請求延遲越來越大

然而,在Compaction執行程序中,不可避免的仍然會對線上造成影響

  1. 對于Major Compaction來說,合并程序將會占用大量帶寬IO資源,此時線上的讀延遲將會增大,
  2. 對于Minor Compaction來說,如果Memstore寫入的資料量太多刷寫越來越頻繁超出了HFile合并的速度,
  • 即使不停地在合并,但是HFile檔案仍然越來越多讀延遲也會越來越大
  • HBase通過 hbase.hstore.blockingStoreFiles(默認7)控制Store中的HFile數量
  • 超過配置值時,將會堵塞Memstore Flush阻塞flush操作 ,阻塞超時時間為 hbase.hstore.blockingWaitTime
  • 阻塞Memstore Flush操作將會使Memstore的記憶體占用率越來越高,可能導致完全無法寫入

簡而言之,Compaction機制保證了HBase的讀請求一直保持低延遲狀態,但付出的代價是Compaction執行期間大量的讀延遲毛刺和一定的寫阻塞(寫入量巨大的情況下),

1.6 Region Split

HBase通過 LSM-Tree架構提供了高性能的隨機寫,通過快取、Bloom FilterHFile與Compaction等機制提供了高性能的隨機讀

至此,HBase已經具備了作為一個高性能讀寫資料庫的基本條件,如果HBase僅僅到此為止的話,那么其也只是個在架構上傳統資料庫有所區別的資料庫而已,作為一個高性能讀寫分布式資料庫來說,其擁有近乎可以無限擴展的特性

支持HBase進行自動擴展負載均衡的是Region Split機制,

Split策略與觸發條件

在HBase中,提供了多種Split策略不同的策略觸發條件各不相同,


如上圖所示,不同版本使用的默認策略在變化,

ConstantSizeRegionSplitPolicy
固定值策略:閾值默認大小 hbase.hregion.max.filesize
優點:簡單實作
缺陷:考慮片面,小表不切分、大表切分成很多Region,線上使用弊端多
IncreasingToUpperBoundRegionSplitPolicy
非固定閾值:計算公式 min(R^2 * memstore.flush.size, region.split.size)
R為Region所在的Table,在當前RegionServer上Region的個數
最大大小 hbase.hregion.max.filesize
優點:自動適應大小表,對于Region個數多閾值大,Region個數少閾值小
缺陷:對于小表來說會產生很多小region
SteppingSplitPolicy:
非固定閾值:如果Region個數為1,則閾值為 memstore.flush.size * 2
否則為 region.split.size
優點:對大小表更加友好,小表不會一直產生小Region
缺點:控制力度比較粗

可以看到,不同的切分策略其實只是在尋找切分Region時的閾值不同的策略閾值有不同的定義

切分點

切分閾值確認完之后,首先要做的是尋找切分Region切分點

HBase對Region的切分點定義如下:

  1. Region中最大的Store中,最大的HFile中心block中首個Rowkey,
  2. 如果最大的HFile只有一個block,那么不切分沒有middle key),

得到切分點之后,核心的切分流程分為 prepare - execute - rollback 三個階段,

  • prepare階段

在記憶體中,初始化兩個子RegionHRegionInfo物件),準備進行切分操作

  • execute階段

execute階段執行流程較為復雜,具體實施步驟為:

1RegionServer在Zookeeper上的 /hbase/region-in-transition 節點中,標記該Region狀態為SPLITTING
2HMaster監聽到Zookeeper節點發生變化,在記憶體中,修改此Region狀態為RIT
3在該Region的存盤路徑下創建臨時檔案夾 .split
4父Region close,flush所有資料到磁盤中,停止所有寫入請求,
5在父Region的 .split檔案夾中,生成兩個子Region檔案夾,并寫入reference檔案
1.reference是一個特殊的檔案,體現
在其檔案名檔案內容
2.檔案名組成:{
父Region}
3.檔案內容:[splitkey]
切分點rowkey,[top?]true/false,true為top上半部分false為bottom下半部分
4.根據reference檔案名,可以快速找到對應的父Region、其中的HFile檔案HFile切分點從而確認該子Region的資料范圍
5.資料范圍確認完畢之后,進行正常的資料檢索流程(此時仍然檢索父Region的資料
6子Region的目錄拷貝到HBase根目錄下,形成新的Region
7父Regin通知修改 hbase:meta 表后下線不再提供服務
1.此時并沒有洗掉父Region資料,僅在表中標記split列offline列為true,并記錄兩個子region
8兩個子Region上線服務
9通知 hbase:meta 表標記兩個子Region正式提供服務
  • rollback階段

如果execute階段出現例外,則執行rollback操作,保證Region切分整個程序,是具備事務性原子性的要么切分成功要么回到未切分的狀態

region切分是一個復雜的程序,涉及到父region切分子region生成region下線與上線zk狀態修改元資料狀態修改master記憶體狀態修改 等多個子步驟,回滾程式,會根據當前進展到哪個子階段,清理對應的垃圾資料

為了實作事務性,HBase設計了使用狀態機SplitTransaction類),來保存切分程序中的每個子步驟狀態,這樣一來,一旦出現例外,系統可以根據當前所處的狀態,決定是否回滾以及如何回滾

但是目前實作中,中間狀態存盤在記憶體中,因此一旦在切分程序中,RegionServer宕機或者關閉,重啟之后,將無法恢復切分前的狀態,即Region切分處于中間狀態的情況,也就是RIT

由于Region切分子階段很多,不同階段,解決RIT的處理方式也不一樣,需要通過hbck工具進行,具體查看,并分析解決方案

好訊息是,HBase2.0之后提出了,新的分布式事務框架Procedure V2,將會使用HLog存盤事務中間狀態,從而保證事務處理中,宕機重啟后,可以進行回滾或者繼續處理,從而減少RIT問題產生,

  • 父Region清理

從以上程序中我們可以看到,Region的切分程序,并不會父Region的資料子Region中只是子Region中創建了reference檔案,故Region切分程序是很快的,

只有進行Major Compaction時,才會真正(順便)將資料切分到子Region中,將HFile中的kv順序讀出寫入新的HFile檔案

RegionServer將會定期檢查 hbase:meta 表中的splitoffline為true的Region,對應的子Region,是否存在reference檔案,如果不存在則洗掉父Region資料,

  • 負載均衡

Region切分完畢之后RegionServer將會存在更多的Region塊,為了避免RegionServer熱點,使請求負載均衡集群各個節點上,HMaster將會把一個或者多個子Region移動其他RegionServer上,

移動程序中,如果當前RegionServer繁忙HMaster只會修改Region元資料資訊,其他節點,而Region資料,仍然保留在當前節點中,直至下一次Major Compaction時進行資料移動

至此,我們已經揭開了HBase架構與原理的大部分神秘面紗,在后續做集群規劃性能優化實際應用中,為什么這么調整,以及為什么這么操作,都將一一映射到HBase的實作原理上,

如果你希望了解HBase的更多細節,可以參考《HBase權威指南》,

二、集群部署

經過冗長的理論初步了解過HBase架構與作業原理之后,搭建HBase集群是使用HBase的第一個步驟,
需要注意的是,HBase集群一旦部署使用,再想對其作出調整需要付出慘痛代價(線上環境中),所以如何部署HBase集群是使用的第一個關鍵步驟,

2.1 集群物理架構

硬體混合型+軟體混合型集群:

  1. 硬體混合型 :指的是該集群機器配置參差不齊混搭結構
  2. 軟體混合型 :指的是該集群部署了一套類似CDH全家桶套餐

如以下的軟體混合型集群狀況:

  1. 集群規模:30
  2. 部署服務:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
  3. 硬體情況:記憶體、CPU、磁盤等參差不齊,有高配有低配,混搭結構

這個集群不管是規模、還是服務部署方式相信都是很多都有公司的「標準」配置,

那么這樣的集群有什么問題呢?

如果僅僅HBase是一個非「線上」的系統,或者充當一個歷史冷資料存盤的大資料庫,這樣的集群其實一點問題也沒有因為對其沒有任何苛刻的性能要求,

但是如果希望HBase作為一個線上能夠承載海量并發實時回應的系統,這個集群隨著使用時間的增加很快就會崩潰,

從 硬體混合型 來說,一直以來Hadoop都是以宣稱能夠用低廉、老舊的機器撐起一片天,

這確實是Hadoop的一個大優勢,然而前提是作為離線系統使用,

離線系統的定義,即跑批的系統,如:Spark、Hive、MapReduce等,沒有很強的時間要求,顯著的吞吐量大,延遲高,

因為沒有實時性要求,幾臺拖拉機跑著也沒有問題,只要最后能出結果并且結果正確就OK,

那么在我們現在的場景中,對HBase的定義已經不是一個離線系統,而是一個實時系統

對于一個硬性要求很高的實時系統來說,如果其中幾臺老機器拖了后腿也會引起線上回應的延遲,

統一高配硬體+軟體混合型集群

既然硬體拖后腿,那么硬體升級自然是水到渠成,
現在我們有全新的高配硬體可以使用,參考如下:

  1. 集群規模:30
  2. 部署服務:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
  3. 硬體情況:記憶體、CPU、磁盤統一高配置

這樣的集群可能還會存在什么問題呢?

軟體混合型 來說,離線任務最大的特點就是吞吐量特別高,瞬間讀寫的資料量,可以把IO直接撐到10G/s,最主要的影響因素,就是大型離線任務,帶動高IO,將會影響HBase的回應性能

如果僅止步于此,那么線上的表現僅僅為短暫延遲,真正令人窒息的操作是,如果離線任務再把CPU撐爆RegionServer節點可能會直接宕機,造成嚴重的生產影響,

存在的另外一種情況是,離線任務大量讀寫磁盤讀寫HDFS導致HBase IO連接例外,也會造成RegionServer例外HBase日志反應HDFS connection timeout,HDFS日志反應IO Exception),造成線上故障,

根據觀測,集群磁盤IO到4G以上集群網路IO 8G以上HDFS IO 5G以上任意符合一個條件,線上將會有延遲反應

因為離線任務運行太過強勢,導致RegionServer宕機,無法解決,那么能采取的策略,只能是重新調整離線任務的執行,使用資源執行順序等限制離線計算能力來滿足線上的需求,同時還要限制集群的CPU的使用率,可能出現,某臺機器CPU打滿后,整個機器假死,致服務例外,造成線上故障

軟、硬體獨立的HBase集群

簡而言之,無論是硬體混合型還是軟體混合型集群,其可能因為各種原因帶來的延遲影響,對于一個高性能要求的HBase來說,都是無法忍受的,
所以在集群規劃初始就應該考慮到種種情況,最好使用獨立的集群部署HBase

參考如下一組集群規模配置:

  1. 集群規模:15+5(RS+ZK
  2. 部署服務:HBase、HDFS(另5臺虛擬Zookeeper
  3. 硬體情況:除虛擬機外,物理機統一高配置

雖然從可用節點上來看,比之前的參考配置少了一半,但是從集群部署模式上看,最大程度保證HBase的穩定性,從根本上,分離了軟硬體對HBase所帶來的影響,將會擁有比之前兩組集群配置 更穩定的回應和更高的性能

其他硬體推薦

  • 網卡:網卡是容易產生瓶頸的地方,有條件建議使用雙萬兆網卡
  • 磁盤:沒有特殊要求,空間越大越好轉速越高越好
  • 記憶體:不需要大容量記憶體,建議32-128G(詳見下文)
  • CPU:CPU核數越多越好,HBase本身壓縮資料合并HFile等都需要CPU資源
  • 電源:建議雙電源冗余

另外值得注意的是,Zookeeper節點建議設定5個節點5個節點能保證Leader快速選舉并且最多可以允許2個節點宕機的情況下正常使用,

硬體上可以選擇使用虛擬機,因為zk節點本身消耗資源并不大不需要高配機器,但是5個虛擬節點不能在一個物理機上防止物理機宕機影響所有zk節點,

2.2 安裝與部署

以CDH集群為例安裝HBase,
使用ansible自動化腳本工具進行安裝操作:

# 獲取安裝腳本,上傳相關安裝軟體包至服務器(JDK、MySQL、CM、CDH等)
yum install -y git
git clone https://github.com/chubbyjiang/cdh-deploy-robot.git
cd cdh-deploy-robot
# 編輯節點主機名
vi hosts
# 修改安裝配置項
vi deploy-robot.cnf
# 執行
sh deploy-robot.sh install_all

安裝腳本,將會執行 配置SSH免密登錄、安裝軟體、作業系統優化、Java等開發環境初始化、MySQL安裝、CM服務安裝、作業系統性能測驗等程序,
腳本操作說明見:CDH集群自動化部署工具 ,
等待cloudera-scm-server行程起來后,在瀏覽器輸入 ip:7180 進入CM管理界面部署HDFS、HBase組件即可,

三、性能優化

HBase集群部署完畢運行起來之后,看起來一切順利,但是所有東西都處于「初始狀態」中,

我們需要根據軟硬體環境,針對性地對HBase進行 調優設定,以確保其能夠以最完美的狀態運行,在當前集群環境中,盡可能發揮硬體的優勢,

為了方便后續配置項計算說明,假設我們可用的集群硬體狀況如下:

  • 總記憶體:256G
  • 總硬碟:1.8T * 12 = 21.6T
  • 可分配記憶體:256 * 0.75 = 192G
  • HBase可用記憶體空間:192 * 0.8 = 153G(20%留給HDFS等其他行程)
  • 可用硬碟空間:21.6T * 0.85 = 18.36T

3.1 Region規劃

對于Region的大小,HBase官方檔案推薦單個在10G-30G之間,單臺RegionServer的數量,控制在20-300之間(當然,這僅僅是參考值),
Region過大過小都會有不良影響:

的Region
優點:遷移速度快、減少總RPC請求
缺點:compaction的時候資源消耗非常大、可能會有資料分散不均衡的問題
的Region
優點:集群負載平衡、HFile比較少,compaction影響小
缺點:遷移或者balance效率低、頻繁flush導致頻繁的compaction、維護開銷大

規劃Region的大小與數量時可以參考以下演算法:

0. 計算HBase可用磁盤空間(單臺RegionServer)
1. 設定region最大與最小閾值,region的大小在此區間選擇,如10-30G
2. 設定最佳region數(這是一個經驗值),如單臺RegionServer 200個
3. 從region最小值開始,計算 HBase可用磁盤空間 / (region_size * hdfs副本數) = region個數
4. 得到的region個數如果 > 200,則增大region_size(step可設定為5G),
繼續計算直至找到region個數最接近200的region_size大小
5. region大小建議不小于10G

例如:

當前可用磁盤空間為18T,選擇的region大小范圍為10-30G,最佳region個數為300
那么最接近 最佳Region個數300的 region_size 值為30G

得到以下配置項:

  • hbase.hregion.max.filesize=30G
  • 單節點最多可存盤的Region個數約為300

3.2 記憶體規劃

我們知道RegionServer中的BlockCache有兩種實作方式:

  • LRUBlockCache:On-Heap
  • BucketCache:Off-Heap

這兩種模式的詳細說明可以參考 CDH官方檔案,

為HBase選擇合適的 記憶體模式 以及根據 記憶體模式 計算相關配置項是調優中的重要步驟

首先我們可以根據可用記憶體大小判斷使用哪種記憶體模式

先看 超小記憶體(假設8G以下) 和 超大記憶體(假設128G以上) 兩種極端情況:

  • 對于超小記憶體來說,即使可以使用BucketCache來利用堆外記憶體,但是使用堆外記憶體的主要目的,是避免GC時不穩定的影響,堆外記憶體的效率是要比堆內記憶體的,由于記憶體總體較小,即使讀寫快取都在堆內記憶體中,GC時也不會造成太大影響,所以可以直接選擇LRUBlockCache
  • 對于超大記憶體來說,在超大記憶體上使用LRUBlockCache,將會出現我們所擔憂的情況:GC時,對線上造成很不穩定延遲影響,這種場景下,應該盡量利用堆外記憶體作為讀快取減小堆內記憶體的壓力,所以可以直接選擇BucketCache

在兩邊的極端情況下,我們可以根據記憶體大小,選擇合適的記憶體模式,那么如果記憶體大小,在合理、正常的范圍內該如何選擇呢?

此時我們應該主要關注業務應用的型別:

  • 當業務主要為寫多讀少型應用時,寫快取利用率高,應該使用LRUBlockCache盡量提高堆內寫快取的使用率,
  • 當業務主要為寫少讀多型應用時,讀快取利用率高(通常也意味著需要穩定的低延遲回應),應該使用BucketCache盡量提高堆外讀快取的使用率,
  • 對于不明確或者多種型別混合的業務應用,建議使用BucketCache,保證讀請求穩定性同時,堆內寫快取效率并不會很低,
  • 當前HBase可使用的記憶體高達153G,故將選擇BucketCache的記憶體模型,來配置HBase,該模式下能夠最大化利用記憶體,減少GC影響,對線上的實時服務較為有利,

得到配置項:
hbase.bucketcache.ioengine=offheap: 使用堆外快取

確認使用的記憶體模式之后,接下來將通過計算確認 JavaHeap、對外讀快取堆內寫快取、LRU元資料 等記憶體空間具體的大小,

記憶體與磁盤比

討論具體配置之前,我們從 HBase集群規劃 引入一個Disk / JavaHeap Ratio的概念,來幫助我們設定記憶體相關的引數,

理論上我們假設 最優 情況硬碟維度下的Region個數JavaHeap維度下的Region個數 相等,

相應的計算公式為:

  • 硬碟容量維度Region個數: DiskSize / (RegionSize * ReplicationFactor)
  • JavaHeap維度Region個數JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )

其中:

  • RegionSize:Region大小,配置項:hbase.hregion.max.filesize
  • ReplicationFactor:HDFS的副本數,配置項:dfs.replication
  • HeapFractionForMemstore:JavaHeap寫快取大小,即RegionServer記憶體中Memstore的總大小,配置項:hbase.regionserver.global.memstore.lowerLimit
  • MemstoreSize:Memstore刷寫大小,配置項:hbase.hregion.memstore.flush.size

現在我們已知條件 硬碟維度JavaHeap維度相等,求 1 bytes的JavaHeap大小需要搭配多大的硬碟大小 ?
已知:
DiskSize / (RegionSize * ReplicationFactor) = JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )
求:DiskSize / JavaHeap

進行簡單的交換運算可得:
DiskSize / JavaHeap = (RegionSize / MemstoreSize) * ReplicationFactor * HeapFractionForMemstore * 2

以HBase的默認配置為例:
RegionSize: 10G
MemstoreSize: 128M
ReplicationFactor: 3
HeapFractionForMemstore: 0.4

計算:
10G / 128M) * 3 * 0.4 * 2 = 192

即理想狀態下 ,RegionServer上 1 bytes的Java記憶體大小,需要搭配192bytes的硬碟大小最合適,

套用到當前集群中,HBase可用記憶體為152G,在LRUBlockCache模式下,對應的硬碟空間需要153G * 192 = 29T,這顯然是比較不合理的,

在BucketCache模式下,當前 JavaHeap、HeapFractionForMemstore值還未確定,我們會根據這個 計算關系,和已知條件,對可用記憶體進行規劃和調整,以滿足合理的記憶體/磁盤比

已知條件:
記憶體模式:BucketCache
可用記憶體大小:153G
可用硬碟大小:18T
Region大小:30G
ReplicationFactor:3

未知變數:
JavaHeap
MemstoreSize
HeapFractionForMemstore

記憶體布局

在計算位置變數的具體值之前,我們有必要了解一下當前使用的記憶體模式中對應的記憶體布局,
BucketCache模式下,RegionServer的記憶體劃分如下圖:


簡化版:


寫快取

從架構原理中我們知道,Memstore有4種級別的Flush,需要我們關注的是 Memstore、Region和RegionServer級別的刷寫,

其中MemstoreRegion級別的刷寫,并不會對線上造成太大影響,但是需要控制其閾值刷寫頻次來進一步提高性能,

RegionServer級別的刷寫,將會阻塞請求,直至刷寫完成對線上影響巨大,需要盡量避免

得到以下配置項:

  • hbase.hregion.memstore.flush.size=256M: 控制的Memstore大小默認值為128M太過頻繁的刷寫,會導致IO繁忙重繪佇列阻塞等, 設定太高也有壞處,可能會較為頻繁的觸發RegionServer級別的Flush,這里設定為256M,
  • hbase.hregion.memstore.block.multiplier=3: 控制的Region flush上限默認值為2,意味著一個Region中,最大同時存盤的Memstore大小為2 * MemstoreSize ,如果一個表的列族過多將頻繁觸發,該值視情況調整,

現在我們設定兩個 經驗值變數:

  • RegionServer總記憶體中,JavaHeap占比=0.35
  • JavaHeap最大大小=56G:超出此值表示GC有風險

計算得JavaHeap的大小為 153 * 0.35 = 53.55 ,沒有超出預期的最大JavaHeap,如果超過最大期望值,則使用最大期望值代替,得JavaHeap大小為53G,

現在JavaHeapMemstoreSize已知,可以得到唯一的位置變數 HeapFractionForMemstore 的值為 0.48 ,
得到以下配置項:

  • RegionServer JavaHeap堆疊大小: 53G
  • hbase.regionserver.global.memstore.upperLimit=0.58: 整個RS中Memstore最大比例,比lower大5-15%
  • hbase.regionserver.global.memstore.lowerLimit=0.48: 整個RS中Memstore最小比例
  • 寫快取大小為 53 * 0.48 = 25.44G

讀快取配置

當前記憶體資訊如下:

A 總可用記憶體:153G
J JavaHeap大小:53G
W 寫快取大小:25.44G
R1 LRU快取大小:?
R2 BucketCache堆外快取大小:153 - 53 = 100G

因為讀快取堆內的LRU元資料堆外的資料快取 組成,兩部分占比一般為 1:9(經驗值) ,
而對于總體的堆內記憶體,存在以下限制,如果超出此限制,則應該調低比例

LRUBlockCache + MemStore < 80% * JVM_HEAP

LRUBlockCache + 25.44 < 53 * 0.8
可得R1最大值16.96G
總讀快取:R = R1 + R2
R1:R2 = 1:9
R1 = 11G < 16G
R = 111G

配置堆外快取涉及到的相關引數如下:

  • hbase.bucketcache.size=111 * 1024M: 堆外快取大小,單位為M
  • hbase.bucketcache.percentage.in.combinedcache=0.9: 堆外讀快取所占比例,剩余為堆內元資料快取大小
  • hfile.block.cache.size=0.15: 校驗項,+upperLimit需要小于0.8

現在,我們再來計算 Disk / JavaHeap Ratio 的值,檢查JavaHeap記憶體磁盤的大小是否合理:
RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2
30 * 1024 / 256 * 3 * 0.48 * 2 = 345.6
53G * 345.6 = 18T <= 18T

至此,已得到HBase中記憶體相關的重要引數:

RegionServer JavaHeap堆疊大小: 53G
hbase.hregion.max.filesize=30G
hbase.bucketcache.ioengine=offheap
hbase.hregion.memstore.flush.size=256M
hbase.hregion.memstore.block.multiplier=3
hbase.regionserver.global.memstore.upperLimit=0.58
hbase.regionserver.global.memstore.lowerLimit=0.48
hbase.bucketcache.size=111 * 1024M
hbase.bucketcache.percentage.in.combinedcache=0.9
hfile.block.cache.size=0.15

3.3 合并與切分

HFile合并

Compaction程序中,比較常見的優化措施是:

Major Compaction
停止自動執行
增大其處理執行緒數
Minor Compaction
增加Memstore Flush大小
增加Region中最大同時存盤的Memstore數量

配置項如下:

# 關閉major compaction,定時在業務低谷執行,每周一次
hbase.hregion.majorcompaction=0
# 提高compaction的處理閾值
hbase.hstore.compactionThreshold=6
# 提高major compaction處理執行緒數
hbase.regionserver.thread.compaction.large=5
# 提高阻塞memstore flush的hfile檔案數閾值
hbase.hstore.blockingStoreFiles=100
hbase.hregion.memstore.flush.size=256M
hbase.hregion.memstore.block.multiplier=3

Major Compaction 腳本

關閉自動compaction之后手動執行腳本的代碼示例:

#!/bin/bash
if [ $# -lt 1 ]
then
    echo "Usage: <table key>"
    exit 1
fi
TMP_FILE=tmp_tables
TABLES_FILE=tables.txt
key=$1
echo "list" | hbase shell > $TMP_FILE
sleep 2
sed '1,6d' $TMP_FILE | tac | sed '1,2d' | tac | grep $key > $TABLES_FILE
sleep 2
for table in $(cat $TABLES_FILE); do
  date=`date "+%Y%m%d %H:%M:%S"`
        echo "major_compact '$table'" | hbase shell
  echo "'$date' major_compact '$table'" >> /tmp/hbase-major-compact.log
        sleep 5
done
rm -rf $TMP_FILE
rm -rf $TABLES_FILE
echo "" >> /tmp/hbase-major-compact.log

Region切分

在架構原理中我們知道,Region多有種切分策略,在Region切分時,將會有短暫時間內Region下線無服務,Region切分完成之后的Major Compaction中,將會移動父Region的資料子Region中,HMaster為了集群整體的負載均衡,可能會將子Region分配到其他RegionServer節點,

從以上描述中可以看到,Region的切分行為,其實是會對線上的服務請求,帶來一定影響的,

Region切分設定中,使用默認配置,一般不會有太大問題,但是有沒有 保證資料表負載均衡的情況下,Region不進行切分行為

有一種解決方案是使用 預磁區 + 固定值切分策略,可以一定程度上,通過預估資料表數量,以及Region個數,從而在一段時間內,抑制Region不產生切分,

假設我們可以合理的預判一個表的當前總資料量為150G,每日增量為1G當前Region大小為30G,
那么我們建表的時候,至少要設定 (150 + 1 * 360) / 30 = 17 個磁區,如此一來,一年內(360天)該表的資料增長,都會落到17個Region中,而不再切分,

當然對于一個不斷增長的表,除非時間段設定的非常長,否則總有發生切分的一天,如果無限制的延長時間段,則會在一開始,就產生大量的空Region,這對HBase是極其不友好的,所以時間段是一個需要合理控制的閾值

在hbase-site.xml中配置Region切分策略ConstantSizeRegionSplitPolicy
hbase.regionserver.region.split.policy=org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy

3.4 回應優化

HBase服務端

高并發情況下,如果HBase服務端處理執行緒數不夠,應用層將會收到HBase服務端拋出無法創建新執行緒的例外,從而導致應用層執行緒阻塞,
可以釋放調整HBase服務端配置以提升處理性能:

#  Master處理客戶端請求最大執行緒數   
hbase.master.handler.count=256
# RS處理客戶端請求最大執行緒數,如果該值設定過大,則會占用過多的記憶體,導致頻繁的GC,或者出現OutOfMemory
hbase.regionserver.handler.count=256
# 客戶端快取大小,默認為2M  
hbase.client.write.buffer=8M
# scan快取一次獲取資料的條數,太大也會產生OOM
hbase.client.scanner.caching=100

另外,以下兩項中,默認設定下超時太久、重試次數太多,一旦應用層連接不上HBse服務端將會進行近乎無限的重試,長連接無法釋放,新請求不斷進來,從而導致執行緒堆積應用假死等,影響比較嚴重,可以適當減少:
hbase.client.retries.number=3
hbase.rpc.timeout=10000

HDFS

適當增加處理執行緒等設定:

dfs.datanode.handler.count=64
dfs.datanode.max.transfer.threads=12288
dfs.namenode.handler.count=256
dfs.namenode.service.handler.count=256

同時,對于HDFS的存盤設定也可以做以下優化:

# 可以配置多個,擁有多個元資料備份
dfs.name.dir
# 配置多個磁盤與路徑,提高并行讀寫能力
dfs.data.dir
# dn同時處理檔案的上限,默認為256,可以提高到8192
dfs.datanode.max.xcievers

應用層(客戶端)

之前我們說到,HBase為了保證CP在A的實作上做了一定的妥協,導致HBase出現故障,并轉移的程序中,會有較大的影響,

對于應用服務層來說,保證服務的 穩定性 是最重要的,為了避免HBase可能產生的問題,應用層應該采用 讀寫分離 的模式,來最大程度保證自身穩定性

應用層讀寫分離

可靠的應用層應使用 讀寫分離 的模式提高回應效率與可用性:

  1. 讀寫應用應該分別屬于 不同的服務實體 ,避免牽一發而動全身
  2. 對于寫入服務,資料異步寫入redis或者kafka佇列,由下游消費者同步至HBase,回應性能十分優異
  3. 需要處理資料,寫入失敗的事務處理與重寫機制
  4. 對于讀取服務,如果一個RS掛了,一次讀請求,經過若干重試和超時,可能會持續幾十秒甚至更久,由于和寫入服務分離,可以做到互不影響
  5. 最好使用快取層,緩解RS宕機問題,對于至關重要的資料,先查快取再查HBase(見下文)

在應用層的 代碼 中,同樣有需要注意的小TIPS:

  • 如果在Spring中,將HBaseAdmin配置為Bean加載,則需配置為懶加載避免在啟動時,鏈接HMaster失敗,導致啟動失敗,從而無法進行一些降級操作,
  • scanner使用后及時關閉避免浪費客戶端和服務器的記憶體
  • 查詢時指定列簇或者指定要查詢的列限定掃描范圍
  • Put請求可以關閉WAL,但是優化不大
  • 最后,可以適當調整一下 連接池 設定:
# 組態檔加載為全域共享,可提升tps
setInt(“hbase.hconnection.threads.max”, 512); 
setInt(“hbase.hconnection.threads.core”, 64); 

3.5 使用快取層

即使我們經過大量的準備調優設定,在真實使用場景中,隨著HBase中承載的資料量越來越大、請求越來越多、并發越來越大,HBase不可避免的會有一些「毛刺」問題

如果你現在已經通過HBase,解決了大部分的線上資料存盤與訪問問題,但是有一小部分的資料,需要提供最快速的回應最低的延遲,由于HBase承載的東西太多,總是有延遲比較高的回應,此時需要怎么解決?

其實,對所有資料庫軟體來說,都會存在這樣的場景,于是,類似關系型資料庫中的資料庫拆分等策略,也是可以應用到HBase上的

或者是將最關鍵、最熱點的資料,使用 獨立的HBase集群 來處理,或者是使用諸如 Redis等高性能的快取軟體,其核心思想就是,將最關鍵的業務資料獨立存盤,以提供最優質的服務,這個服務統稱為快取層,

3.6 其他配置

hbase-env.sh 的 HBase 客戶端環境高級配置代碼段
配置了G1垃圾回收器和其他相關屬性:

-XX:+UseG1GC 
-XX:InitiatingHeapOccupancyPercent=65 
-XX:-ResizePLAB 
-XX:MaxGCPauseMillis=90  
-XX:+UnlockDiagnosticVMOptions 
-XX:+G1SummarizeConcMark 
-XX:+ParallelRefProcEnabled 
-XX:G1HeapRegionSize=32m 
-XX:G1HeapWastePercent=20 
-XX:ConcGCThreads=4 
-XX:ParallelGCThreads=16  
-XX:MaxTenuringThreshold=1 
-XX:G1MixedGCCountTarget=64 
-XX:+UnlockExperimentalVMOptions 
-XX:G1NewSizePercent=2 
-XX:G1OldCSetRegionThresholdPercent=5

hbase-site.xml 的 RegionServer 高級配置代碼段安全閥
手動split region配置

<property>
    <name>hbase.regionserver.wal.codec</name>
    <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value>
</property>
<property>
    <name>hbase.region.server.rpc.scheduler.factory.class</name>
    <value>org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactory</value>
    <description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata updates</description>
</property>
<property>
    <name>hbase.rpc.controllerfactory.class</name>
    <value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactory</value>
    <description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata updates</description>
</property>
<property>
    <name>hbase.regionserver.thread.compaction.large</name>
    <value>5</value>
</property>
<property>
    <name>hbase.regionserver.region.split.policy</name>
    <value>org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy</value>
</property>

四、使用技巧

4.1 建表規約

Rowkey規范

如無特殊情況,長度應控制在64位元組內
充分分析業務需求后,確認需要查詢的維度欄位
get請求,則rowkey散列處理,
scan請求,rowkey前綴維度散列后,后續維度依照查詢順序或者權重拼接(視具體情況決定是否散列處理),
各個欄位保持相同長度以支持左對齊的部分鍵掃描
scan形式的資料表中,
需要提前統計,單個scan可掃描出的最大數量

列簇規范

如無特殊情況,一個表只有一個列簇統一使用info命名,
如果需要1以上的列簇,則原則上,一次請求的資料,不可跨列簇存盤不超過3個列簇
示例:NAME =>'info'

壓縮

統一使用SNAPPY壓縮,
示例:COMPRESSION => 'SNAPPY'

版本

默認版本數為3,前期存盤空間緊張的情況下,設定為1,
示例:VERSIONS => 1

布隆過濾器

視情情況使用,主要針對get查詢提高性能,
kv示例:BLOOMFILTER => 'ROW',根據rowkey中的資訊,生成布隆過濾器資料
kv+col示例:BLOOMFILTER => 'ROWCOL',根據rowkey+列資訊生成布隆過濾器,針對get+指定列名的查詢,產生的過濾器檔案會比ROW大,

預磁區

預磁區需要通過評估整體表資料量來確認,當前hbase集群region塊大小為30G

  • 歷史大增量小的資料:給定的預磁區數,足夠支撐該表,永遠(或者相當長的時間內)不split,即更新的所有資料,將進入已存在的region中,以減少splitcompaction造成的影響,
  • 歷史小增量大的資料:預磁區個數,需滿足歷史資料分存盤,并支撐未來一段時間內(一個月以上)的增量資料,

預磁區區間計算屬性相同的表中,隨機取出部分樣本資料rowkey維度欄位),將樣本轉換成rowkey之后排序,并以樣本個數/預磁區個數為步長,預磁區個數rowkey組成預磁區區間
預磁區代碼示例:

/**
    * hbase region預磁區工具
    *
    * @param filePath    樣本檔案路徑
    * @param numOfSPlits 預磁區個數
    **/
  def rowkeySplitedArr(filePath: String, numOfSPlits: Int) = {
    val file = Source.fromFile(filePath).getLines()
    val res = file.map {
      line =>
        val arr = line.split("_")
        val card = arr(0)
        val name = arr(1)
        MathUtil.MD5Encrypt32(card) + MathUtil.MD5Encrypt32(card)
    }.toList.sorted
    val count = res.length / numOfSPlits
    var str = ""
    for (i <- 0 until numOfSPlits) {
      str += s"\'${res(i * count)}\',"
    }
    println(str.substring(0, str.length - 1))
  }

4.2 客戶端使用

服務端配置完成之后,如何更好的使用HBase集群,也需要花點心思測驗與調整,
以Spark作為HBase讀寫客戶端為例,

批量查詢

Spark有對應的API,可以批量讀取HBase資料,但是使用程序比較繁瑣,這里安利一個小組件Spark DB Connector批量讀取HBase的代碼可以這么簡單:

val rdd = sc.fromHBase[(String, String, String)]("mytable")
      .select("col1", "col2")
      .inColumnFamily("columnFamily")
      .withStartRow("startRow")
      .withEndRow("endRow")
done!

實時查詢

以流式計算為例,Spark Streaming中,我們要實時查詢HBase只能通過HBase Client API(沒有隊友提供服務的情況下),
那么HBase Connection每條資料創建一次,肯定是不允許的,效率太低,對服務壓力比較大,并且ZK的連接數,暴增影響服務

比較可行的方案是每個批次創建一個鏈接(類似for each Partiton中,每個磁區創建一個鏈接,磁區中資料共享鏈接),但是這種方案也會造成部分連接浪費效率低下等,

如果可以做到一個Streaming中,所有批次所有資料,始終復用一個連接池是最理想的狀態,

Spark中提供了Broadcast,這個重要工具可以幫我們實作這個想法,只要將創建的HBase Connection廣播出去,所有節點就都能復用,但是真實運行代碼時,你會發現HBase Connection不可序列化的物件,無法廣播,,,

其實利用scala的lazy關鍵字可以繞個彎子來實作:

//實體化該物件,并廣播使用
class HBaseSink(zhHost: String, confFile: String) extends Serializable {
  //延遲加載特性
  lazy val connection = {
    val hbaseConf = HBaseConfiguration.create()
    hbaseConf.set(HConstants.ZOOKEEPER_QUORUM, zhHost)
    hbaseConf.addResource(confFile)
    val conn = ConnectionFactory.createConnection(hbaseConf)
    sys.addShutdownHook {
      conn.close()
    }
    conn
  }
}

在Driver程式中,實體化該物件并廣播,在各個節點中取廣播變數的value進行使用,

廣播變數只在具體呼叫value的時候,才會去創建物件,并copy到各個節點,而這個時候被序列化的物件,其實是外層的HBaseSink,當在各個節點上,具體呼叫connection,進行操作的時候,Connection才會被真正創建(在當前節點上),從而繞過了HBase Connection無法序列化的情況(同理也可以推導RedisSink、MySQLSink等),

這樣一來,一個Streaming Job,將會使用同一個資料庫連接池,在Structured Streaming中的for each Write也可以直接應用

批量寫入

同理安利組件

rdd.toHBase("mytable")
      .insert("col1", "col2")
      .inColumnFamily("columnFamily")
      .save()

這里邊其實對HBase Client的Put介面包裝了一層,但是當線上有大量實時請求,同時線下又有大量資料需要更新時直接這么寫會對線上的服務造成沖擊,具體表現可能為持續一段時間的短暫延遲嚴重的甚至可能會把RS節點整掛

大量寫入的資料帶來具體大GC開銷,整個RS的活動都被阻塞了,當ZK來監測心跳時,發現無回應,就將該節點列入宕機名單,而GC完成后,RS發現自己“被死亡”了,那么就干脆自殺,這就是HBase的“朱麗葉死亡”,

這種場景下,使用bulkload是最安全、快速的,唯一的缺點是帶來的IO比較高

大批量寫入更新的操作,建議使用bulkload工具來實作,

實時寫入

理同,實時查詢,可以使用創建的Connection做任何操作,

結束語

我們從HBase的架構原理出發,接觸了HBase大部分的核心知識點,
理論基礎決定上層建筑,有了對HBase的總體認知,在后續的集群部署性能優化,以及實際應用中都能夠比較游刃有余,
知其然而之所以然保持對技術原理的探索,不僅能學習到,其中許多令人驚嘆的設計與操作,最重要的是,能夠真正在業務應用中充分發揮其應有的性能,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/157715.html

標籤:AI

上一篇:分布式鏈路追蹤技術適?場景和技術核?思想

下一篇:2020-10-04:java中GC Root 物件有哪些?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more