文章目录
一、流处理数据Sink到目的地的N种错误操作
pom.xml添加依赖:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
在MySQL中创建wc表:
create table wc(
word varchar(20),
`count` int(10)
);
编写StreamingSinkApp.scala代码:
package com.huiq.test
import java.sql.DriverManager
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object StreamingSinkApp {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf()
.setMaster("local[2]")
.setAppName("StreamingSinkApp")
val ssc = new StreamingContext(sparkConf, Seconds(5))
val lines = ssc.socketTextStream("hadoop001", 8999)
// lines.print()
// TODO... 需要将从socke端接收到的数据进行处理(WordCount)之后写入到MySQL中
val result = lines.flatMap(_.split(",")).map((_,1)).reduceByKey(_+_)
// 这是一种固定的开发模式,只要streaming输出数据到外部系统,都是按照这个套路来即可
result.foreachRDD(rdd => {
rdd.foreach(pair =>{
val connection = createConnection()
val sql = s"insert into wc(word,count) values('${pair._1}', ${pair._2})"
connection.createStatement().execute(sql)
connection.close()
})
})
ssc.start()
ssc.awaitTermination()
}
def createConnection() = {
Class.forName("com.mysql.cj.jdbc.Driver")
DriverManager.getConnection("jdbc:mysql://localhost:3306/database1", "root", "123456")
}
}
在服务器上用nc命令传输数据:
nc -lk 8999
1.序列化异常:
如果上面的代码写成这样会报错:
result.foreachRDD(rdd => {
val connection = createConnection()
rdd.foreach(pair =>{
val sql = s"insert into wc(word,count) values('${pair._1}', ${pair._2})"
connection.createStatement().execute(sql)
connection.close()
})
})
参考官网:https://spark.apache.org/docs/latest/streaming-programming-guide.html
2.高性能写结果数据:
但是上面的代码性能好吗,看官网说明其实并不是很好:
二、对接Kafka的方式
注:以前官网还有对Kafka 0.8版本(可能有的公司还在使用0.8版本)的对接方式,现在已经更新没有了。
spark-streaming作为一个24*7不间断运行的程序来设计,但是程序都会crash,如果crash了,如何保证数据不丢失,不重复。spark streaming提供了两种streaming input source:
- basic source: Source directly avaliable in the StreamingContext API. Examples: file,socket connnection.
- advanced source: Source like kafka/kinesis, etc. are avaliable through extra utility classes.
这里只讨论高级数据源,因为针对流计算场景,基本数据源不适用。高级数据源,以kafka为例,kafka作为输入源,有两种方式:receive模式(开启WAL即预写日志,将从kafka中接受到的数据写入到日志文件中,所有数据从失败中可恢复)和direct 模式(依靠checkpoint机制来保证)。
三、流处理过程的零数据丢失
当我们正确地部署好Spark Streaming,我们就可以使用Spark Streaming提供的零数据丢失机制。为了体验这个关键的特性,你需要满足以下几个先决条件:
- 输入的数据来自可靠的sources(数据源)和可靠的receivers(接收器);
- 应用程序的metadata被application的driver持久化了(checkpointed );
- 启用了WAL特性(Write ahead log)。
1.可靠的数据源和可靠的接收器:
对于一些输入数据源(比如Kafka),Spark Streaming可以对已经接收的数据进行确认。输入的数据首先被接收器(receivers )所接收,然后存储到Spark中(默认情况下,数据保存到2个执行器中以便进行容错)。数据一旦存储到Spark中,接收器可以对它进行确认(比如,如果消费Kafka里面的数据时可以更新Zookeeper里面的偏移量)。这种机制保证了在接收器突然挂掉的情况下也不会丢失数据:因为数据虽然被接收,但是没有被持久化的情况下是不会发送确认消息的。所以在接收器恢复的时候,数据可以被原端重新发送。
2.元数据持久化(Metadata checkpointing):
可靠的数据源和接收器可以让我们从接收器挂掉的情况下恢复(或者是接收器运行的Exectuor和服务器挂掉都可以)。但是更棘手的问题是,如果Driver挂掉如何恢复?对此开发者们引入了很多技术来让Driver从失败中恢复。其中一个就是对应用程序的元数据进行Checkpint。利用这个特性,Driver可以将应用程序的重要元数据持久化到可靠的存储中,比如HDFS、S3;然后Driver可以利用这些持久化的数据进行恢复。元数据包括:
- 配置;
- 代码;
- 那些在队列中还没有处理的batch(仅仅保存元数据,而不是这些batch中的数据)
由于有了元数据的Checkpint,所以Driver可以利用他们重构应用程序,而且可以计算出Driver挂掉的时候应用程序执行到什么位置。
令人惊讶的是,即使是可靠的数据源、可靠的接收器和对元数据进行Checkpint,仍然不足以阻止潜在的数据丢失。我们可以想象出以下的糟糕场景:两个Exectuor已经从接收器中接收到输入数据,并将它缓存到Exectuor的内存中;接收器通知输入源数据已经接收;Exectuor根据应用程序的代码开始处理已经缓存的数据;这时候Driver突然挂掉了;从设计的角度看,一旦Driver挂掉之后,它维护的Exectuor也将全部被kill;既然所有的Exectuor被kill了,所以缓存到它们内存中的数据也将被丢失。结果,这些已经通知数据源但是还没有处理的缓存数据就丢失了;缓存的时候不可能恢复,因为它们是缓存在Exectuor的内存中,所以数据被丢失了。
这对于很多关键型的应用程序来说非常的糟糕。
3.WAL(Write ahead log):
为了解决上面提到的糟糕场景,Spark Streaming 1.2开始引入了WAL机制。
启用了WAL机制,所以已经接收的数据被接收器写入到容错存储中,比如HDFS或者S3。由于采用了WAl机制,Driver可以从失败的点重新读取数据,即使Exectuor中内存的数据已经丢失了。在这个简单的方法下,Spark Streaming提供了一种即使是Driver挂掉也可以避免数据丢失的机制。
At-least-once语义:
虽然WAL可以确保数据不丢失,它并不能对所有的数据源保证exactly-once语义。想象一下可能发生在Spark Streaming整合Kafka的糟糕场景:接收器接收到输入数据,并把它存储到WAL中;接收器在更新Zookeeper中Kafka的偏移量之前突然挂掉了;Spark Streaming假设输入数据已成功收到(因为它已经写入到WAL中),然而Kafka认为数据没有被消费,因为相应的偏移量并没有在Zookeeper中更新;过了一会,接收器从失败中恢复;那些被保存到WAL中但未被处理的数据被重新读取;一旦从WAL中读取所有的数据之后,接收器开始从Kafka中消费数据。因为接收器是采用Kafka的High-Level Consumer API实现的,它开始从Zookeeper当前记录的偏移量开始读取数据,但是因为接收器挂掉的时候偏移量并没有更新到Zookeeper中,所有有一些数据被处理了2次。
WAL的缺点:
除了上面描述的场景,WAL还有其他两个不可忽略的缺点:WAL减少了接收器的吞吐量,因为接受到的数据必须保存到可靠的分布式文件系统中;对于一些输入源来说,它会重复相同的数据。比如当从Kafka中读取数据,你需要在Kafka的brokers中保存一份数据,而且你还得在Spark Streaming中保存一份。
4.Direct API:
为了解决由WAL引入的性能损失,并且保证 exactly-once 语义,Spark Streaming 1.3中引入了名为Direct API。
这个想法对于这个特性是非常明智的。Spark driver只需要简单地计算下一个batch需要处理Kafka中偏移量的范围,然后命令Spark Exectuor直接从Kafka相应Topic的分区中消费数据。换句话说,这种方法把Kafka当作成一个文件系统,然后像读文件一样来消费Topic中的数据。
在这个简单但强大的设计中:不再需要Kafka接收器,Exectuor直接采用Simple Consumer API从Kafka中消费数据;不再需要WAL机制,我们仍然可以从失败恢复之后从Kafka中重新消费数据;exactly-once语义得以保存,我们不再从WAL中读取重复的数据。
综合以上,direct 模式比receive模式的优点:
- 简化并行读取:如果要读取多个partition,不需要创建多个输入DStream,然后对它们进行union操作。Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据。所以在Kafka partition和RDD partition之间,有一个一对一的映射关系。
- 高性能:如果要保证零数据丢失,在基于receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,Kafka自己本身就有高可靠的机制会对数据复制一份,而这里又会复制一份到WAL中。而基于direct的方式,不依赖Receiver,不需要开启WAL机制,只要Kafka中作了数据的复制,那么就可以通过Kafka的副本进行恢复。
- 一次且仅一次的事务机制:基于receiver的方式,是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和ZooKeeper之间可能是不同步的。基于direct的方式,使用kafka的简单api,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。由于数据消费偏移量是保存在checkpoint中,因此,如果后续想使用kafka高级API消费数据,需要手动的更新zookeeper中的偏移量。