|
大家好,我是明哥! 本篇文章,我们回顾一次 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.Yarn一次Child$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.exe Hc.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)&nbsbap; 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) &nsebsp; 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(HT 线able.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内部存储的是一个个 Key上问Value, 所以从细节来讲也需要限制每个 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 protec题的tion 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(RpcExecuto和解r.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 在进行 P记录UT 操作的时候,会逐个检查要插入的每个列 (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,和服务端参数hbas一次e.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;
|
