大数据篇--SparkStreaming调优

一、流处理数据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机制来保证)。

1.Spark streaming消费kafka数据通过手动管理kafkaoffset保证实时流消费数据的一致性

  第一点:需要明白 kafka 的底层机制及工作原理,这里只简要说明,详细的参考 kafka 官网。kafka 是将每一条写入 kafka 的数据按分区分布存储,将每条写入的数据作一个 offset 标记,这个标记的顺序是按插入数据自增的。当消费程序的时候,会按照分区区分,逐个根据 offset 顺序消费。当在消费数据时,如果将自动提交 offset 参数设置为 true(enable.auto.commit=true),那么不管消费数据的结果是否正确,只要消费数据程序没有因为异常而中断,kafka 都会讲数据的 offset 信息按照分区组合的方式存在依赖的 zookeeper上。反之当 enable.auto.commit=false 时,消费程序及时消费结果正确,程序没有中断都不会提交 offset,需要程序手动提交 offset。举一个场景,如果消费程序没有出现异常,但消费数据的结果不对,应该是不提交 offset 的,当优化了流式消费程序,在启动消费程序,应该必须能消费到之前消费结果不对的数据。但是前者已经提交了 offset,没法拿到了。而后者能够很好的解决这个问题,提交与不提交 offset,由消费程序自己决定。

  第二点:需要清楚spark的底层机制,这里做简要说明,详细的参考spark官网。spark在对数据进行分布式计算时(不管是流式还是离线批),都是将数据读成RDD,然后在对RDD进行spark自带算子计算和spark的方法API进行业务处理,而这两种本质上是一样的。这里以计算数据写入下游某个组件举例重点说方法API。一般情况下,spark都是将获取的数据RDD做如下操作:

  1. 先对rdd进行foreach得到每个Partition
  2. 在对每个Partition进行遍历得到Partition里面的数据,这里是一个迭代器(iterator),iterator里面就是实际每一条数据
	rdd.foreachRDD(new VoidFunction<JavaRDD<String>>() {
            @Override
            public void call(JavaRDD<String> partition) throws Exception {
                partition.foreachPartition(new VoidFunction<Iterator<String>>() {
                    @Override
                    public void call(Iterator<String> iterator) throws Exception {                     
                        while (iterator.hasNext()) {
                            String message = iterator.next();
                    	}
                });
            }
        });

  明白一二两点后就需要思考 spark streaming 消费 kafka 数据时如何保存数据一致性,怎样去保证。这里其实不难,有两种方式:

  第一种:将 spark 获取到的 kafka 数据转化成的 rdd 对应的 kafka 的 offset 全部拿出来,以 rdd 为实体,当 rdd.foreachRDD 整个无误后手动提交 offset。

  第二种:在 rdd.foreachRDD 里面的 partition.foreachPartition 里面将每个 partition 对应的数据的 kafka 数据的 offset 查询出来,然后单个 partition 处理无误后提交单个 partition 对应的kafka数据的 offset。

  第一种和第二种比较,他们的原理基本相同,第一种获取的 offset 其实也是第二种的数组,而第一种更加广义,第二种更加详细。

  可能有读者会问,为什么不更加详细点,在 partition.foreachPartition 里面迭代每一条数据时,将每一条数据的 offset 获取出来,成功一条,提交该条对应的 offset。为什么不这样做,有一下三点原因:

  1. 分而治之,在保证数据质量的同时,要确保性能和其他指标,如果消费一条数据保存该数据的 offset,势必会带来性能的影响。而按照分区的方式,一个分区里面的每一条记录都消费成功,证明这个分区处理是无误的,则提交 offset。如果分区里的每一条和几条数据消费不成功,则认为该分区处理是不成功的,不提交 offset,待修复后再一次消费这个 partition 对应的 kafka 的 offset 数据,这样肯定会造成数据重复,但一定不会造成数据遗漏。而大数据处理中,数据重复从来不是问题。但数据遗漏是不被允许的。
  2. spark streaming 消费 kafka 数据的官方 api 中并没有这样的 api,而是将 partition 作为一个整体的到 offset 的信息。

参考:Spark streaming消费kafka数据通过手动管理kafkaoffset保证实时流消费数据的一致性

  实操:

            // 控制日志级别
            Logger.getLogger("org.apache.spark").setLevel(Level.WARN);
            SparkConf sparkConf = new SparkConf()
//                    .setMaster("local[*]")
                    .setAppName("SparkStreamingTest")
                    .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
                    .set("spark.driver.allowMultipleContexts", "true")
                    .set("spark.streaming.backpressure.enabled", "true"); // 是否开启反压

            // 窗口间隔
            JavaSparkContext sc = new JavaSparkContext(sparkConf);

            // 设置调用时间5秒获取一批数据
            JavaStreamingContext streamingContext = new JavaStreamingContext(sc, Durations.seconds(30));

            // 从MySQL中读取该主题对应的消费者组的分区偏移量
            Map<TopicPartition, Long> offsetMap = JDBCConnPool.getOffsetMap(groupId, topic);

            JavaInputDStream<ConsumerRecord<String, String>> javaInputDStream;

            //如果MySQL中已经存在了偏移量,则应该从该偏移量处开始消费
            if (offsetMap.size() > 0) {
                System.out.println("存在偏移量,从该偏移量处进行消费!!" + offsetMap);
                // 存在偏移量,从该偏移量处进行消费
                javaInputDStream = KafkaUtils.createDirectStream(
                        streamingContext,
                        LocationStrategies.PreferConsistent(),
                        ConsumerStrategies.<String, String>Subscribe(topics, kafkaParams, offsetMap));
            } else {
                System.out.println("不存在偏移量,从最早开始消费!!");
                //如果MySQL中没有存在了偏移量,从最早开始消费
                javaInputDStream = KafkaUtils.createDirectStream(
                        streamingContext,
                        LocationStrategies.PreferConsistent(),
                        ConsumerStrategies.Subscribe(topics, kafkaParams));
            }

            // kafka实时数据计算和保存HBASE
            javaInputDStream.foreachRDD(rdd -> {
                // 获取rdd中offset相关的信息:offsetRanges里面就包含了该批次各个分区的offset信息
                OffsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges();

                // 循环分区, 处理 Kafka 消息
                rdd.map(ConsumerRecord::value).foreachPartition(partitionRecords -> {
                    OffsetRange offset = offsetRanges[TaskContext.get().partitionId()];
                    long dataCount = offset.count();//数据总量
                    long successCount = 0;//写入成功总量
                    
                	while (partitionRecords.hasNext()) {
                		String originalData = partitionRecords.next();
                		System.out.println("originalData-->"+originalData);

						// ...
						successCount++;
                	}
                    // 根据offset将数据的处理结果写到mysql表中,如果dataCount=0,证明这一批流没有数据,不需要写
                    if (dataCount > 0) {
                        long failedCount = dataCount - successCount;//写入失败总量
                        if (failedCount == 0) {
                            JDBCConnPool.saveOffsetPartition(groupId, offset);
                        }
                    }
                });
            });

            streamingContext.start();
            streamingContext.awaitTermination();

三、流处理过程的零数据丢失

  当我们正确地部署好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模式的优点:

  1. 简化并行读取:如果要读取多个partition,不需要创建多个输入DStream,然后对它们进行union操作。Spark会创建跟Kafka partition一样多的RDD partition,并且会并行从Kafka中读取数据。所以在Kafka partition和RDD partition之间,有一个一对一的映射关系。
  2. 高性能:如果要保证零数据丢失,在基于receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,Kafka自己本身就有高可靠的机制会对数据复制一份,而这里又会复制一份到WAL中。而基于direct的方式,不依赖Receiver,不需要开启WAL机制,只要Kafka中作了数据的复制,那么就可以通过Kafka的副本进行恢复。
  3. 一次且仅一次的事务机制:基于receiver的方式,是使用Kafka的高阶API来在ZooKeeper中保存消费过的offset的。这是消费Kafka数据的传统方式。这种方式配合着WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为Spark和ZooKeeper之间可能是不同步的。基于direct的方式,使用kafka的简单api,Spark Streaming自己就负责追踪消费的offset,并保存在checkpoint中。Spark自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。由于数据消费偏移量是保存在checkpoint中,因此,如果后续想使用kafka高级API消费数据,需要手动的更新zookeeper中的偏移量。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小强签名设计

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值