spark streaming 实现用户的准实时更新
- spark thrift server 替换hiveserver2
- HBase创建app_users 表 :
基本属性字段,firsttime(第一次启动时间), lasttime(最后一次启动时间) - 使用phoenix,Hbase上的SQL支持
实现思路
- spark streaming 从kafka接收消息,设置每5秒为一个窗口
- 一个窗口期会来很多数据,要做的是:
1)如果该 [app, 用户, version] 没在表中出现,则插入一天新记录,firsttime 和 lasttime 相同
2) 如果该 [ app, 用户, version ] 出现过,则更新lasttime即可 - 由于我们只需要第一次和最后一次的数据,因此需要对窗口期内的数据,按【app,用户和version】聚合,然后排序, 取出第一次和最后一次做插入更新HBase即可
具体实现
1.创建StreamingContext,配置Kafka Consumer, 使用Kafka消费队列作为spark输入流
import com.alibaba.fastjson.JSONObject;
import com.it18zhang.app.common.AppStartupLog;
import com.it18zhang.app.common.Constants;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.streaming.Seconds;
import org.apache.spark.api.java.function.*;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaInputDStream;
import org.apache.spark.streaming.api.java.JavaPairDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import org.apache.spark.streaming.kafka010.ConsumerStrategies;
import org.apache.spark.streaming.kafka010.KafkaUtils;
import org.apache.spark.streaming.kafka010.LocationStrategies;
import scala.Tuple2;
import java.sql.*;
import java.util.*;
// 创建spark context
SparkConf conf = new SparkConf();
conf.setAppName("App-Logs-Spark-Streaming");
conf.setMaster("local[4]") ;
JavaStreamingContext sc = new JavaStreamingContext(conf, Seconds.apply(5));
// 配置kafka
Map props = new HashMap();
// kafka 集群地址
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, Constants.KAFKA_CONSUMER_SERVERS);
// 序列化方式
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getCanonicalName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getCanonicalName());
props.put(ConsumerConfig.GROUP_ID_CONFIG, "1");
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"lastest");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
// 设置主题
List topics = new ArrayList() ;
topics.add("topic-app-startup");
// 创建kafka流
JavaInputDStream> ds1 = KafkaUtils.createDirectStream(sc,
LocationStrategies.PreferBrokers(), ConsumerStrategies.Subscribe(topics,props));
2.将输入流的JSON String 通过Map 转为自定义的Log对象
// 通过Map实现流变化
Function, AppStartupLog> mapFunc = new Function, AppStartupLog>() {
public AppStartupLog call(ConsumerRecord
3.将log对象 通过pairMap 映射为(key, logAgg)的键值对
其中logAgg类是为了方便聚合操作的自定义类,在Log类的基础上加入了firstTime,lastTime的属性,还包括log对象的成员
// 通过AppStartupLog 抽取出appid deviceid version 作为key值
// 将AppStartupLog转换为包含firstTime 和lastTime 的聚合类AppStartupLogAgg
// AppStartupLog => (key, AppStartupLogAgg)
//
PairFunction pairFunc = new PairFunction() {
public Tuple2 call(AppStartupLog appStartupLog) throws Exception {
String appid = appStartupLog.getAppId();
String deviceid = appStartupLog.getDeviceId();
String appversion = appStartupLog.getAppVersion();
String key = appid + "," + deviceid + ","+appversion;
AppStartupLogAgg value = new AppStartupLogAgg();
value.setLog(appStartupLog);
value.setFirstTime(appStartupLog.getCreatedAtMs());
value.setLastTime(appStartupLog.getCreatedAtMs());
return new Tuple2(key, value);
}
};
JavaPairDStream ds3 = ds2.mapToPair(pairFunc);
4.根据新的键值通过reduceByKey聚合
// 通过reduceByKey聚合
Function2 reduceFunc = new Function2() {
public AppStartupLogAgg call(AppStartupLogAgg v1, AppStartupLogAgg v2) throws Exception {
v1.setFirstTime(Math.min(v1.getFirstTime(),v2.getFirstTime()));
v1.setLastTime(Math.max(v1.getLastTime(), v2.getLastTime()));
return v1;
}
};
JavaPairDStream ds4 = ds3.reduceByKey(reduceFunc);
5.原来的想法是对聚合以后的每个partition(相同key)的数据进行一次数据插入,但由于key值为appid+deviceid+version,每个分区内的数据较少, 这样频繁插入的代价(比如开启链接)相对较高。因此考虑按appid再分一次组,将相同appid的数据放在一起后再进行插入。
// 将key值 转变为 appid
PairFunction,String,AppStartupLogAgg> pairFunc2 = new PairFunction, String, AppStartupLogAgg>() {
public Tuple2 call(Tuple2 tuple) throws Exception {
String key = tuple._2().getLog().getAppId();
return new Tuple2(key, tuple._2());
}
};
JavaPairDStream ds5 = ds4.mapToPair(pairFunc2);
6.循环插入
//循环聚合结果,插入phoenix库
ds6.foreachRDD(new VoidFunction>>() {
public void call(JavaPairRDD> rdd) throws Exception {
// 对每个rdd 里对每个 执行插入操作
rdd.foreach(new VoidFunction>>() {
public void call(Tuple2> tt) throws Exception {
String appid = tt._1() ;
Iterator it = tt._2().iterator();
Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
Connection conn = DriverManager.getConnection("jdbc:phoenix:s202:2181");
conn.setAutoCommit(false);
//循环所有聚合数据
while(it.hasNext()){
AppStartupLogAgg agg = it.next();
upsert2Phoenix(conn, appid, agg);
}
conn.commit();
}
});
}
});
7.执行
sc.start();
sc.awaitTermination();
简单扩展:HIVE和HBASE区别
限制
Hive目前不支持更新操作。另外,由于hive在hadoop上运行批量操作,它需要花费很长的时间,通常是几分钟到几个小时才可以获取到查询的结果。Hive必须提供预先定义好的schema将文件和目录映射到列,并且Hive与ACID不兼容。
HBase查询是通过特定的语言来编写的,这种语言需要重新学习。类SQL的功能可以通过Apache Phonenix实现,但这是以必须提供schema为代价的。另外,Hbase也并不是兼容所有的ACID特性,虽然它支持某些特性。最后但不是最重要的–为了运行Hbase,Zookeeper是必须的,zookeeper是一个用来进行分布式协调的服务,这些服务包括配置服务,维护元信息和命名空间服务。
应用场景
Hive适合用来对一段时间内的数据进行分析查询,例如,用来计算趋势或者网站的日志。Hive不应该用来进行实时的查询。因为它需要很长时间才可以返回结果。
Hbase非常适合用来进行大数据的实时查询。Facebook用Hbase进行消息和实时的分析。它也可以用来统计Facebook的连接数。
总结
Hive和Hbase是两种基于Hadoop的不同技术–Hive是一种类SQL的引擎,并且运行MapReduce任务,Hbase是一种在Hadoop之上的NoSQL 的Key/vale数据库。当然,这两种工具是可以同时使用的。就像用Google来搜索,用FaceBook进行社交一样,Hive可以用来进行统计查询,HBase可以用来进行实时查询,数据也可以从Hive写到Hbase,设置再从Hbase写回Hive。