Spark Streaming中foreachRDD算子使用详解

前言

foreachRDD算子会将DStream中的RDD里的数据给写到外部的系统中去;需要注意一点的是,这个函数将会被执行在driver进程当中,而从driver端到executor端必然会涉及到序列化的问题,在本篇文章中将进行详细介绍
官网文档:http://spark.apache.org/docs/latest/streaming-programming-guide.html#design-patterns-for-using-foreachrdd

version 1.0

    // v1.0,会产生报错 Task not serializable
    result.foreachRDD(rdd => {
      // 执行在driver端
      val conn = MySQLUtils.getConnection()

      // 执行在executor端
      rdd.foreach(pair => {
        val sql = s"insert into wc (word, cnt) values ('${pair._1}', ${pair._2})"
        conn.createStatement().execute(sql)
      })

      MySQLUtils.closeConnection(conn)
    })

会报错,Task not serializable:
在这里插入图片描述
从报错信息中我们可以看到closure这个词,即闭包的意思
查看具体的报错原因:
在这里插入图片描述
我们可以发现mysql的驱动就是序列化不了

通过查阅官网文档我们其实也可以发现缘由,因为foreachRDD这个算子是在driver端执行的,而foreach这个算子是在executor端执行的,由于一个在driver端一个在executor端,那么涉及到数据势必是需要进行序列化然后进行传输的,而对于connection对象几乎很少能够跨机器进行传输的,因此会抛Task not serializable的错

闭包

通过上一小节,也就引出了闭包的概念,具体可见官方文档:
http://spark.apache.org/docs/latest/rdd-programming-guide.html#understanding-closures-
当我们的代码在集群中跨机器运行的时候,我们必须理解变量或者方法的生命周期

闭包:在函数内部引用了一个外部的变量
针对v1.0的代码,我们可以发现在foreach方法内部引用了外部的connection变量,因此这就是一个闭包问题

version 2.0

为了解决闭包问题,将外部的connection变量给挪到foreach算子内部来:

    // v2.0,优化解决闭包问题
    result.foreachRDD(rdd => {
      rdd.foreach(pair => {
        val conn = MySQLUtils.getConnection()
        val sql = s"insert into wc (word, cnt) values ('${pair._1}', ${pair._2})"
        conn.createStatement().execute(sql)
        MySQLUtils.closeConnection(conn)
      })
    })

这样运行就OK了,数据也会成功的被写入到MySQL中来
这时,我们假设该RDD有一亿条数据,读取每条数据的时候都会去创建一个connection,这样就会创建一亿个connection,这样在生产中肯定是不可取的,因为不管在时间还是资源上面都是会造成损耗的,是没有必要每条记录都去创建对应的connection并随后去销毁的

version 3.0

针对v2.0的问题,我们再度进行改进之后,如下:

    // v3.0,优化conn创建次数问题
    result.foreachRDD(rdd => {
      rdd.foreachPartition(partition => {
        val conn = MySQLUtils.getConnection()

        partition.foreach(pair => {
          val sql = s"insert into wc (word, cnt) values ('${pair._1}', ${pair._2})"
          conn.createStatement().execute(sql)
        })

        MySQLUtils.closeConnection(conn)
      })
    })

从对RDD中的每条记录进行创建销毁connection的操作,改为了对RDD中的每个partition进行创建销毁connection的操作,这样改进之后,性能肯定是会好很多的
但是这样,还会存在问题,因为当partition数量过多后,一样也是会有问题的

version 4.0

最好的方法是使用连接池,初始化一个连接池,需要连接从连接池中去拿,用完后再放回连接池,这样做的效果肯定是最好的
可以借助scalikejdbc来实现,因为scalikejdbc默认就是实现了连接池,使用的是Apache Commons DBCP,见代码:

    // v4.0,借助scalikejdbc实现连接池
    DBs.setupAll() //解析配置文件application.conf
    result.foreachRDD(rdd => {

      rdd.foreachPartition(partition => {

        partition.foreach(pair => {
          // NamedDB(""),如果配置的DB名称不是default可以在使用其进行指定,默认是default名字无需指定
          // 默认就使用了连接池,可以点进去看源码
          DB.autoCommit {
            implicit session => {
              SQL("insert into wc(word,cnt) values (?, ?)")
                .bind(pair._1, pair._2)
                .update().apply()
            }
          }


        })
      })

    })

我们可以观察autoCommit方法:
在这里插入图片描述
我们可以发现scalikejdbc已经帮我们实现了ConnectionPool了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值