:從建表到性能調(diào)優(yōu)的完整指南)
1. 為什么要在Java里直接操作HBase而不是繞道走很多剛接觸大數(shù)據(jù)存儲層的人會有一個疑問HBase本身已經(jīng)提供了Shell客戶端敲幾行命令就能建表、插數(shù)據(jù)、查記錄為什么還要費勁用Java寫代碼去操作它這個問題我在帶新人的時候被問過不下十次。答案其實不復(fù)雜——Shell適合調(diào)試和臨時驗證但真正落到生產(chǎn)系統(tǒng)里數(shù)據(jù)寫入、讀取、掃描這些動作必須嵌入到業(yè)務(wù)邏輯中由程序自動完成不可能靠人工敲命令。你總不能讓運維同事半夜爬起來手動往HBase里插一條記錄。HBase作為構(gòu)建在HDFS之上的分布式列式存儲系統(tǒng)天然適合海量數(shù)據(jù)的隨機(jī)讀寫。它的數(shù)據(jù)模型和傳統(tǒng)關(guān)系型數(shù)據(jù)庫完全不同沒有固定的列定義每一行可以有任意多的列族和列限定符同一列族下的數(shù)據(jù)物理上存儲在一起。這種設(shè)計讓它在稀疏數(shù)據(jù)、寬表場景下表現(xiàn)非常出色。而Java API是HBase最原生、最完整的操作接口官方提供的客戶端就是Java寫的其他語言的客戶端本質(zhì)上也是通過Thrift網(wǎng)關(guān)做了一層封裝。所以如果你想真正吃透HBaseJava API是繞不開的一關(guān)。這篇文章面向的是已經(jīng)了解HBase基本概念表、行鍵、列族、時間戳但還沒動手用Java寫過操作代碼的開發(fā)者。我會從環(huán)境依賴開始一步步把建表、寫入、查詢、掃描、刪除這些核心操作講清楚同時把我在實際項目中踩過的坑和總結(jié)的經(jīng)驗一并分享出來。代碼可以直接復(fù)制到你的工程里跑參數(shù)和配置我會解釋清楚為什么這么設(shè)。2. 動手之前依賴、配置與連接池的取舍2.1 Maven依賴到底該引哪些包用Java操作HBase第一步是把客戶端依賴引入工程。HBase的客戶端依賴并不算輕量因為它會連帶拉入Hadoop相關(guān)的若干jar包。以HBase 2.x版本為例核心依賴是hbase-client但實際使用中往往還需要hbase-common和hadoop-common。如果你用的是Maven可以這樣配置dependency groupIdorg.apache.hbase/groupId artifactIdhbase-client/artifactId version2.4.11/version /dependency dependency groupIdorg.apache.hbase/groupId artifactIdhbase-common/artifactId version2.4.11/version /dependency這里有個容易踩的坑hbase-client的版本必須和服務(wù)端HBase的版本保持兼容。HBase 1.x和2.x的API差異不小比如Connection接口在1.x里還是HConnection到了2.x才統(tǒng)一成Connection。如果你拿2.x的客戶端去連1.x的服務(wù)端編譯能過但運行時可能報各種奇怪的錯。我的建議是客戶端版本和服務(wù)端大版本保持一致小版本盡量對齊。另外很多教程會讓你引入hbase-server這是不對的。hbase-server包含了RegionServer的實現(xiàn)體積巨大客戶端根本不需要。引入它只會讓你的工程打包出來多出幾十兆的無用依賴還可能引起類沖突。2.2 Configuration對象的正確構(gòu)造方式連接HBase需要一個Configuration對象它承載了ZooKeeper地址、端口等關(guān)鍵信息。最直接的方式是手動設(shè)置Configuration conf HBaseConfiguration.create(); conf.set(hbase.zookeeper.quorum, zk-host-1,zk-host-2,zk-host-3); conf.set(hbase.zookeeper.property.clientPort, 2181);HBaseConfiguration.create()會自動加載classpath下的hbase-site.xml如果這個文件存在里面的配置會覆蓋你手動設(shè)置的值。這一點要特別注意有時候你代碼里明明設(shè)了正確的ZooKeeper地址但連上去發(fā)現(xiàn)是另一套集群八成就是hbase-site.xml在作怪。排查的時候可以先打印一下conf.get(hbase.zookeeper.quorum)看看實際生效的值是什么。ZooKeeper的地址建議寫全所有節(jié)點用逗號分隔。只寫一個節(jié)點雖然也能連上但那個節(jié)點掛了客戶端就找不到集群了。HBase客戶端會從ZooKeeper上讀取hbase:meta表的位置信息所以ZooKeeper的穩(wěn)定性直接決定了客戶端能否正常連接。2.3 Connection是重量級對象千萬別每次new這是新手最容易犯的錯誤也是性能殺手。Connection對象的創(chuàng)建涉及與ZooKeeper的握手、元數(shù)據(jù)表的讀取等一系列網(wǎng)絡(luò)交互開銷很大。如果你每次操作都ConnectionFactory.createConnection(conf)在高并發(fā)場景下會把ZooKeeper壓垮連接數(shù)暴漲最終導(dǎo)致整個集群響應(yīng)變慢。正確的做法是把Connection做成單例整個應(yīng)用生命周期內(nèi)只創(chuàng)建一次所有線程共享。Connection本身是線程安全的可以放心地在多線程環(huán)境下使用。但要注意從Connection獲取的Table對象不是線程安全的每個線程應(yīng)該自己從Connection里拿一個Table實例。public class HBaseConnector { private static volatile Connection connection; public static Connection getConnection() throws IOException { if (connection null || connection.isClosed()) { synchronized (HBaseConnector.class) { if (connection null || connection.isClosed()) { Configuration conf HBaseConfiguration.create(); conf.set(hbase.zookeeper.quorum, zk-host-1,zk-host-2,zk-host-3); connection ConnectionFactory.createConnection(conf); } } } return connection; } }這段代碼用了雙重檢查鎖來保證線程安全。volatile關(guān)鍵字不能省否則可能出現(xiàn)指令重排序?qū)е履玫轿闯跏蓟耆膶ο?。雖然看起來有點啰嗦但這是經(jīng)過驗證的可靠寫法。3. 建表這件事表描述符與預(yù)分區(qū)的實際寫法3.1 TableDescriptor和ColumnFamilyDescriptor的構(gòu)建邏輯在HBase 2.x里建表用的是Admin對象配合TableDescriptorBuilder和ColumnFamilyDescriptorBuilder。這套Builder模式比1.x時代的HTableDescriptor要清晰不少但初次接觸的人容易被繞暈。核心邏輯是先構(gòu)建列族描述符再把列族加進(jìn)表描述符最后通過Admin創(chuàng)建表。Admin admin connection.getAdmin(); ColumnFamilyDescriptor cfDescriptor ColumnFamilyDescriptorBuilder .newBuilder(Bytes.toBytes(info)) .setMaxVersions(3) .setTimeToLive(7 * 24 * 60 * 60) .build(); TableDescriptor tableDescriptor TableDescriptorBuilder .newBuilder(TableName.valueOf(user_behavior)) .setColumnFamily(cfDescriptor) .build(); if (!admin.tableExists(TableName.valueOf(user_behavior))) { admin.createTable(tableDescriptor); }setMaxVersions(3)表示每個單元格保留最近3個版本的數(shù)據(jù)。HBase的版本控制是按時間戳來的每次寫入都會帶上時間戳不指定的話默認(rèn)用當(dāng)前時間。如果你只需要最新值設(shè)成1就行能省不少存儲空間。setTimeToLive設(shè)置的是數(shù)據(jù)的存活時間單位是秒上面設(shè)的是7天。超過這個時間的數(shù)據(jù)會在Major Compaction時被清理掉。3.2 預(yù)分區(qū)別讓所有數(shù)據(jù)擠在一個Region里新建的表默認(rèn)只有一個Region所有寫入都會落到這一個Region上。在數(shù)據(jù)量小的時候沒問題但當(dāng)寫入量上來之后單個Region會成為瓶頸而且自動分裂的過程會引起短暫的不可用。所以生產(chǎn)環(huán)境建表時一定要做預(yù)分區(qū)。預(yù)分區(qū)有兩種常用方式。第一種是直接指定分割點byte[][] splitKeys new byte[][] { Bytes.toBytes(1), Bytes.toBytes(2), Bytes.toBytes(3), Bytes.toBytes(4), Bytes.toBytes(5), Bytes.toBytes(6), Bytes.toBytes(7), Bytes.toBytes(8), Bytes.toBytes(9) }; admin.createTable(tableDescriptor, splitKeys);這樣會創(chuàng)建10個Region行鍵以0開頭的落在第一個Region以1開頭的落在第二個以此類推。分割點的選擇取決于你的行鍵分布特征。如果行鍵是用戶ID的哈希值那均勻取模分割就行如果行鍵有時間前綴那就要小心熱點問題。第二種方式是用createTable的重載方法指定Region數(shù)量和分割算法但實際用得最多的還是手動指定分割點因為可控性最強(qiáng)。注意預(yù)分區(qū)的分割點必須是有序的而且不能重復(fù)。如果傳入無序的數(shù)組創(chuàng)建表時會直接拋異常。另外分割點的數(shù)量決定了Region的數(shù)量Region數(shù)量等于分割點數(shù)量加一。3.3 列族數(shù)量不是越多越好HBase的列族在物理上是分開存儲的每個列族對應(yīng)一組HFile。列族越多寫入時的flush和compaction開銷就越大。官方建議一張表的列族數(shù)量控制在2到3個以內(nèi)。我見過有人把每個字段都設(shè)成一個列族結(jié)果寫入性能慘不忍睹。列族的命名也有講究。用簡短的英文單詞比如info、data、meta不要用中文或者特殊字符。雖然HBase底層用字節(jié)數(shù)組存儲理論上支持任意字符但列族名會出現(xiàn)在HFile路徑和監(jiān)控指標(biāo)里用奇怪的字符會給運維帶來麻煩。4. 寫入與讀取Put、Get和批量操作的性能差異4.1 單條Put的寫法與時間戳控制寫入數(shù)據(jù)用Put對象指定行鍵、列族、列限定符和值Table table connection.getTable(TableName.valueOf(user_behavior)); Put put new Put(Bytes.toBytes(user_001)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(name), Bytes.toBytes(張三)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(age), Bytes.toBytes(28)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(city), Bytes.toBytes(北京)); table.put(put);addColumn有多個重載版本可以指定時間戳。如果不指定HBase會用RegionServer的當(dāng)前時間。在需要回放歷史數(shù)據(jù)或者做數(shù)據(jù)修正的場景下手動指定時間戳很有用。但要注意時間戳是long類型單位是毫秒。如果你寫入的時間戳比已有數(shù)據(jù)的時間戳小那這條數(shù)據(jù)雖然寫進(jìn)去了但查詢時默認(rèn)返回的是時間戳最大的版本你可能看不到剛寫的數(shù)據(jù)。table.put(put)每次調(diào)用都是一次RPC請求。如果你要寫入大量數(shù)據(jù)一條一條put效率極低。這時候應(yīng)該用BufferedMutator或者批量put(ListPut)。4.2 批量寫入BufferedMutator與List 的選擇table.put(ListPut)會把一批Put打包成一個RPC請求發(fā)給RegionServer減少了網(wǎng)絡(luò)往返次數(shù)。但它的缺點是同步的調(diào)用線程會阻塞直到所有數(shù)據(jù)寫入完成。而且如果一批數(shù)據(jù)太大會導(dǎo)致單次RPC超時。BufferedMutator則是異步的它內(nèi)部維護(hù)了一個緩沖區(qū)數(shù)據(jù)先寫到緩沖區(qū)達(dá)到閾值后自動批量發(fā)送。寫入線程不需要等待服務(wù)端確認(rèn)吞吐量更高。BufferedMutatorParams params new BufferedMutatorParams(TableName.valueOf(user_behavior)) .writeBufferSize(4 * 1024 * 1024); BufferedMutator mutator connection.getBufferedMutator(params); for (int i 0; i 100000; i) { Put put new Put(Bytes.toBytes(user_ i)); put.addColumn(Bytes.toBytes(info), Bytes.toBytes(age), Bytes.toBytes(String.valueOf(i % 100))); mutator.mutate(put); } mutator.flush(); mutator.close();writeBufferSize設(shè)的是緩沖區(qū)大小默認(rèn)是2MB。設(shè)大一點能提高吞吐量但也會增加內(nèi)存占用和丟數(shù)據(jù)的風(fēng)險——如果客戶端進(jìn)程崩潰緩沖區(qū)里還沒發(fā)送的數(shù)據(jù)就丟了。所以BufferedMutator適合對可靠性要求不是極端高的場景比如日志采集。如果數(shù)據(jù)一條都不能丟還是老老實實用同步批量寫。4.3 Get查詢指定列族和列限定符的粒度控制讀取單行數(shù)據(jù)用GetGet get new Get(Bytes.toBytes(user_001)); get.addFamily(Bytes.toBytes(info)); get.addColumn(Bytes.toBytes(info), Bytes.toBytes(name)); Result result table.get(get); byte[] nameBytes result.getValue(Bytes.toBytes(info), Bytes.toBytes(name)); String name Bytes.toString(nameBytes);addFamily和addColumn是可選的。如果不指定Get會返回該行所有列族的所有列。在寬表場景下一行可能有上百個列全查出來既浪費網(wǎng)絡(luò)帶寬又浪費內(nèi)存。所以精確指定需要的列是一個好習(xí)慣。Result對象還提供了rawCells()方法可以拿到所有單元格的詳細(xì)信息包括時間戳。如果你需要做版本對比或者數(shù)據(jù)審計這個方法很有用。4.4 Scan掃描setStartRow和setStopRow的邊界陷阱Scan用于范圍查詢是HBase最強(qiáng)大的功能之一Scan scan new Scan(); scan.setStartRow(Bytes.toBytes(user_001)); scan.setStopRow(Bytes.toBytes(user_100)); scan.addFamily(Bytes.toBytes(info)); scan.setCaching(500); scan.setBatch(100); ResultScanner scanner table.getScanner(scan); for (Result result : scanner) { // 處理每一行 } scanner.close();這里有幾個關(guān)鍵點。setStartRow是包含的setStopRow是不包含的。也就是說上面的Scan會返回行鍵從user_001到user_099的數(shù)據(jù)user_100不會被返回。這個左閉右開的區(qū)間設(shè)計經(jīng)常讓人搞混我第一次用的時候就多查了一條數(shù)據(jù)。setCaching(500)控制的是每次RPC從服務(wù)端拉取多少行到客戶端。默認(rèn)值很小好像是100在掃描大量數(shù)據(jù)時會導(dǎo)致頻繁的RPC調(diào)用。設(shè)大一點能顯著提高掃描速度但也會增加客戶端內(nèi)存壓力。500到1000是一個比較平衡的值。setBatch(100)控制的是每行返回多少個單元格。這個參數(shù)只在行特別寬列非常多的時候才需要設(shè)置。如果一行有幾千個列一次性返回會導(dǎo)致單次RPC響應(yīng)過大甚至OOM。設(shè)成100表示每行分多次返回。注意ResultScanner使用完畢后必須關(guān)閉否則會泄漏連接資源。建議用try-with-resources或者try-finally保證關(guān)閉。5. 刪除操作與過濾器那些文檔里不會細(xì)說的細(xì)節(jié)5.1 Delete的三種粒度行、列族、列刪除數(shù)據(jù)用Delete對象粒度可以細(xì)到列// 刪除整行 Delete deleteRow new Delete(Bytes.toBytes(user_001)); // 刪除整個列族 Delete deleteFamily new Delete(Bytes.toBytes(user_001)); deleteFamily.addFamily(Bytes.toBytes(info)); // 刪除指定列 Delete deleteColumn new Delete(Bytes.toBytes(user_001)); deleteColumn.addColumn(Bytes.toBytes(info), Bytes.toBytes(city)); table.delete(deleteColumn);HBase的刪除并不是立即物理刪除數(shù)據(jù)而是寫入一個墓碑標(biāo)記tombstone。真正的數(shù)據(jù)清理發(fā)生在Major Compaction時。所以刪除操作本身很快但不會立刻釋放存儲空間。如果你刪了大量數(shù)據(jù)后發(fā)現(xiàn)磁盤沒降不要慌等compaction跑完就好了。還有一個容易忽略的點刪除操作也會帶上時間戳。如果你刪除時指定的時間戳小于數(shù)據(jù)的時間戳那刪除標(biāo)記不會生效數(shù)據(jù)依然能查到。默認(rèn)情況下Delete使用當(dāng)前時間戳所以一般不會有問題。但如果你在做數(shù)據(jù)修正手動指定了舊時間戳就要小心這個坑。5.2 Filter的實戰(zhàn)用法PrefixFilter和RowFilter過濾器是HBase Scan的利器可以在服務(wù)端過濾數(shù)據(jù)減少網(wǎng)絡(luò)傳輸。常用的有PrefixFilter、RowFilter、SingleColumnValueFilter等。// 前綴過濾只返回行鍵以user_1開頭的行 PrefixFilter prefixFilter new PrefixFilter(Bytes.toBytes(user_1)); Scan scan new Scan(); scan.setFilter(prefixFilter); // 行鍵正則過濾 RowFilter rowFilter new RowFilter( CompareFilter.CompareOp.EQUAL, new RegexStringComparator(user_\\d{3}) ); scan.setFilter(rowFilter);PrefixFilter的效率很高因為它可以直接定位到起始行然后順序掃描到不滿足前綴為止。RowFilter配合正則表達(dá)式則靈活得多但正則是逐行匹配的性能不如PrefixFilter。如果前綴能滿足需求優(yōu)先用PrefixFilter。SingleColumnValueFilter用于按列值過濾但要注意它的一個特性如果某行沒有指定的列默認(rèn)行為是跳過這行。如果你希望沒有該列的行也被返回需要調(diào)用setFilterIfMissing(false)。這個默認(rèn)行為坑過不少人。5.3 過濾器組合FilterList的AND與OR多個過濾器可以組合使用FilterList filterList new FilterList(FilterList.Operator.MUST_PASS_ALL); filterList.addFilter(new PrefixFilter(Bytes.toBytes(user_))); filterList.addFilter(new RowFilter( CompareFilter.CompareOp.EQUAL, new RegexStringComparator(.*_00[1-9]) )); scan.setFilter(filterList);MUST_PASS_ALL相當(dāng)于AND所有過濾器都通過才返回MUST_PASS_ANY相當(dāng)于OR任意一個通過就返回。過濾器組合的順序會影響性能把過濾力度大的放在前面能減少后續(xù)過濾器的計算量。6. 連接管理與異常處理生產(chǎn)環(huán)境必須考慮的事6.1 連接泄漏的常見原因與排查方法Connection和Table都是需要關(guān)閉的資源。雖然Connection建議做成單例長期持有但Table對象在用完后應(yīng)該及時關(guān)閉。如果你在代碼里頻繁connection.getTable()卻不關(guān)閉RegionServer上的連接數(shù)會持續(xù)增長最終達(dá)到上限后拒絕新連接。排查連接泄漏的一個實用方法是看RegionServer的Web UI里面有當(dāng)前活躍的RPC連接數(shù)。如果這個數(shù)字只增不減基本可以確定有泄漏。另一個方法是在代碼里加日志記錄每次getTable和close的調(diào)用對比數(shù)量是否匹配。6.2 RetriesExhaustedException的觸發(fā)條件與應(yīng)對RetriesExhaustedException是HBase客戶端最常見的異常之一通常意味著客戶端重試了多次仍然無法完成操作。觸發(fā)原因可能有很多RegionServer宕機(jī)、網(wǎng)絡(luò)抖動、Region正在分裂、ZooKeeper會話超時等。應(yīng)對這個異常首先要看異常信息里的詳細(xì)描述它會告訴你重試了多少次、每次失敗的原因是什么。如果是短暫的網(wǎng)絡(luò)問題客戶端會自動重試并恢復(fù)如果是RegionServer真的掛了那需要等Master把Region重新分配后才能恢復(fù)。調(diào)整重試參數(shù)可以改變客戶端的行為conf.set(hbase.client.retries.number, 5); conf.set(hbase.client.pause, 1000); conf.set(hbase.client.operation.timeout, 30000);hbase.client.retries.number是重試次數(shù)默認(rèn)是15次設(shè)小一點可以讓失敗更快暴露出來。hbase.client.pause是重試間隔的基數(shù)實際間隔會在這個基礎(chǔ)上做指數(shù)退避。hbase.client.operation.timeout是單次操作的超時時間單位毫秒。6.3 批量寫入時的部分失敗處理用table.put(ListPut)批量寫入時如果部分?jǐn)?shù)據(jù)失敗HBase會拋出RetriesExhaustedWithDetailsException。這個異常里包含了成功和失敗的記錄列表try { table.put(putList); } catch (RetriesExhaustedWithDetailsException e) { ListThrowable exceptions e.getCauses(); ListRow failedRows e.getFailedRows(); // 對失敗的行做重試或記錄 }拿到失敗的行之后可以根據(jù)異常類型決定是重試還是丟棄。如果是NotServingRegionException說明Region正在遷移等一會兒重試通常能成功如果是DoNotRetryIOException那重試也沒用需要檢查數(shù)據(jù)或表結(jié)構(gòu)。7. 我踩過的那些坑與性能調(diào)優(yōu)的實戰(zhàn)心得7.1 行鍵設(shè)計不當(dāng)導(dǎo)致的熱點問題行鍵設(shè)計是HBase使用中最重要的一環(huán)沒有之一。我見過一個項目用時間戳作為行鍵前綴結(jié)果所有寫入都集中在最后一個Region上其他Region閑著集群資源嚴(yán)重浪費。這就是典型的熱點問題。解決熱點問題的常用方法是給行鍵加鹽或者做哈希。加鹽就是在行鍵前面拼一個隨機(jī)前綴比如(hash(userId) % 10) _ userId。這樣數(shù)據(jù)會均勻分布到10個Region上。但加鹽之后按userId范圍查詢就變得困難了因為相同userId的數(shù)據(jù)可能在不同的Region上。所以加鹽適合隨機(jī)讀寫的場景不適合范圍掃描。另一種方法是反轉(zhuǎn)行鍵。如果行鍵是遞增的ID反轉(zhuǎn)之后高位變化低位不變也能起到分散寫入的效果。比如user_0001反轉(zhuǎn)成1000_resu。這種方法適合ID長度固定的場景。7.2 Scan時setCaching設(shè)太大反而變慢setCaching設(shè)大一點能減少RPC次數(shù)但并不是越大越好。如果設(shè)成10000客戶端一次要從服務(wù)端拉10000行數(shù)據(jù)這會導(dǎo)致單次RPC響應(yīng)非常大網(wǎng)絡(luò)傳輸時間長客戶端內(nèi)存占用高。而且如果掃描到一半客戶端處理不過來服務(wù)端可能已經(jīng)超時了。我的經(jīng)驗值是500到1000。在這個范圍內(nèi)RPC次數(shù)和單次數(shù)據(jù)量比較平衡。當(dāng)然具體值還要看單行數(shù)據(jù)的大小如果每行數(shù)據(jù)很大比如幾KB那caching設(shè)小一點更合適。7.3 客戶端版本與服務(wù)端不匹配的詭異報錯前面提過版本兼容的問題這里再展開說一下。HBase 2.x的客戶端連1.x的服務(wù)端最常見的報錯是NoSuchMethodError或者ClassNotFoundException。因為2.x的API里有些方法在1.x里不存在編譯時用的是2.x的jar所以能過運行時加載的是1.x的類就找不到了。還有一種情況是Hadoop版本不匹配。HBase依賴Hadoop的Configuration類如果客戶端引入的Hadoop版本和服務(wù)端的差異太大Configuration的序列化格式可能不兼容導(dǎo)致連接建立后讀取配置出錯。排查這類問題先確認(rèn)客戶端和服務(wù)端的版本矩陣然后看異常堆棧里是哪個類出的問題。7.4 關(guān)于Bytes工具類的使用建議HBase提供了Bytes工具類來做byte[]和基本類型之間的轉(zhuǎn)換。用的時候要注意Bytes.toBytes(String)用的是UTF-8編碼Bytes.toString(byte[])也是按UTF-8解碼。如果數(shù)據(jù)里包含非UTF-8的字節(jié)轉(zhuǎn)換會出問題。對于二進(jìn)制數(shù)據(jù)直接用byte[]操作不要經(jīng)過String轉(zhuǎn)換。另外Bytes.toBytes(int)和Bytes.toBytes(long)生成的字節(jié)數(shù)組長度不同比較的時候要注意。行鍵比較是按字節(jié)序進(jìn)行的不是按數(shù)值大小。比如Bytes.toBytes(10)和Bytes.toBytes(9)比較結(jié)果是10小于9因為字符1的ASCII碼小于9。如果行鍵是數(shù)值建議統(tǒng)一用固定長度編碼或者用Bytes.toBytes(long)保證長度一致。7.5 監(jiān)控與調(diào)優(yōu)的日常檢查項在生產(chǎn)環(huán)境跑HBase客戶端有幾個指標(biāo)需要定期關(guān)注。RegionServer的blockCacheHitRatio反映讀緩存命中率如果低于0.8說明緩存不夠或者訪問模式太隨機(jī)。compactionQueueSize如果持續(xù)大于0說明compaction跟不上寫入速度需要調(diào)整compaction策略或者增加RegionServer資源??蛻舳说膔pcQueueSize如果經(jīng)常滿說明寫入壓力超過了RegionServer的處理能力需要考慮擴(kuò)容或者優(yōu)化行鍵設(shè)計。這些指標(biāo)可以通過HBase自帶的Web UI查看也可以接入監(jiān)控系統(tǒng)做告警。我的習(xí)慣是每天早上花五分鐘掃一眼關(guān)鍵指標(biāo)有異常趨勢就提前處理不要等到用戶反饋慢了才去查。7.6 一個實用的調(diào)試技巧用Shell驗證Java代碼的結(jié)果當(dāng)你用Java代碼寫入數(shù)據(jù)后不確定是否成功最快的驗證方式是用HBase Shell查一下scan user_behavior, {LIMIT 5} get user_behavior, user_001Shell的結(jié)果和Java API的結(jié)果應(yīng)該一致。如果不一致先檢查是不是連到了不同的集群ZooKeeper地址配錯了再檢查表名和列族名是否拼寫正確。這個技巧在調(diào)試階段能省很多時間。代碼寫多了會發(fā)現(xiàn)HBase的Java API本身并不復(fù)雜難的是對數(shù)據(jù)模型的理解和對集群行為的把握。同樣的API調(diào)用在不同的行鍵設(shè)計和表結(jié)構(gòu)下性能可能差幾十倍。所以不要只盯著代碼怎么寫多想想數(shù)據(jù)怎么組織、訪問模式是什么、集群能不能扛住。這些才是決定項目成敗的關(guān)鍵。