原创 大数据手稿笔记 大数据技术架构 2019-05-07 06:45
聊一聊 NoSQL
NoSQL(Not only SQL)数据库,可以理解为区别于关系型数据库如mysql、oracle等的非关系型数据库。聊到NoSQL不得不提著名的CAP理论,全称 Consistency Available and Partition tolerance,即一致性、可用性与分区容错性,这是Eric Brewer教授提出的分布式系统设计理念,并给出了定论:任何分布式系统只能同时满足其中二点,无法做到三者兼顾。这可以说是NoSQL数据库的理论基石,至今NoSQL领域也称得上是百花齐放了,一直也没有哪一款NoSQL同时兼顾着这三点特性。
NoSQL必须要在一致性、可用性与分区容错性之间做出取舍,目前而言,几乎所有的NoSQL都是在保有分区容错性的基础上选择一致性或可用性,例如HBase就是牺牲了部分可用性换取了完全的一致性,与HBase类似的Cassandra则是牺牲了强一致性换来了可用性的保证。
NoSQL能做哪些事情、不能做哪些事情?NoSQL作为分布式系统的实现,海量数据永久性存储、非结构化数据存储、超大规模数据高效读写、超强水平扩展能力等这些特征让NoSQL得到了广泛应用。然而,事务支持、关联特性,甚至于SQL查询,这些却是NoSQL的短板,也决定了NoSQL尚且取代不了关系型数据库。
HBase(Hadoop database)是一个分布式、可扩展、面向列的NoSQL数据库,本质上是一个Key-Value系统,底层存储基于HDFS,原生支持MapReduce计算框架,具有高吞吐、低延时的读写特点。
HBase 的主要特性
HBase包含很多特性,这里列举了HBase的一些关键特性:
HBase作为一款NoSQL数据库,前面也提及了并不能解决所有问题。关于我们在实际生产过程中满足哪些条件的时候可以选择HBase作为底层存储,这里给出几点建议:
1、数据量规模非常庞大
一般而言,单表数据量如果只有百万级或者更少,不是非常建议使用HBase而应该考虑关系型数据库是否能够满足需求;单表数据量超过千万或者十亿百亿的时候,并且伴有较高并发,可以考虑使用HBase。这主要是充分利用分布式存储系统的优势,如果数据量比较小,单个节点就能有效存储的话则其他节点的资源就会存在浪费。
2、要求是实时的点查询
HBase是一个Key-Value数据库,默认对Rowkey即行键做了索引优化,所以即使数据量非常庞大,根据行键的查询效率依然会很高,这使得HBase非常适合根据行键做单条记录的查询。值得说明的是,允许根据行键的一部分做范围查询,这里涉及到Rowkey的设计问题,不再赘言。
3、能够容忍NoSQL短板
前面提及了NoSQL并不能解决所有问题,HBase也是一样,如果业务场景是需要事务支持、表与表的关联查询等,不建议使用HBase。HBase有它适合的业务场景,我们不能苛求它能够帮我们解决所有问题。
4、数据分析需求并不多
虽然说HBase是一个面向列的数据库,但它有别于真正的列式存储系统比如Parquet、Kudu等,再加上自身存储架构的设计,使得HBase并不擅长做数据分析,或者说数据分析是HBase的弱项,所以如果主要的业务需求就是为了做数据分析,比如做报表,那么不建议直接使用HBase。
如果能够满足上诉的几点,硬件条件也满足的情况下,强烈建议考虑使用HBase作为底层存储解决你的问题。
由于HBase丰富的特性,加上自身的海量数据存储能力与超大规模并发访问能力,使得HBase应用非常广泛。目前已经在金融、交通、医疗、车联网、IoT等众多领域有了最佳实践,涉及到订单/账单存储、用户画像、时空/时序数据、对象存储、Cube分析等各个使用场景。

1.Client访问zookeeper,获取Hbase:meta所在HregionServer的节点信息
2.Client访问Hbase:meta所在的HregionServer,获取Hbase:meta记录的元数据后先加载到内存中,然后再从内存中查询出RowKey所在的Hregion
3.Client对RowKey所在的Hregion对应的HregionServer发起读取数据的请求
4.HregionServer构建RegionScanner ,(查询rowkey涉及多少个Region,就构建多少个RegionScanner),用于对该Hregion的数据检索
5.RegionScanner构建storeScanner(Hregion中有多少个Store就需要构建多少个storeScanner,store的数量取决于table的ColumnFamily的数量),用于对该列族的数据检索
6.所有的storeScanner合并构建最小堆(已排序的完全二叉树)StoreHeap:PriorityQueue
7.storeScanner构建一个MemStoreScanner和一个或多个StoreFileScanner(数量取决于StoreFile的数量)
8.过滤掉能够确定所要查询的 RowKey 一定不在的 StoreFileScanner 或 MemStoreScanner(Bloom Filter布隆过滤器);
9.经过筛选后留下的 Scanner 开始做读取数据的准备,将对应的 StoreFile 定位到满足的 RowKey 的起始位置;
10.将所有的 StoreFileScanner 和 MemStoreScanner 合并构建最小堆 KeyValueHeap:PriorityQueue,排序的规则按照 KeyValue 从小到大排序;
11.从 KeyValueHeap:PriorityQueue 中经过一系列筛选后一行行的得到需要查询的 KeyValue。
默认情况下:先从 BlockCache 查找数据,如果没有,再从 MemStore 上查找,如果 MemStore 中也没有,再到 StoreFile 上进行查找。其 中 StoreFile 的扫瞄先会使用 Bloom Filter 过滤那些不可能符合条件的 HFile,然后使用 Data Block Index 快速定位 Cell,并将其加载到 BlockCache 中,然后从 BlockCache 中读取,目的是为了加快后续的查询,然后在返回结果给客户端。
读缓存,每次查询出的数据会缓存在BlockCache中,方便下次查询

1.Client 访问 ZooKeeper,获取 hbase:meta 所在 HRegionServer 的节点信息;
2.Client 访问 hbase:meta 所在的 HRegionServer,获取 hbase:meta 记录的元数据后先加载到内存中,然后再从内存中查询出 RowKey 所在 的 HRegion (HRegion 所在的 HRegionServer); 3.Client 对 RowKey 所在的 HRegion 对应的 HRegionServer 发起写入数据请求;
4.建立连接后,首先将 DML 要做的操作写入到日志 HLog;
5.然后将数据的修改更新到 MemStore 中,本次操作结束。一个 HRegion 由多个 Store 组成,一个 Store 对应一个列族,Store 包括位于内存 中的 Memstore 和位于磁盘的 StoreFile,写操作先写入 MemStore;
当 MemStore 数据达到阈值后(默认 128M),创建一个新的 MemStore; 旧的 MemStore 将刷写为一个独立的 StoreFile(HRegionServer 会启动 FlushCache 进程写入 StoreFile)并存放到 HDFS,最后删除 HLog 中 的历史数据。
Region是HBase数据管理的基本单位,region有一点像关系型数据的分区。 Region中存储这用户的真实数据,而为了管理这些数据,HBase使用了RegionSever来管理region。
一个表中可以包含一个或多个Region。
每个Region只能被一个RS(RegionServer)提供服务,RS可以同时服务多个Region,来自不同RS上的Region组合成表格的整体逻辑视图。
regionServer其实是hbase的服务,部署在一台物理服务器上,region有一点像关系型数据的分区,数据存放在region中,当然region下面还有很多结构,确切来说数据存放在memstore和hfile中。我们访问hbase的时候,先去hbase 系统表查找定位这条记录属于哪个region,然后定位到这个region属于哪个服务器,然后就到哪个服务器里面查找对应region中的数据


flus流程分为三个阶段:
hbase中的合并机制分为自动合并和手动合并
minor compaction(小合并) 将 Store 中多个 HFile 合并为一个相对较大的 HFile 过程中会选取一些小的、相邻的 StoreFile 将他们合并成一个更大的 StoreFile,对于超过 TTL 的数据、更新的数据、删除的数据仅仅只是做了标记,并没有进行物理删除。一次 minor compaction 过后,storeFile会变得更少并且更大,这种合并的触发频率很高
小合并的触发方式: memstore flush会产生HFile文件,文件越来越多就需要compact.每次执行完Flush操作之后,都会对当前Store中的文件数进行判断,一旦文件数大于配置3,就会触发compaction。compaction都是以Store为单位进行的,而在Flush触发条件下,整个Region的所有Store都会执行compact
后台线程周期性检查
检查周期可配置:
hbase.server.thread.wakefrequency 默认10000毫秒)
hbase.server.compactchecker.interval.multiplier 默认1000
CompactionChecker大概是2hrs 46mins 40sec 执行一次
xxxxxxxxxx<!--表示至少需要三个满足条件的store file时,minor compaction才会启动--><property> <name>hbase.hstore.compactionThreshold</name> <value>3</value></property>
<!--表示一次minor compaction中最多选取10个store file--><property> <name>hbase.hstore.compaction.max</name> <value>10</value></property>
<!--默认值为128m,表示文件大小小于该值的store file 一定会加入到minor compaction的store file中--><property> <name>hbase.hstore.compaction.min.size</name> <value>134217728</value></property>
<!--默认值为LONG.MAX_VALUE,表示文件大小大于该值的store file 一定会被minor compaction排除--><property> <name>hbase.hstore.compaction.max.size</name> <value>9223372036854775807</value></property>major compaction(大合并)
合并 Store 中所有的 HFile 为一个 HFile,将所有的 StoreFile 合并成为一个 StoreFile,这个过程中还会清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。合并频率比较低,默认7天执行一次,并且性能消耗非常大,建议生产关闭(设置为0),在应用空间时间手动触发。一般是可以手动控制进行合并,防止出现在业务高峰期。
xxxxxxxxxx线程先检查小文件数是否大于配置3,一旦大于就会触发compaction。大文件周期性合并成Major Compaction如果不满足,它会接着检查是否满足major compaction条件如果当前store中hfile的最早更新时间早于某个值mcTime就会触发major compaction (默认7天触发一次,可配置手动触发)
<!--默认值为7天进行一次大合并,--><property> <name>hbase.hregion.majorcompaction</name> <value>604800000</value></property>一般来讲,手动触发compaction通常是为了执行major compaction,一般有这些情况需要手动触发合并是因为很多业务担心自动maior compaction影响读写性能,因此会选择低峰期手动触发也有可能是用户在执行完alter操作之后希望立刻生效,执行手动触发maiorcompaction:
xxxxxxxxxx# 造数据truncate 'doit:test'put 'doit:test','001','f1:name','zss'put 'doit:test','002','f1:name','zss'put 'doit:test','003','f1:name','zss'put 'doit:test','004','f1:name','zss'flush 'doit:test' put 'doit:test','005','f1:name','zss'put 'doit:test','006','f1:name','zss'put 'doit:test','007','f1:name','zss'put 'doit:test','008','f1:name','zss'flush 'doit:test' put 'doit:test','009','f1:name','zss'put 'doit:test','010','f1:name','zss'put 'doit:test','011','f1:name','zss'put 'doit:test','012','f1:name','zss'flush 'doit:test'put 'doit:test','013','f1:name','zss'put 'doit:test','014','f1:name','zss'put 'doit:test','015','f1:name','zss'put 'doit:test','016','f1:name','zss'flush 'doit:test'put 'doit:test','017','f1:name','zss'put 'doit:test','018','f1:name','zss'put 'doit:test','019','f1:name','zss'put 'doit:test','020','f1:name','zss'flush 'doit:test'put 'doit:test','021','f1:name','zss'put 'doit:test','022','f1:name','zss'put 'doit:test','023','f1:name','zss'put 'doit:test','024','f1:name','zss'flush 'doit:test'put 'doit:test','025','f1:name','zss'put 'doit:test','026','f1:name','zss'put 'doit:test','027','f1:name','zss'put 'doit:test','028','f1:name','zss'flush 'doit:test'put 'doit:test','021','f1:name','zss'put 'doit:test','022','f1:name','zss'put 'doit:test','023','f1:name','zss'put 'doit:test','024','f1:name','zss'flush 'doit:test'put 'doit:test','021','f1:name','zss'put 'doit:test','022','f1:name','zss'put 'doit:test','023','f1:name','zss'put 'doit:test','024','f1:name','zss'flush 'doit:test'put 'doit:test','021','f1:name','zss'put 'doit:test','022','f1:name','zss'put 'doit:test','023','f1:name','zss'put 'doit:test','024','f1:name','zss'flush 'doit:test'
put 'doit:test','021','f1:name','zss'put 'doit:test','022','f1:name','zss'put 'doit:test','023','f1:name','zss'put 'doit:test','024','f1:name','zss'flush 'doit:test'
# 每次flush一下都会在底层生成一个小文件
xxxxxxxxxx##使用major_compact命令major_compact tableName
major_compact 'doit:test'
region中存储的是一张表的数据,当region中的数据条数过多的时候,会直接影响查询效率。当region过大的时候,region会被拆分为两个region,HMaster会将分裂的region分配到不同的regionserver上,这样可以让请求分散到不同的RegionServer上,已达到负载均衡 , 这也是HBase的一个优点
当region中最大的store大小超过某个阈值(hbase.hregion.max.filesize=10G)之后就会触发切分,一个region等分为2个region。
但是在生产线上这种切分策略却有相当大的弊端(切分策略对于大表和小表没有明显的区分): 1.阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触发分裂,极端情况下可能就1个,形成热点,这对业务来说并不是什么好事。 2.如果设置较小则对小表友好,但一个大表就会在整个集群产生大量的region,这对于集群的管理、资源使用、failover来说都不是一件好事。
总体看和ConstantSizeRegionSplitPolicy思路相同,一个region中最大的store大小大于设置阈值就会触发切分。 但是这个阈值并不像ConstantSizeRegionSplitPolicy是一个固定的值,而是会在一定条件下不断调整,调整规则和region所属表在当前regionserver上的region个数有关系.
region split阈值的计算公式是: 1.设regioncount:是region所属表在当前regionserver上的region的个数 2.阈值 = regioncount^3 * 128M * 2,当然阈值并不会无限增长,最大不超过MaxRegionFileSize(10G),当region中最大的store的大小达到该阈值的时候进行region split
例如: • 第一次split阈值 = 1^3 * 256 = 256MB • 第二次split阈值 = 2^3 * 256 = 2048MB • 第三次split阈值 = 3^3 * 256 = 6912MB • 第四次split阈值 = 4^3 * 256 = 16384MB > 10GB,因此取较小的值10GB • 后面每次split的size都是10GB了
特点 • 相比ConstantSizeRegionSplitPolicy,可以自适应大表、小表; • 在集群规模比较大的情况下,对大表的表现比较优秀 • 对小表不友好,小表可能产生大量的小region,分散在各regionserver上 • 小表达不到多次切分条件,导致每个split都很小,所以分散在各个regionServer上
相比 IncreasingToUpperBoundRegionSplitPolicy 简单了一些 region切分的阈值依然和待分裂region所属表在当前regionserver上的region个数有关系 • 如果region个数等于1,切分阈值为flush size 128M * 2 • 否则为MaxRegionFileSize。
这种切分策略对于大集群中的大表、小表会比 IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不会再产生大量的小region,而是适可而止。
根据rowKey的前缀对数据进行分区,这里是指定rowKey的前多少位作为前缀,比如rowKey都是16位的,指定前5位是前缀,那么前5位相同的rowKey在相同的region中
保证相同前缀的数据在同一个region中,例如rowKey的格式为:userid_eventtype_eventid,指定的delimiter为 _ ,则split的的时候会确保userid相同的数据在同一个region中。 按照分隔符进行切分,而KeyPrefixRegionSplitPolicy是按照指定位数切分
按照一定的策略判断Region是不是Busy状态,如果是即进行切分 如果你的系统常常会出现热点Region,而你对性能有很高的追求,那么这种策略可能会比较适合你。它会通过拆分热点Region来缓解热点Region的压力,但是根据热点来拆分Region也会带来很多不确定性因素,因为你也不知道下一个被拆分的Region是哪个
手动合并
xxxxxxxxxxhbase(main):025:0> list_regions 'doit:test' SERVER_NAME | REGION_NAME | START_KEY | END_KEY | SIZE | REQ | LOCALITY | --------------------------- | -------------------------------------------------------------------- | ---------- | ---------- | ----- | ----- | ---------- | linux03,16020,1684200651855 | doit:test,,1684205468848.920ae3e043ad95890c4f5693cb663bc5. | | rowkey_010 | 0 | 0 | 0.0 | linux01,16020,1684205091382 | doit:test,rowkey_010,1684207066858.5e04eb75e5510ad65a0f3001de3c7aa0. | rowkey_010 | rowkey_015 | 0 | 0 | 0.0 | linux02,16020,1684200651886 | doit:test,rowkey_015,1684207066858.ed1b328ca4c485d4fa429922f6c18f0b. | rowkey_015 | rowkey_020 | 0 | 0 | 0.0 | linux02,16020,1684200651886 | doit:test,rowkey_020,1684205468848.25d62e8cc2fdaecec87234b8d28f0827. | rowkey_020 | rowkey_030 | 0 | 0 | 0.0 | linux03,16020,1684200651855 | doit:test,rowkey_030,1684205468848.2b0468e6643b95159fa6e210fa093e66. | rowkey_030 | rowkey_040 | 0 | 0 | 0.0 | linux01,16020,1684205091382 | doit:test,rowkey_040,1684205468848.fb12c09c7c73cfeff0bf79b5dda076cb. | rowkey_040 | | 0 | 0 | 0.0 | 6 rowsTook 0.0299 secondshbase(main):026:0> merge_region 'doit:test,,1684205468848.920ae3e043ad95890c4f5693cb663bc5.','doit:test,rowkey_010,1684207066858.5e04eb75e5510ad65a0f3001de3c7aa0.'Took 1.2638 secondshbase(main):027:0> list_regions 'doit:test' SERVER_NAME | REGION_NAME | START_KEY | END_KEY | SIZE | REQ | LOCALITY | --------------------------- | -------------------------------------------------------------------- | ---------- | ---------- | ----- | ----- | ---------- | linux03,16020,1684200651855 | doit:test,,1684207066859.cdc1226d634c0cf16f58832637f485b6. | | rowkey_015 | 0 | 0 | 0.0 | linux02,16020,1684200651886 | doit:test,rowkey_015,1684207066858.ed1b328ca4c485d4fa429922f6c18f0b. | rowkey_015 | rowkey_020 | 0 | 0 | 0.0 | linux02,16020,1684200651886 | doit:test,rowkey_020,1684205468848.25d62e8cc2fdaecec87234b8d28f0827. | rowkey_020 | rowkey_030 | 0 | 0 | 0.0 | linux03,16020,1684200651855 | doit:test,rowkey_030,1684205468848.2b0468e6643b95159fa6e210fa093e66. | rowkey_030 | rowkey_040 | 0 | 0 | 0.0 | linux01,16020,1684205091382 | doit:test,rowkey_040,1684205468848.fb12c09c7c73cfeff0bf79b5dda076cb. | rowkey_040 | | 0 | 0 | 0.0 | 5 rowsTook 0.0271 seconds手动拆分
xxxxxxxxxxhbase(main):029:0> list_regions 'doit:test' SERVER_NAME | REGION_NAME | START_KEY | END_KEY | SIZE | REQ | LOCALITY | --------------------------- | -------------------------------------------------------------------- | ---------- | ---------- | ----- | ----- | ---------- | linux03,16020,1684200651855 | doit:test,,1684207066860.8ebf4555c58bd0e5fedae5d4efbe4235. | | rowkey_030 | 0 | 0 | 0.0 | linux03,16020,1684200651855 | doit:test,rowkey_030,1684205468848.2b0468e6643b95159fa6e210fa093e66. | rowkey_030 | rowkey_040 | 0 | 0 | 0.0 | linux01,16020,1684205091382 | doit:test,rowkey_040,1684205468848.fb12c09c7c73cfeff0bf79b5dda076cb. | rowkey_040 | | 0 | 0 | 0.0 | 3 rowsTook 0.0329 secondshbase(main):030:0> split 'doit:test,,1684207066860.8ebf4555c58bd0e5fedae5d4efbe4235.','rowkey_025'Took 0.1179 secondshbase(main):031:0> list_regions 'doit:test' SERVER_NAME | REGION_NAME | START_KEY | END_KEY | SIZE | REQ | LOCALITY | --------------------------- | -------------------------------------------------------------------- | ---------- | ---------- | ----- | ----- | ---------- | linux02,16020,1684200651886 | doit:test,,1684207502853.af0819bd7f6daa9db2a8f994fb41682d. | | rowkey_025 | 0 | 0 | 0.0 | linux02,16020,1684200651886 | doit:test,rowkey_025,1684207502853.80d7feace447978ffe4a54418a20afd0. | rowkey_025 | rowkey_030 | 0 | 0 | 0.0 | linux03,16020,1684200651855 | doit:test,rowkey_030,1684205468848.2b0468e6643b95159fa6e210fa093e66. | rowkey_030 | rowkey_040 | 0 | 0 | 0.0 | linux01,16020,1684205091382 | doit:test,rowkey_040,1684205468848.fb12c09c7c73cfeff0bf79b5dda076cb. | rowkey_040 | | 0 | 0 | 0.0 | 4 rowsTook 0.0179 secondshbase(main):032:0> split 'doit:test,,1684207502853.af0819bd7f6daa9db2a8f994fb41682d.','rowkey_015'Took 0.1262 secondshbase(main):033:0> list_regions 'doit:test' SERVER_NAME | REGION_NAME | START_KEY | END_KEY | SIZE | REQ | LOCALITY | --------------------------- | -------------------------------------------------------------------- | ---------- | ---------- | ----- | ----- | ---------- | linux02,16020,1684200651886 | doit:test,,1684207546572.0f550ec8fa1af0ab9e73032d224d9f00. | | rowkey_015 | 0 | 0 | 0.0 | linux02,16020,1684200651886 | doit:test,rowkey_015,1684207546572.09a2022c54dfef68866ac73e3f78bc70. | rowkey_015 | rowkey_025 | 0 | 0 | 0.0 | linux02,16020,1684200651886 | doit:test,rowkey_025,1684207502853.80d7feace447978ffe4a54418a20afd0. | rowkey_025 | rowkey_030 | 0 | 0 | 0.0 | linux03,16020,1684200651855 | doit:test,rowkey_030,1684205468848.2b0468e6643b95159fa6e210fa093e66. | rowkey_030 | rowkey_040 | 0 | 0 | 0.0 | linux01,16020,1684205091382 | doit:test,rowkey_040,1684205468848.fb12c09c7c73cfeff0bf79b5dda076cb. | rowkey_040 | | 0 | 0 | 0.0 | 5 rowsTook 0.0241 seconds
Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议设计在10-100个字节,不过建议是越短越好,不要超过16个字节
原因如下:
如果Rowkey是按时间戳的方式递增,因为rowkey是按照字典顺序排序的,这样会出现大量的数据插入到一个reion中,而其他的region相对比较空闲从而造成热点问题,所以尽量不要将开头相同的内容作为rowkey造成热点问题,可以将时间戳反转后在作为rowkey。
必须在设计Rowkey上保证其唯一性。否则前面插入的数据将会被覆盖。
加盐
这里所说的加盐不是密码学中的加盐,而是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。分配的前缀种类数量应该和你想使用数据分散到不同的region的数量一致。加盐之后的rowkey就会根据随机生成的前缀分散到各个region上,以避免热点。
哈希
哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据
反转
第三种防止热点的方法时反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。 比如手机号的反转,时间戳的反转,当一个连续递增的数字类型想要作为rowkey时,可以用一个很大的数去减这个rowkey,反转后再当成rowkey
很长一段时间以来,一个region同一时间只能在一台RS(Region Server)中打开。如果一个region同时在多个RS上打开,就是multi-assign问题,会导致数据不一致甚至丢数据的情况,这是要避免和解决的。对于正常情况而言,region本质上是单点服务的,当RS宕机时,这个RS上的region无法提供服务,直到他们在另外的RS上重新上线为止。我们首先讨论这种单点服务会导致哪些问题,然后,看看有什么解决方案。
从正常和异常两个方面对region单点可能导致的问题进行分析。因为region只在一台RS上assign,那这台RS直接决定了这个region的服务质量,RS发生的任何问题或多或少都会对region产生影响。
对于RS日常工作时出现的各种问题,导致的region服务质量问题,我们可以简单的将其称为“抖动”。导致抖动的原因包括:
非人为因素(不可预期的)
人为因素(可预期的)
硬件和OS的问题可以暂时丢在一边,region本身的操作在高吞吐场景下会带来非常明显的影响。例如region做flush,意味着memstore的snapshot操作会锁住所有的请求。此时,新到达的读写请求会被阻塞,直到正在执行的读写请求全部完成。如果单个请求的平均执行时间都非常短(1ms至几个ms),那整个region锁住的时间也可以非常短;如果有比较大的batch写,或者scan,那锁住的时间就会变长,从而对单region的整体吞吐产生极大的影响。
考虑到HBase的设计目标是少量的大表,一个大表通常有很多的region(少则数百,多则几十万),单个region的吞吐被影响对于整体而言,通常不会导致明显的流量波动。但如果一个表有相当比例的region出现同时无法服务的情况,则这个影响就无法忽略。一个典型的场景就是修改表属性。比如修改压缩或者ttl之类的,此时master会对表的所有region进行批量reopen。同时有十几到几十个region在做reopen是非常正常的。
另外,如果是RS有问题(硬件问题,或者RS被某些region打爆了),则这个RS上所有的region会同时被影响。这时,影响域就会扩大,甚至产生连锁反应。比如一个以写吞吐为主的日志型或者监控型业务,通常都是大batch写入。一个batch写操作通常会写多个RS,那一个RS慢了,整个batch都会被拖慢。从而导致客户端排队,GC加重,整体吞吐下跌。
上面我们讨论的都是RS日常工作中可能发生的问题,下面我们看一下RS宕机时的情况。RS的宕机处理的简要流程如下:
一个比较难理解的事情是宕机检测居然需要这么久的时间。为什么zk超时时间要设置这么长?设置成几秒不行吗?这里主要的考虑是RS可能会假死,一个典型的假死场景就是RS发生FGC。对于比较大的堆,一次FGC做个几十秒甚至数分钟都是有可能的。如果zk超时设置过短,一次几十秒的FGC就可导致RS“被宕机”。而RS从FGC中恢复后,可以立即服务,但如果被认为是宕机,那后续的处理时间会更长,影响更大。
对于region的单点assign,从RS实际发生宕机到宕机处理完毕,通常需要数分钟甚至更长的时间,在这段时间里,故障机器上的region都无法提供服务。虽然HBase能够在宕机时能够自动恢复,但宕机带来的影响是确实存在的,对于业务来说,往往几分钟的不可用时间就足以带来困扰(比如网络游戏,服务器卡一下你都不能忍,更不要说卡几分钟了)。
对于抖动和宕机导致的region服务质量下降,我们可以有两个思路:
优化系统,减少抖动和宕机处理时间:
副本(冗余),一个reigon有问题时,切换到该region的其他副本中
第一条是必须做的,因为抖动问题就像卡在喉咙里的一根鱼刺,没有问题还好,一旦有问题,就让你疼的不行,甚至还会流点血。不小心扎破颈部大血管,就要打110了。减少宕机恢复时间是有上限的,在当前的硬件体系和能力下,软件做的再好通常也不能在几秒的时间里处理完宕机。所以,必须诉诸于副本方案。一个典型的副本方案就是主备集群。两个集群互为主备,一个挂了就切另一个。一般可以做到秒级检测和切换。但双集群部署会增加额外的成本,所以,HBase 1.x系列提供了单集群的冗余策略,region replica方案,即一个region同时在多个RS上打开,有主备,一写多读。原理上跟mysql的一写多读是类似的。
多副本方案要解决的难题就是一致性问题。目前比较常见的是基于paxos或者raft的一致性算法,在多个副本之间进行数据同步,比如tidb。
从长远来看,第一条是每个分布式存储系统的长期任务,第二条是现代系统要做到高可用所必须具备的能力,或者说目前的技术水平下,可以商用的最好方案。原理都差不多,各家拼的就是工程实现的优劣,即成本,一致性和性能。
马吉辉2019-03-07 16:24:39 博主文章分类:hbase
简述: 和写流程对比起来,HBase读数据是一个更加复杂的操作流程,这主要基于两个方面的原因: 其一是因为整个HBase存储引擎基于LSM-Like树实现,因此一次范围查询可能会涉及多个分片、多块缓存甚至多个数据存储文件; 其二是因为HBase中更新操作以及删除操作实现都很简单,更新操作并没有更新原有数据,而是使用时间戳属性实现了多版本。删除操作也并没有真正删除原有数据,只是插入了一条打上”deleted”标签的数据,而真正的数据删除发生在系统异步执行Major_Compact的时候。 很显然,这种实现套路大大简化了数据更新、删除流程,但是对于数据读取来说却意味着套上了层层枷锁,读取过程需要根据版本进行过滤,同时对已经标记删除的数据也要进行过滤。
1、scan一次,现在内存中找,如果找到了后,就直接返回给客户端 2、如果没有找到再在 blockcache hfile memcache 中一行一行进行扫描,扫到100行之后,就返回给客户端 3、客户端将这100行数据缓存到内存中并返回给一条给上层业务。 这里是每次都会调用100行数据,客户端拿到之后,再扫描100条数据,直到数据被全部拿到
上层业务不断一条一条获取扫描数据,在数据量大的情况下实际上HBase客户端会不断发送next请求到HBase服务器。有的朋友可能会问为什么scan需要设计为多次next请求的模式?个人认为这是基于多个层面的考虑: 1、HBase本身存储了海量数据,所以很多场景下一次scan请求的数据量都会比较大。如果不限制每次请求的数据集大小,很可能会导致系统带宽吃紧从而造成整个集群的不稳定。 2、如果不限制每次请求的数据集大小,很多情况下可能会造成客户端缓存OOM掉。 3、如果不限制每次请求的数据集大小,很可能服务器端扫描大量数据会花费大量时间,客户端和服务器端的连接就会timeout。
get的批处理操作是安装目标region进行分组,不同分组的get请求会并发执行读取。然而scan并没有这样实现。 也就是说,scan不是并行操作。
所以从客户端视角来看整个扫描时间=客户端处理数据时间+服务器端扫描数据时间,这能不能优化?
小结: 根据上面的分析,scan API的效率很大程度上取决于扫描的数据量。通常建议OLTP业务中少量数据量扫描的scan可以使用scan API,大量数据的扫描使用scan API,扫描性能有时候并不能够得到有效保证。
引出问题:HBase作为列式存储,为什么它的scan性能这么低呢,列式存储不是更有利于scan操作么?Parquet格式也是列式,但它的scan这么优秀,他们的性能差异并不是因为数据组织方式造成的么?kudu也是采用的类LSM数据结构,但是却能达到parquet的扫描速度(kudu是纯列式的),kudu的一个列也会形成很多文件,但是好像并没影响它的性能
小结:
个人对Kudu不是很懂,不过旁边有Kudu大神。我的理解是这样的:
所以说hbase相比parquet,这两个方面都是scan的劣势。
参考链接: HBase原理-数据读取流程解析 http://hbasefly.com/2016/12/21/hbase-getorscan/ HBase最佳实践 – Scan用法大观园 http://hbasefly.com/2017/10/29/hbase-scan-3/