前言
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了