使用累加器配合广播变量做码表动态更新

广播变量简单介绍

广播变量是允许程序员缓存一个只读的变量在每个节点上,而不是每个任务保存一份拷贝。例如,利用广播变量,我们能够将配置、较小数据量的码表分发到每个节点上,以减少通信的成本。 
一个广播变量可以通过调用SparkContext.broadcast(v)方法从一个初始变量v中创建。广播变量是v的一个包装变量,它的值可以通过value方法访问,下面的代码说明了这个过程:

scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)

scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)

从上文我们可以看出广播变量的声明很简单,调用broadcast就能搞定,并且scala中一切可序列化的对象都是可以进行广播的,这就给了我们很大的想象空间,可以利用广播变量将一些经常访问的大变量进行广播,而不是每个任务保存一份,这样可以减少资源上的浪费。

更新广播变量(rebroadcast)

广播变量可以用来更新一些大的配置变量,比如数据库中的一张表格,那么有这样一个问题,如果数据库当中的配置表格进行了更新,我们需要重新广播变量该怎么做呢。上文对广播变量的说明中,我们知道广播变量是只读的,也就是说广播出去的变量没法再修改,那么我们应该怎么解决这个问题呢? 
答案是利用spark中的unpersist函数 
Spark automatically monitors cache usage on each node and drops out old data partitions in a least-recently-used (LRU) fashion. If you would like to manually remove an RDD instead of waiting for it to fall out of the cache, use the RDD.unpersist() method. 
上文是从spark官方文档摘抄出来的,我们可以看出,正常来说每个节点的数据是不需要我们操心的,spark会自动按照LRU规则将老数据删除,如果需要手动删除可以调用unpersist函数。 
那么更新广播变量的基本思路:将老的广播变量删除(unpersist),然后重新广播一遍新的广播变量,为此简单包装了一个用于广播和更新广播变量的wraper类,如下:

PS:我们这里结合spark里累加器及spark自动监控hdfs目录功能来触发执行广播变量更新,使用方法见下边的示例: 
不明白累加器的同学自行借助各类搜索查阅资料。

广播变量更新的wraper类

package cn.com.bonc.tools

import java.io.{ ObjectInputStream, ObjectOutputStream }
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.streaming.StreamingContext
import scala.reflect.ClassTag

// This wrapper lets us update brodcast variables within DStreams' foreachRDD
// without running into serialization issues
case class BroadcastWrapper[T: ClassTag](
    @transient private val ssc: StreamingContext,
    @transient private val _v: T) {

  @transient private var v = ssc.sparkContext.broadcast(_v)

  def update(newValue: T, blocking: Boolean = false): Unit = {
    // 删除RDD是否需要锁定
    v.unpersist(blocking)
    v = ssc.sparkContext.broadcast(newValue)
  }

  def value: T = v.value

  private def writeObject(out: ObjectOutputStream): Unit = {
    out.writeObject(v)
  }

  private def readObject(in: ObjectInputStream): Unit = {
    v = in.readObject().asInstanceOf[Broadcast[T]]
  }
}

使用示例

package cn.com.wuy.test

import java.io.FileNotFoundException
import java.text.SimpleDateFormat
import java.util.ArrayList
import java.util.List
import scala.collection.JavaConversions._

import scala.io.Source

import org.apache.commons.lang.StringUtils
import org.apache.spark.SparkConf
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.Seconds
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.dstream.DStream
import org.codehaus.jettison.json.JSONObject
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormat

import com.google.common.collect.HashMultimap

import cn.com.bonc.entity.GeoFencing
import cn.com.bonc.entity.UserLocation
import cn.com.bonc.tools.Geohash
import cn.com.bonc.tools.MyKafkaUntil
import kafka.javaapi.producer.Producer
import kafka.producer.KeyedMessage
import kafka.serializer.StringDecoder
import org.apache.spark.streaming.kafka.KafkaManager
import java.util.HashMap
import cn.com.bonc.tools.BroadcastWrapper
import org.apache.spark.SparkContext
import org.apache.spark.Accumulator
import org.apache.spark.rdd.RDD
import org.apache.hadoop.mapred.InvalidInputException

object BroadcastTest {
  def main(args: Array[String]): Unit = {
    val sourceTopics = "C01002SSIGNAL0000000036" //来源kafka
    val group = "wuy_test" //消费者groupID
    val zkQuorum = "ip1:port,ip2:port" //来源kafka的zk
    val srcBroker = "ip1:port,ip2:port" //来源kafka的zk
    val brokerIP_C01007 = "ip1:port,ip2:port" //信令实时入hbase的kafka broker
    val aimTopics_C01007 = "kafka_wuy_test1" //信令实时入hbase的kafka topic
    val brokerIP_GeoFencing = srcBroker //场景化营销,地理围栏事件kafka topic
    val aimTopics_GeoFencing = "kafka_wuy_test2" //场景营销,地理围栏事件kafka topic
    val offset = "smallest" //场景营销,地理围栏事件kafka topic largest

    val conf = new SparkConf()
    conf.set("spark.speculation", "true") //去掉执行慢的节点
    val ssc: StreamingContext = new StreamingContext(conf, Seconds(60.toInt))
    val sc: SparkContext = ssc.sparkContext

    //先初始化累加器,0为不用更新,1为需要更新
    val accumulator = ssc.sparkContext.accumulator(0) 
    var cellInfo: RDD[String] = null
    var cell: Array[String] = null

    //对码表目录做个是否有文件判断
    try {
      cellInfo = sc.textFile("/user/all_signal_dev/test/wuy/*txt") //地理围栏码表
      cell = cellInfo.filter(line => line.split("\\|").length >= 9).toArray()
    } catch {
      case e: InvalidInputException => println("没有围栏码表,请确认!")
      case t: Throwable => t.printStackTrace() // TODO: handle error
    }

    //上传码表时更新一份日志,该监控日志的目的是为了修改累加器值,并通知各个节点更新广播变量
    val broadLog: DStream[String] = ssc.textFileStream("/user/all_signal_dev/test/wuy/log/")
    //广播码表
    val broadCastCell: BroadcastWrapper[HashMultimap[String, GeoFencing]] = BroadcastWrapper[HashMultimap[String, GeoFencing]](ssc, locatinMap(cell))

    //定义kafka参数
    val kafkaParams = Map(
      "metadata.broker.list" -> srcBroker,
      "group.id" -> group,
      "serializer.class" -> "kafka.serializer.StringDecoder",
      "auto.offset.reset" -> offset,
      "zookeeper.session.timeout.ms" -> "6000",
      "zookeeper.sync.time.ms" -> "2000",
      "auto.commit.interval.ms" -> "1000")

    val topicSets = sourceTopics.split(",").toSet

    //该类是重写了一些方法,主要是自定义了offer更新的方法
    val km = new KafkaManager(kafkaParams)
    //接收kafka消息,没有Receive,是按 partition来接收的
    val lines = km.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topicSets, 1000).persist(StorageLevel.MEMORY_AND_DISK_SER)

    //当有日志更新时,更新累加器值为有效值1
    broadLog.foreachRDD(logRdd => {
      if (!logRdd.isEmpty()) {
        accumulator.setValue(1)
      }
    })

    //业务逻辑方法
    topicConsumer(lines, broadCastCell, aimTopics_C01007, brokerIP_C01007, aimTopics_GeoFencing, brokerIP_GeoFencing, km, ssc, accumulator: Accumulator[Int])

    ssc.start()
    ssc.awaitTermination()
  }

    /**
      具体业务逻辑
   * 接收kafka消息,筛选字段,去重,关联地理围栏码表
   */
  def topicConsumer(dstrem: DStream[(String, String)], broadCell: BroadcastWrapper[HashMultimap[String, GeoFencing]], topicName: String, brokerIP: String, topicName_GeoFencing: String, brokerIP_GeoFencing: String, km: KafkaManager, ssc: StreamingContext, accumulator: Accumulator[Int]) {
    var xl_time: Long = 0
    dstrem.foreachRDD(rdd => {

      //这是去重逻辑
      val rddV = rdd.filter(lines => {
        var colvalues = StringUtils.splitPreserveAllTokens(lines._2, "\\|")
        (lines._2.!=("")) && (colvalues(0) != "" && colvalues(15) != "" && colvalues(16) != "" && colvalues(1) != "")
      }).map(lines => {
          //业务逻辑
          ......
      }).reduceByKey((k, v) => v).mapPartitions(f => {
        f.map(_._2)
      }, true)

      println("==============" + accumulator.value + "update begin==============")
      //有效累加器值,重新读取码表,并把广播变量内容做修改,该修改是广播量所在节点各自修改,和第一次广播出去的变量不是一回事
      if (accumulator.value.==(1)) {
        val cellInfo = ssc.sparkContext.textFile("/user/all_signal_dev/test/wuy/*txt") //地理围栏码表
        val cell = cellInfo.filter(line => line.split("\\|").length >= 9).toArray()
        if (cell.!=(null)) {
          broadCell.update(locatinMap(cell), true)
        }
        //更新完广播变量,重设累加器值为0
        accumulator.setValue(0)
      }
      println("==============" + accumulator.value + "update end==============")

      rddV.foreachPartition(iter => {
          //业务逻辑,使用到广播变量的值,结果数据输出到外部
          ...
      })

      //往kafka zk更新offset
      km.updateZKOffsets(rdd)
      rddV.unpersist(true)
      rdd.unpersist(true)
    })


  }
}

踩坑记录

1、两个不同的DStream不能嵌套遍历,嵌套遍历的结果就是只能遍历出其中一个DStream。在DStream初始化的时候,DStream每个具体的逻辑是分发到各个节点上的。

DStream1.foreachRDD{
    DStream2.foreachRDD{
        ...
    }
    ...
}

2、由于DStream初始化的特点,两个并行的DStream,内部的变量方法是互相访问不了的,所以想用其中一个DStream直接来修改广播变量供另一个DStream使用是不可行的,因此我们借助了累加器,累加器起到了整个job的全局变量作用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值