HBaseClient源码分析

本文原作者Jasmine_Du,趋势科技中国研发中心SPN研发团队成员,SPN团队在Hadoop/HBase研究上积累了很多心得,他们的团队Blog是Hadoop/HBase学习者一定要去细细研读的地方。这篇文章比较详细的讲述了HBase Client的主要接口和内部实现。

————————————– 毫无理由的分割线 ———————————

1 Client端主要类和接口

1.1 HConnection:

管理连接的接口,定位server端 , master端的位置,建立连接,规定调用server端的接口。HConnection真正的实现是HConnectionManager.TableServers

主要方法:

processBatchOfDeletes:批量插入,删除操作

getRegionServerWithRetries:首先连接所操作数据所属的region。其次,该函数参数需要一个 servercallable的对象,通过该对象回调servercallable中的call方法,在call()方法中会直接调用regionserver端接口。例如put,delete,scan操作。

getHRegionConnection:根据特定的地址建立一个到regionserver的连接。通过HBaseRPC类得到一个proxy实例HRegionInterface。HRegionInterface是在RegionServer端实现的。其中有get,put,delete,scan,rowlock等操作。

locateRegion:查找某张表内某行的region信息。

getMaster:获得一个到masterserver的连接,通过代理类HBaseRPC得到一个HMasterInterface实例。Client调用该接口和Master通信。

getZooKeeperWrapper:获得该连接可以使用的一个ZooKeeperWrapper,进而或许ROOT,META表信息,session信息等。

1.2 Htable

Htable主要提供表内的操作,put,delete,get,scan等操作

scannerTimeout: scan的超时时间,如果超过这个时间没有返回则抛出异常

configuration:配置文件对象

ArrayList writeBuffer:调用put()方法时,会先加到这个writerBuffer里,同时增加统计writerBuffer的大小,大小超出writeBufferSize ,或者设置了autoFlush标志,那么马上提交put操作。writeBufferSize在配置文件中设置。

autoFlush :自动提交标记,如果没有设置会缓存部分put。

scannerCaching:设置一次扫描多少条记录,在配置文件中设置。在scan操作时会用来实例化clientscanner的caching值,当然也可以通过Scan对象来设置。

maxScannerResultSize:这个值用于设置scan的时候最大能扫描多长的记录,如果超出这个长度,那么需要终止此次扫描,下次扫描再继续。

put/delete/get:增 删 查询

class ClientScanner:这个内部类主要用于Scan操作,稍后详细分解

1.3 HBaseAdmin

和表的上线下线相关的操作,创建,删除表。对表或者是column进行增删改。查询集群工作状态。

HConnection connection:一个HConnection引用,用tableserver实例化。

HMasterInterface master:和master通信的接口,最终通过RPC机制获得一个代理对象来实例化

createTable/deleteTable/modifyTable

createColumn/deleteColumn/modifyColumn

spilt:切分表

compact:合并表

getClusterStatus:查询群集状态

2 主要API内部实现

2 .1 Add/Delete

批量的添加或删除操作内部实现机制基本相同,最后调用RegionServer的接口不同。当提交一个批处理Add/Delete请求的时候,这一批数据可能分布在不同的RegionServer上,所以这里主要的操作就是找到相应的regionserver,调用server上的接口操作数据。

下面以删除数据为例说明一下这一过程:

(1) 客户端接口

?
1
2
3
4
5
6
7
8
9
</p>
<p>ArrayList<Delete> d = new ArrayList<Delete>();</p>
<p> Delete d1 = new Delete(“row1″.getBytes());</p>
<p> Delete d2 = new Delete(“row2″.getBytes());</p>
<p> d.add(d1);</p>
<p> d.add(d2);</p>
<p> ……</p>
<p>table.delete(d);</p>
<p>

(2) 客户端处理

客户端接受到delete请求以后调用HConnection的接口:

public int processBatchOfDeletes(final ArrayList list, final byte[] tableName)

该接口在HConnectionManager类的内部类TableServers中有具体实现。

processBatchOfDeletes创建一个Batch()类匿名对象,实现其中的doCall(),等待回调
Proces()遍历list中所有数据,重复下面所有调用。定位数据所属的region,对于一个region下面的数据调用doCall()方法
doCall()创建一个ServerCallable的匿名对象,实现call()方法,做为参数调用getRegionServerWithRetries()
getRegionServerWithRetries()连接region,获取一个PRC的代理对象实例化 HRegionserverInterface 对象server回调call()
Call() 
Server.delete()通过RPC的代理对象透明的调用delete()操作

Delete操作的主要流程概括为:定位待发送的list中的数据所属的region,连接该regionserver并返回一个代理类对象,通过该对象执行delete操作。client的函数调用栈如表所示Add和Delete的主要区别就于Call()方法的实现不同,一个调用server的add,一个调用delete。其余的过程完全相同。

在上面的调用栈里有两个关键的过程

(1) 如何根据rowkey定位region?

参照blog:http://www.spnguru.com/?p=127

(2)list里的数据属于不同的region,相同的region进行一次doCall()调用,这一过程是如何实现的?这个过程在process里实现:

?
1
2
3
4
5
6
7
8
9
10
11
</p>
<p>process(){</p>
<p> currentRegion = 第一条记录的region;</p>
<p> for (i= 0 ; i< list.length; i++){</p>
<p> //添加list[i]到currentlist;</p>
<p> if (currentregion != list[i+ 1 ]的region){</p>
<p> 回调docall(); //对currentlist的数据调用server端接口,delete</p>
<p> currentregion = list[i+ 1 ]的region</p>
<p> }</p>
<p>}</p>
<p>

2.2 scan

2.2.1 客户端接口

?
1
2
3
4
5
6
7
8
9
10
11
</p>
<p>Scan s = new Scan();</p>
<p> s.setMaxVersions();</p>
<p> ResultScanner ss = table.getScanner(s);</p>
<p> for (Result r:ss){</p>
<p> System.out.println( new String(r.getRow()));</p>
<p> for (KeyValue kv:r.raw()){</p>
<p> System.out.println( new String(kv.getValue()));</p>
<p> }</p>
<p> }</p>
<p>

2.2.2 客户端主要函数

Scan操作涉及两个非常重要的类ClientScanner和ScanCallable,前者实现了迭代器Iterator,实现了迭代扫描方法。后者和其他的Callable一样需要实现一个call()函数用于和server交互时,回调。这里的call()有三种状态实现和server不同的交互。

(1) ClientScanner

Scan scan:scan对象,可以设置要扫描的起止rowkey,filter等等属性。

boolean closed:

HRegionInfo currentRegion:记录当前region,因为扫描过程可能跨region

ScannerCallable callable:

LinkedList cache:缓存扫描结果

int caching:设置一次扫描多少条

nextScanner(): 根据扫描的startkey确定开始扫描的位置,初始化和next()迭代扫描的时候都要调用这个判断下究竟从哪里开始扫描了,然后通过调用函数:getRegionServerWithRetries(callable),回调callable里边的call函数去服务器端openScanner(),为读取数据做准备。注意:如果是next()调用该函数时候,首先第一步要关闭上次打开的scanner().

next():真正读取数据的地方。首先会判断缓存cache里有没有,如果有就不用去服务器去了。如果没有,只要动用服务器端了,依然是这个接口getRegionServerWithRetries(callable),callable的回调函数call()会调用server端的next()来取得数据, 这个call()和上面那个不一样吗?为什么实现不一样的功能,稍后分解。

(2) ScanCallable

scannerId :记录服务器段scanner的打开状态。

Closed:是否要关闭scanner

Call():后续分解。

2.2.3 Scan 执行过程

一次scan操作的主要过程可描述为:根据起始rowkey先确定要扫描的数据的位置,然后openscanner(),开始读取,读取预设数量的Result[]出来存在缓冲区里,然后记住最后一次读取的rowkey,下次执行next()操作时,先关闭上次打开的scanner,然后根据最后记录的rowkey再次去确定要扫描数据的位置,openscanner().. . 如此循环之道读出所有需要的数据。当然这期间可能要重新定位regionserver,在一个regionserver上读到关于该表的最后一条记录时,记住这个rowkey然后返回,下一轮读还是会从这个rowkey开始去寻找新的regionserver,然后openscanner(),继续循环。

要扫描的数据可能分布在不同的regionserver上,所以扫描操作的过程可描述为:

(1) 初始化,调用nextScanner()判断起始数据的region

(2) next(),读取数据

(3) 重定位region,调用nextScanner()从上次的endkey开始再定位下次的region

(4) next(),读取数据

(5) 重复步骤3-4,直到读完所有需要的数据。

这些步骤中重要涉及两个函数nextScanner(),next()

next()的实现如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
</p>
<p>next(){</p>
<p> if (cache不为空){</p>
<p> return cache里取一条</p>
<p> }</p>
<p> do {</p>
<p> 连接regionserver;</p>
<p> Call(); //从server读取数据</p>
<p> 加入到cache中;</p>
<p> } while (condition)</p>
<p>}</p>
<p>

Condition有三个条件

(1) remainingResultSize > 0:最大读取长度,配置文件决定

(2) countdown > 0:就是前面的caching,一次读取多少条,没有读完规定数目,可能是因为需要更换regionserver,所以需要重新定位,再读

(3) nextScanner(): 定位下次要读的region,打开一个scanner,如果返回false,说明读完了,不需要再读了

关键是nextScanner()怎么定位的呢?下面看nextScanner()的实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</p>
<p>nextScanner(){</p>
<p> //关闭前面打开的scanner,如果是初始化则不需要</p>
<p> if (callable不为空){</p>
<p> callable.closed= true ;</p>
<p> call() // 去服务器端关闭前面打开的scanner</p>
<p>}</p>
<p>If(currentRegion!= null ){</p>
<p> Starkey = endkey //说明不是初始化,本次接着上次扫描结束的key开始扫描</p>
<p> }</p>
<p> else {</p>
<p> Startkey= scan对象的startkey //初始化时预设值</p>
<p> }</p>
<p> Call() // 去服务器端openscanner</p>
<p> 修改currentRegion为当前打开的region;</p>
<p>}</p>
<p>

2.2.4 Call()函数的三次调用。

前面讲到这个调用服务器程序的专用函数getRegionServerWithRetries(callable)会做两件事,一是连接regionserver,二是回调callable的call()函数,在上一小节call()出现了三次。这个call()干了不少事,在这里它是有状态的。在三种状态下有三种功能。功能根据它的状态为scannerId,closed状态来决定

(1) close() ,这个是nextscanner()函数调用的,每次调用next()之前,都需要关闭以前打开的。因为之前打开过server端的scanner,所以scannerId!=-1L,调用之前Callable设置了closed=true;

(2) openScanner(),这个是nextscanner()函数调用的,next()之前不仅要关闭以前打开的,还有打开本次需要打开的,本次需要根据新的rowkey进行新一轮的扫描之前需要进行打开服务器的scanner,由于第一步已经关闭了之前的,所以scannerId=-1 。注意如果是next调用的话会在while循环的条件处来调用,如果nextscanner()返回了false,那么扫描就结束了。

(3)server.next(),从regionserver读取数据。前面定位好了,现在只需要读了。

2.3 解析Result

Result主要有下面几个成员:

KeyValue [] kvs: 一个result获得的关于一行的所有信息,一条记录是一个keyValue

NavigableMap<byte[], byte[]="" navigablemap<long,="" navigablemap>> familyMap:对结果构造一个Map视图。

byte[] row :rowkey

ImmutableBytesWritable bytes:一个result所有keyValue的字节流

keyValue结构如图所示。

表里的一个记录组成一个keyValue对,附加一个key和value的长度,所有的记录都以这样的形式组织称一个字节流,这个流就存在bytes里边,通过调用函数readFields()从这个流里边读取key和value的值存到KeyValue[]类型的kvs里边。Row代表这行的rowkey。kvs里边包含了所有的值,各个column的各个版本。 familyMap是对result结果里的keyvalues[]数组做一个映射,抽象出一个新的视图,便于访问和操作。根据这个视图可以想象一个表的结构,这个视图抽象出三层map,对应于这个结构<family, value="" ,="" timestamp="" <="" >>,在Result.class里有个函数getMap, 就是用于构建这个map的。遍历keyvalues[]数组里的所有数据,进行切割,把数据放到对应层次的map里。通过这个map可以方便的取到某个family下面的某个版本的column。

3 RPC 机制

Client端访问RegionServer端是通过hadoop自己实现的RPC机制实现的,其原理类似于RMI,主要分为三个步骤。(1)client端访问RPC模块得到一个实例化RegionserverInterface接口的的代理类对象。对应于图中的1和2。(2)client通过代理对象访问代理机制实现的Invoker类。其中的方法invoke()调用一个call()函数建立连接,通过socket建立连接,序列化发送的数据,发送到regionserver。对应于图中的3和4 。(3)HBaseClient会开启一个线程connection,监听regionserver的执行结果,监听到结果后反序列化,还原对象。并回复给client调用端。对应与图中的5和6。三个具体的过程下面详细介绍。

3.1 HregionInterface实例化

Client端最终提交操作put,delete,scan等,都需要通过ServerCallable类中的对象server来调用服务器端的接口。例如:server.put(location.getRegionInfo().getRegionName(), puts)。就像其名字一样,该调用的确调用到了regionserver端,server是怎么实例化的呢?经考证,server的实例化采用了java的反射机制和动态代理。其实现在RPC模块。RPC是hadoop自己实现的序列化传输,远程调用。类似与RMI。

server的类型是HregionInterface,这是client访问regionserver的接口。在Hconnection接口中有个方法getHRegionConnection(),该方法在Tableserver类中有实现,返回一个实例化了的HregionInterface的对象server。该接口该有方法getMaster(),返回的是client和Master通信的接口HmasterInterface。在这里以HregionInterface为例分析其实例过程。在client端server是通过调用接口HBaseRPC.waitForProxy()来实例化的。下面看RPC模块是如何做的。

HregionInterface的实例化采用了动态代理机制。其真正的实现当然是在RegionServer端。RPC实现了一个动态代理类,生成一个动态代理对象返回给client,这个类也实现了HregionInterface接口,在这个代理类的内部又去调用server端的真实实现。client透明调用代理的方法,在它看来就像调用服务器端的方法一样。RPC代理里,把client调用传输过来的方法,参数序列化,通过socket发送到RegionServer端,RegionServer端会反序列化,还原数据,然后调用真正的HregionInterface的实现类,并返回结果。

waitForProxy()方法返回一个VersionedProtocol类型的接口,这个接口是HregionInterface和HmasterInterface的父接口,根据具体环境对其做类型转化便得到相应的对象(调用时候,参数不同,Address)。WaitForProxy()内部调用getProxy()。getProxy()内调用java.lang.reflect.Proxy类方法:Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)该函数需要三个参数:

(1) ClassLoader loader:String serverClassName = conf.get(REGION_SERVER_CLASS, DEFAULT_REGION_SERVER_CLASS);

(2) Interfaces:代理类和真实类都实现的接口,这里指HregionInterface

(3) InvocationHandler h :java动态代理机制要求实现一个类h去实现接口InvocationHandler。代理类会去调用这个类h中的invoke()方法,在该方法会去和被代理的真实类交涉。

Proxy.newProxyInstance()这个方法首先根据参数loader和interface调用方法getProxyClass(loader,interfaces)动态创建了一个代理类$Proxy0,这个$Proxy0实现了HregionInterface接口,继承了Proxy类。然后这个类构造的时候会实例化InvocationHandler 对象,接着,当client调用比如put()时候,会调用到这个代理类的Put(),这个Put()会转而调用InvocationHandler.Invoke()方法。Invoke()就会去和RegionServer端交涉。通过这个调用client端就拿到了一个实例化好的HregionInterface类型的对象。

InvocationHandler的实现类叫做Invoker,这个类里要实现接口里的方法invoke(),代理类的调用都会通过这个方法。invoke(Object proxy, Method method, Object[] args),proxy是动态代理的对象,method是client调用的方法名(比如put,delete),args是函数的参数列表。该方法内部会调用HbaseClient内的方法call(),序列化方法,对象,然后通过socket传输到RegionServer,调用HregionInterface接口的真实实现。

3.2 远程调用,序列化

上节提到方法 HBaseClient.call(Writable param, InetSocketAddressaddr, UserGroupInformation ticket),该方法建立一个到RegionServer的连接,并序列话参数param,通过socket,param的内容通过socket发送到server端。

传入param的是Invocation对象。Invocation类包装了要传送的方法和参数:methodName,parameterClasses,parameters,这个类里边有两个很重要的Map,一个是函数名,参数到字节编码Code的映射,另外一个是相反方向的映射。Invocation类实现了Writeable接口,用于序列化。Writeable接口有两个方法:

(1) write(DataOutput out) 将要传输的函数名,参数写到流中,序列化时候调用。Write方法最终会通过HbaseObjectWritable.writeObject把要传送的方法和参数发到输出流。

(2) readFields(DataInput in),从流中读出数据并实例化对象,用户反序列化的时候调用。

Call()方法首先会根据参数addr建立一个到regionserver的socket连接,通过该连接序列化,发送。申请一个DataOutputBuffer,调用invocation的write方法,把要传输的数据写到这个buffer里,然后flush(),完成序列化。

补充一下,call()方法内部实现了一个类call,因为RPC支持多个远程调用,这个类call 有id标识每次调用, call还有个标识done,用于监听server端返回的状态,如果为true,则表示有结果可以读取。当调用call()方法时,会wait 这个Call对象的状态, 一旦done为true表示server端的执行结果已经返回,可以给调用方返回结果value。

3.3 调用返回 反序列化

call()方法把参数序列化传输到server端,实现远程调用,那么结果是如何返回的呢?在这里设计了一个线程Connection类,这个类专门监听一组远程调用的状态,其中有个Hashtable calls,记录了每个在线的Call,监听他们的状态,一旦有结果返回从socket流中凡序列化,读取执行结果。Connection类的主要成员如下:

ConnectionId remoteId: 标识到一个server的连接

Socket socket: 保存到一个server连接的socket

DataInputStream in /DataOutputStream out: 读写套接口的数据

Hashtable calls: client端当前活跃的调用,保存在此,key是call id,connection线程监听每个call的状态。

setupIOstreams():call()方法做两件事,建立socket连接,发送数据,这个函数便是做第一件事,call()会调用这个方法,建立真正的socket,并且,得到输入输出流对象,用于后来读写数据。

sendParam(Call call): call()方法做的第二件事,调用该接口,序列化,从socket流发送。

Run():线程主要的任务在此,线程监控server端的返回,如果有返回开始读取数据。线程需要执行下面的操作:while (waitForWork()) {

receiveResponse();

}

waitForWork() :如果当前calls列表不为空,也就是只要有远程调用,并且client端还激活,就需要执行下一步操作。

reciveResponse() :从socket流中读出执行结果,首先是call id,根据这个在本地hashtable calls 里查询该call的记录,然后反序列化,把call id对应的远程调用结果读取出来保存在writeble对象call.value里,在这里调用了方法readFields,解序列化。从hashtable里移除这个call,然后标记他的done为true,notify()等待这个done的地方,即call()方法发送的时候,发送的地方知道发送反正,于是可以把返回值call.value返回给client了。

补充一下,client端的调用都是阻塞的,所以调用了call()以后会等待结果返回,而server端处理的时候是非阻塞的。来自client端的请求都放在一个queue里,启动Server的时候会产生一个一堆Hanlder来处理,这些Handler会主动去取一个请求然后执行,Handler的数量有配置问价参数hbase.regionserver.handler.count来确定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值