Spark Streaming持久化设计模式
DStreams输出操作
- print:打印driver结点上每个Dstream中的前10个batch元素,常用于开发和调试
- saveAsTextFiles(prefix, [suffix]):将当前Dstream保存为文件,每个interval batch的文件名命名规则基于prefix和suffix:"prefix-TIME_IN_MS[.suffix]".
- saveAsObjectFiles(prefix, [suffix]):将当前的Dstream内容作为Java可序列化对象的序列化文件进行保存,每个interval batch的文件命名规则基于prefix和suffix:: "prefix-TIME_IN_MS[.suffix]".
- saveAsHadoopFiles(prefix, [suffix]):将Dstream以hadoop文件的形式进行保存,每个interval batch的文件命名规则基于prefix和suffix:: "prefix-TIME_IN_MS[.suffix]".
- foreachRDD(func):最通用的输出操作,可以对从数据流中产生的每一个RDD应用函数_fun_。通常_fun_会将每个RDD中的数据保存到外部系统,如:将RDD保存到文件,或者通过网络连接保存到数据库。值得注意的是:_fun_执行在跑应用的driver进程中,并且通常会包含RDD action以促使数据流RDD开始计算。
使用foreachRDD的设计模式
dstream.foreachRDD对于开发而言提供了很大的灵活性,但在使用时也要避免很多常见的坑。我们通常将数据保存到外部系统中的流程是:建立远程连接->通过连接传输数据到远程系统->关闭连接。针对这个流程我们很直接的想到了下面的程序代码:
//遍历数据流中的每一个RDD与已有数据进行匹配 ds.foreachRDD(r => { println("监控到" + r.count() + "条数据") if (r.count() > 0) { //获取mysql的连接 val conn: Connection = MySqlUtil.getConnection //遍历RDD r.foreach(tuple => { insertIntoMySQL(conn, sql, tuple) }) MySqlUtil.close(conn) } })插入数据库的方法
def insertIntoMySQL(con: Connection, sql: String, data: Tuple8[String, String, String, String, String, String, String, String]): Unit = { try { val ps = con.prepareStatement(sql) ps.setString(1, data._1) ps.setString(2, data._2) ps.setString(3, data._3) ps.setString(4, data._4) ps.setString(5, data._5) ps.setString(6, data._6) ps.setString(7, data._7) ps.setString(8, data._8) ps.executeUpdate() ps.close() } catch { case exception: Exception => exception.printStackTrace() } }
在 spark踩坑记——初试中 ,对spark的worker和driver进行了整理,我们知道在集群模式下,上述代码中的connection需要通过序列化对象的形式从driver发送到worker,但是connection是无法在机器之间传递的,即connection是无法序列化的,这样可能会引起_serialization errors (connection object not serializable)_的错误。为了避免这种错误,我们将conenction在worker当中建立,代码如下:
//遍历数据流中的每一个RDD与已有数据进行匹配 ds.foreachRDD(r => { println("监控到" + r.count() + "条数据") if (r.count() > 0) { r.foreach(tuple => { //获取mysql的连接 val conn: Connection = MySqlUtil.getConnection insertIntoMySQL(conn, sql, tuple) MySqlUtil.close(conn) }) } })
似乎这样问题解决了?但是细想下,我们在每个rdd的每条记录当中都进行了connection的建立和关闭,这会导致不必要的高负荷并且降低整个系统的吞吐量。所以一个更好的方式是使用_rdd.foreachPartition_即对于每一个rdd的partition建立唯一的连接(注:每个partition是内的rdd是运行在同一worker之上的),代码如下:
ds.foreachRDD(r => { println("监控到" + r.count() + "条数据") if (r.count() > 0) { //遍历RDD r.foreachPartition(x => { //获取mysql的连接 while (x.hasNext) { val conn: Connection = MySqlUtil.getConnection insertIntoMySQL(conn, sql, x.next()) MySqlUtil.close(conn) } }) } })
这样我们降低了频繁建立连接的负载,通常我们在连接数据库时会使用连接池,通过持有一个静态连接池对象,我们可以重复利用connection而进一步优化了连接建立的开销,从而降低了负载。另外值得注意的是,同数据库的连接池类似,我们这里所说的连接池同样应该是lazy的按需建立连接,并且及时的收回超时的连接。
另外值得注意的是:
- 如果在spark streaming中使用了多次foreachRDD,它们之间是按照程序顺序向下执行的
- Dstream对于输出操作的执行策略是lazy的,所以如果我们在foreachRDD中不添加任何RDD action,那么系统仅仅会接收数据然后将数据丢弃。
Spark访问Mysql
我们需要有一个可序列化的类来建立Mysql连接,这里我们利用了Mysql的C3P0连接池
MySQL通用连接类
import java.sql.Connection import java.util.Properties import com.mchange.v2.c3p0.ComboPooledDataSource class MysqlPool extends Serializable { private val cpds: ComboPooledDataSource = new ComboPooledDataSource(true) private val conf = Conf.mysqlConfig try { cpds.setJdbcUrl(conf.get("url").getOrElse("jdbc:mysql://127.0.0.1:3306/test_bee?useUnicode=true&characterEncoding=UTF-8")); cpds.setDriverClass("com.mysql.jdbc.Driver"); cpds.setUser(conf.get("username").getOrElse("root")); cpds.setPassword(conf.get("password").getOrElse("")) cpds.setMaxPoolSize(200) cpds.setMinPoolSize(20) cpds.setAcquireIncrement(5) cpds.setMaxStatements(180) } catch { case e: Exception => e.printStackTrace() } def getConnection: Connection = { try { return cpds.getConnection(); } catch { case ex: Exception => ex.printStackTrace() null } } } object MysqlManager { var mysqlManager: MysqlPool = _ def getMysqlManager: MysqlPool = { synchronized { if (mysqlManager == null) { mysqlManager = new MysqlPool } } mysqlManager } }
我们利用c3p0建立Mysql连接池,然后访问的时候每次从连接池中取出连接用于数据传输。
Mysql输出操作
同样利用之前的foreachRDD设计模式,将Dstream输出到mysql的代码如下:
dstream.foreachRDD(rdd => { if (!rdd.isEmpty) { rdd.foreachPartition(partitionRecords => { //从连接池中获取一个连接 val conn = MysqlManager.getMysqlManager.getConnection val statement = conn.createStatement try { conn.setAutoCommit(false) partitionRecords.foreach(record => { val sql = "insert into table..." // 需要执行的sql操作 statement.addBatch(sql) }) statement.executeBatch conn.commit } catch { case e: Exception => // do some log } finally { statement.close() conn.close() } }) } })
值得注意的是:
- 我们在提交Mysql的操作的时候,并不是每条记录提交一次,而是采用了批量提交的形式,所以需要将conn.setAutoCommit(false),这样可以进一步提高mysql的效率。
- 如果我们更新Mysql中带索引的字段时,会导致更新速度较慢,这种情况应想办法避免,如果不可避免,那就没办法了