1 问题
近期业务反馈 HBase GET 读性能不佳,查看监控,每天有几个时间波段,GET P99 请求时延很高。
2 问题原因
定位有一个业务有 7 张 Hive HBase 外部表,每天有两个时间定时通过 sql 的方式,并发导出全表到 hive 。代码类似于:
insert into table_hive select * from hive_hbase_external_table;
相当于每天定时并发对 7 张大的 HBase 全表 Scan,Scan 默认会写缓存,这个会导致:
- Bucket Cache 被 scan block 写满, 缓存开始淘汰,出现 cache miss (缓存命中率降低) 和 cache eviction (缓存淘汰)。cache 命中率直接影响 HBase 读性能。
- 出现 I/O 风暴,读请求没有命中缓存需要读 hdfs,此时由于 I/O 瓶颈,读性能问题进一步被放大。
3 问题分析
查看这几个时间段 GET 请求没有明显变多(单节点 1k/s),而 READ 请求猛增,单节点 read request 达到 600-900k/s。
缓存命中率直接影响读性能,查看 BucketCache 命中率情况,这个时间段 cache miss 和 eviction 都陡增。推断是出现了大量 scan 或者大 scan。这里为什么要推理,因为 Ambari grafana 的 HBase scan 请求数无法显示。
没有命中缓存,就会走 hdfs,查看 datanode 监控有大量的 block read,查看 datanode 磁盘监控,磁盘 I/O 这段时间被打满。推断是 scan 导致缓存写满,所以出现缓存被驱逐(eviction)。
大量的读导致了磁盘瓶颈,读性能更差了
到这里,剩下的就是揪出是谁在进行大 Scan 操作,HBase 上承载的业务众多,一个个问业务不太现实。
于是通过热点 Regionserver 节点的日志看到该时间段,有一些 Scan 的报错,找到表名,拿去一问业务就问出来问题了。(这种方式着实有点原始)。
业务每天定时并发对 7 张 HBase 大表进行全表 Scan,查询导出数据到 Hive。
4 Hive HBase 外部表使用与坑
创建 HBase 的 hive 外部表典型建表语句如下,rowkey 字段为 hbase 的 rowkey,label 为列族:
CREATE EXTERNAL TABLE user_profile.mix_label_scene_tag_test_1(rowkey string COMMENT 'rowkey', label map<string,string> COMMENT 'cf')
ROW FORMAT SERDE 'org.apache.hadoop.hive.hbase.HBaseSerDe'
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES('hbase.columns.mapping'=':key,label:')
TBLPROPERTIES('hbase.table.name'='profile:mix_label_scene_tag_pre_release')
HBase 相关的参数可以直接通过 set 的方式配置进去,例如:
set hbase.client.scanner.timeout.period=600000;
但是这里有一个坑需要注意,默认 hive hbase 外部表 scan 是会缓存 block 的,这个不容易被开发注意到。如果有大量的 scan 类型的批处理、 mr 任务等,会对缓存是一种破坏性的写入。影响其他正常读请求。HBase 官方也有建议,针对批处理类型的 scan 操作,要设置 setCacheBlocks 为 false:
Scan instances can be set to use the block cache in the RegionServer via the setCacheBlocks method. For input Scans to MapReduce jobs, this should be false. https://hbase.apache.org/book.html#perf.hbase.client.blockcache
Hive 社区也有人提了这个问题 HIVE-20484,在 Hive 3 以上的版本进行了修复,将外部表 scan.setCacheBlocks 默认设为 false。hive 这块的代码如下:
public static final String HBASE_SCAN_CACHEBLOCKS = "hbase.scan.cacheblock";
...
String scanCacheBlocks = tableProperties.getProperty(HBaseSerDe.HBASE_SCAN_CACHEBLOCKS);
if (scanCacheBlocks != null) {
jobProperties.put(HBaseSerDe.HBASE_SCAN_CACHEBLOCKS, scanCacheBlocks);
}
...
String scanCacheBlocks = jobConf.get(HBaseSerDe.HBASE_SCAN_CACHEBLOCKS);
if (scanCacheBlocks != null) {
scan.setCacheBlocks(Boolean.parseBoolean(scanCacheBlocks));
}
如果没法升级 hive ,解决办法有两个:
- 重新创建外部表,SERDEPROPERTIES 里指定: 'hbase.scan.cacheblock'='false',这种方式可以屏蔽 scan 写缓存,又保留正常的 get 读缓存。
CREATE EXTERNAL TABLE user_profile.mix_label_scene_tag(rowkey string COMMENT 'rowkey', label map<string,string> COMMENT 'cf')
ROW FORMAT SERDE 'org.apache.hadoop.hive.hbase.HBaseSerDe'
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES('hbase.columns.mapping'=':key,label:','hbase.scan.cacheblock'='false')
TBLPROPERTIES('hbase.table.name'='profile:mix_label_scene_tag_pre_release')
- 修改表的属性,修改列族的缓存策略,这种方式全局屏蔽读缓存,会影响正常的 get 读写缓存
alter 'profile:mix_label_scene_tag', {NAME => 'label', BLOCKCACHE => 'false'}
值得一提的是,setCacheBlocks 是设置:是否把读取的数据写入到缓存中。所以不管怎么设定 setCacheBlocks 的 bool 值,HBase 默认的读请求都会先查缓存。
4.1 Hive HBase 外部表相关参数
附上其他的配置,源码:
public static final String HBASE_COLUMNS_MAPPING = "hbase.columns.mapping";
public static final String HBASE_TABLE_NAME = "hbase.table.name";
public static final String HBASE_TABLE_DEFAULT_STORAGE_TYPE = "hbase.table.default.storage.type";
public static final String HBASE_KEY_COL = ":key";
public static final String HBASE_TIMESTAMP_COL = ":timestamp";
public static final String HBASE_PUT_TIMESTAMP = "hbase.put.timestamp";
public static final String HBASE_COMPOSITE_KEY_CLASS = "hbase.composite.key.class";
public static final String HBASE_COMPOSITE_KEY_TYPES = "hbase.composite.key.types";
public static final String HBASE_COMPOSITE_KEY_FACTORY = "hbase.composite.key.factory";
public static final String HBASE_STRUCT_SERIALIZER_CLASS = "hbase.struct.serialization.class";
public static final String HBASE_SCAN_CACHE = "hbase.scan.cache";
public static final String HBASE_SCAN_CACHEBLOCKS = "hbase.scan.cacheblock";
public static final String HBASE_SCAN_BATCH = "hbase.scan.batch";
public static final String HBASE_AUTOGENERATE_STRUCT = "hbase.struct.autogenerate";
/**
* Determines whether a regex matching should be done on the columns or not. Defaults to true.
* <strong>WARNING: Note that currently this only supports the suffix wildcard .*</strong>
*/
public static final String HBASE_COLUMNS_REGEX_MATCHING = "hbase.columns.mapping.regex.matching";
/**
* Defines the type for a column.
**/
public static final String SERIALIZATION_TYPE = "serialization.type";
/**
* Defines if the prefix column from hbase should be hidden.
* It works only when @HBASE_COLUMNS_REGEX_MATCHING is true.
* Default value of this parameter is false
*/
public static final String HBASE_COLUMNS_PREFIX_HIDE = "hbase.columns.mapping.prefix.hide";
5 todo
HBase 怎么提升缓存命中率,除了缓存命中率,还有什么会影响读性能?
HBase 如何监控大 Scan?
HBase 如何进行表级别的监控?
HBase Hive 外部表如何有效监控管理,避免用户随意 scan 操作?