記錄一次 Hbase 線上問題的分析和解決
大家好,我是明哥!
本篇文章,我們回顧一次 hbase 線上問題的分析和解決 - KeyValue size too large,總結下背后的知識點,并分享一下查看開源組件不同版本差異點的方法。
希望大家有所收獲,謝謝大家!
01 Hbase 簡介
Hbase 作為 hadoop database, 是一款開源,分布式易擴展,面向大數據場景的,多版本,非關系型,數據庫管理系統,是 Google Bigtable 的 JAVA 版開源實現。
Hbase 的底層存儲引擎是 HDFS,可以在普通商業級服務器硬件(即常說的 x86 架構的服務器)的基礎上,提供對超大表(表的數據量可以有百萬行,每行的列數可以有百萬級)的隨機實時讀寫訪問。
Hbase具有以下特征:
- 模塊化的線性擴展性;
- 強一致性并發讀寫支持;
- 可配置的表的自動 sharding;
- 表分區在 RegionServer 間的自動 failover;
- 基于 Block cache 和 Bloom 過濾器的實時讀取;
- 基于 server 端過濾器的查詢謂詞下推;
正是因為 Hbase 的上述特征,Hbase 在各行各業有許多線上應用案列,可以說是 NoSql 數據庫的一個典型代表:
- 在各種超大數據量級
- 在需要實時并發讀寫支持
- 在表的結構比較靈活(即有很多稀疏列:有很多行和很多列,但每一行只有眾多列中的少數列有值)
- 筆者就在車聯網場景下重度使用過 Hbase
題外話:
Nosql 數據庫有幾大類,幾個典型代表是:Hbase, ElasticSearch, MongoDb;
有個有趣的現象,筆者發現國內 Hbase 使用的多,而國外似乎 Cassandra 使用的多。
02 一次線上 Hbase 問題的問題現象
某線上應用使用了 Hive 到 Hbase 的映射表,在使用 insert overwrite 從 hive 表查詢數據并插入 HBASE 表時,發生了錯誤。
通過查看 HIVE 背后 YARN 上的作業的日志,發現主要錯誤信息是 java.lang.IllegalArgumentException: KeyValue size too large,詳細報錯截屏和日志如下:
- 2020-04-08 09:34:38,120 ERROR [main] ExecReducer: org.apache.hadoop.hive.ql.metadata.HiveException: Hive Runtime Error while processing row (tag=0) {"key":{"_col0":"0","_col1":"","_col2":"2020-04-08","_col3":"joyshebaoBeiJing","_col4":"105","_col5":"北京,"},"value":null}
- at org.apache.hadoop.hive.ql.exec.mr.ExecReducer.reduce(ExecReducer.java:253)
- at org.apache.hadoop.mapred.ReduceTask.runOldReducer(ReduceTask.java:444)
- at org.apache.hadoop.mapred.ReduceTask.run(ReduceTask.java:392)
- at org.apache.hadoop.mapred.YarnChild$2.run(YarnChild.java:164)
- at java.security.AccessController.doPrivileged(Native Method)
- at javax.security.auth.Subject.doAs(Subject.java:422)
- at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1924)
- at org.apache.hadoop.mapred.YarnChild.main(YarnChild.java:158)
- Caused by: org.apache.hadoop.hive.ql.metadata.HiveException: java.lang.IllegalArgumentException: KeyValue size too large
- at org.apache.hadoop.hive.ql.exec.GroupByOperator.processOp(GroupByOperator.java:763)
- at org.apache.hadoop.hive.ql.exec.mr.ExecReducer.reduce(ExecReducer.java:244)
- ... 7 more
- Caused by: java.lang.IllegalArgumentException: KeyValue size too large
- at org.apache.hadoop.hbase.client.HTable.validatePut(HTable.java:1577)
- at org.apache.hadoop.hbase.client.BufferedMutatorImpl.validatePut(BufferedMutatorImpl.java:158)
- at org.apache.hadoop.hbase.client.BufferedMutatorImpl.mutate(BufferedMutatorImpl.java:133)
- at org.apache.hadoop.hbase.client.BufferedMutatorImpl.mutate(BufferedMutatorImpl.java:119)
- at org.apache.hadoop.hbase.client.HTable.put(HTable.java:1085)
- at org.apache.hadoop.hive.hbase.HiveHBaseTableOutputFormat$MyRecordWriter.write(HiveHBaseTableOutputFormat.java:146)
- at org.apache.hadoop.hive.hbase.HiveHBaseTableOutputFormat$MyRecordWriter.write(HiveHBaseTableOutputFormat.java:117)
- at org.apache.hadoop.hive.ql.io.HivePassThroughRecordWriter.write(HivePassThroughRecordWriter.java:40)
- at org.apache.hadoop.hive.ql.exec.FileSinkOperator.processOp(FileSinkOperator.java:717)
- at org.apache.hadoop.hive.ql.exec.Operator.forward(Operator.java:815)
- at org.apache.hadoop.hive.ql.exec.SelectOperator.processOp(SelectOperator.java:84)
- at org.apache.hadoop.hive.ql.exec.Operator.forward(Operator.java:815)
- at org.apache.hadoop.hive.ql.exec.GroupByOperator.forward(GroupByOperator.java:1007)
- at org.apache.hadoop.hive.ql.exec.GroupByOperator.processAggr(GroupByOperator.java:818)
- at org.apache.hadoop.hive.ql.exec.GroupByOperator.processKey(GroupByOperator.java:692)
- at org.apache.hadoop.hive.ql.exec.GroupByOperator.processOp(GroupByOperator.java:758)
- ... 8 more
03 該線上 Hbase 問題的問題原因
其實以上作業的報錯日志還是比較詳細的:Caused by: java.lang.IllegalArgumentException: KeyValue size too large at org.apache.hadoop.hbase.client.HTable.validatePut(HTable.java:1577);
- 即報錯詳細信息是:KeyValue size too large;
- 報錯來自 Hbase (而不是 HIVE)的類,從類名看是 hbase 客戶端在對待插入數據校驗時發現了錯誤:org.apache.hadoop.hbase.client.HTable.validatePut(HTable.java:1577);
熟悉 Hbase 的小伙伴(其實不熟悉 Hbase 的小伙伴,也能從報錯信息和報錯類上猜到一點點),從以上信息能夠猜到,是 Hbase 對每條記錄的 KyeValue 的大小做了限制,當實際插入的 KeyValue 的大小超過該大小限制閾值時,就會報上述錯誤。
什么是 KeyValue 呢?
- 這涉及到 Hbase 的存儲結構,Hbase 內部是通過 KeyValue 結構來存儲表數據的;
- 大致上大家可以認為:某個 KeyValue 對應的是 Hbase 大寬表中的某行的某列;
- 每個 KeyVlue 內部內容包含:rowKey+CF(ColumnFamily)+CQ(Column qualifier)+Timestamp+Value;
- 詳細說明見官方文檔,截圖如下:
Hbase 為什么要限制每個 KeyValue 的大小呢?
- 究其原因,是因為 Hbase 需要在 hdfs 存儲引擎(基于分磁盤)之上提供對數據實時讀寫的支持;
- Hbase 在內部數據讀寫時使用了 LSM 數據結構 (Log-Structured-Merge Tree),這背后涉及到了大量對內存的使用(讀數據時使用 BlockCache,寫數據時使用 memStore),也涉及到了內存數據的異步 flush 和 hfile 文件的異步 compaction;(當然為了容錯,又涉及到了 wal Hlog);
- 因為涉及到基于內存提供對數據讀寫的支持,所以需要限制使用的內存的總大小,由于內存 BlockCache 中緩存的數據是以Block 為單位的,而Block內部存儲的是一個個 KeyValue, 所以從細節來講也需要限制每個 KeyValue的大小;
- 這里的 Block 是 Hbase的概念,不是 HDFS Block; (Hbase 每個 Block 的默認大小是 64KB, HDFS 每個BLOCK的默認大小一般是 128MB);
04 該線上 Hbase 問題的擴展知識-不同 Hbase 版本相關的參數
Hbase 作為一個流行的 Nosql數據庫,推出十多年來,目前有多個經典版本:
- 0.98;-- 該版本比較老了,部分遺留線上應用還有使用該版本的;
- 1.2.x;(1.4.x) -- 1.2.x 和 1.4.x 都是1.x 系列下用的比較多的穩定版本
- 2.1.x(2.4.x) -- 2.1.x 和 2.4.x 都是2.x系列下用的比較多的穩定版本
- 3.0.0-alpha-1 -- 3.x 系列目前還不是穩定版
對應該 “KeyValue size too large” 問題,不同版本推出了不同的相關參數:
- 在hbase-1.4 以前的版本中,(包括hbase 1.2.0-cdh5.14.2 和 hbase 1.2.0-cdh5.16.2),只有一個客戶端參數 hbase.client.keyvalue.maxsize;
- 在 hbase-1.4 及以后版本中,除了該客戶端參數 hbase.client.keyvalue.maxsize,還有一個服務端參數 hbase.server.keyvalue.maxsize;
其實,由于筆者并沒有持續跟進 HBASE 社區對 feautre 和 issue相關的討論(大部分使用者可能都不會),所以也是在查閱不同版本的官方文檔時留意到了上述細節,然后通過在本地 IDEA 中使用 git->show history 對比不同版本 HBASE 中 hbase-default.xml 的源碼,進而確認到了JIRA記錄號,并在JIRA中確認了這點:
正如該 JIRA 中描述:
HBASE-18043:
For sake of service protection we should not give absolute trust to clients regarding resource limits that can impact stability, like cell size limits. We should add a server side configuration that sets a hard limit for individual cell size that cannot be overridden by the client. We can keep the client side check, because it's expensive to reject a RPC that has already come in.
所以,不同版本中遇到不同情況,可能會包的錯誤主要有兩個:
- 情況1:Hbase KeyValue size too large
- 情況2:Cell with size 25000046 exceeds limit of 10485760 bytes
報錯情況一和報錯情況而,問題原因如下:
報錯情況一:沒有配置客戶端參數 hbase.client.keyvalue.maxsize,且實際插入的 keyvalue 的大小超過了該客戶端參數的默認大小限制;
報錯情況二:程序設置調大了客戶端參數 hbase.client.keyvalue.maxsize,但沒有調大服務端參數 hbase.server.keyvalue.maxsize,且實際插入的 keyvalue 小于該客戶端參數,但大于該服務端參數:
報錯情況二,某次作業日志:
- Exception in thread "main"org.apache.hadoop.hbase.DoNotRetryIOException:
- org.apache.hadoop.hbase.DoNotRetryIOException: Cell with size 25000046 exceeds limit of 10485760 bytes at org.apache.hadoop.hbase.regionserver.RSRpcServices.checkCellSizeLimit(RSRpcServices.java:944)
- at org.apache.hadoop.hbase.regionserver.RSRpcServices.mutate(RSRpcServices.java:2792)
- at org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos$ClientService$2.callBlockingMethod(ClientProtos.java:42000)
- at org.apache.hadoop.hbase.ipc.RpcServer.call(RpcServer.java:413)
- at org.apache.hadoop.hbase.ipc.CallRunner.run(CallRunner.java:130)
- at org.apache.hadoop.hbase.ipc.RpcExecutor$Handler.run(RpcExecutor.java:324)
- at org.apache.hadoop.hbase.ipc.RpcExecutor$Handler.run(RpcExecutor.java:304)
報錯情況二,某次報錯截圖:
05 該線上 Hbase 問題的解決方案
知道了問題原因,其實解決方法也就呼之欲出了,在確認要插入的業務數據沒有異常,確實需要調大 keyvalue 限制的閾值時,大體總結下,有以下解決辦法:
- 方法一:修改配置文件 hbase-site.xml, 調大客戶端參數 hbase.client.keyvalue.maxsize 的值;
- 方法二:如果使用了 HBASE JAVA API, 可以修改代碼使用 configuration 對象修改此客戶端參數的默認配置:Configuration conf = HBaseConfiguration.create();
- conf.set("hbase.client.keyvalue.maxsize","20971520");
- 方法三:如果使用了 HIVE,可以在客戶端覆蓋該客戶端參數:set hbase.client.keyvalue.maxsize=0; (hive sql中)
- 說明:一般該客戶端參數和服務端參數,默認值應該配置一樣;當更改服務端參數時,需要重啟服務端才能生效;當更改客戶端參數時,不同客戶端設置的值可以不同,且不需要重啟服務端;
- 在CDH中配置更改截圖如下:
06 該線上 Hbase 問題的技術背景
- Hbase 內部是通過 KeyValue 結構來存儲表數據的,某個 KeyValue 對應的是 Hbase 大寬表中的某行的某列,每個 KeyVlue 內部內容包含:rowKey+CF(ColumnFamily)+CQ(Column qualifier)+Timestamp+Value;
- hbase 在進行 PUT 操作的時候,會逐個檢查要插入的每個列 (keyvalue cell) 的大小,當其大小大于 maxKeyValueSize 時,就會拋出異常,拒絕寫入;
- maxKeyValueSize 大小相關的參數,在hbase-1.4 以前的版本中,(包括hbase 1.2.0-cdh5.14.2 和 hbase 1.2.0-cdh5.16.2),只有一個客戶端參數 hbase.client.keyvalue.maxsize;
- maxKeyValueSize 大小相關的參數,在 hbase-1.4 及以后版本中,除了該客戶端參數 hbase.client.keyvalue.maxsize,還有一個服務端參數 hbase.server.keyvalue.maxsize;
- Hbase 限制每個 KeyValue 大小的原因,主要在于:
- Hbase 需要在 hdfs 存儲引擎(基于分磁盤)之上提供對數據實時讀寫的支持,因此 Hbase 在內部數據讀寫時使用了 LSM 數據結構 (Log-Structured-Merge Tree),這背后涉及到了大量對內存的使用(讀數據時使用 BlockCache,寫數據時使用 memStore),也涉及到了內存數據的異步 flush 和 hfile 文件的異步 compaction;(當然為了容錯,又涉及到了 wal Hlog);
- 因為涉及到基于內存提供對數據讀寫的支持,所以需要限制使用的內存的總大小,由于內存 BlockCache 中緩存的數據是以Block 為單位的,而Block內部存儲的是一個個 KeyValue, 所以從細節來講也需要限制每個 KeyValue的大小;
- 這里的 Block 是 Hbase的概念,不是 HDFS Block; (Hbase 每個 Block 的默認大小是 64KB, HDFS 每個BLOCK的默認大小一般是 128MB);
- 該客戶端參數 hbase.client.keyvalue.maxsize,和服務端參數hbase.server.keyvalue.maxsize,集群級別的默認配置推薦保持一致,且不推薦在集群級別配置這兩個參數為0或更小(即禁用大小檢查),因為此時可能會造成某個cell 存很大的數據比如 1G,此時集群性能就會大打折扣;
- 服務端參數只能在服務端進行配置,且配置后要重啟服務端的 hbase, 其做用是 “This is a safety setting to protect the server from OOM situations.”;
- 客戶端參數可以在服務端進行全局默認配置,也可以在客戶端進行定制配置,不同客戶端設置的值可以不同,但不能大于服務端的值,且客戶端配置后不需要重啟服務端,其含義是:一個KeyValue實例的最大size(一個KeyValue在io時是不能進一步分割的);
- 客戶端參數,具體需要設置為多大,需要根據業務允許的最大keyValue是多少來進行配置,默認是10M,一般是以默認值10M為基礎,如果有客戶遇到以上報錯,且實際的KEYVALUE也不是業務臟數據,就調大1倍到20MB看看;
- 在通過hive外表使用 HBaseStorageHandler 向 HBASE 寫數據時,可以直接在beeline中更改配置客戶端參數 (set hbase.client.keyvalue.maxsize=10485760),不需要重啟hbase服務端;
- 在探究某個開源組件的某個類或配置文件,在不同版本的變化歷史時,可以通過 git clone 在本地克隆創建 git repository,然后在本地 IDEA 中通過使用命令 git->show history 對比不同版本中類或配置文件的,進而確認到 JIRA 記錄號,并在JIRA中確認相關細節;
- 更多關于hbase數據存儲結構的細節:
- hbase 中的數據,可以有多個columnFamily, 每個 columnFamily 內部可以有多個column;
- 每個 ColumnsFamily 落地到hdfs上的文件是 hfile/storeFile, storeFile 是由 data block組成的(block 是IO和壓縮的基本單位,默認64KB);(Within an HFile, HBase cells are stored in data blocks as a sequence of KeyValues;KeyValue instances are aggregated into blocks, which are indexed and Indexes also have to be stored; Blocksize is configurable on a per-ColumnFamily basis);
- 每個 block 內部存儲了多個columns,每個column都是以 keyvalue 的形式存儲的,每個keyvalue的大小受hbase.clent.keyvalue.maxsize/hbase.server.keyvalue.maxsize限制的;
- compression and DATA BLOCK ENCODING doesn't help with the cell size check, as compress and data block encoding happens when flush memstore to hfile and compaction of hfile;
- HBase supports several different compression algorithms which can be enabled on a ColumnFamily. Data block encoding attempts to limit duplication of information in keys, taking advantage of some of the fundamental designs and patterns of HBase, such as sorted row keys and the schema of a given table. Compressors reduce the size of large, opaque byte arrays in cells, and can significantly reduce the storage space needed to store uncompressed data.