Hbase系列1-Hbase适用场景

#HBase 使用场景

HBase 采用 LSM 架构,适合写多读少场景

##HBase 适用场景

  • 支持海量数据,需要 TB/PB 级别的在线服务(亿行以上)
  • 拥有良好扩展性,数据量增长速度快,对水平扩展能力有需求
  • 高性能读写,简单 kv 读写,响应延迟低
  • 写入很频繁,吞吐量大
  • 满足强一致性要求
  • 批量读取数据需求
  • schema 灵活多变
  • 无跨行跨表事务要求

##HBase 的限制

只有主索引(Rowkey),不支持表联接、聚合、order by 等高级查询

##案例分析

A. 统一日志案例

场景:写多读少,写入量巨大(日写入>37 亿),读取随机且访问较少。
设计:将 Rowkey 用 UUID 类似方式生成随机字段,转换成 Byte 数组,经过优化,截取为 4 个 Byte。既
随机,又很短,能满足 40 年存储。通过 HBase 组针对性的预分区,将数据区域划分到所有节点上,写入均
衡到每个节点,因此压力平均。读取的时候,因为随机,设置了 BLOOMFILTER,所以随机读性能提高。因
为读取少,所以对于读取的优化就没有写那么重要了。

B. 一次导入,多次读取

场景:晚上导入大量数据,白天提供用户访问,每次查询是带有客户 ID 的随机数据,数据量可以估算,每
条数据<100B。
设计:晚上导入大量数据,如果只有一台服务器提供服务,TPS 上不去,且容易导致节点宕机,因此需要写
入是均衡分布的。另外一方面,用户访问的时间和方式是比较随机的,数据又是随机的,因此将 Rowkey
随机分布有助于大量请求比较均衡在多个节点上。因为用户的访问更重要,因此将客户 ID 和相关业务类型
作为 Rowkey。如果写入数据是对用户顺序处理的,就会出现压力集中在某个节点上,导致宕机。于是将导
入 worker 设计为先在本地存储,然后分 10 个线程,将用户 Hash 成 10 组。每个线程读取一个用户的 300
条数据,批量写入 HBase,然后写下一个用户的 300 条数据,继续直到第 10 个用户,再返回写本组第一个
用户,直到所有数据写完。最终集群的表现比较平稳,且 TPS 很高。

C. 监控数据(京东云容器监控cap,京东大脑,jdh监控,bdp监控数据查询,ump,mjdos)

D. 海量存储(商家罗盘,供应商罗盘,Storm实时应用,搜索推荐)

Hbase系列4-rowkey设计技巧

#HBase Rowkey 设计

##一 引言

HBase Rowkey是唯一索引(Rowkey用来表示唯一一行记录),Rowkey设计的优劣直接影响读写性能。

HBase中的行是按照Rowkey的ASCII字典顺序进行全局排序的。

举例说明:假如有5个Rowkey:”012”, “0”, “123”, “234”, “3”,按ASCII字典排序后的结果为:”0”, “012”, “123”, “234”, “3”。(注:文末附常用ASCII码表)

Rowkey排序时会先比对两个Rowkey的第一个字节,如果相同,然后会比对第二个字节,依次类推… 对比到第X个字节时,已经超出了其中一个Rowkey的长度,短的Rowkey排在前面。

由于HBase是通过Rowkey查询的,一般Rowkey上都会存一些比较关键的检索信息,建议提前考虑数据具体需要如何查询,根据查询方式进行数据存储格式的设计,要避免做全表扫描,因为效率特别低,且会损耗集群性能。

##二 Rowkey设计原则

Rowkey设计应遵循以下原则:

###Rowkey唯一原则

Rowkey在设计上必须保证唯一性,Rowkey在HBase中只插入不更新(不做update操作)。HBase中所有操作都是“流式“操作,不会更改原来存储过的数据,只会进行append,并通过加上新的版本标识表示是最新数据(VERSION为1会覆盖,默认是1)。

###Rowkey的散列原则

Rowkey设计应散列,均匀的分布在各个HBase节点上
如果Rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。

Rowkey的第一部分如果是时间戳,会将造成所有新数据都在最后一个Region,造成访问热点。

热点:大量的client直接访问集群的一个或极少数个节点(访问可能是读、写或者其他操作)。大量访问会使热点Region所在的单个机器超出自身承受能力,引起性能下降(Full GC)甚至Region不可用,这也会影响同一个RegionServer上的其他Region,由于主机无法服务其他Region的请求。

通常有3种方式来解决热点问题

1、Reverse反转

针对固定长度的Rowkey反转后存储,这样可以使Rowkey中经常改变的部分放在最前面,可以有效的随机Rowkey。
反转Rowkey的例子通常以手机举例,可以将手机号反转后的字符串作为Rowkey,这样的就避免了以手机号那样比较固定开头(137x、15x等)导致热点问题。缺点是牺牲了Rowkey的有序性。

2、Salt加盐

Salting是将每一个Rowkey加一个前缀,前缀使用一些随机字符,使得数据分散在多个不同的Region,达到Region负载均衡的目标。

比如在一个有4个Region(注:以 [ ,a)、[a,b)、[b,c)、[c, )为Region起至)的HBase表中,
加Salt前的Rowkey:abc001、abc002、abc003

我们分别加上a、b、c前缀,加Salt后Rowkey为:a-abc001、b-abc002、c-abc003

可以看到,加盐前的Rowkey默认会在第2个region中,加盐后的Rowkey数据会分布在3个region中,理论上处理后的吞吐量应是之前的3倍。由于前缀是随机的,读这些数据时需要耗费更多的时间,所以Salt增加了写操作的吞吐量,不过缺点是同时增加了读操作的开销。

3、Hash散列或者MOD

用Hash散列来替代随机Salt前缀的好处是能让一个给定的行有相同的前缀,这在分散了Region负载的同时,使读操作也能够推断。

确定性Hash(比如md5后取前4位做前缀)能让客户端重建完整的RowKey,可以使用Get操作直接Get想要的行。

例如将上述的原始Rowkey经过hash处理,此处我们采用md5散列算法取前4位做前缀,结果如下

9bf0-abc001 (abc001在md5后是9bf049097142c168c38a94c626eddf3d,取前4位是9bf0)

7006-abc002

95e6-abc003

若以前4个字符作为不同分区的起止,上面几个Rowkey数据会分布在3个region中。实际应用场景是当数据量越来越大的时候,这种设计会使得分区之间更加均衡。

如果Rowkey是数字类型的,也可以考虑MOD方法(其他类型可以使用hashcode MOD),示意图:

###Rowkey长度原则

Rowkey设计建议定长,长度在10~100个字节,越短越好。
RowKey是一个二进制码流,可以是任意字符串,最大长度 64kb,实际应用中一般为10-100bytes,以 byte[] 形式保存,一般设计成定长。建议越短越好,不要超过16个字节,原因如下:

(1)数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光RowKey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;

(2)MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。

(3)目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性。

其他的如列族名、列名等属性名也是越短越好。value永远和它的key一起传输的。当具体的值在系统间传输时,它的RowKey、列名、时间戳也会一起传输。如果你的RowKey和列名和值相比较很大,那么你将会遇到一些有趣的问题。HFile中的索引最终占据了HBase分配的大量内存。

###Rowkey有序原则

充分利用Rowkey字典顺序排序特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。时间戳反转设计:一个常见的数据库处理问题是快速获取数据的最近版本,使用反转的时间戳作为Rowkey的一部分对这个问题十分有用,可以将Long.MAX_VALUE-timestamp追加到key的末尾,例如[key][reverse_timestamp]。

表中[key]的最新值可以通过scan [key]获得 [key]的第一条记录,因为HBase中rowkey是有序的,最新的[key]在任何更旧的[key]之前,所以第一条记录就是最新的。

这个技巧可以替代使用多版本数据,多版本数据会永久(很长时间)保存数据的所有版本。同时,这个技巧用一个scan操作就可以获得数据的所有版本。

Hbase的排序,是按照rowkey,columnKey(columnFamily + qualifier),timestamp 三维排序,且都是按照字典顺序。首先比较rowkey字典排序,同样rowkey的按columnKey字典排序,最后按timestamp最新排到最前。

###HBase Rowkey设计实战

在实际的设计中我们可能更多的是结合多种设计方法来实现Rowkey的最优化设计。

列1:设计订单状态表

使用Rowkey: reverse(order_id) + (Long.MAX_VALUE – timestamp)

设计优点:一、通过reverse订单号避免Region热点,二、可以按时间倒排显示。

列2:使用HBase作为事件(事件指的的终端在APP中发生的行为,比如登录、下单等等统称事件(event))的临时存储(HBase存储最近10分钟的热数据)

设计event事件的Rowkey为:两位随机数Salt + EventId + Date + Kafka的Offset

设计优点:
加盐的目的是为了增加查询的并发性,假如Salt的范围是0~n,那我们在查询的时候,可以将数据分为n个split同时做Scan操作。经过测试验证,增加并发度能够将整体的查询速度提升5~20倍以上。

随后的EventId和Date是用来做范围Scan使用的。在大部分的查询场景中,都指定了EventId,因此把EventId放在了第二个位置上,同时EventId的取值有几十个,通过Salt + EventId的方式可以保证不会形成热点。

把Date放在Rowkey的第三个位置上以实现按Date做Scan。这里可以考虑对Data进行反转(参考时间戳反转设计)

这样的Rowkey设计能够很好的支持如下查询场景:

1、只按照EventId查询

2、按照EventId和Date查询

非必要情况,一般不建议进行全表的Scan查询,全表Scan对性能的消耗很大。

###HBase的表设计

HBase表设计通常可以是宽表(Wide Table)模式,即一行包括很多列。同样的信息也可以用高表(Tall Table)形式存储,通常高表的性能比宽表要高出50%以上。所以推荐使用高表来完成表设计。

表设计时考虑HBase数据库的一些特性:

1、在HBase表中是通过Rowkey的字典序来进行数据排序的

2、所有存储在HBase表中的数据都是二进制的字节

3、原子性只在行内保证,HBase不支持跨行事务

4、列族(Column Family)在表创建之前就要定义好

5、列族中的列标识(Column Qualifier)可以在表创建完以后动态插入数据时添加

##总结

在Rowkey设计时,请先考虑业务场景,比如是读比写多、还是读比写少。HBase本身是为写优化的,即便是这样,也可能会出现热点问题,设计时尽量避免热点。如果读场景比较多,除了考虑以上Rowkey设计原则外,还可以考虑和其他缓存数据库如Redis等结合。

##附录

常用ASCLL码

img

Hbase系列2-Hbase架构深度解析

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

HBase架构组成

HBase采用Master/Slave架构搭建集群,它隶属于Hadoop生态系统,由一下类型节点组成:HMaster节点、HRegionServer节点、ZooKeeper集群,而在底层,它将数据存储于HDFS中,因而涉及到HDFS的NameNode、DataNode等,总体结构如下:

img

其中HMaster节点用于:

  1. 管理HRegionServer,实现其负载均衡。
  2. 管理和分配HRegion,比如在HRegion split时分配新的HRegion;在HRegionServer退出时迁移其内的HRegion到其他HRegionServer上。
  3. 实现DDL操作(Data Definition Language,namespace和table的增删改,column familiy的增删改等)。
  4. 管理namespace和table的元数据(实际存储在HDFS上)。
  5. 权限控制(ACL)。

HRegionServer节点用于:

  1. 存放和管理本地HRegion。
  2. 读写HDFS,管理Table中的数据。
  3. Client直接通过HRegionServer读写数据(从HMaster中获取元数据,找到RowKey所在的HRegion/HRegionServer后)。

ZooKeeper集群是协调系统用于:

  1. 存放整个 HBase集群的元数据以及集群的状态信息。
  2. 实现HMaster主从节点的failover。

HBase Client通过RPC方式和HMaster、HRegionServer通信;一个HRegionServer可以存放1000个HRegion;底层Table数据存储于HDFS中,而HRegion所处理的数据尽量和数据所在的DataNode在一起,实现数据的本地化;数据本地化并不是总能实现,比如在HRegion移动(如因Split)时,需要等下一次Compact才能继续回到本地化。

这个架构图比较清晰的表达了HMaster和NameNode都支持多个热备份,使用ZooKeeper来做协调;ZooKeeper并不是云般神秘,它一般由三台机器组成一个集群,内部使用PAXOS算法支持三台Server中的一台宕机,也有使用五台机器的,此时则可以支持同时两台宕机,既少于半数的宕机,然而随着机器的增加,它的性能也会下降;RegionServer和DataNode一般会放在相同的Server上实现数据的本地化。

HRegion

HBase使用RowKey将表水平切割成多个HRegion,从HMaster的角度,每个HRegion都纪录了它的StartKey和EndKey(第一个HRegion的StartKey为空,最后一个HRegion的EndKey为空),由于RowKey是排序的,因而Client可以通过HMaster快速的定位每个RowKey在哪个HRegion中。HRegion由HMaster分配到相应的HRegionServer中,然后由HRegionServer负责HRegion的启动和管理,和Client的通信,负责数据的读(使用HDFS)。每个HRegionServer可以同时管理1000个左右的HRegion(这个数字怎么来的?没有从代码中看到限制,难道是出于经验?超过1000个会引起性能问题?

来回答这个问题:感觉这个1000的数字是从BigTable的论文中来的(5 Implementation节):Each tablet server manages a set of tablets(typically we have somewhere between ten to a thousand tablets per tablet server))。

img

HMaster

HMaster没有单点故障问题,可以启动多个HMaster,通过ZooKeeper的Master Election机制保证同时只有一个HMaster出于Active状态,其他的HMaster则处于热备份状态。一般情况下会启动两个HMaster,非Active的HMaster会定期的和Active HMaster通信以获取其最新状态,从而保证它是实时更新的,因而如果启动了多个HMaster反而增加了Active HMaster的负担。前文已经介绍过了HMaster的主要用于HRegion的分配和管理,DDL(Data Definition Language,既Table的新建、删除、修改等)的实现等,既它主要有两方面的职责:

  1. 协调HRegionServer
    1. 启动时HRegion的分配,以及负载均衡和修复时HRegion的重新分配。
    2. 监控集群中所有HRegionServer的状态(通过Heartbeat和监听ZooKeeper中的状态)。
  2. Admin职能
    1. 创建、删除、修改Table的定义。

img

ZooKeeper:协调者

ZooKeeper为HBase集群提供协调服务,它管理着HMaster和HRegionServer的状态(available/alive等),并且会在它们宕机时通知给HMaster,从而HMaster可以实现HMaster之间的failover,或对宕机的HRegionServer中的HRegion集合的修复(将它们分配给其他的HRegionServer)。ZooKeeper集群本身使用一致性协议(PAXOS协议)保证每个节点状态的一致性。

img

How The Components Work Together

ZooKeeper协调集群所有节点的共享信息,在HMaster和HRegionServer连接到ZooKeeper后创建Ephemeral节点,并使用Heartbeat机制维持这个节点的存活状态,如果某个Ephemeral节点实效,则HMaster会收到通知,并做相应的处理。

另外,HMaster通过监听ZooKeeper中的Ephemeral节点(默认:/hbase/rs/*)来监控HRegionServer的加入和宕机。在第一个HMaster连接到ZooKeeper时会创建Ephemeral节点(默认:/hbasae/master)来表示Active的HMaster,其后加进来的HMaster则监听该Ephemeral节点,如果当前Active的HMaster宕机,则该节点消失,因而其他HMaster得到通知,而将自身转换成Active的HMaster,在变为Active的HMaster之前,它会创建在/hbase/back-masters/下创建自己的Ephemeral节点。

img

HBase的第一次读写

在HBase 0.96以前,HBase有两个特殊的Table:-ROOT-和.META.(如BigTable中的设计),其中-ROOT- Table的位置存储在ZooKeeper,它存储了.META. Table的RegionInfo信息,并且它只能存在一个HRegion,而.META. Table则存储了用户Table的RegionInfo信息,它可以被切分成多个HRegion,因而对第一次访问用户Table时,首先从ZooKeeper中读取-ROOT- Table所在HRegionServer;然后从该HRegionServer中根据请求的TableName,RowKey读取.META. Table所在HRegionServer;最后从该HRegionServer中读取.META. Table的内容而获取此次请求需要访问的HRegion所在的位置,然后访问该HRegionSever获取请求的数据,这需要三次请求才能找到用户Table所在的位置,然后第四次请求开始获取真正的数据。当然为了提升性能,客户端会缓存-ROOT- Table位置以及-ROOT-/.META. Table的内容。如下图所示:

img

可是即使客户端有缓存,在初始阶段需要三次请求才能直到用户Table真正所在的位置也是性能低下的,而且真的有必要支持那么多的HRegion吗?或许对Google这样的公司来说是需要的,但是对一般的集群来说好像并没有这个必要。在BigTable的论文中说,每行METADATA存储1KB左右数据,中等大小的Tablet(HRegion)在128MB左右,3层位置的Schema设计可以支持2^34个Tablet(HRegion)。即使去掉-ROOT- Table,也还可以支持2^17(131072)个HRegion, 如果每个HRegion还是128MB,那就是16TB,这个貌似不够大,但是现在的HRegion的最大大小都会设置的比较大,比如我们设置了2GB,此时支持的大小则变成了4PB,对一般的集群来说已经够了,因而在HBase 0.96以后去掉了-ROOT- Table,只剩下这个特殊的目录表叫做Meta Table(hbase:meta),它存储了集群中所有用户HRegion的位置信息,而ZooKeeper的节点中(/hbase/meta-region-server)存储的则直接是这个Meta Table的位置,并且这个Meta Table如以前的-ROOT- Table一样是不可split的。这样,客户端在第一次访问用户Table的流程就变成了:

  1. 从ZooKeeper(/hbase/meta-region-server)中获取hbase:meta的位置(HRegionServer的位置),缓存该位置信息。
  2. 从HRegionServer中查询用户Table对应请求的RowKey所在的HRegionServer,缓存该位置信息。
  3. 从查询到HRegionServer中读取Row。

从这个过程中,我们发现客户会缓存这些位置信息,然而第二步它只是缓存当前RowKey对应的HRegion的位置,因而如果下一个要查的RowKey不在同一个HRegion中,则需要继续查询hbase:meta所在的HRegion,然而随着时间的推移,客户端缓存的位置信息越来越多,以至于不需要再次查找hbase:meta Table的信息,除非某个HRegion因为宕机或Split被移动,此时需要重新查询并且更新缓存。

img

hbase:meta表

hbase:meta表存储了所有用户HRegion的位置信息,它的RowKey:tableName,regionStartKey,regionId,replicaId等,它只有info列族,这个列族包含三个列,他们分别是:info:regioninfo列是RegionInfo的proto格式:regionId,tableName,startKey,endKey,offline,split,replicaId;info:server格式:HRegionServer对应的server:port;info:serverstartcode格式是HRegionServer的启动时间戳。

img

HRegionServer详解

HRegionServer一般和DataNode在同一台机器上运行,实现数据的本地性。HRegionServer包含多个HRegion,由WAL(HLog)、BlockCache、MemStore、HFile组成。

  1. WAL即Write Ahead Log,在早期版本中称为HLog,它是HDFS上的一个文件,如其名字所表示的,所有写操作都会先保证将数据写入这个Log文件后,才会真正更新MemStore,最后写入HFile中。采用这种模式,可以保证HRegionServer宕机后,我们依然可以从该Log文件中读取数据,Replay所有的操作,而不至于数据丢失。这个Log文件会定期Roll出新的文件而删除旧的文件(那些已持久化到HFile中的Log可以删除)。WAL文件存储在/hbase/WALs/${HRegionServer_Name}的目录中(在0.94之前,存储在/hbase/.logs/目录中),一般一个HRegionServer只有一个WAL实例,也就是说一个HRegionServer的所有WAL写都是串行的(就像log4j的日志写也是串行的),这当然会引起性能问题,因而在HBase 1.0之后,通过HBASE-5699实现了多个WAL并行写(MultiWAL),该实现采用HDFS的多个管道写,以单个HRegion为单位。关于WAL可以参考Wikipedia的Write-Ahead Logging。顺便吐槽一句,英文版的维基百科竟然能毫无压力的正常访问了,这是某个GFW的疏忽还是以后的常态?
  2. BlockCache是一个读缓存,即“引用局部性”原理(也应用于CPU,分空间局部性和时间局部性,空间局部性是指CPU在某一时刻需要某个数据,那么有很大的概率在一下时刻它需要的数据在其附近;时间局部性是指某个数据在被访问过一次后,它有很大的概率在不久的将来会被再次的访问),将数据预读取到内存中,以提升读的性能。HBase中提供两种BlockCache的实现:默认on-heap LruBlockCache和BucketCache(通常是off-heap)。通常BucketCache的性能要差于LruBlockCache,然而由于GC的影响,LruBlockCache的延迟会变的不稳定,而BucketCache由于是自己管理BlockCache,而不需要GC,因而它的延迟通常比较稳定,这也是有些时候需要选用BucketCache的原因。这篇文章BlockCache101对on-heap和off-heap的BlockCache做了详细的比较。
  3. HRegion是一个Table中的一个Region在一个HRegionServer中的表达。一个Table可以有一个或多个Region,他们可以在一个相同的HRegionServer上,也可以分布在不同的HRegionServer上,一个HRegionServer可以有多个HRegion,他们分别属于不同的Table。HRegion由多个Store(HStore)构成,每个HStore对应了一个Table在这个HRegion中的一个Column Family,即每个Column Family就是一个集中的存储单元,因而最好将具有相近IO特性的Column存储在一个Column Family,以实现高效读取(数据局部性原理,可以提高缓存的命中率)。HStore是HBase中存储的核心,它实现了读写HDFS功能,一个HStore由一个MemStore 和0个或多个StoreFile组成。
    1. MemStore是一个写缓存(In Memory Sorted Buffer),所有数据的写在完成WAL日志写后,会 写入MemStore中,由MemStore根据一定的算法将数据Flush到地层HDFS文件中(HFile),通常每个HRegion中的每个 Column Family有一个自己的MemStore。
    2. HFile(StoreFile) 用于存储HBase的数据(Cell/KeyValue)。在HFile中的数据是按RowKey、Column Family、Column排序,对相同的Cell(即这三个值都一样),则按timestamp倒序排列。

img

虽然上面这张图展现的是最新的HRegionServer的架构(但是并不是那么的精确),但是我一直比较喜欢看以下这张图,即使它展现的应该是0.94以前的架构。

img

HRegionServer中数据写流程图解

当客户端发起一个Put请求时,首先它从hbase:meta表中查出该Put数据最终需要去的HRegionServer。然后客户端将Put请求发送给相应的HRegionServer,在HRegionServer中它首先会将该Put操作写入WAL日志文件中(Flush到磁盘中)。

img

写完WAL日志文件后,HRegionServer根据Put中的TableName和RowKey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中。此时写成功,并返回通知客户端。

img

MemStore Flush

MemStore是一个In Memory Sorted Buffer,在每个HStore中都有一个MemStore,即它是一个HRegion的一个Column Family对应一个实例。它的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序,如下所示:

img

每一次Put/Delete请求都是先写入到MemStore中,当MemStore满后会Flush成一个新的StoreFile(底层实现是HFile),即一个HStore(Column Family)可以有0个或多个StoreFile(HFile)。有以下三种情况可以触发MemStore的Flush动作,

需要注意的是MemStore的最小Flush单元是HRegion而不是单个MemStore。据说这是Column Family有个数限制的其中一个原因,估计是因为太多的Column Family一起Flush会引起性能问题?具体原因有待考证。

  1. 当一个HRegion中的所有MemStore的大小总和超过了hbase.hregion.memstore.flush.size的大小,默认128MB。此时当前的HRegion中所有的MemStore会Flush到HDFS中。
  2. 当全局MemStore的大小超过了hbase.regionserver.global.memstore.upperLimit的大小,默认40%的内存使用量。此时当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush顺序是MemStore大小的倒序(一个HRegion中所有MemStore总和作为该HRegion的MemStore的大小还是选取最大的MemStore作为参考?有待考证),直到总体的MemStore使用量低于hbase.regionserver.global.memstore.lowerLimit,默认38%的内存使用量。
  3. 当前HRegionServer中WAL的大小超过了hbase.regionserver.hlog.blocksize hbase.regionserver.max.logs的数量,当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush使用时间顺序,最早的MemStore先Flush直到WAL的数量少于hbase.regionserver.hlog.blocksize hbase.regionserver.max.logs。这里说这两个相乘的默认大小是2GB,查代码,hbase.regionserver.max.logs默认值是32,而hbase.regionserver.hlog.blocksize是HDFS的默认blocksize,32MB。但不管怎么样,因为这个大小超过限制引起的Flush不是一件好事,可能引起长时间的延迟,因而这篇文章给的建议:“Hint: keep hbase.regionserver.hlog.blocksize hbase.regionserver.maxlogs just a bit above hbase.regionserver.global.memstore.lowerLimit HBASE_HEAPSIZE.”。并且需要注意,这里给的描述是有错的(虽然它是官方的文档)。

在MemStore Flush过程中,还会在尾部追加一些meta数据,其中就包括Flush时最大的WAL sequence值,以告诉HBase这个StoreFile写入的最新数据的序列,那么在Recover时就直到从哪里开始。在HRegion启动时,这个sequence会被读取,并取最大的作为下一次更新时的起始sequence。

img

HFile格式

HBase的数据以KeyValue(Cell)的形式顺序的存储在HFile中,在MemStore的Flush过程中生成HFile,由于MemStore中存储的Cell遵循相同的排列顺序,因而Flush过程是顺序写,我们直到磁盘的顺序写性能很高,因为不需要不停的移动磁盘指针。

img

HFile参考BigTable的SSTable和Hadoop的TFile实现,从HBase开始到现在,HFile经历了三个版本,其中V2在0.92引入,V3在0.98引入。首先我们来看一下V1的格式:

img

V1的HFile由多个Data Block、Meta Block、FileInfo、Data Index、Meta Index、Trailer组成,其中Data Block是HBase的最小存储单元,在前文中提到的BlockCache就是基于Data Block的缓存的。一个Data Block由一个魔数和一系列的KeyValue(Cell)组成,魔数是一个随机的数字,用于表示这是一个Data Block类型,以快速监测这个Data Block的格式,防止数据的破坏。Data Block的大小可以在创建Column Family时设置(HColumnDescriptor.setBlockSize()),默认值是64KB,大号的Block有利于顺序Scan,小号Block利于随机查询,因而需要权衡。Meta块是可选的,FileInfo是固定长度的块,它纪录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等。Data Index和Meta Index纪录了每个Data块和Meta块的其实点、未压缩时大小、Key(起始RowKey?)等。Trailer纪录了FileInfo、Data Index、Meta Index块的起始位置,Data Index和Meta Index索引的数量等。其中FileInfo和Trailer是固定长度的。

HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构:

img

开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。随着HFile版本迁移,KeyValue(Cell)的格式并未发生太多变化,只是在V3版本,尾部添加了一个可选的Tag数组。

HFileV1版本的在实际使用过程中发现它占用内存多,并且Bloom File和Block Index会变的很大,而引起启动时间变长。其中每个HFile的Bloom Filter可以增长到100MB,这在查询时会引起性能问题,因为每次查询时需要加载并查询Bloom Filter,100MB的Bloom Filer会引起很大的延迟;另一个,Block Index在一个HRegionServer可能会增长到总共6GB,HRegionServer在启动时需要先加载所有这些Block Index,因而增加了启动时间。为了解决这些问题,在0.92版本中引入HFileV2版本:

img

在这个版本中,Block Index和Bloom Filter添加到了Data Block中间,而这种设计同时也减少了写的内存使用量;另外,为了提升启动速度,在这个版本中还引入了延迟读的功能,即在HFile真正被使用时才对其进行解析。

FileV3版本基本和V2版本相比,并没有太大的改变,它在KeyValue(Cell)层面上添加了Tag数组的支持;并在FileInfo结构中添加了和Tag相关的两个字段。关于具体HFile格式演化介绍,可以参考这里。

对HFileV2格式具体分析,它是一个多层的类B+树索引,采用这种设计,可以实现查找不需要读取整个文件:

img

Data Block中的Cell都是升序排列,每个block都有它自己的Leaf-Index,每个Block的最后一个Key被放入Intermediate-Index中,Root-Index指向Intermediate-Index。在HFile的末尾还有Bloom Filter用于快速定位那么没有在某个Data Block中的Row;TimeRange信息用于给那些使用时间查询的参考。在HFile打开时,这些索引信息都被加载并保存在内存中,以增加以后的读取性能。

参考:

https://www.mapr.com/blog/in-depth-look-hbase-architecture#.VdNSN6Yp3qx

http://jimbojw.com/wiki/index.php?title=Understanding_Hbase_and_BigTable

http://hbase.apache.org/book.html

http://www.searchtb.com/2011/01/understanding-hbase.html

http://research.google.com/archive/bigtable-osdi06.pdf

Hbase系列3-Hbase深入浅出

转自原文:https://www.ibm.com/developerworks/cn/analytics/library/ba-cn-bigdata-hbase/index.html

HBase 在大数据生态圈中的位置

提到大数据的存储,大多数人首先联想到的是 Hadoop 和 Hadoop 中的 HDFS 模块。大家熟知的 Spark、以及 Hadoop 的 MapReduce,可以理解为一种计算框架。而 HDFS,我们可以认为是为计算框架服务的存储层。因此不管是 Spark 还是 MapReduce,都需要使用 HDFS 作为默认的持久化存储层。那么 HBase 又是什么,可以用在哪里,解决什么样的问题?简单地,我们可以认为 HBase 是一种类似于数据库的存储层,也就是说 HBase 适用于结构化的存储。并且 HBase 是一种列式的分布式数据库,是由当年的 Google 公布的 BigTable 的论文而生。不过这里也要注意 HBase 底层依旧依赖 HDFS 来作为其物理存储,这点类似于 Hive。

可能有的读者会好奇 HBase 于 Hive 的区别,我们简单的梳理一下 Hive 和 HBase 的应用场景:

Hive 适合用来对一段时间内的数据进行分析查询,例如,用来计算趋势或者网站的日志。Hive 不应该用来进行实时的查询(Hive 的设计目的,也不是支持实时的查询)。因为它需要很长时间才可以返回结果;HBase 则非常适合用来进行大数据的实时查询,例如 Facebook 用 HBase 进行消息和实时的分析。对于 Hive 和 HBase 的部署来说,也有一些区别,Hive 一般只要有 Hadoop 便可以工作。而 HBase 则还需要 Zookeeper 的帮助(Zookeeper,是一个用来进行分布式协调的服务,这些服务包括配置服务,维护元信息和命名空间服务)。再而,HBase 本身只提供了 Java 的 API 接口,并不直接支持 SQL 的语句查询,而 Hive 则可以直接使用 HQL(一种类 SQL 语言)。如果想要在 HBase 上使用 SQL,则需要联合使用 Apache Phonenix,或者联合使用 Hive 和 HBase。但是和上面提到的一样,如果集成使用 Hive 查询 HBase 的数据,则无法绕过 MapReduce,那么实时性还是有一定的损失。Phoenix 加 HBase 的组合则不经过 MapReduce 的框架,因此当使用 Phoneix 加 HBase 的组成,实时性上会优于 Hive 加 HBase 的组合,我们后续也会示例性介绍如何使用两者。最后我们再提下 Hive 和 HBase 所使用的存储层,默认情况下 Hive 和 HBase 的存储层都是 HDFS。但是 HBase 在一些特殊的情况下也可以直接使用本机的文件系统。例如 Ambari 中的 AMS 服务直接在本地文件系统上运行 HBase。

HBase 与传统关系数据库的区别

首先让我们了解下什么是 ACID。ACID 是指数据库事务正确执行的四个基本要素的缩写,其包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)以及持久性(Durability)。对于一个支持事务(Transaction)的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction Processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。下面,我们就简单的介绍下这 4 个特性的含义。

  • 原子性(Atomicity)是指一个事务要么全部执行,要么全部不执行。换句话说,一个事务不可能只执行了一半就停止了。比如一个事情分为两步完成才可以完成,那么这两步必须同时完成,要么一步也不执行,绝不会停留在某一个中间状态。如果事物执行过程中,发生错误,系统会将事物的状态回滚到最开始的状态。

  • 一致性(Consistency)是指事务的运行并不改变数据库中数据的一致性。也就是说,无论并发事务有多少个,但是必须保证数据从一个一致性的状态转换到另一个一致性的状态。例如有 a、b 两个账户,分别都是 10。当 a 增加 5 时,b 也会随着改变,总值 20 是不会改变的。

  • 隔离性(Isolation)是指两个以上的事务不会出现交错执行的状态。因为这样可能会导致数据不一致。如果有多个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。

  • 持久性(Durability)指事务执行成功以后,该事务对数据库所作的更改便是持久的保存在数据库之中,不会无缘无故的回滚。

在具体的介绍 HBase 之前,我们先简单对比下 HBase 与传统关系数据库的(RDBMS,全称为 Relational Database Management System)区别。如表 1 所示。

表 1. HBase 与 RDBMS 的区别

HBase RDBMS
硬件架构 类似于 Hadoop 的分布式集群,硬件成本低廉 传统的多核系统,硬件成本昂贵
容错性 由软件架构实现,由于由多个节点组成,所以不担心一点或几点宕机 一般需要额外硬件设备实现 HA 机制
数据库大小 PB GB、TB
数据排布方式 稀疏的、分布的多维的 Map 以行和列组织
数据类型 Bytes 丰富的数据类型
事物支持 ACID 只支持单个 Row 级别 全面的 ACID 支持,对 Row 和表
查询语言 只支持 Java API (除非与其他框架一起使用,如 Phoenix、Hive) SQL
索引 只支持 Row-key,除非与其他技术一起应用,如 Phoenix、Hive 支持
吞吐量 百万查询/每秒 数千查询/每秒

理解了上面的表格之后,我们在看看数据是如何在 HBase 以及 RDBMS 中排布的。首先,数据在 RDBMS 的排布大致如表 2。

表 2. 数据在 RDBMS 中的排布示例

ID 密码 时间戳
1 111 20160719
2 222 20160720

那么数据在 HBase 中的排布会是什么样子呢?如表 3 所示(这只是逻辑上的排布)。

表 3. 数据在 HBase 中的排布(逻辑上)

Row-Key Value(CF、Qualifier、Version)
1 info{‘姓’: ‘张’,’名’:’三’} pwd{‘密码’: ‘111’}
2 Info{‘姓’: ‘李’,’名’:’四’} pwd{‘密码’: ‘222’}

从上面示例表中,我们可以看出,在 HBase 中首先会有 Column Family 的概念,简称为 CF。CF 一般用于将相关的列(Column)组合起来。在物理上 HBase 其实是按 CF 存储的,只是按照 Row-key 将相关 CF 中的列关联起来。物理上的数据排布大致可以如表 4 所示。

表 4. 数据在 HBase 中的排布

Row-Key CF:Column-Key 时间戳 Cell Value
1 info:fn 123456789
1 info:ln 123456789
2 info:fn 123456789
2 info:ln 123456789

我们已经提到 HBase 是按照 CF 来存储数据的。在表 3 中,我们看到了两个 CF,分别是 info 和 pwd。info 存储着姓名相关列的数据,而 pwd 则是密码相关的数据。上表便是 info 这个 CF 存储在 Hbase 中的数据排布。Pwd 的数据排布是类似的。上表中的 fn 和 ln 称之为 Column-key 或者 Qulifimer。在 Hbase 中,Row-key 加上 CF 加上 Qulifier 再加上一个时间戳才可以定位到一个单元格数据(Hbase 中每个单元格默认有 3 个时间戳的版本数据)。初学者,在一开始接触这些概念是很容易混淆。其实不管是 CF 还是 Qulifier 都是客户定义出来的。也就是说在 HBase 中创建表格时,就需要指定表格的 CF、Row-key 以及 Qulifier。我们会在后续的介绍中,尝试指定这些相关的概念,以便加深理解。这里我们先通过下图理解下 HBase 中,逻辑上的数据排布与物理上的数据排布之间的关系。

图 1. Hbase 中逻辑上数据的排布与物理上排布的关联

img

从上图我们看到 Row1 到 Row5 的数据分布在两个 CF 中,并且每个 CF 对应一个 HFile。并且逻辑上每一行中的一个单元格数据,对应于 HFile 中的一行,然后当用户按照 Row-key 查询数据的时候,HBase 会遍历两个 HFile,通过相同的 Row-Key 标识,将相关的单元格组织成行返回,这样便有了逻辑上的行数据。讲解到这,我们就大致了解 HBase 中的数据排布格式,以及与 RDBMS 的一些区别。

对于 RDBMS 来说,一般都是以 SQL 作为为主要的访问方式。而 HBase 是一种”NoSQL”数据库。”NoSQL”是一个通用词表示该数据库并

是 RDBMS 。现在的市面上有许多种 NoSQL 数据库,如 BerkeleyDB 是本地 NoSQL 数据库的例子, HBase 则为大型分布式 NoSql 数据库。从技术上来说,Hbase 更像是”数据存储”而非”数据库”(HBase 和 HDFS 都属于大数据的存储层)。因此,HBase 缺少很多 RDBMS 特性,如列类型,二级索引,触发器和高级查询语言等。然而, HBase 也具有许多其他特征同时支持线性化和模块化扩充。最明显的方式,我们可以通过增加 Region Server 的数量扩展 HBase。并且 HBase 可以放在普通的服务器中,例如将集群从 5 个扩充到 10 个 Region Server 时,存储空间和处理容量都可以同时翻倍。当然 RDBMS 也能很好的扩充,但仅对一个点,尤其是对一个单独数据库服务器而言,为了更好的性能,往往需要特殊的硬件和存储设备(往往价格也非常昂贵)。

HBase 相关的模块以及 HBase 表格的特性

在这里,让我们了解下 HBase 都有哪些模块,以及大致的工作流程。前面我们提到过 HBase 也是构建于 HDFS 之上,这是正确的,但也不是完全正确。HBase 其实也支持直接在本地文件系统之上运行,不过这样的 HBase 只能运行在一台机器上,那么对于分布式大数据的环境是没有意义的(这也是所谓的 HBase 的单机模式)。一般只用于测试或者验证某一个 HBase 的功能,后面我们在详细的介绍 HBase 的几种运行模式。这里我们只要记得在分布式的生产环境中,HBase 需要运行在 HDFS 之上,以 HDFS 作为其基础的存储设施。HBase 上层提供了访问的数据的 Java API 层,供应用访问存储在 HBase 的数据。在 HBase 的集群中主要由 Master 和 Region Server 组成,以及 Zookeeper,具体模块如下图所示。

图 2. HBase 的相关模块

img

接下来,我们简单的一一介绍下 HBase 中相关模块的作用。

  • Master

    HBase Master 用于协调多个 Region Server,侦测各个 Region Server 之间的状态,并平衡 Region Server 之间的负载。HBase Master 还有一个职责就是负责分配 Region 给 Region Server。HBase 允许多个 Master 节点共存,但是这需要 Zookeeper 的帮助。不过当多个 Master 节点共存时,只有一个 Master 是提供服务的,其他的 Master 节点处于待命的状态。当正在工作的 Master 节点宕机时,其他的 Master 则会接管 HBase 的集群。

  • Region Server

    对于一个 Region Server 而言,其包括了多个 Region。Region Server 的作用只是管理表格,以及实现读写操作。Client 直接连接 Region Server,并通信获取 HBase 中的数据。对于 Region 而言,则是真实存放 HBase 数据的地方,也就说 Region 是 HBase 可用性和分布式的基本单位。如果当一个表格很大,并由多个 CF 组成时,那么表的数据将存放在多个 Region 之间,并且在每个 Region 中会关联多个存储的单元(Store)。

  • Zookeeper

    对于 HBase 而言,Zookeeper 的作用是至关重要的。首先 Zookeeper 是作为 HBase Master 的 HA 解决方案。也就是说,是 Zookeeper 保证了至少有一个 HBase Master 处于运行状态。并且 Zookeeper 负责 Region 和 Region Server 的注册。其实 Zookeeper 发展到目前为止,已经成为了分布式大数据框架中容错性的标准框架。不光是 HBase,几乎所有的分布式大数据相关的开源框架,都依赖于 Zookeeper 实现 HA。

一个完整分布式的 HBase 的工作原理示意图如下:

图 3. HBase 的工作原理

img

在上面的图中,我们需要注意几个我们之前没有提到的概念:Store、MemStore、StoreFile 以及 HFile。带着这几个新的概念,我们完整的梳理下整个 HBase 的工作流程。

首先我们需要知道 HBase 的集群是通过 Zookeeper 来进行机器之前的协调,也就是说 HBase Master 与 Region Server 之间的关系是依赖 Zookeeper 来维护。当一个 Client 需要访问 HBase 集群时,Client 需要先和 Zookeeper 来通信,然后才会找到对应的 Region Server。每一个 Region Server 管理着很多个 Region。对于 HBase 来说,Region 是 HBase 并行化的基本单元。因此,数据也都存储在 Region 中。这里我们需要特别注意,每一个 Region 都只存储一个 Column Family 的数据,并且是该 CF 中的一段(按 Row 的区间分成多个 Region)。Region 所能存储的数据大小是有上限的,当达到该上限时(Threshold),Region 会进行分裂,数据也会分裂到多个 Region 中,这样便可以提高数据的并行化,以及提高数据的容量。每个 Region 包含着多个 Store 对象。每个 Store 包含一个 MemStore,和一个或多个 HFile。MemStore 便是数据在内存中的实体,并且一般都是有序的。当数据向 Region 写入的时候,会先写入 MemStore。当 MemStore 中的数据需要向底层文件系统倾倒(Dump)时(例如 MemStore 中的数据体积到达 MemStore 配置的最大值),Store 便会创建 StoreFile,而 StoreFile 就是对 HFile 一层封装。所以 MemStore 中的数据会最终写入到 HFile 中,也就是磁盘 IO。由于 HBase 底层依靠 HDFS,因此 HFile 都存储在 HDFS 之中。这便是整个 HBase 工作的原理简述。

我们了解了 HBase 大致的工作原理,那么在 HBase 的工作过程中,如何保证数据的可靠性呢?带着这个问题,我们理解下 HLog 的作用。HBase 中的 HLog 机制是 WAL 的一种实现,而 WAL(一般翻译为预写日志)是事务机制中常见的一致性的实现方式。每个 Region Server 中都会有一个 HLog 的实例,Region Server 会将更新操作(如 Put,Delete)先记录到 WAL(也就是 HLog)中,然后将其写入到 Store 的 MemStore,最终 MemStore 会将数据写入到持久化的 HFile 中(MemStore 到达配置的内存阀值)。这样就保证了 HBase 的写的可靠性。如果没有 WAL,当 Region Server 宕掉的时候,MemStore 还没有写入到 HFile,或者 StoreFile 还没有保存,数据就会丢失。或许有的读者会担心 HFile 本身会不会丢失,这是由 HDFS 来保证的。在 HDFS 中的数据默认会有 3 份。因此这里并不考虑 HFile 本身的可靠性。

前面,我们很多次提到了 HFile,也就是 HBase 持久化的存储文件。也许有的读者还不能完全理解 HFile,这里我们便详细的看看 HFile 的结构,如下图。

图 4. HFile 的结构

img

从图中我们可以看到 HFile 由很多个数据块(Block)组成,并且有一个固定的结尾块。其中的数据块是由一个 Header 和多个 Key-Value 的键值对组成。在结尾的数据块中包含了数据相关的索引信息,系统也是通过结尾的索引信息找到 HFile 中的数据。HFile 中的数据块大小默认为 64KB。如果访问 HBase 数据库的场景多为有序的访问,那么建议将该值设置的大一些。如果场景多为随机访问,那么建议将该值设置的小一些。一般情况下,通过调整该值可以提高 HBase 的性能。

如果要用很短的一句话总结 HBase,我们可以认为 HBase 就是一个有序的多维 Map,其中每一个 Row-key 映射了许多数据,这些数据存储在 CF 中的 Column。我们可以用下图来表示这句话。

图 5. HBase 的数据映射关系

img

HBase 的使用建议

之前我介绍了很多 HBase 与 RDBMS 的区别,以及一些优势的地方。那么什么时候最需要 HBase,或者说 HBase 是否可以替代原有的 RDBMS?对于这个问题,我们必须时刻谨记——HBase 并不适合所有问题,其设计目标并不是替代 RDBMS,而是对 RDBMS 的一个重要补充,尤其是对大数据的场景。当需要考量 HBase 作为一个备选项时,我们需要进行如下的调研工作。

首先,要确信有足够多数据,如果有上亿或上千亿行数据,HBase 才会是一个很好的备选。其次,需要确信业务上可以不依赖 RDBMS 的额外特性,例如,列数据类型, 二级索引,SQL 查询语言等。再而,需要确保有足够硬件。且不说 HBase,一般情况下当 HDFS 的集群小于 5 个数据节点时,也干不好什么事情 (HDFS 默认会将每一个 Block 数据备份 3 分),还要加上一个 NameNode。

以下我给了一些使用 HBase 时候对表格设计的一些建议,读者也可以理解背后的含义。不过我并不希望这些建议成为使用 HBase 的教条,毕竟也有不尽合理的地方。首先,一个 HBase 数据库是否高效,很大程度会和 Row-Key 的设计有关。因此,如何设计 Row-key 是使用 HBase 时,一个非常重要的话题。随着数据访问方式的不同,Row-Key 的设计也会有所不同。不过概括起来的宗旨只有一个,那就是尽可能选择一个 Row-Key,可以使你的数据均匀的分布在集群中。这也很容易理解,因为 HBase 是一个分布式环境,Client 会访问不同 Region Server 获取数据。如果数据排布均匀在不同的多个节点,那么在批量的 Client 便可以从不同的 Region Server 上获取数据,而不是瓶颈在某一个节点,性能自然会有所提升。对于具体的建议我们一般有几条:

  1. 当客户端需要频繁的写一张表,随机的 RowKey 会获得更好的性能。
  2. 当客户端需要频繁的读一张表,有序的 RowKey 则会获得更好的性能。
  3. 对于时间连续的数据(例如 log),有序的 RowKey 会很方便查询一段时间的数据(Scan 操作)。

上面我们谈及了对 Row-Key 的设计,接着我们需要想想是否 Column Family 也会在不同的场景需要不同的设计方案呢。答案是肯定的,不过 CF 跟 Row-key 比较的话,确实也简单一些,但这并不意味着 CF 的设计就是一个琐碎的话题。在 RDBMS(传统关系数据库)系统中,我们知道如果当用户的信息分散在不同的表中,便需要根据一个 Key 进行 Join 操作。而在 HBase 中,我们需要设计 CF 来聚合用户所有相关信息。简单来说,就是需要将数据按类别(或者一个特性)聚合在一个或多个 CF 中。这样,便可以根据 CF 获取这类信息。上面,我们讲解过一个 Region 对应于一个 CF。那么设想,如果在一个表中定义了多个 CF 时,就必然会有多个 Region。当 Client 查询数据时,就不得不查询多个 Region。这样性能自然会有所下降,尤其当 Region 夸机器的时候。因此在大多数的情况下,一个表格不会超过 2 到 3 个 CF,而且很多情况下都是 1 个 CF 就足够了。

Mac环境下搭建大数据开发环境

Mac下搭建大数据开发环境

环境比较浪费时间,结合自己搭建环境的经验,整理成文档。

安装JDK 和 SCALA

所有的前置条件是安装JDK,我用的是JDK8.x

java version “1.8.0_162”

Java(TM) SE Runtime Environment (build 1.8.0_162-b12)

Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)

Scala code runner version 2.12.4 – Copyright 2002-2017, LAMP/EPFL and Lightbend, Inc.

Spark V2.3.0

img

Spark安装比较简单,也没什么坑。网上一大堆,随意搞。

vmvare虚拟机

安装vmware fusion

虚拟下安装Hadoop & hive

1、hadoop伪分布式部署,版本 2.7.1, sbin/start-all.sh , sbin/stop-all.sh。jps验证其服务是否启动完全

2、hive用mysql(我用的是mariaDB,比安装Mysql省事)做元数据库,版本2.1.1 hive。hive-env.sh,hive-site.xml hdfs dfs -ls /user/hive/warehouse hive表都存到这个目录下

4、关闭防火墙 永久关闭: chkconfig iptables off 及时生效service iptables stop

5、mac 下 虚拟机vmware Fution

6、ssh -p 22 root@172.16.192.130 ssh链接虚拟机

core-site.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<configuration>
<property>
<name>hadoop.tmp.dir</name>
<value>/soft/data/hadoop/tmp</value>
<description>Abase for other temporary directories.</description>
</property>
<property>
<name>fs.defaultFS</name>
<value>hdfs://172.16.192.130:9000</value>
</property>
<property>
<name>hadoop.native.lib</name>
<value>false</value>
<description>Should native hadoop libraries, if present, be used. </description>
</property>
</configuration>

hdfs-site.xml

1
2
3
4
5
6
7
8
9
10
<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<property>
<name>dfs.permissions</name>
<value>false</value>
</property>
</configuration>

启动方式:

$HADOOP_HOME/sbin/start-all

3825 Jps

3089 DataNode

3252 SecondaryNameNode

2903 NameNode

3405 ResourceManager

3695 NodeManager

虚拟机下安装Mysql,当做Hive的元数据库

service mysql status 查看mysql是否启动

service mysql start 启动mysql服务

mysql用的mariaDB,yum安装,搜索“centOS 6 yum安装 mariaDB”。mySQL 启动:service mysql start,service mysql status

查看HIVE元数据:

mysql -uroot -p 访问mysql

use hive;

show tables;

虚拟机下安装Hbase

1、 进入hbase官网,下载hbase1.2.1,因为本地的hadoop版本是2.7.1,要注意hbase和hadoop的兼容性。

2、 下载之后,tar -zxvf hbase-1.2.1-bin.tar.gz

3、 修改hbase conf目录下hbase-site.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<configuration>
<property>
<name>hbase.rootdir</name>
<value>hdfs://192.168.2.100:9000/hbase</value>
注意别忘了端口号,URL要和hadoop core-site.xml保持一致
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/soft/zkdata</value>
</property>注意/soft/zkdata要有读写权限
</configuration>

4、 修改conf 下的 hbase-env.sh

export JAVA_HOME=/soft/jdk1.8.0_171

export HBASE_CLASSPATH=$HADOOP_HOME/conf

export HBASE_MANAGES_ZK=true 这个值默认是true的,作用是让HBase启动的时候同时也启动zookeeper.

5、 启动hbase ./start-hbase.sh,执行JPS会看到如下进程。

[root@localhost bin]# jps

20979 DataNode

21315 ResourceManager

22180 HRegionServer

21605 NodeManager

21158 SecondaryNameNode

21961 HQuorumPeer

23116 Jps

22060 HMaster

20845 NameNode

6、 启动Hbase shell, bin/hbase shell,执行命令测试下Hbase shell能否正常工作。

hbase(main):001:0> list “test”

TABLE

test

1 row(s) in 0.3870 seconds

=> [“test”]

遇到的雷

本地SPARK连HIVE,需要把虚拟机中 hive_home/conf/hive-site.xml 复制到spark_home/conf下一份,并且需要把mysql驱动放到$spark_home/jars中

需要把虚拟机中的MYSQL (hive元数据库)权限调成任何IP都能访问。GRANT ALL PRIVILEGES ON . TO ‘root‘@’%’ IDENTIFIED BY ‘123456’; 执行flush privileges;刷新权限。

如果遇到这个错误。 Hive Schema version 1.2.0 does not match metastore’s schema version 2.1.0 Metastore is not upgraded or corrupt。 需要把hive-site.xml设置成hive.metastore.schema.verification = false

hive.exec.local.scratchdir,hive.querylog.location,hive.server2.logging.operation.log.location 都配置成 /tmp/root。之前放到apache-hive目录下,会报错。

ElasticSearch-hadoop 6.2.3 匹配es 6.x ;ElasticSearch-hadoop 5.2.3 匹配 es 5.x

mac 改成静态IP https://blog.csdn.net/zhishengqianjun/article/details/77046796

本地调用Hbase API的时候,需要执行以下三个步骤,不然就会找不到regionServer反复重试,照成阻塞。

1、 配置Linux的hostname :vim /etc/sysconfig/network

NETWORKING=yes

HOSTNAME=master

2、 配置Linux的hosts,映射ip的hostname的关系

127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4

::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

193.168.2.100 master

3、配置访问windows的hosts,192.168.2.100 master

mac下安装ElasticSearch

特别简单,自行google

brew services restart elasticsearch

brew services restart kibana

elasticsearch –version 观察es版本

安装目录: /usr/local/Cellar/elasticsearch/6.2.4

brew info elasticsearch 可以查看elasticsearch的详情。很实用的命令。

Mac下vmware 静态IP

虚拟机默认是动态分配IP,一旦IP变更,大数据的环境会有问题。静态IP是必须要解决的问题。

https://blog.csdn.net/zhishengqianjun/article/details/77046796

照着这个帖子,一步一步来。稳稳的。

Idea中执行spark sql

img

需要把spark_home/conf下的hive-site.xml复制到resource目录下。

另外,如果pom中没有mysql jar包的话,需要加入。

将hdfs 文件关联到hive上

1、 首先在虚拟机hive上,创建好表结构

2、 登录线上的hive,找到hive对应的HDFS文件,copy一个part下来。

3、 传到虚拟机的hdfs上

4、执行ALTER TABLE xxx ADD PARTITION (dt=’2013-02-28’)
LOCATION ‘/wsy’; (如果是lzo压缩过的文件,执行lzop -dv 001008_0.lzo 解压缩,不然hive读的时候会乱码)

5、 大功告成。

Hbase系列5-javademo

基于Hadoop2.7.1 + Hbase 1.2.1 + HbaseClientAPI 1.2.8

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package com.jd.union.spark

import java.io.IOException
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util
import java.util._

import org.apache.hadoop.hbase._
import org.apache.hadoop.hbase.client.{ConnectionFactory, HBaseAdmin, _}
import org.apache.hadoop.hbase.util.Bytes

object SkuDataToHBaseJob extends Serializable {
val dateFormat = new SimpleDateFormat("yyyyMMdd")

var conf = HBaseConfiguration.create()
conf.set("hbase.zookeeper.quorum", "192.168.2.100")
conf.set("hbase.zookeeper.property.clientPort", "2181")
val conn = ConnectionFactory.createConnection(conf)


def main(args: Array[String]): Unit = {
val skuId = "129399424324"
val orKey = dateFormat.format(new Date()) + "$" + skuId
//todo 取前四位 + skuId + date 当做rowkey,避免热点数据
val rowkey = md5(orKey)
val data: String = " {\"abParam\":\"nature\",\"abVersion\":\"comm\",\"adowner\":\"p_10026704\",\"bestCouponId\":\"92737319\",\"brandCode\":382820,\"brandName\":\"谷邦(GUBANG)\",\"cid1\":1620,\"cid1Name\":\"家居日用\",\"cid2\":13780,\"cid2Name\":\"收纳用品\",\"cid3\":13785,\"cid3Name\":\"收纳架/篮\",\"color\":\"2个装北欧粉\",\"comments\":11,\"couponId\":0,\"couponLink\":\"\",\"createTime\":\"2019-01-22T05:44:42.969Z\",\"deliveryType\":0,\"endTime\":\"2999-01-01 23:59:59\",\"goodComments\":11,\"goodCommentsShare\":100,\"hasCoupon\":1,\"imageUrl\":\"jfs/t1/10013/33/7229/24225/5c286dfaEacd0c6a2/f16290038d59eea6.jpg\",\"imgList\":\"jfs/t1/8948/19/11086/256803/5c286df9E594f189f/5a2af0eeda043878.jpg|jfs/t1/23576/23/3598/256769/5c286df9E1e32d799/62ced8312a144260.jpg|jfs/t1/10013/33/7229/24225/5c286dfaEacd0c6a2/f16290038d59eea6.jpg|jfs/t1/26566/34/4201/410351/5c2f5aa6Eb441c098/fc86afded2d6334d.png|jfs/t1/20665/7/3549/279989/5c286dfaE5a253816/1dfab92f62045e00.jpg|jfs/t1/20297/14/3513/296706/5c286df4Efecc5d55/971d9dad36ea6788.jpg\",\"inOrderComm30Days\":129.07,\"inOrderComm30DaysSku\":84.35,\"inOrderCount30Days\":58,\"inOrderCount30DaysSku\":36,\"isCare\":0,\"isHot\":1,\"isLock\":0,\"isNew\":0,\"isOversea\":0,\"isPinGou\":1,\"isSeckill\":0,\"isStuPrice\":0,\"itemTag\":0,\"lowestPrice\":9.90,\"majorSuppBrevityCode\":\"\",\"orientationFlag\":0,\"owner\":\"p\",\"pcCommission\":5.97,\"pcCommissionShare\":30.0,\"pcPrice\":19.90,\"pid\":40520301670,\"pingouActiveId\":15461534722120,\"pingouEnd\":\"2037-08-16 09:30:48\",\"pingouPrice\":9.90,\"pingouStart\":\"2018-12-30 15:05:02\",\"pingouTmCount\":2,\"planId\":1507475989,\"qualityScore\":0.0631,\"qualityScoreNew\":0.4147,\"rfId\":0,\"ruleType\":7,\"seckillOriPrice\":0.0,\"seckillPrice\":0.0,\"shelvesTm\":\"2018-12-30 15:04:32.0\",\"shopId\":863648,\"size\":\"\",\"skuId\":40520301670,\"skuName\":\"谷邦-两个装卫生间牙刷架壁挂式免打孔浴室置物吸盘梳子筒座牙膏杯吸壁收纳盒 2个装北欧粉\",\"startTime\":\"2019-01-11 00:00:00\",\"stuPrice\":0.0,\"themeFlag\":2,\"unionCoupon\":[{\"batchId\":92737319,\"beginTime\":\"2019-01-10 14:22:00\",\"couponKind\":3,\"couponType\":1,\"createTime\":\"2019-01-18T09:50:26.589Z\",\"createUser\":\"lisen43\",\"discount\":8.0,\"endTime\":\"2019-01-31 14:23:00\",\"expireType\":5,\"key\":\"3172a45b3f8242d2ba92141a7c26b66d\",\"link\":\"http://coupon.m.jd.com/coupons/show.action?key=3172a45b3f8242d2ba92141a7c26b66d&roleId=17184533&to=mall.jd.com/index-863648.html\",\"ownerType\":\"92,95,444,498\",\"platformType\":0,\"quota\":17.0,\"remainCnt\":19965,\"source\":3,\"updateTime\":\"2019-01-18T09:50:26.589Z\",\"useEndTime\":\"2019-01-31 23:59:59\",\"useStartTime\":\"2019-01-10 00:00:00\",\"venderId\":10026704}],\"updateTime\":\"2019-01-22T15:01:31.194Z\",\"venderName\":\"谷邦家居拼购店\",\"vid\":10026704,\"wareId\":12567523910,\"wlCommission\":5.97,\"wlCommissionShare\":30.0,\"wlPrice\":19.90}"
val tableName = "union_search_sku"
try {
addData(rowkey, tableName, "cf", "value", data)
get(rowkey, tableName)
println("scan----------------")
scan(tableName)
//createTable("admin_create")
//del(tableName, "20190909$2")
println("创建表成功")
} catch {
case e: Exception => {
e.printStackTrace()
null
}
} finally {
}
}


def md5(source: String): String = {
val sb = new StringBuffer(32)
try {
val md = MessageDigest.getInstance("MD5")
val bytes = md.digest(source.getBytes("utf-8"))

for (i <- 0 to bytes.length) {
sb.append(Integer.toHexString((bytes.apply(i) & 0xFF) | 0x100).toLowerCase().substring(1, 3))
}
} catch {
case e: Exception => null
}
sb.toString()
}

@throws[IOException]
def addData(rowKey: String, tableName: String, cf: String, column: String, value: String): Unit = {
val put = new Put(Bytes.toBytes(rowKey))
println("开始创建table")
val table = conn.getTable(TableName.valueOf(tableName))
println("创建结束")
// 获取表
put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(column), Bytes.toBytes(value))
table.put(put)
System.out.println("add data Success!")
}

@throws[IOException]
def get(rowKey: String, tableName: String): Unit = {
val get = new Get(Bytes.toBytes(rowKey))
val table = conn.getTable(TableName.valueOf(tableName))
val result: Result = table.get(get)
val it = result.listCells().iterator()
while (it.hasNext) {
val cell: Cell = it.next()
cell.getValueArray
println("value:" + Bytes.toString(cell.getValueArray, cell.getValueOffset, cell.getValueLength))
println("column familly:" + Bytes.toString(cell.getValueArray, cell.getFamilyOffset, cell.getFamilyLength))
println("Qualifier:" + Bytes.toString(cell.getValueArray, cell.getQualifierOffset, cell.getQualifierLength))
}
}

def scan(tableName: String): Unit = {
val scan = new Scan()
var rs: ResultScanner = null
val table = conn.getTable(TableName.valueOf(tableName))
try {
rs = table.getScanner(scan)
val it = rs.iterator()
while (it.hasNext) {
val cellIt = it.next().listCells().iterator()
while (cellIt.hasNext) {
val kv = cellIt.next()
println("value:" + Bytes.toString(kv.getValueArray, kv.getValueOffset, kv.getValueLength))
println("column familly:" + Bytes.toString(kv.getValueArray, kv.getFamilyOffset, kv.getFamilyLength))
println("Qualifier:" + Bytes.toString(kv.getValueArray, kv.getQualifierOffset, kv.getQualifierLength))
System.out.println("timestamp:" + kv.getTimestamp)
System.out.println("-------------------------------------------")
}

}
} finally rs.close()

}

def createTable(tableName: String): Unit = {
val admin = conn.getAdmin.asInstanceOf[HBaseAdmin]

val tn = TableName.valueOf(tableName)
val descriptor: HTableDescriptor = new HTableDescriptor(tn)
val cf = new HColumnDescriptor("cf")
descriptor.addFamily(cf)
admin.createTable(descriptor)
admin.close()
}

def del(tableName: String, rowkey: String): Unit = {
val table = conn.getTable(TableName.valueOf(tableName))
val del = new Delete(Bytes.toBytes(rowkey))
table.delete(del)

table.close()
println("删除成功")
}

/**
* 根据时间戳,删除数据
* @param tableName
* @param minTime
* @param maxTime
*/
def deleteTimeRange(tableName: String, minTime: Long, maxTime: Long) {

var table = conn.getTable(TableName.valueOf(tableName))
try {
val scan = new Scan()
scan.setTimeRange(minTime, maxTime)

val rs = table.getScanner(scan)

val list = getDeleteList(rs)
if (list.size() > 0) {
table.delete(list)
}
} catch {
case e: Exception => e.printStackTrace()
} finally {
table.close()
}
}

def getDeleteList(rs: ResultScanner): util.List[Delete] = {

val list = new util.ArrayList[Delete]()
try {

for (r: Result <- rs) {
val d = new Delete(r.getRow())
list.add(d)
}
} finally {
rs.close()
}
list
}

}