hbase进阶与大数据存储调优案例。解决高并发大批量写入hbase丢数据问题,优化查询效率从5min减少到7s。

hbase介绍

hbase底层原理

每个ColumnFamily建一个HStore,HStore由一个Memstore和一系列HFile组成。Memstore位于Region Server的主内存,HFile则被写入到HDFS。需要被写入的数据首先写入Memstore,达到一定阈值flush到HFile中,每次flush都会创建一个新的HFile。

StoreFile底层以HFile格式KeyValue(hadoop二进制文件)保存,一个StoreFile对应一个HFile。

当region的某个列族达到一个阈值(默认256M)时就会split出新的region。

memstore文件比hfile文件大,原因在于heapSize()内存中一个KeyValue对象除了本身实际内容外,还有 64byte是对象的内部实例等占用了部分空间。参考:HBase MemStore与HStoreFile的大小分析

关于LSM

LSM树原理:把一棵大树拆分成N棵小树,它首先写入内存中,随着小树越来越大,内存中的小树会flush到磁盘中,磁盘中的树定期可以做merge操作,合并成一棵大树,以优化读性能。No-SQL数据库一般采用LSM树作为数据结构,HBase也不例外。

LSM数据更新只在内存中操作,没有磁盘访问,因此比B+树要快。对于数据读来说,如果读取的是最近访问过的数据,LSM树能减少磁盘访问,提高性能。 LSM树实质上就是在读写之间得取平衡,和B+树比相,它牲牺了部份读性能,用来大幅进步写性能。

全量日志表

该表存储接口请求的所有业务上报数据,每天几亿。

region server

数量:29

单个 server 的 max heap 50G,Direct Memory Configured 50G,Memstore Size 14.6G

region

共80个,每个region40G存储空间

但是只有一个region有效,其他region的数据在spilit完以后已经过期删除,该region信息如下:

Region Name Num. Stores Num. Storefiles Storefile Size Uncompressed Storefile Size Index Size Bloom Size Data Locality
hotel:shield_log_total,202004131244_e74d,1586919481374.2bc43dc5c0a8a33675177eb79b48a861. 1 39 6188861m 1728442m 4368608k 4219944k 1.0

参考:单个region server 的 region 数目上限hbase split操作

表结构

1
'hotel:shield_log_total', {NAME => 'log_info', BLOOMFILTER => 'ROW', VERSIONS => '1', IN_MEMORY => 'false', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', TTL => '2592000 SECONDS (30 DAYS)', COMPRESSION => 'LZO', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0', CONFIGURATION => {'hbase.hregion.max.filesize' => '42949672960'}}

配置

mob说明:https://blog.csdn.net/bingdianone/article/details/84863248

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# region 副本数
dfs.replication=3
# 该值>=3时才开启mob特性
hfile.format.version=2
# mob 压缩批处理中允许的最大mob文件数量,合并小的mob文件,mob特性用于存储二进制,如图片等
hbase.mob.file.compaction.batch.size=100

# 写入 memstore 的累计大小超过该值时执行flush,生成 hfile
hbase.hregion.memstore.flush.size=134217728(128M)
# 单个Region内所有的memstore大小总和,超过则flush到磁盘,默认值0.4。这个参数的作用是防止内存占用过大,当ReigonServer内所有region的memstores所占用内存总和达到heap的40%时,HBase会强制block所有的更新并flush这些region以释放所有memstore占用的内存。
hbase.regionserver.global.memstore.upperLimit
# 默认值0.35。在所有region的memstores所占用内存达到Heap的35%时,不flush所有的memstore。它会找一个memstore内存占用最大的region,做个别flush,此时写更新还是会被block。
base.regionserver.global.memstore.lowerLimit
# 单个memstore大小超过该倍数值则block所有写入请求,自我保护
hbase.hregion.memstore.block.multiplier
# 一次 minor compaction最少合并的HFile数量,高版本叫 hbase.hstore.compaction.min
hbase.hstore.compactionThreshold=3
# Major compaction周期性时间间隔,默认值604800000,单位ms。HBase 0.96.x及之前默认为1天调度一次。设置为 0 表示禁用自动触发major compaction。
hbase.hregion.majorcompaction=0
# 一次 minor compaction最多合并的HFile数量,默认值 10。这个参数也是控制着一次压缩的时间。
hbase.hstore.compaction.max=10
# compaction过程中,每次从Hfile中读取kv的个数
hbase.hstore.compaction.kv.max=100
# 每隔一定时间检查是否需要进行compaction
hbase.server.thread.wakefrequency=100
# large 和 small compaction 线程池线程数
hbase.regionserver.thread.compaction.small=40
hbase.regionserver.thread.compaction.large=2
# compact(major和minor)请求进入large和small compact线程池的临界点,默认 2 * this.minFilesToCompact * this.region.memstoreFlushSize
hbase.regionserver.thread.compaction.throttle=
# 超过该值阻塞写入
hbase.hstore.blockingStoreFiles=40
# 阻塞写入的时间,单位ms
hbase.hstore.blockingWaitTime=30000

# 一个region中最大store的压缩后的hfile大小,大于设置阈值之后触发region切分(对于下面的策略来说)
hbase.hregion.max.filesize=10737418240(10G)
# 切分策略,线上使用hbase版本是1.2.0,默认采用该策略。使用上面的store配置进行切分
# 0.98版本后默认使用IncreasingToUpperBoundRegionSplitPolicy
hbase.regionserver.region.split.policy=org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy

写入问题

热点问题

rowkey写入规则为yyyyMMddHHmm_唯一标识,存在写入热点问题,频繁超时、日志丢失,需要优化。

flush频率较高

如图所示,flush间隔时间大约10s,每次flush128M数据

compact问题

现在的配置是每个region最多40个hfile,flush频繁导致compact也会很频繁。

  1. 参考

    HBase compact 总结 及 调优配置

    深入理解 HBase Compaction 机制

    HBase Compaction详解

  2. compaction触发条件:memstore flush时检查、定时任务周期检查、hbase shell 或 master ui 或 hbase api 等方式手动触发。

  3. 写请求非常多导致不断生成HFile,但compact的速度远远跟不上HFile生成的速度,这样就会使HFile的数量会越来越多,导致读性能急剧下降。为了避免这种情况,在HFile的数量过多的时候会限制写请求的速度:在每次执行MemStore flush的操作前,如果HStore的HFile数超过hbase.hstore.blockingStoreFiles (默认7),则会阻塞flush操作hbase.hstore.blockingWaitTime时间,在这段时间内,如果compact操作使得HStore文件数下降到回这个值,则停止阻塞。另外阻塞超过时间后,也会恢复执行flush操作。这样做就可以有效地控制大量写请求的速度,但同时这也是影响写请求速度的主要原因之一。

  4. HBase主要有两种minor策略:RatioBasedCompactionPolicy(0.94默认,我们采用的)和ExploringCompactionPolicy(0.96后默认,IO性能提升10%)

region split 问题

  1. 参考:HBase最佳实践之Region数量&大小HBase Region自动切分的所有细节

    memstore总大小默认为堆内存的40%

  2. 当单个hfile大小超过限额时,会出现 region split。目前全量日志表没有做预分区,因此所有日志都向一个region写,当写入频繁而region非常大时,会出现Hbase写入量大导致region过大无法split

    正常split:隔几天split一次

    太大无法spilit:很久没有split

写入问题分析和解决思路

  1. 由于hbase数据是按照字典序排序,没有分区会导致连续数据集中写入同一个region,所以解决写入热点问题主要原理是将并发写入的数据打散,分布均匀的写入到多个region。

  2. 写入数据总览

    region名:hotel:shield_log_total,202010281022_abdf:,1604395921838.f6a7dfcbe71eb36e12047c2c1e1dae40.

    文件起始 202010281022,截止到202011041022,Storefile Size Uncompressed 4788807m(4676G),Storefile Size 1352194m(1320G)

    共计7天的数据,按照压缩后的文件大小计算后约 188.6G/d,约 7.86G/h,按照总量是35G/h。

  3. 解决方案先预分区,然后根据读取性能需要,将messageId按照hash方式映射到各个region中,再加上时间戳。分区案例:create 'hotel:shield_log_analyse_ip', {NAME => 'info', TTL => '7776000'}, {SPLITS => ['20', '40', '60', '80']}

  4. 计算每RS(假设一个表)region的数量的公式为:

    ((RS memory) * (total memstore fraction)) / ((memstore size)*(# column families))

    例如: 如果 一个RegionServer配置的内存是16g,使用默认配置( hbase默认regionserver分给memstore的比例是0.4 , 默认的menstore的占用128M内存 ), 一个CF,那么这个regionServer下的region的个数大约为 16384 * 0.4 / (128*1) = 51个,实际测试大于这个数 一两倍 也没太大的问题。 一个HBase表包含一至多个region,那么表的数目上限也是可以估算出来的。

读取问题

全量扫描读取缓慢

读取问题分析和解决思路

  1. 查询条件均包含时间,核心的几个处理流程基本上都会指定渠道或actions,日志分析还包括resultType因此在设计rowkey可以给rowkey增加这些字段,让查询速度更快。

  2. 避免热点的话就需要预分区,比如可以分为100个预分区(00-99),按照上面统计的35G/h,这样每个分区每小时0.35G数据。提高写性能主要是减少compact次数,按照每个memstore 128M,大约350M/128M=3次/h,约20分钟写一次hfile,而超过40次才会发生compact,这样compact的间隔可以控制在40/3=13h。每天大约发生两次compact,hbase写入由原先的10s一次,大约12h发生4000次compact,写入性能大大提高。

  3. 预分区前缀会导致查询时遍历所有region,导致查询性能降低这个是不可避免的一个趋势,可以通过多线程去解决这个问题。但是由于需要兼容之前的逻辑,所以需要在每个region里查询时都要尽量带上时间戳。但是rowkey也不能设计的过长避免浪费内存(写入和compact都以kv形式存在),所以初步设计rowkey如下:

    分区号_yyyyMMddHHmm_source_method_resultType_uuid,例如 25_202011042324_Andriod_getHotelList_1_ask54n83fal4

  4. 最后就是考虑查询时的代码兼容。老表数据量严重超标而且无法split已经不堪重负,直接写入新rowkey没多大意义。而且预分区需要在建表时指定,新建一个表数据双写,等过了几天之后一键切换表名。然后就是查询,考虑兼容时间问题,在每个region查询时都要在分区数字之后加上时间戳。website项目里主要是去修改公共的查询方法,在时间戳前面加上region分区号,遍历每个region指定时间的数据后统一返回。mapreduce需要在main入口输入起始startRowkey和endRowkey,加入正则或者字符串过滤器。

  5. bloomfilter提高随机读性能,row只针对行,rowcol针对行和列,可以只查满足条件的storefile。但该表主要是scan,没有get场景,不需要设置bloomfilter。

  6. 第一版建表语句如下:(随机分区)

    1
    create 'hotel:shield_log', {NAME => 'log_info',  TTL => '2592000', VERSION => '1'}, {SPLITS => ['00', '01', '02', '03', ..., '99']}

    保留30天数据,预分区100个,每份数据保留版本1个

    效果:但无法兼容之前的分页查询

  7. 第二版建表语句如下:(新版,翻转时间串)

    1
    create 'hotel:shield_log', {NAME => 'log_info',  TTL => '2592000', VERSION => '1'}, {SPLITS => ['00', '01', '02', '03', ..., '59']}

    保留30天数据,预分区60个(一分钟一个分区),每份数据保留版本1个

    rowkey设计:

    二期:mmHHddMM_periodId_sourceId_uri_resultType_uuid

    例如42082511_2_112_123_/abc/dss_ask54n83fal4

    一期:mmHHddMM_periodId_sourceId_method_resultType_uuid

    例如 42082511_1_100_102_getHotelDetail_kasdlew23fd

    效果:原来需要查一分钟的日志量需要耗时60s左右,现在只需要3s。但是需要对原来的时间格式做修改,仍然无法做到不改动其他逻辑兼容之前顺序设计的时间格式

  8. 最终修改版建表语句:(分钟数单独作为分区位,时间串保持之前的顺序格式)

    mm_yyyyMMddHHmm_periodId_sourceId_uri_resultType_uuid

    例如 12_202012031012_2_233_/abc/ddd_asdlkfasd3

    效果:完全兼容老版本,并减少scan扫描的数据量。经测试,原表查询5分钟的数据,新表只需7s。

hbase其他优化

http://www.blogjava.net/DLevin/archive/2015/08/22/426877.html

http://www.blogjava.net/DLevin/archive/2015/08/22/426950.html

https://zhuanlan.zhihu.com/p/69740865

压缩

HBase在写入数据块到HDFS之前会首先对数据进行压缩,再落盘,从而减少磁盘空间使用量。而在读数据的时候首先从HDFS中加载出block块之后进行解压缩,然后再缓存到BlockCache,最后返回给用户。

压缩算法 压缩比率 压缩速度 解压速度
GZIP 13.4% 21 MB/s 118 MB/s
LZO 20.5% 135 MB/s 410 MB/s
Snappy 22.2% 172 MB/s 409 MB/s

综合来看,Snappy的压缩率最低,但是编解码速率最高,对CPU的消耗也最小,目前一般建议使用Snappy。

后续跟踪

按照新方案设计的全量日志表shield_log在读写方面完全解决掉了之前的痛点问题,但是时间长了以后会发现,仅过去一个月切分出了800个分区。。原因还是日志量过大split分区频繁。此时需要加以其他辅助,例如将老的region进行合并或删除等。

 Comments