spark常见操作系列(3)--spark读写hbase(1)

spark读写hbase,先写一下hbase的常用操作方式.

hbase建表:

create 'hbase_test_table', 'info', {NAME=>'info', SPLITALGO => 'HexStringSplit', REPLICATION_SCOPE =>0}, SPLITS => ['S0','S1','S2', 'S3', 'S4','S5','S6','S7','S8','S9']

建表提示:

(1)
通过

describe ‘hbase_test_table’

可查看该表所有默认属性.实际中发现,有些属性不能同时设置,比如设置:

REPLICATION_SCOPE =>1

,则会使某些属性无效,比如我建立2个column family.

(2)
一般加上压缩属性,SPLITALGO => ‘HexStringSplit’

(3)
预分区要写明每个分区的名称,如写成NUMREGIONS => 100 实际创建的分区只有一个.
随着该分区装载的数据增多,默认达到256M的时候,hbase会自动生成一个新的分区,如果数据的rowkey是有序列的,则写入会继续写入最大rowkey所在的分区,也就是新的分区.旧的分区不会继续写入,这个会造成:

  • hbase自动生成分区,也就是split 操作时,会有一些时间消耗,写数据时,会有效率降低的情况;
  • 一般一个分区一段时间才会达到生成新分区阈值,假设一个月.当然我可以把分区设大一点,可以设置hbase.hregion.max.filesize
    参数.新的分区继续写数据,旧的分区不再写入数据,这种写法,在查询的时候,比如某时间段查询,结果数据存在某几个分区可能性较大;不仅如此,随便某个条件查询,结果集中在某几个分区的几率会增大,
    这个会导致分区查询IO 的不均. 另一方面, 有资料说,这个新分区阈值跟hbase.hregion.max.filesize
    参数并不是同一个参数,可能是小于filesize ,这样,
    继续写入,会出现旧的分区不会写满,每个分区所写入的数据大小都只能是这个阈值.是这样吗?本人没有实际测试过.
    除了通过hbase.hregion.max.filesize 参数来预分区,还有通过rowkey的方式. 设置splitKey
    ,可以在某个分区生成临界splitKey
    后,然后程序来控制新分区的建立.不过这种方式依赖于rowkey的设计,而rowkey设计不确定性较高,个人感觉可操作性不高.
    这让我想起手动建预分区,长远来看,也不是太可靠,有没有在代码中规划分区和表的.这个对我们也是个难题.
  • 分区命名是否需要意义?
    如果是hbase自动生成的分区,自然意义不明确.手工预设分区,以序列命名,表面上比较直观.一般在查询中,并没有设置查询哪个分区的必要,分区命名也无意义.但如把分区信息写入rowkey,数据分类有那么一丁点作用.
  • 数据倾斜的情况.
    hbase默认某段时间,只有一个分区会写入,这个也会有数据倾斜.另外,在实际中,我们还遇到数据写入节点不均,这个也是数据倾斜.这个多数情况跟rowkey设计有关.当然,节点性能不均,节点网络IO不均,配置不均也会有这种情况.

rowkey设计

rowkey设计,我们把几个关键字段拼接,生成一个新的字段,为了使rowkey可用,好用,我们做了以下步骤:

(1)
新的字段必须唯一;

(2)
rowkey尽量简短,意义紧凑,比如时间:2018/03/01 17:54, 转换成1803011754;

(3)
每一行rowkey等长,比如每一行rowkey都是16个字母长度;

(4)
对于数据倾斜问题,一开始我们参照网络资料,使用

new_rowkey=hash(rowkey)

这个直接有一个问题不能忽视,就是rowkey查询问题,rowkey命名变成无意义了,然后改成

new_rowkey=hash(rowkey)+rowkey

方式,这种是比较好的方法.可是严谨一点的话,hash比较长,其它散列也如此,为了使rowkey尽量短,我们改进了以上方式,

new_rowkey=hash(rowkey)%分区数+rowkey 

比如我们建表,预设10个分区, 那么便是:

new_rowkey=hash(rowkey)%10+rowkey 

(5)

hbase java api 对rowkey主要有scan,filter,get查询.scan可以查询(startRowKey,endRowKey).当然,我们需要一些匹配查询,则会用到filter查询.

filter主要有5种方式:全部匹配;前缀匹配;子串匹配;后缀匹配;正则匹配. 根据这些匹配,我们需要调整rowkey中每个字段的顺序,来适应查询需求.

(6)
实际上,以上的scan也好,filter匹配也好,我们感觉都不实用.我们也用了get方式,尽管目前也在用,但是本人感觉还是不能满足应用需求. 其中get的查询效率,是本人还存有疑惑的地方,下文会讲到.还有get方式对条件查询无解,所以,需要引进新的组件,比较可行的有solr,elasticsearch 充当存储二级索引的仓库.

rowkey查询

上面说的hbase的一些东西比较虚,有几点本人也没有实际测试过.大部分本人都做过,问题是:怎么辨识何为好的思路?对于rowkey相关的,本人存疑的几点:

问题(1)
查询hbase,要连hbase client,hbase client 是要查一次,连一次?还是长连接的?

问题(2)
scan有

scan.setCaching(10000)

scan.setCacheBlocks(true)

等设定.setCaching ,个人感觉不够用.hbase 默认是在内存里面放一块数据用来读取,所以读取效率比较高,可是,其余大部分数据还是在硬盘中,这个内存数据块的设定和意义,待清晰研究.

问题(3)
不使用其它索引组件,在scan 或filter 数据时,遇到效率瓶颈.好像速度就那样,流程似乎也是按照hbase官方流程,看了一些hbase IO测试表格,有的比较快;有的非常快,让我对hbase实际平均效率存有疑惑,需要作分析.

问题(4)
scan 方式比较慢,filter方式也比较慢,它们慢在哪里?

以上问题分析:

(1)对于问题(1),hbase client连接,一般代码是这样写的:

val conf = HBaseConfiguration.create()
conf.set("hbase.zookeeper.property.clientPort", "2181")
conf.set("hbase.zookeeper.quorum", "master")
//Connection 的创建是个重量级的工作,线程安全,是操作hbase的入口
val conn = ConnectionFactory.createConnection(conf)
//从Connection获得 Admin 对象(相当于以前的 HAdmin)
val admin = conn.getAdmin
//本例将操作的表名
val userTable = TableName.valueOf("user")
//创建 user 表
val tableDescr = new HTableDescriptor(userTable)
tableDescr.addFamily(new HColumnDescriptor("basic".getBytes))
println("Creating table `user`. ")
if(admin.tableExists(userTable)) {
  admin.disableTable(userTable)
  admin.deleteTable(userTable)
}
admin.createTable(tableDescr)
println("Done!")
try{
  //获取 user 表
  val table = conn.getTable(userTable)
  //...
  }finally {
  conn.close()
}

以及spark 连接hbase:

/*把RDD中的列写进hbase
 sc:SparkContext
 tableName: 表名
 servers: 集群服务器别名,比如Master
 indataRDD:包含列数据的rdd
 columnFamily:列族
 newColumn:新列的命名
 2017-09-04
 */
def saveNewColumnToHbase(sc:SparkContext, tableName:String, servers:String, indataRDD:RDD[Row], columnFamily:String, newColumn : String) : Unit = {
  sc.hadoopConfiguration.set("hbase.zookeeper.quorum",servers)
  sc.hadoopConfiguration.set("hbase.zookeeper.property.clientPort", "2181")
  sc.hadoopConfiguration.set("zookeeper.znode.parent","/hbase")
  sc.hadoopConfiguration.set(TableOutputFormat.OUTPUT_TABLE, tableName)
  val job = new Job(sc.hadoopConfiguration)
  job.setOutputKeyClass(classOf[ImmutableBytesWritable])
  job.setOutputValueClass(classOf[Result])
  job.setOutputFormatClass(classOf[TableOutputFormat[ImmutableBytesWritable]])


  val rdd = indataRDD.map{
    arr=>{
     //
    }}
  rdd.cache()
  rdd.saveAsNewAPIHadoopDataset(job.getConfiguration())
}
  1. 连接需要zookeeper支持,且zookeeper节点数为奇数;
  2. 每次连接需要的必要参数:
    sc:SparkContext
    tableName: 表名
    servers: 集群服务器别名,比如Master
    columnFamily:列族
  3. 虽然推测hbase client连接应该是socket,但在写代码中,用户每一次连接请求操作都需要以上连接步骤,2个用户分别请求了一次,这2个请求,需要生成2个client来分别处理吗?
    应该不是这样的.查询hbase配置文件,发现有rpc请求配置,rpc请求是处理什么的?实际上,hbase的get/put/delete等操作都需要rpc.在一次查询周期内,client会发出多次rpc请求,每个rpc请求有一个请求周期,hbase.rpc.timeout 用来配置rpc的超时时间.每次rpc请求,会向server请求一定数量的数据,类似sql的limit 操作, client发起查询等请求,直至server将数据全部返回,中间有多个rpc请求,将请求的数据一批一批的返回.
    单次rpc请求是发生在client发起的某次数据操作请求范围内,它的请求次数可能由请求查询的数据量决定,它并不是这次本人要说的重点. 这里,某次数据操作请求的超时时间配置是 hbase.client.operation.timeout , 它跟client连接套接字有关,如果是用户请求密集的应用场景,我们需要考虑这个.
    查询hbase配置文件,发现有个ipc配置. 感觉这个配置牵涉的关联比较广.一般socket新开一个client,会创建一个线程用于这个client通信, 多个client就是多个线程.如果线程比较多,可以使用线程池来有效利用线程. hbase 的socket也是如此吗?
    不完全这样.ipc有个配置:
    <property>
          <name>hbase.client.ipc.pool.typename>
          <value>RoundRobinPoolvalue>
       property>
       <property>
          <name>hbase.client.ipc.pool.sizename>
          <value>1value>
       property>

这里配置显然也有一个线程池管理ipc请求.ipc.pool.size 默认是1.
意思是,对于每个client连接, 会创建1个线程用于与regionServer通信.
但这里,ipc.pool.size配多大,ipc.pool 内部如何工作的?并不清楚.查找资料,发现hbase论坛有一段话:

In general, sharing sockets across multiple client threads is a good idea, but limiting the number of such sockets to one may be overly restrictive for certain cases. Here, we propose a way of defining multiple sockets per server endpoint, access to which may be managed through either a load-balancing or thread-local pool. To that end, we define the notion of a SharedMap, which maps a key to a resource pool, and supports both of those pool types. Specifically, we will apply that map in the HBaseClient, to associate multiple connection threads with each server endpoint (denoted by a connection id).

Currently, the SharedMap supports the following types of pools:

- A ThreadLocalPool, which represents a pool that builds on the ThreadLocal class. It essentially binds the resource to the thread from which it is accessed.
- A ReusablePool, which represents a pool that builds on the LinkedList class. It essentially allows resources to be checked out, at which point it is (temporarily) removed from the pool. When the resource is no longer required, it should be returned to the pool in order to be reused.
- A RoundRobinPool, which represents a pool that stores its resources in an ArrayList. It load-balances access to its resources by returning a different resource every time a given key is looked up.

To control the type and size of the connection pools, we give the user a couple of parameters (viz. "hbase.client.ipc.pool.type" and "hbase.client.ipc.pool.size"). In case the size of the pool is set to a non-zero positive number, that is used to cap the number of resources that a pool may contain for any given key. A size of Integer#MAX_VALUE is interpreted to mean an unbounded pool.

大致意思是,多个client共享一个套接字在hbase这里不适用,尽管这是个好的思路.他们创建了一个SharedMap, 以密钥的方式管理线程池,应该每个线程分配一个特定密钥.这种SharedMap支持3种线程池,分别是:
ThreadLocalPool,应该是java那一套;
ReusablePool, 不清楚;
RoundRobinPool,就是上面配置里的那一种.
实际效果,感觉应该是对线程池的优化,提高线程池的效率和资源复用率.

直到这里,本人有2点总结:
1.hbase socket 使用了线程池管理client线程;
2.使用了一种优化技术,优化了线程池.
是这样吗?

这里,ipc.pool.size 默认是1,可能以为1个就够了. 当然,我可以改成更大值,比如5,就是一个client请求,regionServer开5个线程来处理通信. 可是,具体效果如何,本人还没有针对尝试,并不清楚.

现在,本人知道,hbbase的socket 确实是一次put/get/delete操作请求,就创建一个client来处理请求,内部使用优化过的线程池管理client. 默认情况,一个用户请求,regionserver会创建一个线程.
那么,用户密集型请求应用场景,以上办法是否能有效应对? 感觉这个还需要讨论.


说了问题(1),再分析下
问题(2):
hbase读取数据的方式是怎样的?
请看:

spark常见操作系列(3)–spark读写hbase(2)

参考资料:
https://jaskiratbhatiablog.wordpress.com/2016/10/06/hbase-client-tuning/
https://issues.apache.org/jira/browse/HBASE-2939
https://blog.csdn.net/gingerredjade/article/details/63703479

你可能感兴趣的:(大数据,spark,hbase)