玩转HBase: Coprocessor Endpoint (2):coprocessorProxy和coprocessorExec的合理运用

前言:

相比HBase,关系型数据库有两大问题:动态横向扩展分布式并行计算。

架设在HDFS上的HBase,在动态横向扩展方面具有先天的优势。

分布式并行计算则需要MapReduce和Coprocessor-Endpoint来实现。

可是玩过Hadoop的童鞋都了解,MapReduce分布式计算框架有一个致命弱点:高延时

很多实时的分布式查询和计算业务中,MapReduce更本无法胜任

在实时计算的业务中,HBase0.92的Coprocessor-Endpoint成为了MapReduce的接班人,从此走上了神坛


----------------------------------------------------------------------------------------------------------------------


Coprocessor(协处理器)是HBase 0.92版后加入的新组件,详情请见:HBase: Coprocessor Introduction

本系列主要探讨Coprocessor的Endpoint方法。

本篇将探讨coprocessorProxy和coprocessorExec的合理运用。


上一篇曾提到过两个方法在功能上的区别,coprocessorProxy是串行方法,coprocessorExec是并行的。

首先看一下并行方法coprocessorExec的官方解释,我简单的翻译一下:

The application code client side performs a batch call. This initiates parallel RPC invocations of the registered dynamic protocol on every target table region. The results of those invocations are returned as they become available. The client library manages this parallel communication on behalf of the application, messy details such as dealing with retries and errors, until all results are returned (or in the event of an unrecoverable error). Then the client library rolls up the responses into a Map and hands it over to the application. If an unrecoverable error occurs, then an exception will be thrown for the application code to catch and take action.

客户端程序代码执行并调用了一个批处理方法(注:如下图的Batch.Call)。这将激活并启动一批并行RPC调用注册的动态协议在每一个Region上(注:下图的Table表)。最终,这个并行的批次调用可以到的一批返回结果(注:返回批次的数量等于被调用的Region数量)。其中那些复杂的逻辑由客户端应用程序调用的Lib库管理(注:这里指的Lib我理解为Coprocessor-Client的后台调用方法库),比如重试和错误的异常处理,直到所有结果(注:或在发生不可恢复的错误)。最后客户端Lib库把结果分装到一个Map<byte[],integer>对象中,并把这个对象交换给应用程序。如果出现一个不可恢复的错误,就会抛出异常。


通过官方的解释,并结合官方模块图,我认为coprocessorExec方法有以下优缺点:

优点:

1、分布式结构清晰:coprocessorExec的服务端(这指的是Regions)和MapReduce的Map方法很相似

2、返回数据效率高:Client接收的返回数据是一次性Return的,不像ResultScanner那样有一个迭代的过程(Endpoint也是可以返回List<Result>的)。

3、服务端运算:利用coprocessorExec运行聚合运算时,Client不会有太大的运算量,大部分的工作都由Region coprocessor完成。

缺点:

1、部署比较麻烦:A、配置hbase-site,需要重启HBase~~  B、alter 'TB', METHOD => 'table_att' 需要disable 数据表~~ C、代码配置参照B

2、个别性能差的Region会影响整个集群Endpoint调用性能

3、一旦有Region出现异常,会导致Endpoint停滞

如何克服以上三个问题,这里不做讨论,以后会单独写一篇。

接下来放上一个实例源码:用户在线时长统计

有一张表User_Login_Log,记录了用户登入登出的日志信息


Row Key Column Family : _0
10000001_20130513000000_1 IP : 10.10.11.12 CT : 1
10000001_20130513001000_2 IP : 10.10.11.12 CT : 1
10000001_20130513001100_1 IP : 10.10.11.12 CT : 2
10000002_20130513000001_1 IP : 10.10.21.12 CT : 1
10000002_20130513001301_2 IP : 10.10.21.12 CT : 1
………………………………… ………………… …....
11000000_20130513000001_1 IP : 10.11.21.12 CT : 3
11000000_20130514110001_2 IP : 10.11.21.12 CT : 3

RowKey分为三段:用户编号,登陆登出时间,类型(1:登入 2:登出)

这张表只有一个Column Family,其中保存了用户登入的IP地址,登陆次数等等。

计算用户的在线时长,只需要RowKey即可,所以Column Family数据不用关心。

这里需要说明一下:为了方便展示,我这里用yyyyMMddHHmmss的数据结构来存放时间数据,事实上最好的设计往往是以TimeStamp来存储日期时间字段。

//计算在线时长的EndPoint代码
public String getOnlineTime(Scan scan,String _stopTime) throws IOException {
		RegionCoprocessorEnvironment environment = (RegionCoprocessorEnvironment) getEnvironment();
		InternalScanner scanner = environment.getRegion().getScanner(scan);
		ArrayList<KeyValue> curVal = new ArrayList<KeyValue>();
		String userID = "";
		String stopTime = _stopTime;
		String LastLoginTime = "";
		String LastType = "";
		StringBuilder Result = new StringBuilder("");
		int OnlineTime = 0;
		while (scanner.next(curVal)) {
			Result r = new Result(curVal);
			String[] RowKey = null;
			for (KeyValue kv : r.list()) {
				RowKey = new String(kv.getRow()).split("_");
			}
			if (userID.equals(""))
				userID = RowKey[0];

			if (userID.equals(RowKey[0])) {
				if (LastLoginTime.equals("")) {
					LastLoginTime = RowKey[1];
				}
				if (RowKey[2].equals("1")) {
					LastLoginTime = RowKey[1];
				} else {
					try {
						OnlineTime += compTimeByMilliSecond(LastLoginTime, RowKey[1]);
					}
					catch (Exception e) {}
				}
			} else {
				if (LastType.equals("1")) {
					try {
						OnlineTime += compTimeByMilliSecond(LastLoginTime, stopTime);
					}
					catch (Exception e) {}
				}
				Result.append(userID);
				Result.append("\t");
				Result.append(OnlineTime);
				OnlineTime = 0;
				LastLoginTime = RowKey[1];
				userID = RowKey[0];
				if (RowKey[2].equals("1")) {
					LastLoginTime = RowKey[1];
				} else {
					try {
						OnlineTime += compTimeByMilliSecond(LastLoginTime, RowKey[1]);
					}
					catch (Exception e) {}
				}
			}
			LastType = RowKey[2];
		}
		return Result.toString();
	}

	public static Date getStringTimeNow(String _Date) throws Exception {
		SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
		Date dateString = new Date();
		dateString = formatter.parse(_Date);
		return dateString;
	}

	public static int compTimeByMilliSecond(String d1, String d2) throws Exception {
		Date dr1 = getStringTimeNow(d1);
		Date dr2 = getStringTimeNow(d2);
		return (int) ((dr2.getTime() - dr1.getTime()));
	}

在getOnlineTime方法中,除了Scan对象外,我还加上了一个StopTime,其实这个StopTime是为了控制时间最后期限。当一个玩家在线时,Log中是不会有登出记录的,在实际应用中这个StopTime往往就是计算的当前时间。

为了方便输出,我用了一个StringBuilder对象,把每个Region上的计算数据以字符串的形式返回,虽然山寨了点,但好调试。

//调用方法
public static void main(String[] args) throws IOException {

		Configuration conf = HBaseConfiguration.create();
		conf.set("hbase.zookeeper.quorum", "slaver04.nj.hadoop");
		conf.set("hbase.zookeeper.property.clientPort", "2181");
		HTable table = new HTable(conf, "User_Login_Log");
		final Scan scan = new Scan();
		scan.setCaching(500);
		Map<byte[], String> results = table.coprocessorExec(
					ScanCoprocessorProtocol.class,
					null,
					null,
					new Batch.Call<ScanCoprocessorProtocol, String>() {
						public String call(ScanCoprocessorProtocol counter)
								throws IOException {
							return counter.getOnlineTime(scan,"20140514000000");
						}
					});
		for (Map.Entry<byte[], String> entry : results.entrySet()) {
			System.out.println(entry);
			}
	}

这个方法调用方法有一个问题,就是如果一个用户的数据是跨越多个Region的话,会造成这些用户统计数据出错。

说一下我的解决方案:把每个Region返回的结果,带上某一个玩家的最后登录时间(如果该玩家在这个Region上的最后一条记录是登出记录,可以忽略),然后在Main方法中,再对这些数据作一次时间差的计算,这样就可以避免数据出错。最后再把每一个玩家的统计数据在Main方法中累加起来。

其实,在实际应用过程中,我们的团队已经对此类问题见怪不怪了。这就相当于一个Reduce去处理所有Map的输出结果。

从这个案例可以看出,这个长的很像Reduce的coprocessorExec 为了并行计算的汇总点。

所以我个人认为coprocessorExec 并不能完全取代MapReduce,毕竟coprocessorExec 只有一个Reduce,在某些大输出的数据统计应用中,coprocessorExec 的Client很可能成为性能瓶颈点。所以我们应该根据不同的计算场景,合理选择Endpoint和MapReduce,而不能盲目的追求新技术。

现在再来看一下cproecessorProxy,很可惜官方没有对这个方法举例,只有下面这短短一句话。

Executing against a single region: HTableInterface.coprocessorProxy(Class<T> protocol, byte[] row)

在单个region上执行....其实很好理解。coprocessorProxy是一个串行方法,在调用的过程中,只需要定义一个row参数就可以锁定某一个Region。

任然用OnlineTime这个应用举例,如果我要查找10000001这个用户的在线时长,就可以这样写。

//调用方法
public static void main(String[] args) throws IOException {

		Configuration conf = HBaseConfiguration.create();
		conf.set("hbase.zookeeper.quorum", "slaver04.nj.hadoop");
		conf.set("hbase.zookeeper.property.clientPort", "2181");
		HTable table = new HTable(conf, "User_Login_Log");
		final Scan scan = new Scan("10000001".getBytes(),"10000002".getBytes());
		scan.setCaching(500);
		String results = table.coprocessorProxy(
					ScanCoprocessorProtocol.class,
					"10000001".getBytes()).getOnlineTime(scan,"20140514000000"); 
		System.out.println(results);
	}
如果该用户的数据跨多个Region,那就需要在coprocessorProxy的方法外层去加一个迭代,循环访问每一个Region,直到访问到这个用户的最后一条数据。

这里可能会有人问,coprocessorExec也可以做到同样的功能,为什么还要coprocessorProxy呢?

不错,在这个应用中coprocessorProxy就是个累赘,没有使用价值。但不代表coprocessorProxy是无用的废物,在某些应用中coprocessorProxy绝对是神器。

欲知后事如何,请看下回分解:玩转HBase: Coprocessor Endpoint (3):利用coprocessorProxy实现HBase分页查询

本文是CSDN-撸大湿原创,如要转载请注明出处,谢谢。

如有任何异议请留言,或去CSDN-Hadoop论坛找我,欢迎拍砖~


你可能感兴趣的:(hadoop,hbase,endpoint,coprocessor,协处理器)