Spark 實時計算整合案例

哥不是小蘿莉發表於2016-06-05

1.概述

  最近有同學問道,除了使用 Storm 充當實時計算的模型外,還有木有其他的方式來實現實時計算的業務。瞭解到,在使用 Storm 時,需要編寫基於程式語言的程式碼。比如,要實現一個流水指標的統計,需要去編寫相應的業務程式碼,能不能有一種簡便的方式來實現這一需求。在解答了該同學的疑惑後,整理了該實現方案的一個案例,供後面的同學學習參考。

2.內容

  實現該方案,整體的流程是不變的,我這裡只是替換了其計算模型,將 Storm 替換為 Spark,原先的資料收集,儲存依然可以保留。

2.1 Spark Overview

  Spark 出來也是很久了,說起它,應該並不會陌生。它是一個開源的類似於 Hadoop MapReduce 的通用平行計算模型,它擁有 Hadoop MapReduce 所具有的有點,但與其不同的是,MapReduce 的 JOB 中間輸出結果可以儲存在記憶體中,不再需要回寫磁碟,因而,Spark 能更好的適用於需要迭代的業務場景。

2.2 Flow

  上面只是對 Spark 進行了一個簡要的概述,讓大家知道其作用,由於本篇部落格的主要內容並不是講述 Spark 的工作原理和計算方法,多的內容,這裡筆者就不再贅述,若是大家想詳細瞭解 Spark 的相關內容,可參考官方文件。[參考地址

  接下來,筆者為大家呈現本案例的一個實現流程圖,如下圖所示:

  通過上圖,我們可以看出,首先是採集上報的日誌資料,將其存放於訊息中介軟體,這裡訊息中介軟體採用的是 Kafka,然後在使用計算模型按照業務指標實現相應的計算內容,最後是將計算後的結果進行持久化,DB 的選擇可以多樣化,這裡筆者就直接使用了 Redis 來作為演示的儲存介質,大家所示在使用中,可以替換該儲存介質,比如將結果存放到 HDFS,HBase Cluster,或是 MySQL 等都行。這裡,我們使用 Spark SQL 來替換掉 Storm 的業務實現編寫。

3.實現

  在介紹完上面的內容後,我們接下來就去實現該內容,首先我們要生產資料來源,實際的場景下,會有上報好的日誌資料,這裡,我們就直接寫一個模擬資料類,實現程式碼如下所示:

object KafkaIPLoginProducer {
  private val uid = Array("123dfe", "234weq","213ssf")

  private val random = new Random()

  private var pointer = -1

  def getUserID(): String = {
    pointer = pointer + 1
    if (pointer >= users.length) {
      pointer = 0
      uid(pointer)
    } else {
      uid(pointer)
    }
  }

  def plat(): String = {
    random.nextInt(10) + "10"
  }

  def ip(): String = {
    random.nextInt(10) + ".12.1.211"
  }

  def country(): String = {
    "中國" + random.nextInt(10)
  }

  def city(): String = {
    "深圳" + random.nextInt(10)
  }

  def location(): JSONArray = {
    JSON.parseArray("[" + random.nextInt(10) + "," + random.nextInt(10) + "]")
  }

  def main(args: Array[String]): Unit = {
    val topic = "test_data3"
    val brokers = "dn1:9092,dn2:9092,dn3:9092"
    val props = new Properties()
    props.put("metadata.broker.list", brokers)
    props.put("serializer.class", "kafka.serializer.StringEncoder")

    val kafkaConfig = new ProducerConfig(props)
    val producer = new Producer[String, String](kafkaConfig)

    while (true) {
      val event = new JSONObject()

      event
        .put("_plat", "1001")
        .put("_uid", "10001")
        .put("_tm", (System.currentTimeMillis / 1000).toString())
        .put("ip", ip)
        .put("country", country)
        .put("city", city)
        .put("location", JSON.parseArray("[0,1]"))
      println("Message sent: " + event)
      producer.send(new KeyedMessage[String, String](topic, event.toString))
      
      event
        .put("_plat", "1001")
        .put("_uid", "10001")
        .put("_tm", (System.currentTimeMillis / 1000).toString())
        .put("ip", ip)
        .put("country", country)
        .put("city", city)
        .put("location", JSON.parseArray("[0,1]"))
      println("Message sent: " + event)
      producer.send(new KeyedMessage[String, String](topic, event.toString))
      
      event
        .put("_plat", "1001")
        .put("_uid", "10002")
        .put("_tm", (System.currentTimeMillis / 1000).toString())
        .put("ip", ip)
        .put("country", country)
        .put("city", city)
        .put("location", JSON.parseArray("[0,1]"))
      println("Message sent: " + event)
      producer.send(new KeyedMessage[String, String](topic, event.toString))

      event
        .put("_plat", "1002")
        .put("_uid", "10001")
        .put("_tm", (System.currentTimeMillis / 1000).toString())
        .put("ip", ip)
        .put("country", country)
        .put("city", city)
        .put("location", JSON.parseArray("[0,1]"))
      println("Message sent: " + event)
      producer.send(new KeyedMessage[String, String](topic, event.toString))
      Thread.sleep(30000)
    }
  }
}

   上面程式碼,通過 Thread.sleep() 來控制資料生產的速度。接下來,我們來看看如何實現每個使用者在各個區域所分佈的情況,它是按照座標分組,平臺和使用者ID過濾進行累加次數,邏輯用 SQL 實現較為簡單,關鍵是在實現過程中需要注意的一些問題,比如物件的序列化問題。這裡,細節的問題,我們先不討論,先看下實現的程式碼,如下所示:

object IPLoginAnalytics {

  def main(args: Array[String]): Unit = {
    val sdf = new SimpleDateFormat("yyyyMMdd")
    var masterUrl = "local[2]"
    if (args.length > 0) {
      masterUrl = args(0)
    }

    // Create a StreamingContext with the given master URL
    val conf = new SparkConf().setMaster(masterUrl).setAppName("IPLoginCountStat")
    val ssc = new StreamingContext(conf, Seconds(5))

    // Kafka configurations
    val topics = Set("test_data3")
    val brokers = "dn1:9092,dn2:9092,dn3:9092"
    val kafkaParams = Map[String, String](
      "metadata.broker.list" -> brokers, "serializer.class" -> "kafka.serializer.StringEncoder")

    val ipLoginHashKey = "mf::ip::login::" + sdf.format(new Date())

    // Create a direct stream
    val kafkaStream = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics)

    val events = kafkaStream.flatMap(line => {
      val data = JSONObject.fromObject(line._2)
      Some(data)
    })

    def func(iter: Iterator[(String, String)]): Unit = {
      while (iter.hasNext) {
        val item = iter.next()
        println(item._1 + "," + item._2)
      }
    }

    events.foreachRDD { rdd =>
      // Get the singleton instance of SQLContext
      val sqlContext = SQLContextSingleton.getInstance(rdd.sparkContext)
      import sqlContext.implicits._
      // Convert RDD[String] to DataFrame
      val wordsDataFrame = rdd.map(f => Record(f.getString("_plat"), f.getString("_uid"), f.getString("_tm"), f.getString("country"), f.getString("location"))).toDF()

      // Register as table
      wordsDataFrame.registerTempTable("events")
      // Do word count on table using SQL and print it
      val wordCountsDataFrame = sqlContext.sql("select location,count(distinct plat,uid) as value from events where from_unixtime(tm,'yyyyMMdd') = '" + sdf.format(new Date()) + "' group by location")
      var results = wordCountsDataFrame.collect().iterator

      /**
       * Internal Redis client for managing Redis connection {@link Jedis} based on {@link RedisPool}
       */
      object InternalRedisClient extends Serializable {

        @transient private var pool: JedisPool = null

        def makePool(redisHost: String, redisPort: Int, redisTimeout: Int,
          maxTotal: Int, maxIdle: Int, minIdle: Int): Unit = {
          makePool(redisHost, redisPort, redisTimeout, maxTotal, maxIdle, minIdle, true, false, 10000)
        }

        def makePool(redisHost: String, redisPort: Int, redisTimeout: Int,
          maxTotal: Int, maxIdle: Int, minIdle: Int, testOnBorrow: Boolean,
          testOnReturn: Boolean, maxWaitMillis: Long): Unit = {
          if (pool == null) {
            val poolConfig = new GenericObjectPoolConfig()
            poolConfig.setMaxTotal(maxTotal)
            poolConfig.setMaxIdle(maxIdle)
            poolConfig.setMinIdle(minIdle)
            poolConfig.setTestOnBorrow(testOnBorrow)
            poolConfig.setTestOnReturn(testOnReturn)
            poolConfig.setMaxWaitMillis(maxWaitMillis)
            pool = new JedisPool(poolConfig, redisHost, redisPort, redisTimeout)

            val hook = new Thread {
              override def run = pool.destroy()
            }
            sys.addShutdownHook(hook.run)
          }
        }

        def getPool: JedisPool = {
          assert(pool != null)
          pool
        }
      }

      // Redis configurations
      val maxTotal = 10
      val maxIdle = 10
      val minIdle = 1
      val redisHost = "dn1"
      val redisPort = 6379
      val redisTimeout = 30000
      InternalRedisClient.makePool(redisHost, redisPort, redisTimeout, maxTotal, maxIdle, minIdle)
      val jedis = InternalRedisClient.getPool.getResource
      while (results.hasNext) {
        var item = results.next()
        var key = item.getString(0)
        var value = item.getLong(1)
        jedis.hincrBy(ipLoginHashKey, key, value)
      }
    }

    ssc.start()
    ssc.awaitTermination()

  }
}

/** Case class for converting RDD to DataFrame */
case class Record(plat: String, uid: String, tm: String, country: String, location: String)

/** Lazily instantiated singleton instance of SQLContext */
object SQLContextSingleton {

  @transient private var instance: SQLContext = _

  def getInstance(sparkContext: SparkContext): SQLContext = {
    if (instance == null) {
      instance = new SQLContext(sparkContext)
    }
    instance
  }
}

  我們在開發環境進行測試的時候,使用 local[k] 部署模式,在本地啟動 K 個 Worker 執行緒來進行計算,而這 K 個 Worker 在同一個 JVM 中,上面的示例,預設使用 local[k] 模式。這裡我們需要普及一下 Spark 的架構,架構圖來自 Spark 的官網,[連結地址]

  這裡,不管是在 local[k] 模式,Standalone 模式,還是 Mesos 或是 YARN 模式,整個 Spark Cluster 的結構都可以用改圖來闡述,只是各個元件的執行環境略有不同,從而導致他們可能執行在分散式環境,本地環境,亦或是一個 JVM 實利當中。例如,在 local[k] 模式,上圖表示在同一節點上的單個程式上的多個元件,而對於 YARN 模式,驅動程式是在 YARN Cluster 之外的節點上提交 Spark 應用,其他元件都是執行在 YARN Cluster 管理的節點上的。

  而對於 Spark Cluster 部署應用後,在進行相關計算的時候會將 RDD 資料集上的函式傳送到叢集中的 Worker 上的 Executor,然而,這些函式做操作的物件必須是可序列化的。上述程式碼利用 Scala 的語言特性,解決了這一問題。

4.結果預覽

  在完成上述程式碼後,我們執行程式碼,看看預覽結果如下,執行結果,如下所示:

4.1 啟動生產執行緒

4.2 Redis 結果預覽

5.總結

  整體的實現內容不算太複雜,統計的業務指標,這裡我們使用 SQL 來完成這部分工作,對比 Storm 來說,我們專注 SQL 的編寫就好,難度不算太大。可操作性較為友好。

6.結束語

  這篇部落格就和大家分享到這裡,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或傳送郵件給我,我會盡我所能為您解答,與君共勉!

相關文章