spark用程序提交任务到yarn Spark自定义分区(Partitioner) textfile使用小技巧 createDirectStream

因为spark文档中只介绍了两种用脚本提交到yarn的例子,并没有介绍如何通过程序提交yarn,但是我们的需求需要这样。网上很难找到例子,经过几天摸索,终于用程序提交到yarn成功,下面总结一下。

先介绍官网提交的例子,我用的是spark 0.9.0 hadoop2.2.0

一.使用脚本提交

1.使用spark脚本提交到yarn,首先需要将spark所在的主机和hadoop集群之间hosts相互配置(也就是把spark主机的ip和主机名配置到hadoop所有节点的/etc/hosts里面,再把集群所有节点的ip和主机名配置到spark所在主机的/etc/hosts里面)。

2.然后需要把hadoop目录etc/hadoop下面的*-sit.xml复制到${SPARK_HOME}的conf下面.

3.确保hadoop集群配置了 HADOOP_CONF_DIR or YARN_CONF_DIR 

1.yarn-standalone方式提交到yarn

${SPARK_HOME}下面执行:

?
1
2
3
4
5
6
7
8
9
SPARK_JAR=. /assembly/target/scala-2 .10.4 /spark-assembly-0 .9.0-incubating-hadoop2.2.0.jar \
     . /bin/spark-class org.apache.spark.deploy.yarn.Client \
       --jar . /examples/target/scala-2 .10 /spark-examples_2 .10-assembly-0.9.0-incubating.jar \
       --class org.apache.spark.examples.SparkPi \
       --args yarn-standalone \
       --num-workers 3 \
       --master-memory 2g \
       --worker-memory 2g \
       --worker-cores 1



2.   yarn-client   方式提交到yarn


${SPARK_HOME}下面执行:

?
1
2
3
SPARK_JAR=. /assembly/target/scala-2 .10.4 /spark-assembly-0 .9.0-incubating-hadoop2.2.0.jar \
SPARK_YARN_APP_JAR=examples /target/scala-2 .10 /spark-examples_2 .10-assembly-0.9.0-incubating.jar \
. /bin/run-example org.apache.spark.examples.SparkPi yarn-client



二、使用程序提交


1.必须使用linux主机提交任务,使用windows提交到linux hadoop集群会报


?
1
org.apache.hadoop.util.Shell$ExitCodeException: /bin/bash: 第  0 行: fg: 无任务控制


错误。hadoop2.2.0不支持windows提交到linux hadoop集群,网上搜索发现这是hadoop的bug。

2.提交任务的主机和hadoop集群主机名需要在hosts相互配置。

3.因为使用程序提交是使用yarn-client方式,所以必须像上面脚本那样设置环境变量SPARK_JAR 和 SPARK_YARN_APP_JAR

比如我的设置为向提交任务主机~/.bashrc里面添加:


?
1
2
export SPARK_JAR= file : ///home/ndyc/software/sparkTest/lib/spark-assembly-0 .9.0-incubating-hadoop2.2.0.jar
      export SPARK_YARN_APP_JAR= file : ///home/ndyc/software/sparkTest/ndspark-0 .0.1.jar


如果不加file:// spark会任务是hdfs,会在hdfs里面找,file://表明是本地文件。


其中SPARK_JAR是${SPARK_HOME}/assembly/target/scala-2.10.4/spark-assembly-0.9.0-incubating-hadoop2.2.0.jar

SPARK_YARN_APP_JAR是自己程序打的jar包,包含自己的测试程序。

4.程序中加入hadoop、yarn、依赖。

注意,如果引入了hbase依赖,需要这样配置


?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
             <groupId>org.apache.hbase</groupId>
             <artifactId>hbase-thrift</artifactId>
             <version>${hbase.version}</version>
             <exclusions>
                 <exclusion>
                     <groupId>org.apache.hadoop</groupId>
                     <artifactId>hadoop-mapreduce-client-jobclient</artifactId>
                 </exclusion>
                 <exclusion>
                     <groupId>org.apache.hadoop</groupId>
                     <artifactId>hadoop-client</artifactId>
                 </exclusion>
             </exclusions>
         </dependency>

然后再加入


?
1
2
3
4
5
<dependency>
             <groupId>org.ow2.asm</groupId>
             <artifactId>asm-all</artifactId>
             <version> 4.0 </version>
         </dependency>


否则会报错:



?
1
IncompatibleClassChangeError has  interface org.objectweb.asm.ClassVisitor as  super class



异常是因为Hbase jar hadoop-mapreduce-client-jobclient.jar里面使用到了asm3.1 而spark需要的是asm-all-4.0.jar

5. hadoop conf下的*-site.xml需要复制到提交主机的classpath下,或者说maven项目resources下面。

6.编写程序

代码示例:


?
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
package com.sdyc.ndspark.sys;
 
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFunction;
import scala.Tuple2;
 
import java.util.ArrayList;
import java.util.List;
 
/**
  * Created with IntelliJ IDEA.
  * User: zarchary
  * Date: 14-1-19
  * Time: 下午6:23
  * To change this template use File | Settings | File Templates.
  */
public class  ListTest {
 
     public static  void  main(String[] args)  throws Exception {
         SparkConf sparkConf =  new SparkConf();
         sparkConf.setAppName( "listTest" );
         //使用yarn模式提交
         sparkConf.setMaster( "yarn-client" );
 
         JavaSparkContext sc =  new JavaSparkContext(sparkConf);
 
         List<String> listA =  new ArrayList<String>();
 
         listA.add( "a" );
         listA.add( "a" );
         listA.add( "b" );
         listA.add( "b" );
         listA.add( "b" );
         listA.add( "c" );
         listA.add( "d" );
 
         JavaRDD<String> letterA = sc.parallelize(listA);
 
         JavaPairRDD<String, Integer> letterB = letterA.map( new PairFunction<String, String, Integer>() {
             @Override
             public Tuple2<String, Integer> call(String s)  throws Exception {
                 return new  Tuple2<String, Integer>(s,  1 );
             }
         });
 
         letterB = letterB.reduceByKey( new Function2<Integer, Integer, Integer>() {
             public Integer call(Integer i1, Integer i2) {
                 return i1 + i2;
             }
         });
 
         //颠倒顺序
         JavaPairRDD<Integer, String> letterC = letterB.map( new PairFunction<Tuple2<String, Integer>, Integer, String>() {
             @Override
             public Tuple2<Integer, String> call(Tuple2<String, Integer> stringIntegerTuple2)  throws Exception {
                 return new  Tuple2<Integer, String>(stringIntegerTuple2._2, stringIntegerTuple2._1);
             }
         });
 
         JavaPairRDD<Integer, List<String>> letterD = letterC.groupByKey();
//        //false说明是降序
         JavaPairRDD<Integer, List<String>> letterE = letterD.sortByKey( false );
 
         System.out.println( "========" + letterE.collect());
 
         System.exit( 0 );
     }
}



代码中master设置为yar-client表明了是使用提交到yarn.


关于spark需要依赖的jar的配置可以参考我的博客spark安装和远程调用。

以上弄完之后就可以运行程序了。

运行后会看到yarn的ui界面出现:


正在执行的过程中会发现hadoop yarn 有的nodemanage会有下面这个进程:


?
1
13247 org.apache.spark.deploy.yarn.WorkerLauncher


这是spark的工作进程。


如果接收到异常为:

?
1
WARN YarnClientClusterScheduler: Initial job has not accepted any resources; check your cluster UI to ensure that workers are registered and have sufficient memory

 出现这个错误是因为提交任务的节点不能和spark工作节点交互,因为提交完任务后提交任务节点上会起一个进程,展示任务进度,大多端口为4044,工作节点需要反馈进度给该该端口,所以如果主机名或者IP在hosts中配置不正确,就会报 

WARN YarnClientClusterScheduler: Initial job has not accepted any resources; check your cluster UI to ensure that workers are registered and have sufficient memory错误。 
所以请检查主机名和IP是否配置正确。

我自己的理解为,程序提交任务到yarn后,会上传SPARK_JAR和SPARK_YARN_APP_JAR到hadoop节点, yarn根据任务情况来分配资源,在nodemanage节点上来启动org.apache.spark.deploy.yarn.WorkerLauncher工作节点来执行spark任务,执行完成后退出。 


Spark自定义分区(Partitioner)

我们都知道Spark内部提供了HashPartitionerRangePartitioner两种分区策略,这两种分区策略在很多情况下都适合我们的场景。但是有些情况下,Spark内部不能符合咱们的需求,这时候我们就可以自定义分区策略。为此,Spark提供了相应的接口,我们只需要扩展Partitioner抽象类,然后实现里面的三个方法:

01 package org.apache.spark
02  
03 /**
04  * An object that defines how the elements in a key-value pair RDD are partitioned by key.
05  * Maps each key to a partition ID, from 0 to `numPartitions - 1`.
06  */
07 abstract class Partitioner extends Serializable {
08   def numPartitions: Int
09   def getPartition(key: Any): Int
10 }

  def numPartitions: Int:这个方法需要返回你想要创建分区的个数;
  def getPartition(key: Any): Int:这个函数需要对输入的key做计算,然后返回该key的分区ID,范围一定是0到numPartitions-1
  equals():这个是Java标准的判断相等的函数,之所以要求用户实现这个函数是因为Spark内部会比较两个RDD的分区是否一样。

  假如我们想把来自同一个域名的URL放到一台节点上,比如:http://www.iteblog.comhttp://www.iteblog.com/archives/1368,如果你使用HashPartitioner,这两个URL的Hash值可能不一样,这就使得这两个URL被放到不同的节点上。所以这种情况下我们就需要自定义我们的分区策略,可以如下实现:

01 package com.iteblog.utils
02  
03 import org.apache.spark.Partitioner
04  
05 /**
06  * User: 过往记忆
07  * Date: 2015-05-21
08  * Time: 下午23:34
09  * bolg: http://www.iteblog.com
10  * 本文地址:http://www.iteblog.com/archives/1368
11  * 过往记忆博客,专注于hadoop、hive、spark、shark、flume的技术博客,大量的干货
12  * 过往记忆博客微信公共帐号:iteblog_hadoop
13  */
14  
15 class IteblogPartitioner(numParts: Int) extends Partitioner {
16   override def numPartitions: Int = numParts
17  
18   override def getPartition(key: Any): Int = {
19     val domain = new java.net.URL(key.toString).getHost()
20     val code = (domain.hashCode % numPartitions)
21     if (code < 0) {
22       code + numPartitions
23     else {
24       code
25     }
26   }
27  
28   override def equals(other: Any): Boolean = other match {
29     case iteblog: IteblogPartitioner =>
30       iteblog.numPartitions == numPartitions
31     case _ =>
32       false
33   }
34  
35   override def hashCode: Int = numPartitions
36 }

因为hashCode值可能为负数,所以我们需要对他进行处理。然后我们就可以在partitionBy()方法里面使用我们的分区:

1 iteblog.partitionBy(new IteblogPartitioner(20))

  类似的,在Java中定义自己的分区策略和Scala类似,只需要继承org.apache.spark.Partitioner,并实现其中的方法即可。

  在Python中,你不需要扩展Partitioner类,我们只需要对iteblog.partitionBy()加上一个额外的hash函数,如下:

1 import urlparse
2  
3 def iteblog_domain(url):
4   return hash(urlparse.urlparse(url).netloc)
5  
6 iteblog.partitionBy(20, iteblog_domain)


spark中的SparkContext实例的textFile使用的小技巧

网上很多例子,包括官网的例子,都是用textFile来加载一个文件创建RDD,类似sc.textFile("hdfs://n1:8020/user/hdfs/input")

textFile的参数是一个path,这个path可以是:

1. 一个文件路径,这时候只装载指定的文件

2. 一个目录路径,这时候只装载指定目录下面的所有文件(不包括子目录下面的文件

3. 通过通配符的形式加载多个文件或者加载多个目录下面的所有文件

第三点是一个使用小技巧,现在假设我的数据结构为先按天分区,再按小时分区的,在hdfs上的目录结构类似于:

/user/hdfs/input/dt=20130728/hr=00/

/user/hdfs/input/dt=20130728/hr=01/

...

/user/hdfs/input/dt=20130728/hr=23/

具体的数据都在hr等于某个时间的目录下面,现在我们要分析20130728这一天的数据,我们就必须把这个目录下面的所有hr=*的子目录下面的数据全部装载进RDD,于是我们可以这样写:sc.textFile("hdfs://n1:8020/user/hdfs/input/dt=20130728/hr=*/"),注意到hr=*,是一个模糊匹配的方式。



spark的kafka的低阶API createDirectStream的一些总结。

大家都知道在spark1.3版本后,kafkautil里面提供了两个创建dstream的方法,一个是老版本中有的createStream方法,还有一个是后面新加的createDirectStream方法。关于这两个方法的优缺点,官方已经说的很详细(http://spark.apache.org/docs/latest/streaming-kafka-integration.html),总之就是createDirectStream性能会更好一点,通过新方法创建出来的dstream的rdd partition和kafka的topic的partition是一一对应的,通过低阶API直接从kafka的topic消费消息,但是它不再往zookeeper中更新consumer offsets,使得基于zk的consumer offsets的监控工具都会失效。

官方只是蜻蜓点水般的说了一下可以在foreachRDD中更新zookeeper上的offsets:

[plain]  view plain  copy
  1. directKafkaStream.foreachRDD { rdd =>   
  2.      val offsetRanges = rdd.asInstanceOf[HasOffsetRanges]  
  3.      // offsetRanges.length = # of Kafka partitions being consumed  
  4.      ...  
  5.  }  

对应 Exactly-once semantics要自己去实现了,大致的实现思路就是在driver启动的时候先从zk上获得consumer offsets信息,createDirectStream有两个重载方法,其中一个可以设置从任意offsets位置开始消费,部分代码如下:

[plain]  view plain  copy
  1. def createDirectStream(implicit streamingConfig: StreamingConfig, kc: KafkaCluster) = {  
  2.   
  3.       val extractors = streamingConfig.getExtractors()  
  4.       //从zookeeper上读取offset开始消费message  
  5.       val messages = {  
  6.         val kafkaPartitionsE = kc.getPartitions(streamingConfig.topicSet)  
  7.         if (kafkaPartitionsE.isLeft) throw new SparkException("get kafka partition failed:")  
  8.         val kafkaPartitions = kafkaPartitionsE.right.get  
  9.         val consumerOffsetsE = kc.getConsumerOffsets(streamingConfig.group, kafkaPartitions)  
  10.         if (consumerOffsetsE.isLeft) throw new SparkException("get kafka consumer offsets failed:")  
  11.         val consumerOffsets = consumerOffsetsE.right.get  
  12.         consumerOffsets.foreach {  
  13.           case (tp, n) => println("===================================" + tp.topic + "," + tp.partition + "," + n)  
  14.         }  
  15.         KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, (String, String)](  
  16.           ssc, kafkaParams, consumerOffsets, (mmd: MessageAndMetadata[String, String]) => (mmd.key, mmd.message))  
  17.       }  
  18.       messages  
  19.     }  

这里会有几个问题,就是在一个group是新的consumer group时,即首次消费,zk上海没有相应的group offsets目录,这时要先初始化一下zk上的offsets目录,或者是zk上记录的offsets已经过时,由于kafka有定时清理策略,直接从zk上的offsets开始消费会报ArrayOutofRange异常,即找不到offsets所属的index文件了,针对这两种情况,做了以下处理:

[plain]  view plain  copy
  1. def setOrUpdateOffsets(implicit streamingConfig: StreamingConfig, kc: KafkaCluster): Unit = {  
  2.     streamingConfig.topicSet.foreach(topic => {  
  3.       println("current topic:" + topic)  
  4.       var hasConsumed = true  
  5.       val kafkaPartitionsE = kc.getPartitions(Set(topic))  
  6.       if (kafkaPartitionsE.isLeft) throw new SparkException("get kafka partition failed:")  
  7.       val kafkaPartitions = kafkaPartitionsE.right.get  
  8.       val consumerOffsetsE = kc.getConsumerOffsets(streamingConfig.group, kafkaPartitions)  
  9.       if (consumerOffsetsE.isLeft) hasConsumed = false  
  10.       if (hasConsumed) {  
  11.         //如果有消费过,有两种可能,如果streaming程序执行的时候出现kafka.common.OffsetOutOfRangeException,说明zk上保存的offsets已经过时了,即kafka的定时清理策略已经将包含该offsets的文件删除。  
  12.         //针对这种情况,只要判断一下zk上的consumerOffsets和leaderEarliestOffsets的大小,如果consumerOffsets比leaderEarliestOffsets还小的话,说明是过时的offsets,这时把leaderEarliestOffsets更新为consumerOffsets  
  13.         val leaderEarliestOffsets = kc.getEarliestLeaderOffsets(kafkaPartitions).right.get  
  14.         println(leaderEarliestOffsets)  
  15.         val consumerOffsets = consumerOffsetsE.right.get  
  16.         val flag = consumerOffsets.forall {  
  17.           case (tp, n) => n < leaderEarliestOffsets(tp).offset  
  18.         }  
  19.         if (flag) {  
  20.           println("consumer group:" + streamingConfig.group + " offsets已经过时,更新为leaderEarliestOffsets")  
  21.           val offsets = leaderEarliestOffsets.map {  
  22.             case (tp, offset) => (tp, offset.offset)  
  23.           }  
  24.           kc.setConsumerOffsets(streamingConfig.group, offsets)  
  25.         }  
  26.         else {  
  27.           println("consumer group:" + streamingConfig.group + " offsets正常,无需更新")  
  28.         }  
  29.       }  
  30.       else {  
  31.         //如果没有被消费过,则从最新的offset开始消费。  
  32.         val leaderLatestOffsets = kc.getLatestLeaderOffsets(kafkaPartitions).right.get  
  33.         println(leaderLatestOffsets)  
  34.         println("consumer group:" + streamingConfig.group + " 还未消费过,更新为leaderLatestOffsets")  
  35.         val offsets = leaderLatestOffsets.map {  
  36.           case (tp, offset) => (tp, offset.offset)  
  37.         }  
  38.         kc.setConsumerOffsets(streamingConfig.group, offsets)  
  39.       }  
  40.     })  
  41.   }  
这里又碰到了一个问题,从consumer offsets到leader latest offsets中间延迟了很多消息,在下一次启动的时候,首个batch要处理大量的消息,会导致spark-submit设置的资源无法满足大量消息的处理而导致崩溃。因此在spark-submit启动的时候多加了一个配置:--conf spark.streaming.kafka.maxRatePerPartition=10000。限制每秒钟从topic的每个partition最多消费的消息条数,这样就把首个batch的大量的消息拆分到多个batch中去了,为了更快的消化掉delay的消息,可以调大计算资源和把这个参数调大。

OK,driver启动的问题解决了,那么接下来处理处理完消息后更新zk offsets的工作,这里要注意是在处理完之后再更新,想想如果你消费了消息先更新zk offset在去处理消息将处理好的消息保存到其他地方去,如果后一步由于处理消息的代码有BUG失败了,前一步已经更新了zk了,会导致这部分消息虽然被消费了但是没被处理,等你把处理消息的BUG修复再重新提交后,这部分消息在下次启动的时候不会再被消费了,因为你已经更新了ZK OFFSETS,针对这些因素考虑,部分代码实现如下:

[plain]  view plain  copy
  1. def updateZKOffsets(rdd: RDD[(String, String)])(implicit streamingConfig: StreamingConfig, kc: KafkaCluster): Unit = {  
  2.     println("rdd not empty,update zk offset")  
  3.     val offsetsList = rdd.asInstanceOf[HasOffsetRanges].offsetRanges  
  4.   
  5.   
  6.     for (offsets <- offsetsList) {  
  7.       val topicAndPartition = TopicAndPartition(offsets.topic, offsets.partition)  
  8.       val o = kc.setConsumerOffsets(streamingConfig.group, Map((topicAndPartition, offsets.untilOffset)))  
  9.       if (o.isLeft) {  
  10.         println(s"Error updating the offset to Kafka cluster: ${o.left.get}")  
  11.       }  
  12.     }  
  13.   }  
  14.   
  15.   def processData(messages: InputDStream[(String, String)])(implicit streamingConfig: StreamingConfig, kc: KafkaCluster): Unit = {  
  16.     messages.foreachRDD(rdd => {  
  17.       if (!rdd.isEmpty()) {  
  18.   
  19.         val datamodelRDD = streamingConfig.relation match {  
  20.           case "1" =>  
  21.             val (topic, _) = streamingConfig.topic_table_mapping  
  22.             val extractor = streamingConfig.getExtractor(topic)  
  23.             // Create direct kafka stream with brokers and topics  
  24.             val topicsSet = Set(topic)  
  25.             val datamodel = rdd.filter(msg => {  
  26.               extractor.filter(msg)  
  27.             }).map(msg => extractor.msgToRow(msg))  
  28.             datamodel  
  29.           case "2" =>  
  30.             val (topics, _) = streamingConfig.topic_table_mapping  
  31.             val extractors = streamingConfig.getExtractors(topics)  
  32.             val topicsSet = topics.split(",").toSet  
  33.   
  34.             //kafka msg为key-value形式,key用来对msg进行分区用的,为了散列存储消息,采集器那边key采用的是:topic|加一个随机数的形式,例如:rd_e_pal|20,split by |取0可以拿到对应的topic名字,这样union在一起的消息可以区分出来自哪一个topic  
  35.             val datamodel = rdd.filter(msg => {  
  36.               //kafka msg为key-value形式,key用来对msg进行分区用的,为了散列存储消息,采集器那边key采用的是:topic|加一个随机数的形式,例如:rd_e_pal|20,split by |取0可以拿到对应的topic名字,这样union在一起的消息可以区分出来自哪一个topic  
  37.               val keyValid = msg != null && msg._1 != null && msg._1.split("\\|").length == 2  
  38.               if (keyValid) {  
  39.                 val topic = msg._1.split("\\|")(0)  
  40.                 val (_, extractor) = extractors.find(p => {  
  41.                   p._1.equalsIgnoreCase(topic)  
  42.                 }).getOrElse(throw new RuntimeException("配置文件中没有找到topic:" + topic + " 对应的extractor"))  
  43.                 //trim去掉末尾的换行符,否则取最后一个字段时会有一个\n  
  44.                 extractor.filter(msg._2.trim)  
  45.               }  
  46.               else {  
  47.                 false  
  48.               }  
  49.   
  50.             }).map {  
  51.               case (key, msgContent) =>  
  52.                 val topic = key.split("\\|")(0)  
  53.                 val (_, extractor) = extractors.find(p => {  
  54.                   p._1.equalsIgnoreCase(topic)  
  55.                 }).getOrElse(throw new RuntimeException("配置文件中没有找到topic:" + topic + " 对应的extractor"))  
  56.                 extractor.msgToRow((key, msgContent))  
  57.             }  
  58.             datamodel  
  59.         }  
  60.         //先处理消息  
  61.         processRDD(datamodelRDD)  
  62.         //再更新offsets  
  63.         updateZKOffsets(rdd)  
  64.       }  
  65.     })  
  66.   }  
  67.   
  68.   def processRDD(rdd: RDD[Row])(implicit streamingConfig: StreamingConfig) = {  
  69.     if (streamingConfig.targetType == "mongo") {  
  70.       val target = streamingConfig.getTarget().asInstanceOf[MongoTarget]  
  71.       if (!MongoDBClient.db.collectionExists(target.collection)) {  
  72.         println("create collection:" + target.collection)  
  73.         MongoDBClient.db.createCollection(target.collection, MongoDBObject("storageEngine" -> MongoDBObject("wiredTiger" -> MongoDBObject())))  
  74.         val coll = MongoDBClient.db(target.collection)  
  75.         //创建ttl index  
  76.         if (target.ttlIndex) {  
  77.           val indexs = coll.getIndexInfo  
  78.           if (indexs.find(p => p.get("name") == "ttlIndex") == None) {  
  79.             coll.createIndex(MongoDBObject(target.ttlColumn -> 1), MongoDBObject("expireAfterSeconds" -> target.ttlExpire, "name" -> "ttlIndex"))  
  80.           }  
  81.         }  
  82.       }  
  83.   
  84.     }  
  85.   
  86.     val (_, table) = streamingConfig.topic_table_mapping  
  87.     val schema = streamingConfig.getTableSchema(table)  
  88.   
  89.     // Get the singleton instance of SQLContext  
  90.     val sqlContext = HIVEContextSingleton.getInstance(rdd.sparkContext)  
  91.   
  92.     // Convert RDD[String] to RDD[case class] to DataFrame  
  93.     val dataFrame = sqlContext.createDataFrame(rdd, schema)  
  94.   
  95.     // Register as table  
  96.     dataFrame.registerTempTable(table)  
  97.   
  98.     // Do word count on table using SQL and print it  
  99.     val results = sqlContext.sql(streamingConfig.sql)  
  100.     //select dt,hh(vtm) as hr,app_key, collect_set(device_id) as deviceids  from rd_e_app_header where dt=20150401 and hh(vtm)='01' group by dt,hh(vtm),app_key limit 100 ;  
  101.     //          results.show()  
  102.     streamingConfig.targetType match {  
  103.       case "mongo" => saveToMongo(results)  
  104.       case "show" => results.show()  
  105.     }  
  106.   
  107.   }  
  108.   
  109.   
  110.   def saveToMongo(df: DataFrame)(implicit streamingConfig: StreamingConfig) = {  
  111.     val target = streamingConfig.getTarget().asInstanceOf[MongoTarget]  
  112.     val coll = MongoDBClient.db(target.collection)  
  113.     val result = df.collect()  
  114.     if (result.size > 0) {  
  115.       val bulkWrite = coll.initializeUnorderedBulkOperation  
  116.       result.foreach(row => {  
  117.         val id = row(target.pkIndex)  
  118.         val setFields = target.columns.filter(p => p.op == "set").map(f => (f.name, row(f.index))).toArray  
  119.         val incFields = target.columns.filter(p => p.op == "inc").map(f => {  
  120.           (f.name, row(f.index).asInstanceOf[Long])  
  121.         }).toArray  
  122.         //        obj=obj.++($addToSet(MongoDBObject("test"->MongoDBObject("$each"->Array(3,4)),"test1"->MongoDBObject("$each"->Array(1,2)))))  
  123.         var obj = MongoDBObject()  
  124.         var addToSetObj = MongoDBObject()  
  125.         target.columns.filter(p => p.op == "addToSet").foreach(col => {  
  126.           col.mType match {  
  127.             case "Int" =>  
  128.               addToSetObj = addToSetObj.++(col.name -> MongoDBObject("$each" -> row(col.index).asInstanceOf[ArrayBuffer[Int]]))  
  129.             case "Long" =>  
  130.               addToSetObj = addToSetObj.++(col.name -> MongoDBObject("$each" -> row(col.index).asInstanceOf[ArrayBuffer[Long]]))  
  131.             case "String" =>  
  132.               addToSetObj = addToSetObj.++(col.name -> MongoDBObject("$each" -> row(col.index).asInstanceOf[ArrayBuffer[String]]))  
  133.           }  
  134.   
  135.         })  
  136.         if (addToSetObj.size > 0) obj = obj.++($addToSet(addToSetObj))  
  137.         if (incFields.size > 0) obj = obj.++($inc(incFields: _*))  
  138.         if (setFields.size > 0) obj = obj.++($set(setFields: _*))  
  139.         bulkWrite.find(MongoDBObject("_id" -> id)).upsert().updateOne(obj)  
  140.       })  
  141.       bulkWrite.execute()  
  142.     }  
  143.   }  

仔细想一想,还是没有实现精确一次的语义,写入mongo和更新ZK由于不是一个事务的,如果更新mongo成功,然后更新ZK失败,则下次启动的时候这个批次的数据就被重复计算,对于UV由于是addToSet去重操作,没什么影响,但是PV是inc操作就会多算这一个批次的的数据,其实如果batch time比较短的话,其实都还是可以接受的。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值