15-Hbase深入理解数据读写流程数据刷写合并切分和表设计原则
Posted 大数据下的画像人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了15-Hbase深入理解数据读写流程数据刷写合并切分和表设计原则相关的知识,希望对你有一定的参考价值。
8.6 HBase读写流程⭐️
对于HBase框架,读比写慢(与其他框架相反)
8.6.1 公共流程(三层索引)
- 对于数据对应的Region位置在哪里的问题的提出
- HBase中单表的数据量通常可以达到TB级或PB级,但大多数情况下数据读取可以做到毫秒级。HBase是如何做到的呢?要想实现表中数据的快速访问,通用的做法是数据保持有序并尽可能的将数据保存在内存里。HBase也是这样实现的
- 对于海量级的数据,首先要解决存储的问题。数据存储上,HBase将表切分成小一点的数据单位region,托管到RegionServer上,和以前关系数据库分区表类似。但比关系数据库分区、分库易用。这一点在数据访问上,HBase对用户是透明的。数据表切分成多个Region,用户在访问数据时,如何找到该条数据对应的region呢?
1. HBase 0.96以前
-
两个表
-Root-
和.Meta.
-
系统维护的两个特殊的表
-Root-
和.Meta.
用来查找各种表的region位置在哪里 -
-Root-和.Meta.也像HBase中其他表一样会切分成多个region
-
.Meta. 表
- 存储了表对应Region对应的RegionServer RowKey的区间(目录)
- 但是.Meta. 表也是一张普通的Hbase表,也需要存放到RegionServer中
-
-Root- 表
- 专门用来记录 .meta.表的存放位置(目录的目录),通过-root-表就可以找到 .meta. 表的region位置
- 它的机构和meta一模一样,但是它只维护meta表的切分信息
- 理论上-root-表不会被切分(数据量),一般 -root- 表只需要一个Region即可(目录的目录,内容不会超过一个Region的10G大小),-root- 的Region信息存放在Zookeeper
-
-
用户访问表的流程
- client–>Zookeeper --> .root. 表–>-meta-表–>RegionServer–>Region
2.HBase 0.96以后
-
-ROOT-表被移除,直接将.Meta表region位置信息存放在Zookeeper中。Meta表更名为hbase:meta,
-
查询流程
Client—>Zookeeper—>HBase:meta—>用户表Region
8.6.2 读取数据流程
版本一:
- Client访问Zookeeper,获取hbase:meta所在的RegionServer的节点信息
- Client访问hbase:meta所在的RegionServer,获取hbase:meta记录的元数据后先加载到内存中,然后再从内存中根据需要查询的RowKey来查询出RowKey所在的Region的相关信息(Region在RegionServer中)
- Client访问RowKey所在的RegionServer,发起数据读取请求
- RegionServer构建RegionScanner,用于对该Region的数据检索
- 需要查询的RowKey分布在多少个Region中就需要构建多少个RegionScanner
- RegionScanner构建StoreScanner,用于对该列族的数据检索
- Region中有多少个Store就需要构建多少个StoreScanner,Store的数量取决于Table的ColumnFamily的数量
- 多个StoreScanner合并构建最小堆(已排序的完全二叉树)StoreHeap:PriorityQueue
- StoreScanner构建一个MemStoreScanner和一个或多个StoreFileScanner(数量取决于StoreFile的数量)
- 过滤掉某些能够确定要查询的RowKey一定不在StoreFile内对于的StoreFileScanner或MEMStoreScanner
- 经过筛选后留下的Scanner开始做读取数据的准备,将对应的StoreFile定位到满足RowKey的起始位置
- 将所有的StoreFileScanner和MemStoreScanner合并构建最小堆
- KeyValueHeap:PriorityQueue,排序的规则按照KeyValue从小到大排序
- 从KeyValueHeap:PriorityQueue中经过一系列筛选后一行行的得到需要查询的KeyValue。
8.6.3 写入数据流程
版本一:
- 首先访问Zookeeper,获取hbase:meta表位于哪个RegionServer中
- 访问对应的RegionServer,获取hbase:meta表,将其缓存到连接中,作为连接属性MetaCache,由于meta表具有一定的数据量,导致了创建连接比较慢
- 之后使用创建的连接获取Table,这是一个轻量级的连接,只有在第一次创建连接的时候会检查表格是否存在访问RegionServer,之后再获取Table时不会访问RegionServer
- 调用Table的put方法写入数据,此时还需要解析RowKey,对照缓存的MetaCache,查看具体写入的位置有哪个REgionServer
- 将数据顺序写入(追加)到WAL,此处写入是直接写到HDFS上,并设置专门的线程控制WAL预写日志的滚动(类似于Flume)
- 根据写入命令的RowKey和Column Family查看具体写入到哪个Memstore,并且在MemStore中排序
- 向客户端发送ack
- 等达到MemStore的刷写时机后,将数据写到对应的Store中
版本二:
- 首先客户端和RegionServer建立连接
- 然后将DML要做的操作写入到日志wa-log中
- 将数据的修改更新到memstore中,本次的操作介绍
- 一个region由多个store组成,一个store对应一个CF(列族),store包括位于内存中的memstore和位于磁盘中的StoreFile,写操作先写入memstore
- 当memstore数据写到阈值之后,创建一个新的memstore
- 旧的memstore写成一个独立的StoreFile,RegionServer会启动flashcache进程写入StoreFile,每次写入形成单独的一个StoreFile,存放到HDFS上⛔️
- 当StoreFile文件的数量增长到一定阈值后,系统会进行合
- minor compaction
- major compaction
- 在合并的过程中会进行版本合并和删除工作,形成更大的StoreFile
- 当一个region所有StoreFile的大小和数量超过一定阈值后,会把当前的region分割成两个,并由HMaster分配到相应的RegionServer服务器,实现负载均衡
- Store负载管理当前列族的数据
- Store=1 memstore+n storefile
- 当我们进行数据DML的时候,以插入数据为例
- 我们会将数据先存到memstore中,当memstore达到阈值(128M)时
- 首先我们会创建一个新的memstore
- 然后会将memstore中的数据写成一个StoreFile,然后StoreFile会存储到HDFS上
- 随着时间的推移,数据会进行合并
- HFile中会存放大量的失效数据(删除、修改)
- 会产生多个HFile
- 等达到阈值(时间、数量)会进行合并
- 多个HFile会合并成一个大的HFile
- 合并会触发连锁反应,相邻的store也会进行合并
- 在Hbase中,表被分割成多个更小的块然后分散的存储在不同的服务器上,这些小块叫做Regions,存放Regions的地方叫做RegionServer。Master进程负责处理不同的RegionServe之间的Region的分发。在Hbase实现中HRegionServer和HRegion类代表RegionServer和Region。HRegionServer除了包含一些HRegions之外,还处理两种类型的文件用于数据存储
- HLog 预写日志文件,也叫做WAL(write-ahead log)
- HFile 是HDFS中真实存在的数据存储文件
8.7 数据刷写(Memstore Flush)
8.7.1触发时机
-
Region中所有的MemStore占用的内存超过相关阈值
- hbase.hregion.memstore.flush.size 参数控制,默认为128MB
- 如果我们的数据增加得很快,达到了 hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier的大小hbase.hregion.memstore.block.multiplier 默认值为4,也就是128*4=512MB的时候,那么除了触发 MemStore 刷写之外,HBase 还会在刷写的时候同时阻塞所有写入该 Store的写请求!
-
整个RegionServer的MemStore占用内存总和大于相关阈值
- HBase 为 RegionServer 所有的 MemStore 分配了一定的写缓存(),大小等于
- hbase_heapsize(RegionServer 占用的堆内存大小)* hbase.regionserver.global.memstore.size (默认值是 0.4)。
- 如果整个 RegionServer 的 MemStore 占用内存总和大于阈值将会触发 MemStore 的刷写。
- hbase.regionserver.global.memstore.size.lower.limit (默认值为 0.95)* MAX_SIZE
- 例如:HBase 堆内存总共是 32G ,MemStore 占用内存为:32 * 0.4 * 0.95 = 12.16G将触发刷写
- 如果达到了 RegionServer 级别的 Flush,当前 RegionServer 的所有写操作将会被阻塞,这个阻塞可能会持续到分钟级别。
- HBase 为 RegionServer 所有的 MemStore 分配了一定的写缓存(),大小等于
-
WAL数量大于相关阈值
- 数据到达 Region 的时候是先写入 WAL,然后再被写到 Memstore 。
- 如果 WAL 的数量越来越大,这就意味着 MemStore 中未持久化到磁盘的数据越来越多。
- 当 RS 挂掉的时候,恢复时间将会变得很长,所以有必要在 WAL 到达一定的数量时进行一次刷写操作
-
定期自动刷写
- 默认值 3600000(即 1 小时),HBase 定期 Flush 所有 MemStore 的时间间隔。
- 一般建议调大,比如 10 小时,因为很多场景下 1 小时 Flush 一次会产生很多小文件,一方面导致 Flush 比较频繁,另一方面导致小文件很多,影响随机读性能
-
数据更新超过一定阈值
- 如果 HBase 的某个 Region 更新的很频繁,而且既没有达到自动刷写阀值,也没有达到内存的使用限制,但是内存中的更新数量已经足够多,也会触发刷写的
- 比如超过 hbase.regionserver.flush.per.changes 参数配置,默认为30000000,那么也是会触发刷写的。
-
手动触发刷写
- Shell 中通过执行 flush 命令
hbase> flush 'TABLENAME' hbase> flush 'REGIONNAME' hbase> flush 'ENCODED_REGIONNAME' hbase> flush 'REGION_SERVER_NAME'
-
特别注意:
- 以上所有条件触发的刷写操作最后都会检查对应的 HStore 包含的 StoreFiles 文件数是否超过hbase.hstore.blockingStoreFiles 参数配置的个数,默认值是16。
- 如果满足这个条件,那么当前刷写会被推迟到hbase.hstore.blockingWaitTime 参数设置的时间后再刷写。
- 在阻塞刷写的同时,HBase 还会请求 Compaction 或者Split 操作。
8.7.2 刷写策略
- HBASE1.1之前:
- MemStore 刷写是 Region 级别的。就是说,如果要刷写某个 MemStore ,MemStore 所在的 Region 中其他 MemStore 也是会被一起刷写的
- HBASE2.x之后
- FlushAllStoresPolicy
- 每次刷写都是对 Region 里面所有的 MemStore 进行的
- FlushAllLargeStoresPolicy
- 判断 Region 中每个 MemStore 的使用内存是否大于某个阀值,大于这个阀值的MemStore 将会被刷写。
- flushSizeLowerBound = max((long)128 / 3, 16) = 42
- FlushNonSloppyStoresFirstPolicy
- FlushAllStoresPolicy
8.7.3 刷写流程
- prepareFlush 阶段:
- 刷写的第一步是对 MemStore 做 snapshot(快照)
- 为了防止刷写过程中更新的数据同时在 snapshot 和 MemStore 中而造成后续处理的困难
- 所以在刷写期间需要持有 updateLock 。持有了 updateLock 之后,这将阻塞客户端的写操作。
- 所以只在创建 snapshot 期间持有 updateLock
- 而且 snapshot 的创建非常快,所以此锁期间对客户的影响一般非常小。
- 对 MemSt
- ore 做 snapshot 是 internalPrepareFlushCache 里面进行的。
- flushCache 阶段:
- 如果创建快照没问题,那么返回的 result.result 将为 null。
- 这时候我们就可以进行下一步 internalFlushCacheAndCommit。
- 其实 internalFlushCacheAndCommit 里面包含两个步骤:flushCache 和 commit 阶段。
- flushCache 阶段:
其实就是将 prepareFlush 阶段创建好的快照写到临时文件里面,临时文件是存放在对应 Region 文件夹下面的 .tmp 目录里面。 - commit 阶段:
将 flushCache 阶段生产的临时文件移到(rename)对应的列族目录下面,并做一些清理工作,比如删除第一步生成的 snapshot。
- flushCache 阶段:
8.8 数据合并(Compaction)
8.8.1 合并分类
HBase 根据合并规模将 Compaction 分为了两类:MinorCompaction 和 MajorCompaction
-
Minor Compaction
- 是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell,但是会处理超过TTL的数据
- 一次Minor Compaction的结果是让小的storefile变的更少并且产生更大的StoreFile。
-
Major Compaction
- 是指将所有的StoreFile合并成一个StoreFile
- 清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。
- 一般情况下,Major Compaction时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。因此线上业务都会将关闭自动触发Major Compaction功能,改为手动在业务低峰期触发。
8.8.3 合并时机
触发compaction的方式有三种:Memstore刷盘、后台线程周期性检查、手动触发。
-
Memstore刷盘
- memstore flush会产生HFile文件,文件越来越多就需要compact。
- 每次执行完Flush操作之后,都会对当前Store中的文件数进行判断,一旦文件数大于配置3,就会触发compaction。
- compaction都是以Store为单位进行的,而在Flush触发条件下,整个Region的所有Store都会执行compact
-
后台线程周期性检查
- 后台线程定期触发检查是否需要执行compaction,检查周期可配置。
- hbase.server.thread.wakefrequency(默认10000毫秒)*hbase.server.compactchecker.interval.multiplier(默认1000)
- CompactionChecker大概是2hrs 46mins 40sec 执行一次
- 小文件周期性合并成大文件
- 线程先检查小文件数是否大于配置3,一旦大于就会触发compaction。
- 大文件周期性合并成Major Compaction
- 如果不满足,它会接着检查是否满足major compaction条件,
- 如果当前store中hfile的最早更新时间早于某个值mcTime,
- 就会触发major compaction(默认7天触发一次,可配置手动触发)。
- 如果不满足,它会接着检查是否满足major compaction条件,
- 后台线程定期触发检查是否需要执行compaction,检查周期可配置。
-
手动触发
- 一般来讲,手动触发compaction通常是为了执行major compaction,一般有这些情况需要手动触发合并
- 是因为很多业务担心自动major compaction影响读写性能,因此会选择低峰期手动触发;
- 也有可能是用户在执行完alter操作之后希望立刻生效,执行手动触发major
compaction; - 是HBase管理员发现硬盘容量不够的情况下手动触发major compaction删除大量过期数据;
- 一般来讲,手动触发compaction通常是为了执行major compaction,一般有这些情况需要手动触发合并
8.8.4 合并策略
承载了大量IO请求但是文件很小的HFile,compaction本身不会消耗太多IO,而且合并完成之后对读的性能会有显著提升。
线程池选择
- HBase CompacSplitThread类内部对于Split、Compaction等操作专门维护了各自所使用的线程池和Compaction相关的是如下的longCompactions和shortCompactions
- 前者用来处理大规模compaction,后者处理小规模compaction
- 默认值为2 * maxFlilesToCompact * hbase.hregion.memstore.flush.size
- 如果flush size 大小是128M,该参数默认值就是2 * 10 * 128M = 2.5G
合并策略选择
HBase 主要有两种 minor 策略: RatioBasedCompactionPolicy (0.96.x之前)和ExploringCompactionPolicy(当前默认)
-
RatioBasedCompactionPolicy(基于比列的合并策略)
- 从老到新逐一扫描HFile文件,满足以下条件之一停止扫描
- 当前文件大小<比当前文件新的所有文件大小总和*ratio(高峰期1.2,非高峰期5)
- 当前所剩候选文件数<=阈值(默认为3)
- 从老到新逐一扫描HFile文件,满足以下条件之一停止扫描
-
ExploringCompactionPolicy策略(默认策略)
- 基于Ratio策略,不同之处在于Ratio策略找到一个合适文件集合就停止扫描,而Exploring策略会记录所有合适的文件集合,然后寻找最优解,待合并文件数最多或者待合并文件数相同的情况下文件较小的进行合并
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B6i7Op4K-1656514880617)(https://typora-js.oss-cn-shanghai.aliyuncs.com/img/ic3C6ANrbqFTpeQ.png)]
-
FIFO Compaction策略
- 收集过期文件并删除,对应业务的列簇必须设置有TTL
-
Tier-Based Compaction策略(分层策略)
- 针对数据热点情况设计的策略,根据候选文件的新老程度将其划分为不同的等级,每个等级都有对应的Ratio,表示该等级文件比选择为参与Compation的概率
-
Stripe Compation策略(条纹策略)
- 将整个Store中的文件按照key划分为多个range,此处称为stripe,一个Stripe内部就类似于一个小Region,可以执行Minon Compation和major Compation
执行文件合并
- 分别读出待合并hfile文件的KV,并顺序写到位于./tmp目录下的临时文件中
- 将临时文件移动到对应region的数据目录
- 将compaction的输入文件路径和输出文件路径封装为KV写入WAL日志,并打上compaction标记,最后强制执行sync
- 将对应region数据目录下的compaction输入文件全部删除
8.9 数据切分(Region Split)
通过切分,一个region变为两个近似相同大小的子region,再通过balance机制均衡到不同 region server上,使系统资源使用更加均衡。
8.9.1 切分原因
- 数据分布不均匀
- 同一 region server 上数据文件越来越大,读请求也会越来越多。一旦所有的请求都落在同一个 region server 上,尤其是很多热点数据,必然会导致很严重的性能问题。
- compaction性能损耗严重
- compaction本质上是一个排序归并的操作,合并操作需要占用大量的内存,因此文件越大,占用内存越多
- compaction有可能需要迁移远程数据到本地进行处理(balance之后的compaction就会存在这样的场景),如果需要迁移的数据是大文件的话,带宽资源就会损耗严重
- 资源耗费严重
- HBase的数据写入量也是很很惊人的,每天都可能有上亿条的数据写入
- 不做切分的话一个热点region的新增数据量就有可能几十G,用不了多长时间大量读请求就会把单台region server的资源耗光。
8.9.2 触发时机
- 触发时机
- 每次数据合并之后都会针对相应region生成一个requestSplit请求,requestSplit首先会执行checkSplit,检测file size是否达到阈值,如果超过阈值,就进行切分。
- 检查阈值算法
- 主要有两种:ConstantSizeRegionSplitPolicy( 0.94版本)和IncreasingToUpperBoundRegionSplitPolicy(当前)
- ConstantSizeRegionSplitPolicy
- 系统会遍历Region所有的store的文件大小,如果文件大小>hbase.hregion.max.filesize(默认10G),就会触发切分操作。
- IncreasingToUpperBoundRegionSplitPolicy
- 如果store大小大于一个变化的阀值就允许split。
- 默认只有1个region,那么逻辑这个region的store大小超过 1 * 1 * 1 * flushsize * 2 =128M * 2 =256M 时,才会允许split
- 切分之后会有两个region,其中一个region中的某个store大小大于 2 * 2 * 2 * flushsize * 2= 2048M 时,则允许split
- 后续超过hbase.hregion.max.filesize + hbase.hregion.max.filesize * 随机小数 *
hbase.hregion.max.filesize.jitter才允许split - 基本也就固定了,如果粗劣的计算可以把这个hbase.hregion.max.filesize的大小作为最后的阀值,默认是10G
8.9.3 切分流程
-
寻找切分点
- 将一个region切分为两个近似大小的子region,首先要确定切分点。切分操作是基于region执行的,每个region有多个store(对应多个column famliy)。系统首先会遍历所有store,找到其中最大的一个,再在这个store中找出最大的HFile,定位这个文件中心位置对应的rowkey,作为region的切分点。
-
开启切分事物
切分线程会初始化一个SplitTransaction对象,从字面上就可以看出来Split流程是一个类似于’事物’的过程,整个过程分为三个阶段:prepare-excute-rollback
- prepare阶段
- 在内存中初始化两个子Region,具体是生成两个HRegionInfo对象,包含tableName、RegionName、startkey、endkey等。同时会生成一个transaction journal,这个对象用来记录切分的进展
- execute 阶段
- region server 更改ZK节点 /region-in-transition 中该region的状态为SPLITING。
- master检测到region状态改变。
- region在存储目录下新建临时文件夹.split保存split后的daughter region信息。
- parent region关闭数据写入并触发flush操作,将写入region的数据全部持久化到磁盘。
- 在.split文件夹下新建两个子文件夹,称之为daughter A、daughter B,并在文件夹中生成引用文件,分别指向父region中对应文件。
- 将daughter A、daughter B拷贝到HBase根目录下,形成两个新的region。
- parent region通知修改 hbase.meta 表后下线,不再提供服务。
- 开启daughter A、daughter B两个子region。
- 通知修改 hbase.meta 表,正式对外提供服务。
- rollback阶段
- 如果execute阶段出现异常,则执行rollback操作。
- 为了实现回滚,整个切分过程被分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。
- prepare阶段
8.9.4 切分优化(预分配Region)
- 切分优化
- 对于预估数据量较大的表,需要在创建表的时候根据RowKey执行Region的预分配
- 预分配解决的问题
- 通过Region预分配,数据会被均衡到多态机器上,这样可以一定程度上解决热点应用数据剧增导致的性能问题
8.10 Hbase表设计原则⭐️
8.10.1 行键的设计
行健不能改变,唯一可以改变的方式是先删除后插入,总的来说大体遵循以下四点原则
- 写入的时候能分散到多台服务器上;
- 大量写入的时候,单台节点压力是很大的,因此将写入分散到多台机器上能有效提升 TPS,并且避免单节点宕机;
- 读取的时候都尽量批量顺序访问;
- 读取的时候批量获取结果比单条结果的获取效率要高一到两个数量级以上;不论是mysql 还是 HBase 这都是很好的实践。由于 NoSQL 需要针对访问方式(比如随机或者顺序的查询等方面)设计,而不强制数据结构的规范,因此这点对 HBase 更有意义。如果可能,对于多次不同的读请求最好能比较均匀的分布到多个节点上
- 尽量短;(按 byte 数组越短越优)
- 对于HBase的存储,rowKey是唯一标示数据的关键字,对于每一个Value都会重复存储,因此越短越好。从存储空间利用率讲,也应该是在业务可以接受的情况下,越短越好
- rowKey 在 HBase 中只插入不更新;
- 在 HBase 中所有操作都是“流式”操作,不会更改原来存储过的数据,只会进行 append,并通过加上新的版本标识表示是最新数据
1. 长度原则
-
长度原则
- rowkey是一个二进制码流,可以是任意字符串,最大长度64kb,实际应用中一般为10-100bytes,以byte[]形式保存,一般设计成定长。建议越短越好,不要超过16个字节
-
越短越好的原因
- 数据的持久化文件HFile中是按照KeyValue存储的,如果RowKey过长,比如超过100字节,1000w行数据,光RowKey就业占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率
- MemStore将缓存部分数据到内存,如果RowKey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样就会降低检索效率
- 目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性
2. 散列原则
- 散列原则理解
- 尽量将连续数据( RowKey)存放到更多的RegionServer上
- 我们设计的RowKey应该均匀的分布在各个HBase节点上,避免单个RegionServer机器负载过高,引起性能下降甚至Region不可用
- 散列原则能够避免的问题
- 假如RowKey是按照时间戳的方式递增,RowKey的第一部分如果是时间戳信息的话将会造成所有的新数据都在一个RegionServer上堆积的热点现象,也就是通常说的Region热点问题。
- 热点发生在大量的client直接访问集中在个别RegionServer上(访问可能是读,写或者其他操作),导致单个RegionServer机器自身负载过高,引起性能下降甚至Region不可用,常见的是发生jvm full gc或者显示region too busy异常情况,当然这也会影响同一个RegionServer上的其他Region
- 比如设计 RowKey 的时候,当 Rowkey 是按时间戳的方式递增,就不要将时间放在二进制码的前面,可以将 Rowkey 的高位作为散列字段,由程序循环生成,可以在低位放时间字段,这样就可以提高数据均衡分布在每个 Regionserver 实现负载均衡的几率
3. 唯一原则
-
如何保证唯一原则
- RowKey字典序排序
-
保证唯一原则的原因
- 由于HBase中数据存储是Key-Value形式,若HBase中同一表插入相同Rowkey,则原先的数据会被覆盖掉(如果表的version设置为1的话),所以务必保证Rowkey的唯一性
- 由于 RowKey 用来唯一标识一行记录,所以必须在设计上保证 RowKey 的唯一性
4. 数据热点
- 定义理解
- HBase中的行是按照RowKey的字典序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan操作
- 但是同时RowKey这种字典序排序的设计也是数据热点问题的源头,热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读、写或者其他操作)。大量的访问会使热点Region所在的单个机器超出自身承受能力,引起性能下降甚至Region不可用,这也会影响同一个RegionServer上的其他Region,由于主机无法服务其他Region的请求,设计良好的数据访问模式可以使集群被充分、均衡的利用
- 为了避免写热点,设计RowKey的数据写入到集群的多个Region,而不是一个Region
- 避免热点的方法
- 加盐
- 在 rowkey 的前面增加随机数,具体就是给 rowkey 分配一个随机前缀以使得它和之前的 rowkey 的开头不同
- 分配的前缀种类数量应该和你想使用数据分散到不同的region的数量一致
- 加盐之后的rowkey就会根据随机生成的前缀分散到各个region上,以避免热点
- 哈希
- 哈希是特殊的加盐,只是加盐是在RowKey前面增加随机数,哈希是在RowKey前面加一个固定的不变的前缀,因为必须要让客户端能够完整的重构RowKey
- 所以一般会拿原 RowKey 或其一部分计算 Hash 值,然后再对 Hash 值做运算作为前缀,达到RowKey分散到各个不同的Region上
- 反转
- 反转固定长度或者数字格式的 rowkey
- 这样可以使得 rowkey 中经常改变的部分放在前面。这样可以有效的随机 rowkey,但是牺 牲了 rowkey 的有序性
- 反转分为一般数据反转和时间戳反转,其中以时间戳反转较常见。
- 加盐
8.10.2 列族设计
1. 减少列族的设计
- 减少列族的原因
- HBase本身的设计目标是支持稀疏表,而稀疏表通常会有很多列,但是每一行有值的列又比较少。在HBase中Column Family的数量通常很小,同时HBase建议把经常一起访问的比较类似的列放在同一个Column Family中,这样就可以在访问少数几个列时,只读取尽量少的数据。
- 在设计HBase Schema(架构)的时候,尽量只有一个Column Family的根本原因
- 在设计Hbase schema的时候,**要尽量只有一个column family,至于为什么主要从flush和compaction说起,**它们触发的基本单位都是Region级别,所以当一个column family有大量的数据的时候会触发整个region里面的其他column family的memstore(其实这些memstore可能仅有少量的数据,还不需要flush的)也发生flush动作;另外compaction触发的条件是当store file的个数(不是总的store file的大小)达到一定数量的时候会发生,而flush产生的大量store file通常会导致compaction,flush/compaction会发生很多IO相关的负载,这对Hbase的整体性能有很大影响,所以选择合适的column family个数很重要。
2. 数据块缓存配置
-
如果经常顺序访问或者很少访问,可以关闭列族的缓存,让BLOCKCACHE 参数设置false,列族缓存默认打开。
create 'mytable',NAME=>'colfam1',BLOCKCACHE=>'false'
3. 列族压缩
-
压缩可以节省空间,提高磁盘利用率,但是读写数据会增加CPU的使用率 LZO,SNAPPY,GZIP(不常用)。
create 'mytable',NAME=>'colfam1',COMPRESSION=>'SNAPPY'
-
注意:数据只在硬盘上是压缩的,在内存(MemStore或BlockCache)或通过网络传输是是没有压缩的。
参考资料:HBase的RowKey与列族设计原则 - 跳出 - 博客园 (cnblogs.com)
8.11 HBase经典设计案例
一个系统上线之后,开发和调优将一直贯穿系统的生命周期中,HBase也不列外。这里主要说一些
Hbase的调优
Hbase创建表时,只需指定表名和至少一个列族,基于HBase表结构的设计优化主要是基于列族级别的属性配置
8.12 HBase常用优化
8.12.1 HBase表优化
1. 预分区
- 预分区(Pre-Creating Regions)定义理解
- 默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分
- 预分区具体做法
- 有种加快批量写入速度的方法是通过预先创建一些空的regions(前提是知道这个表的内容会很多,一个Region装不下),这样当数据写入HBase时,会按照region分区情况,在集群内做数据的负载均衡,从而把压力分摊到不同的Region上
2. RowKey
- RowKey检索表中的记录的三种方式
- 通过单个row key访问:即按照某个row key键值进行get操作,可以获得这一行的所有数据
- 通过row key的range进行scan:即通过设置startRowKey和endRowKey,在这个范围内进行扫描
- 全表扫描:即直接扫描整张表中所有行记录。
- Rowkey规则
- 越小越好
- RowKey的设计是要根据实际业务来
- 散列(取反、哈希、加盐)
3. Column Family
不要在一张表里定义太多的column family。目前Hbase并不能很好的处理超过2~3个column family的表。因为某个column family在flush的时候,它邻近的column family也会因关联效应被触发flush,最终导致系统产生更多的I/O
4. Version
- 创建表的时候,可以通过HColumnDescriptor.setMaxVersions(int maxVersions)设置表中数据的最大版本
- 如果只需要保存最新版本的数据,那么可以设置setMaxVersions(1)
- Time To Live
- 创建表的时候,可以通过HColumnDescriptor.setTimeToLive(int timeToLive)设置表中数据的存储生命期,过期数据将自动被删除,例如如果只需要存储最近两天的数据,那么可以设置
setTimeToLive(2 * 24 * 60 * 60)。
5. Compact & Split
- Compact
- 在HBase中,数据在更新时首先写入WAL 日志(HLog)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时, 系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了(minor compact)
- Split
- StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更新其实是不断追加的操作。当一个Store中的StoreFile数量达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当Region的大小达到一定阈值后,又会对StoreFile进行分割(split),逻辑等分为两个StoreFile
- 合并过程
- 由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部的StoreFile和MemStore,将它们按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,通常合并过程还是比较快的。
- 实际应用中,可以考虑必要时手动进行major compact,将同一个row key的修改进行合并形成一个大的StoreFile。同时,可以将StoreFile设置大些,减少split的发生。
- minor compaction:较小、很少文件的合并
- 数量 大小 的设置
- major compaction:完整性合并
- hbase.hregion.majorcompaction 默认7天
8.12.2 HBase写入优化
1. 多Table并发写
创建多个Table客户端用于写操作,提高写数据的吞吐量
2. WAL Flag
- 提交数据前的日志操作
- 在HBae中,客户端向集群中的RegionServer提交数据时(Put/Delete操作),首先会先写WAL(WriteAhead Log)日志(即HLog,一个RegionServer上的所有Region共享一个HLog),只有当WAL日志写成功后,再接着写MemStore,然后客户端被通知提交数据成功;如果写WAL日志失败,客户端则被通知提交失败。这样做的好处是可以做到RegionServer宕机后的数据恢复
- 对于不太重要的数据的日志操作
- 因此,对于相对不太重要的数据,可以在Put/Delete操作时,通过调用Put.setWriteToWAL(false)或Delete.setWriteToWAL(false)函数,放弃写WAL日志,从而提高数据写入的性能
- 值得注意的是:谨慎选择关闭WAL日志,因为这样的话,一旦RegionServer宕机,Put/Delete的数据将会无法根据WAL日志进行恢复。
3. 批量写
通过调用Table.put(Put)方法可以将一个指定的row key记录写入HBase,同样HBase提供了另一个方
法:通过调用Table.put(List)方法可以将指定的row key列表,批量写入多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高,网络传输RTT高的情景下可能带来明显的性能提升
4. HTable参数设置
- Auto Flush
通过调用HTable.setAutoFlush(false)方法可以将HTable写客户端的自动flush关闭,这样可以批量写入数据到HBase,而不是有一条put就执行一次更新,只有当put填满客户端写缓存时,才实际向HBase服务端发起写请求。默认情况下auto flush是开启的。 - Write Buffer
通过调用HTable.setWriteBufferSize(writeBufferSize)方法可以设置HTable客户端的写buffer大小,如果新设置的buffer小于当前写buffer中的数据时,buffer将会被flush到服务端。其中,writeBufferSize的单位是byte字节数,可以根据实际写入数据量的多少来设置该值。 - 多线程并发写
在客户端开启多个HTable写线程,每个写线程负责一个HTable对象的flush操作,这样结合定时flush和写buffer(writeBufferSize),可以既保证在数据量小的时候,数据可以在较短时间内被flush(如1秒内),同时又保证在数据量大的时候,写buffer一满就及时进行flush。
8.12.3 HBase读取优化
作为NoSQL数据库,增删改查是其最基本的功能,其中查询是最常用的一项。
1. 显示的指定列
当使用Scan或者GET获取大量的行时,最好指定所需要的列,因为服务端通过网络传输到客户端,数据量太大可能是瓶颈。如果能有效过滤部分数据,能很大程度的减少网络I/O的花费。
2. 关闭ResultScanner
如果在使用table.getScanner之后,忘记关闭该类,它会一直和服务端保持连接,资源无法释放,从而导致服务端的某些资源不可用。
所以在用完之后,需要执行关闭操作,这点与JDBS操作MySQL类似
scanner.close()
3. 查询结果
对于频繁查询HBase的应用场景,可以考虑在应用程序和Hbase之间做一层缓存系统,新的查询先去缓
存查,缓存没有再去查Hbase。
- 多Table并发读
- scanner cache
- hbase.client.scanner.caching配置项可以设置HBase scanner一次从服务端抓取的数据条
数,默认情况下一次一条。通过将其设置成一个合理的值,可以减少scan过程中next()的时间开销,代价是scanner需要通过客户端的内存来维持这些被cache的行记录 - HBase的conf配置文件中进行配置–>整个集群
- Table.setScannerCaching(int scannerCaching)进行配置–>本次对表的链接
- Scan.setCaching(int caching)–>本次查询
- 优先级 从本次查询–》本次链接–》整个集群
- hbase.client.scanner.caching配置项可以设置HBase scanner一次从服务端抓取的数据条
- Scan指定列族或者列
- scan时指定需要的Column Family,可以减少网络传输数据量
- Close ResultScanner
- 通过scan取完数据后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的Server资源无法释放)
- 批量读
8.12.4 HBase缓存优化
1. 设置Scan缓存
HBase中Scan查询可以设置缓存,方法是setCaching(),这样可以有效的减少服务端与客户端的交互,更有效的提升扫描查询的性能
2. 禁用块缓存
如果批量进行全表扫描,默认是有缓存的,如果此时有缓存,会降低扫描的效率。
scan.setCacheBlocks(true|false);
对于经常读到的数据,建议使用默认值,开启块缓存
3. 缓存查询结果
对于频繁查询HBase的应用场景,可以考虑在应用程序和Hbase之间做一层缓存系统,新的查询先去缓
存查,缓存没有再去查Hbase。
- Blockcache
- 隶属于RegionServer
- mem刷新的时机
- 当前mem达到128M
- 集群mem总内存使用量达到阈值
- 读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache。
- Regionserver上有一个BlockCache和N个Memstore,它们的大小之和不能大于等于heapsize * 0.8
- 默认BlockCache为0.2,而Memstore为0.4。对于注重读响应时间的系统,可以将 BlockCache设大些,比如设置BlockCache=0.4,Memstore=0.39,以加大缓存的命中率
8.13 Hive和HBase的整合
Hive提供了与HBase的集成,使得能够在HBase表上使用HQL语句进行查询 插入操作以及进行Join和Union等复杂查询、同时也可以将hive表中的数据映射到Hbase中。
8.13.1 拷贝jar包
- [123 ~]# cp /opt/yjx/apache-hive-3.1.2-bin/lib/hive-hbase-handler-3.1.2.jar /opt/yjx/hbase-2.2.5/lib/
- 检查jar是否已经上传成功(三台节点)
[123 ~]# ls /opt/yjx/hbase-2.2.5/lib/hive-hbase-handler-*
8.13.2 在Hive的配置文件增加属性
-
vim /opt/yjx/apache-hive-3.1.2-bin/conf/hive-site.xml
<!--Hbase Zk 新增:(三台节点都新增)--> <property> <name>hbase.zookeeper.quorum</name> <value>node01:2181,node02:2181,node03:2181</value> </property>
8.13.3 验证
-
创建hbase_user表
create 'hbase_user','info'
-
向hbase_user表插入
put 'hbase_user','1','info:name','zhangsan' put 'hbase_user','1','info:age','18' put 'hbase_user','1','info:gender','man' put 'hbase_user','2','info:name','lisi' put 'hbase_user','2','info:age','12' put 'hbase_user','2','info:gender','man' put 'hbase_user','3','info:name','wangwu' put 'hbase_user','3','info:age','13' put 'hbase_user','3','info:gender','woman'
-
创建Hive表
CREATE EXTERNAL TABLE hive_user ( id string, name string, age string, gender string) ROW FORMAT SERDE 'org.apache.hadoop.hive.hbase.HBaseSerDe' STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler' with serdeproperties('hbase.columns.mapping'=':key,info:name,info:age,info:gender ') tblproperties('hbase.table.name'='hbase_user');
🚩
以上是关于15-Hbase深入理解数据读写流程数据刷写合并切分和表设计原则的主要内容,如果未能解决你的问题,请参考以下文章