SparkStreaming 输出算子--foreachRDD

​ 在使用SparkStreaming时不可避免的要将处理过后的的数据输出到外部数据源,如redis,mysql,hbase等数据库中。而连接这些外部数据源时需要一些连接对象(如jdbc连接mysql),因而在使用foreachRDD时常出现以下情况:

dstream.foreachRDD { rdd =>
  val connection = createNewConnection()  // 在驱动器(driver)进程执行
  rdd.foreach { record =>
    connection.send(record) // 将在worker节点上执行
  }
}

但是上述写入数据库的代码是错误的。因为它需要把连接对象connection序列化,再从驱动器节点发送到worker节点。而这些连接对象通常都是不能跨节点(机器)传递的。

连接对象通常都不能序列化,或者在另一个进程中反序列化后再次初始化(连接对象通常都需要初始化,因此从驱动节点发到worker节点后可能需要重新初始化)。

为了解决上面代码的问题,需要在worker节点上创建连接对象,如:

dstream.foreachRDD { rdd =>
  rdd.foreach { record =>
    val connection = createNewConnection()
    connection.send(record)
    connection.close()
  }
}

但是每次向数据发送数据都创建一个连接对象是非常消耗资源的,会大大减小系统的吞吐量。

因而,一个比较好的解决方案是使用 rdd.foreachPartition – 为RDD的每个分区创建一个单独的连接对象。

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    val connection = createNewConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    connection.close()
  }
}

foreachPartition 将连接对象的创建开销就摊到很多条记录上,减少了资源的消耗。

wordCount代码如下:

object SaveasMysqlApp {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[2]").setAppName("SaveasMysqlApp")
    val ssc = new StreamingContext(conf,Seconds(5))

    val lines = ssc.socketTextStream("localhost",6789)
    val result = lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)

    result.foreachRDD( rdd => {
      rdd.foreachPartition{ PartitionRecord =>
        val connection = createConnection()  // 这行在驱动器(driver)进程执行
        PartitionRecord.foreach(record=>{
          val sql = "insert into wordcount(word,wordcount) values('" +record._1+ "'," + record._2 + ")"
          connection.createStatement().execute(sql)
        })
        connection.close()
      }
    })
    result.print()

    ssc.start()
    ssc.awaitTermination()
  }
  def createConnection() = {
   Class.forName("com.mysql.jdbc.Driver")
   DriverManager.getConnection("jdbc:mysql://localhost:3306/sparkstream","root","hadoop")
  }
}

而最好的办法是在多个RDD批次之间也复用连接对象。可以创建一个静态连接池,在需要连接的时候从连接池中取出连接,使用结束后放回连接池。示例如下:

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    // ConnectionPool 是一个静态的、懒惰初始化的连接池
    val connection = ConnectionPool.getConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    ConnectionPool.returnConnection(connection)  // 将连接返还给连接池,以便后续复用之
  }
}
posted @ 2020-04-04 00:10  枫林晔雪  阅读(757)  评论(0)    收藏  举报