目录
HBase简介
-
分布式、可扩展、海量数据存储的NoSQL数据库--非关系型数据库
-
存储结构是K-V结构,多维映射(multi-dimensional map)
-
逻辑示例
-
row_key按字典序排列
-
按行切分成Region(计row_key的最大、最小值取中间进行切分)
-
按列切分成列族
-
按列族和Region切分成store,store为逻辑结构的最小结构
-
row_key + 一个列族为一个store,同一行,不同列族的row_key是同一个
-
-
每个store的物理结构与逻辑结构的关系
-
分别是:
-
Key:
row_key,列族,列名,时间戳(区分不同版本的数据,时间戳大的会覆盖小的),操作
-
Value:
数据
-
-
物理存储结构实际存储的即为StoreFile
-
每一行为一个Cell
-
逻辑结构中的一个数据对应物理结构中多个版本的cell(例如电话号码可能修改过,那么就有多个版本)
-
-
Name Space
-
命名空间,即database,自带两个命名空间hbase和default,hbase中存放的是HBase内置的表,default表是用户默认使用的命名空间
-
-
Table
-
类似数据库中表的概念。在定义表时只需要声明列族,不需要声明具体的列。在添加数据的时候可以新创建出列,即动态指定列。
-
-
Row
-
每行数据都由一个RowKey和多个Column(列)组成,数据是按照RowKey的字典顺序存储的,并且查询数据时只能根据RowKey进行检索,所以RowKey的设计十分重要
-
-
Column
-
每个列都由Column Family(列族)和Column Qualifier(列名)进行限定,例如info:name,info:age。建表时,只需指明列族,而列名无需预先定义
-
不同列族里面可以由同名列名
-
-
Time Stamp(时间戳)
-
用于标识数据的不同版本(version),每条数据写入时,系统会自动为其加上该字段,其值为写入HBase的时间。
-
-
Cell
-
由{rowkey, column Family:column Qualifier, time Stamp} 唯一确定的单元。cell中的数据全部是字节码形式存贮。(进行序列化了)
-
-
HBase基本架构
-
Region Server:(小弟)
-
进程、节点,分布式HBase中每一个节点都要启动一个HBase
-
可以对数据进行get、put、delete(即DML语句)
-
对Region进行操作:
-
splitRegion:按照row_key切分region(找row_key中间值)
-
compactRegion:Region、storefile合并并清理
-
-
-
Zookeeper:
-
依赖于Zookeeper实现分布式,HBase通过Zookeeper来做master的高可用、RegionServer的监控、元数据的入口以及集群配置的维护等工作。
-
-
Master:(老大)
-
所有Region Server的管理者,和Zookeeper中的Controler功能一样,允许往Zookeeper中进行读写操作,管理Zookeeper中HBase注册的信息
-
在哪个节点上启动哪个节点就作为Master
-
分配regions到每个RegionServer,监控每个RegionServer的状态,负载均衡和故障转移。如果某一个RegionServer挂了,可以把他管理的Region交给其他的RegionServer
-
对表进行create, delete, alter(即DDL)
-
-
HDFS:
-
为Hbase提供最终的底层数据存储服务,同时为HBase提供高可用的支持
-
-
Master --管理--> Region Server --管理--> Region(里面是HDFS上的文件)
-
HBase入门
-
単起master命令:bin/hbase-daemon.sh start master
-
shell命令:
-
DDL:
创建:create
,删除:drop
,修改:alter
,查表:list
-
DML:
delete:删除插入数据的最新版本
,delete all:删除所有版本的数据信息
get:查某一行
,put:插入和修改(添加一个新版本等于修改)
scan:扫描出多行数据
append:追加(不修改数据,只在原来的信息上追加字符)
-
只要会调用help即可,系统会提供具体格式,复制再进行修改会更加方便
- 记住:双引号""中填的为系统的操作命令,单引号''中填的为用户定义的字符串,大写的为系统定义的字符串
-
HBase进阶
-
HBase的DDL语言
-
创建表:create 'student','info'
表名、列族名、保留版本数量
可以同时创建多个列族,每个列族维护的信息是单独的,不设版本号时默认1,即只保留最新的版本
-
查看表的详细信息:desc ‘表名’
-
修改:
修改列族版本号:alter 'student',{NAME=>'info',VERSIONS=>3}
删除列族:deleteall 'student','1001'
-
删除:drop 'student'
删除表之前要先不可用表 disable 'student'
-
-
HBase的DML语言
-
添加数据:
put,表名,row_key,列族:列名,value
-
读数据:
一次查询多列:
查询多个版本的数据(设计列族时设计的versions=5,此处命令写versions=7,但是只能展示5个版本)
-
扫描:可以指定row_key范围,左闭右开
从当前指定的row_key往下展示limit设置的数字,比如从1001开始展示到1010
-
展示原始数据:
-
删除列族:
(展示原始数据)
删除列族后的展示
-
清空表:
-
可以展示删除了某个版本的数据(看时间戳)
-
-
RegionServer 架构
-
详细架构
-
MemStore:写缓存,K-V在Memstore中进行排序(快排),达到阈值之后才会flush到StoreFile,每次flush生成一个新的StoreFile。
-
StoreFile:存储有序的K-V的文件,存储在HDFS上。每个Store会有一个或多个StoreFile,数据在每个StoreFile中都是有序的。
-
WAL:Write Ahead Log,预写日志,防止RegionServer故障,导致MemStore中的数据丢失。(与HDFS中edit类似,只记录命令,不记录数据)直接落盘到HDFS,如果MemStore中数据丢失数据,可以通过这个日志文件重建。可以理解为不会丢数据。
-
BlockCache:读缓存,每次查询出的数据会缓存在BlockCache中,方便下次查询。
-
每个RegionServer可以服务于多个Region
-
每个RegionServer中有多个Store,1个WAL和1个BlockCache
-
每个Store对应一个列族,包含一个MemStore和若干个StoreFile
-
-
-
HBase写流程
-
StoreFile位置:
-
hbase、数据、命名空间、表、Region、列族、StoreFile
-
-
HBase自带幂等性,数据不会重复
-
等达到MemStore的刷写时机后,将数据刷写到HFile。
-
向客户端发送ack;
-
将数据写入对应的MemStore,数据会在MemStore进行排序;
-
将数据顺序写入(追加)到WAL;
-
与目标Region Server进行通讯;
-
访问对应的Region Server,获取hbase:meta表(记录的是一个table,里面有多个用row_key切分出来的Region,有一个row_key范围),根据读请求的namespace:table/rowkey,查询出目标数据位于哪个Region Server中的哪个Region中。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问。(如果某个RegionServer故障了,就刷新这个缓存表,重新生成)
-
Client先访问zookeeper,获取hbase:meta表位于哪个Region Server。(找位置)
-
-
MemStoreFlush
-
Flume中的HDFSsink往HDFS上写文件(小文件问题):
-
设置三个参数:滚动时间1h,滚动event=0(不使用),滚动大小128M
-
Flume自身没有足够大的内存存储数据,在到达sink时需要及时的把数据写出去,会在HDFS上开辟一个.tmp临时文件,不断地追加写,直到阈值后把临时文件变成正式文件
-
-
HBse刷写文件机制:
-
HBase不可以像Flume一样用临时文件,因为HBase要保证数据有序
-
当某个memstroe的大小达到了128M时,由于一个region中的数据需要一行行对应,其所在region的所有memstore都会刷写。(触发刷写)
-
判断刷写是周期性的,当memstore的大小达到了128*4时,会阻止继续往该memstore写数据。(不再往MemStore里追加数据,和上一条并行,不冲突)
-
当region server中memstore的总大小达到 JVM内存* 0.4 *0.95 (接近JVM给HBase分配的内存大小时)(RegionServer给写缓存分配内存时分配了40%内存)(95%时认为比较危险)region会按照其所有memstore的大小顺序(由大到小)依次进行刷写。直到region server中所有memstore的总大小减小到上述值以下。
当region server中memstore的总大小达到 JVM内存* 0.4 时,会阻止继续往所有的memstore写数据。
-
到达自动刷写的时间,也会触发memstore flush。自动刷新的时间间隔由该属性进行配置hbase.regionserver.optionalcacheflushinterval(默认1小时)。
-
-
-
HBase读流程
-
进行读的时候三处同时读:BlockCache(读缓存)、MemStore(写缓存)、HFile(HDFS上的文件),从MemSrtore上读数据是最快的(内存到内存)但是从HDFS上读速度很慢,进行优化
-
过滤器:
-
row_key范围:单个文件根据row_key有序,比较每个HFile的最大row_key和最小row_key
-
时间范围:一个写缓存刷写出来时间范围是一样的,由于自动刷写时间是一小时,所以可以比对待读取数据和HFile中的时间范围
-
布隆过滤器(效果最好):将每一个row_key进行取Hash值得出一个大数字,将其转化成二进制,读数据的时候将待读row_key的Hash值和过滤器中的位图进行对比,如果1的位置重合则有可能在此HFile里。在设置时过滤器中会设置三个位图,都匹配上之后则说明在此HFile里
-
-
-
将合并后的最终结果返回给客户端。
-
将查询到的新的数据块(Block,HFile数据存储单元,默认大小为64KB)缓存到Block Cache。优化:读缓存中有64k的块,还有整个文件的索引,读文件的时候先找读缓存中的索引
-
分别在MemStore和Store File(HFile)中查询目标数据,并将查到的所有数据进行合并。此处所有数据是指同一条数据的不同版本(time stamp)或者不同的类型(Put/Delete)。
-
与目标Region Server进行通讯;(前面和写流程一样)
-
访问对应的Region Server,获取hbase:meta表,根据读请求的namespace:table/rowkey,查询出目标数据位于哪个Region Server中的哪个Region中。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问。
-
Client先访问zookeeper,获取hbase:meta表位于哪个Region Server。
-
-
StoreFile Compaction (合并)
-
合并时会进行归并排序
-
Compaction分为两种,分别是Minor Compaction(小合并)和Major Compaction(大合并)。Minor Compaction会自动进行,将临近的若干个较小的HFile合并成一个较大的HFile,并清理掉部分过期和删除的数据(例如某个文件中有deleteall命令,但是没有和之前的文件进行合并,即之前文件还没有执行删除命令。会保留这条命令和前面文件的数据,等到大合并的时候再进行删除和清理)。Major Compaction会将一个Store下的所有的HFile合并成一个大HFile,并且会清理掉所有过期和删除的数据。大合并参数控制,默认7天,非常消耗资源,一般会关闭自动合并,待机器空闲时手动进行(归并排序很消耗内存)
-
由于memstore每次刷写都会生成一个新的HFile,且同一个字段的不同版本(timestamp)和不同类型(Put/Delete)有可能会分布在不同的HFile中,因此查询时需要遍历所有的HFile。为了减少HFile的个数,以及清理掉过期和删除的数据,会进行StoreFile Compaction。
-
-
Region Split
-
默认情况下,每个Table起初只有一个Region,随着数据的不断写入,Region会自动进行拆分。刚拆分时,两个子Region都位于当前的Region Server,但处于负载均衡的考虑,HMaster有可能会将某个Region转移给其他的Region Server。
-
当1个region中的某个Store下所有StoreFile的总大小超过10G,该Region就会进行拆分(0.94版本之前)
-
当1个region中的某个Store下所有StoreFile的总大小超过Min(initialSizeR^3 ,hbase.hregion.max.filesize"),该Region就会进行拆分。其中initialSize的默认值为256M,具体切分为256M、2048M、6912M、10G,后面每次是10G(0.94版本之后)。
-
Hbase 2.0引入了新的split策略:如果当前RegionServer上该表只有一个Region,按照256M分裂,否则按照10G分裂。(第一次256,后面是10G)
-
切分时可能会造成热点问题(数据倾斜)解决:row_key设计、预分区设计
-
-
Hash算法和布隆过滤器介绍
-
布隆过滤器算法:
-
首先需要k个hash函数,每个函数可以把key散列成为1个整数
-
初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
-
某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
-
判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。
-
优点:不需要存储key,节省空间
-
缺点: 算法判断key在集合中时,有一定的概率key其实不在集合中 无法删除
-
-
hashcode:
-
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
- 第一 31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一
-
常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面几个常数中,常数 31 被 Java 实现所选用也就不足为奇了。
-
第二 31可以被 JVM 优化,
31 * i = (i << 5) - i
。
-
-
HBase优化
-
预分区
-
每一个region维护着startRow与endRowKey,如果加入的数据符合某个region维护的rowKey范围,则该数据交给这个region维护。将数据所要投放的分区提前大致的规划好
-
手动设定预分区
create 'staff1','info', SPLITS =>['1000','2000','3000','4000']
以上方式是按照1000,2000,3000,4000为分界线,按照ascii码表的顺序进行切割
-
按照文件中设置的规则预分区
创建splits.txt文件内容如下: aaaa bbbb cccc dddd 然后执行: create 'staff3', 'info',SPLITS_FILE => 'splits.txt
同样也能在文件中设置分区的界限
-
-
row_key设计
-
设计rowkey的主要目的 ,就是让数据均匀的分布于所有的region中,在一定程度上防止数据倾斜
-
生成随机数、hash、散列值;hash算法能够将微小的差异进行放大
-
字符串反转:20210524000001转成10000042501202
-
字符串拼接:20210524000001_harry
-
需求:表格记录某用户在特定时间打电话的时长,需要使用该表统计用户特定时间内打电话的次数和总时长
-
用户名 时间 电话时长 id date time 110 2021-01-08 5 -
解决热点问题:使用预分区
300个分区 -> 001|、002| ~ 299|、300| (分区的结尾值:|是asca码中较大的值)
-
row_key的格式为:000_xxxx_xxxx
-
对 “id:yyyy-MM” 取hash值%300 得到分区号
-
根据需求拼接row_key:id_date
-
扫面数据:用户120在2020一年的打电话次数和总时长
对120:2020-01 ... 120:2020-12 取hash%300 分别得到分区号
scan xxx 分区号_120_2020-01 分区号_120_2020-01| 1月份该用户数据 ... scan xxx 分区号_120_2020-12 分区号_120_2020-12| 12月份该用户数据
对扫描的数据union合并
sum
-
-
-
-
内存优化
-
HBase操作过程中需要大量的内存开销,毕竟Table是可以缓存在内存中的,但是不建议分配非常大的堆内存,因为GC过程持续太久会导致RegionServer处于长期不可用状态,一般16~36G内存就可以了,如果因为框架占用内存过高导致系统内存不足,框架一样会被系统服务拖死
-
-
基础优化
-
Zookeeper会话超时时间
-
默认值为90000毫秒(90s)。当某个RegionServer挂掉,90s之后Master才能察觉到。可适当减小此值,以加快Master响应,可调整至60000毫秒。如果太小则要调大
-
-
设置RPC监听数量
-
默认值为30,用于指定RPC监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值,但是也不建议调太大,否则HBase会故障
-
-
手动控制Major Compaction
-
默认值:604800000秒(7天), Major Compaction的周期,若关闭自动Major Compaction,可将其设为0
-
-
优化HStore文件大小(不建议修改)
-
默认值10737418240(10GB),如果需要运行HBase的MR任务,可以减小此值,因为一个region对应一个map任务,如果单个region过大,会导致map任务执行时间过长。该值的意思就是,如果HFile的大小达到这个数值,则这个region会被切分为两个Hfile
-
可以使用LZO文件折中解决
-
-
优化HBase客户端缓存(客户端的批处理。非服务端的读缓存写缓存)(不建议修改)
-
默认值2097152bytes(2M)用于指定HBase客户端缓存,增大该值可以减少RPC调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到减少RPC次数的目的
-
-
指定scan.next扫描HBase所获取的行数
-
用于指定scan.next方法获取的默认行数,值越大,消耗内存越大
-
-
BlockCache占用RegionServer堆内存的比例(不建议修改)
-
默认0.4,读请求比较多的情况下,可适当调大
-
-
Mem Store占用RegionServer堆内存的比例(不建议修改)
-
默认0.4,写请求较多的情况下,可适当调大
-
-
整合Phoenix
-
Phoenix简介
-
Phoenix是HBase的开源SQL皮肤。可以使用标准JDBC API代替HBase客户端API来创建表,插入数据和查询HBase数据。
-
-
Phoenix特点
-
容易集成:如Spark,Hive,Pig,Flume和Map Reduce;
-
操作简单:DML命令以及通过DDL命令创建和操作表和版本化增量更改;
-
支持HBase二级索引创建。
-
-
Phoenix架构
-
瘦客户端链接 胖客户端链接
瘦客户端(thin client)不对数据进行计算逻辑,会交给胖客户端处理
-
-
Phoenix快速入门
-
二级索引
<!-- phoenix regionserver 配置参数--> <property> <name>hbase.regionserver.wal.codec</name> <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value> </property>
- 配置:修改HBase下的HRegionserver节点的hbase-site.xml
- 全局索引:Global Index是默认的索引格式,创建全局索引时,会在HBase中建立一张新表。也就是说索引数据和数据表是存放在不同的表中的,因此全局索引适用于多读少写的业务场景。
创建单个字段的全局索引:CREATE INDEX 索引名 ON 表名 (索引列名);
创建的索引的索引列名只能有一列!
执行如下代码后:CREATE INDEX my_index ON "test" ("info"."name"); - 创建的索引表为:
而原表 :
可以很明显看出来,name在该索引表中充当主键,相当于重新创建了一个表将索引列名设为主键,原主键设为属性的一个表.作用就是在用Phoenix执行where语句时能够更加快捷,不再是full scan(全表扫描),而是range scan(部分扫描),尤其在数据量大的时候效率更能体现
但是有个问题,如果想查询的字段为索引字段,才能实现range scan
如果想查询的字段不是索引字段的话索引表不会被使用,也就是说不会带来查询速度的提升。
如果想解决如上的问题:
1.使用包含索引 2.使用本地索引 - 包含索引:covered index是另一种索引格式,在设置索引表的时候能够加上其他字段
创建携带其他字段的全局索引(也就是包含索引):
CREATE INDEX my_index ON my_table (v1) INCLUDE (v2);
下图为执行语句:CREATE INDEX my_index1 ON "test" ("info"."name") INCLUDE ("info"."age");
索引表为:
可以看出来,索引表中多了另一个字段age,在有对age的查询时,能够提高效率
在使用包含索引后,查询age与name时能够加快速率: - 本地索引:Local Index适用于写操作频繁的场景。
索引数据和数据表的数据是存放在同一张表中(且是同一个Region),避免了在写操作的时候往不同服务器的索引表中写索引带来的额外开销。
CREATE LOCAL INDEX my_index ON my_table (my_column);
-
LSM算法(日志结构合并树)
-
从读写速度入手 -> 读得快(
-
保证存储数据的有序) -> 造成问题:写得慢 -> 末尾追加顺序写速度快 -> 造成矛盾,无法有序
-
首先坚定选择有序,同时保留末尾追加写的操作
-
文件拆分为小文件,不再保留整体有序,只保留单个小文件有序并落盘,但可能造成文件内容重复
-
引入版本号,标记数据,区分重复的数据(时间戳)
-
合并小文件,保持全局有序,同时删除重复的数据
-