资讯详情

Spark Streaming与流处理

Spark Streaming与流处理

一、流处理

1.1 静态数据处理

流处理前,数据通常存储在数据库、文件系统或其他形式的存储系统中。应用程序根据需要查询或计算数据。这是传统的静态数据处理架构。Hadoop 采用 HDFS 采用数据存储 MapReduce 数据查询或分析是典型的静态数据处理架构。

1.2 流处理

在接收数据时,流处理是直接处理运动中的数据,直接计算数据。

大多数数据是连续流动的:传感器事件、网站上的用户活动、金融交易等 ,这些数据都是随着时间的推移而创建的。

接收和发送数据流并执行应用程序或分析逻辑的系统称为。在具有可扩展性和容错性的同时,流处理器的基本职责是扩展性和容错性,Storm 和 Flink 它的代表性实现。

流处理带来了静态数据处理所不具备的诸多优点:

  • :减少数据滞后,使数据更及时,更能反映对未来的期望;
  • :直接处理数据流,只保留数据中有意义的子集,并将其传输到下一个处理单元,逐步过滤数据,减少需要处理的数据量,更大的数据量;
  • :在实际环境中,所有数据都在不断变化。为了通过过去的数据推断未来的趋势,我们必须确保数据的持续输入和模型的不断修正。典型的是金融市场和股票市场。流程处理可以更好地应对这些数据的连续性特征和及时性;
  • :流处理减少了对大型数据库的需求。相反,每个流处理程序通过流处理框架维护自己的数据和状态,使流处理程序更适合微服务架构。

二、Spark Streaming

2.1 简介

Spark Streaming 是 Spark 用于快速构建可扩展、高吞吐量、高容错流处理程序的子模块。具有以下特点:

  • 通过高级 API 构建应用程序,简单易用;
  • 支持多种语言,如 Java,Scala 和 Python;
  • 容错性好,Spark Streaming 支持从失败中快速恢复丢失的操作状态;
  • 能够和 Spark 其它模块无缝集成,流处理与批处理完美结合;
  • Spark Streaming 可以从 HDFS,Flume,Kafka,Twitter 和 ZeroMQ 还支持自定义数据源读取数据。

2.2 DStream

Spark Streaming 提供称为离散流 (DStream) 用于表示连续数据流的高级抽象。 DStream 可以从来自 Kafka,Flume 和 Kinesis 其他数据源的输入数据流也可以创建 DStream 转化而来。

2.3 Spark & Storm & Flink

storm 和 Flink 都是真正意义上的流计算框架,但是 Spark Streaming 只是将数据流分成极小粒度的多批处理,使其接近流处理,但本质上是批处理(或微批处理)。

Spark Streaming 基本操作

一、案例介绍

先介绍一个基本案例来演示流的创建:在指定端口获取数据并进行词频统计。项目依赖和代码实现如下:

<dependency>     <groupId>org.apache.spark</groupId>     <artifactId>spark-streaming_2.12</artifactId>     <version>2.4.3</version> </dependency> 
import org.apache.spark.SparkConf import org.apache.spark.streaming.{ 
        Seconds, StreamingContext}  object NetworkWordCount { 
            def main(args: Array[String]) { 
        

    /*指定时间间隔为 5s*/
    val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf, Seconds(5))

    /*创建文本输入流,并进行词频统计*/
    val lines = ssc.socketTextStream("hadoop001", 9999)
    lines.flatMap(_.split(" ")).map(x => (x, 1)).reduceByKey(_ + _).print()

    /*启动服务*/
    ssc.start()
    /*等待服务结束*/
    ssc.awaitTermination()
  }
}

使用本地模式启动 Spark 程序,然后使用 nc -lk 9999 打开端口并输入测试数据:

[root@hadoop001 ~]# nc -lk 9999
hello world hello spark hive hive hadoop
storm storm flink azkaban

此时控制台输出如下,可以看到已经接收到数据并按行进行了词频统计。

下面针对示例代码进行讲解:

1.1 StreamingContext

Spark Streaming 编程的入口类是 StreamingContext,在创建时候需要指明 sparkConfbatchDuration(批次时间),Spark 流处理本质是将流数据拆分为一个个批次,然后进行微批处理,batchDuration 就是批次拆分的时间间隔。这个时间可以根据业务需求和服务器性能进行指定,如果业务要求低延迟并且服务器性能也允许,则这个时间可以指定得很短。

这里需要注意的是:示例代码使用的是本地模式,配置为 local[2],这里不能配置为 local[1]。这是因为对于流数据的处理,Spark 必须有一个独立的 Executor 来接收数据,然后再由其他的 Executors 来处理,所以为了保证数据能够被处理,至少要有 2 个 Executors。这里我们的程序只有一个数据流,在并行读取多个数据流的时候,也需要保证有足够的 Executors 来接收和处理数据。

1.2 数据源

在示例代码中使用的是 socketTextStream 来创建基于 Socket 的数据流,实际上 Spark 还支持多种数据源,分为以下两类:

  • :包括文件系统、Socket 连接等;
  • :包括 Kafka,Flume,Kinesis 等。

在基本数据源中,Spark 支持监听 HDFS 上指定目录,当有新文件加入时,会获取其文件内容作为输入流。创建方式如下:

// 对于文本文件,指明监听目录即可
streamingContext.textFileStream(dataDirectory)
// 对于其他文件,需要指明目录,以及键的类型、值的类型、和输入格式
streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)

被监听的目录可以是具体目录,如 hdfs://host:8040/logs/;也可以使用通配符,如 hdfs://host:8040/logs/2017/*

1.3 服务的启动与停止

在示例代码中,使用 streamingContext.start() 代表启动服务,此时还要使用 streamingContext.awaitTermination() 使服务处于等待和可用的状态,直到发生异常或者手动使用 streamingContext.stop() 进行终止。

二、Transformation

2.1 DStream与RDDs

DStream 是 Spark Streaming 提供的基本抽象。它表示连续的数据流。在内部,DStream 由一系列连续的 RDD 表示。所以从本质上而言,应用于 DStream 的任何操作都会转换为底层 RDD 上的操作。例如,在示例代码中 flatMap 算子的操作实际上是作用在每个 RDDs 上 (如下图)。因为这个原因,所以 DStream 能够支持 RDD 大部分的transformation算子。

2.2 updateStateByKey

除了能够支持 RDD 的算子外,DStream 还有部分独有的transformation算子,这当中比较常用的是 updateStateByKey。文章开头的词频统计程序,只能统计每一次输入文本中单词出现的数量,想要统计所有历史输入中单词出现的数量,可以使用 updateStateByKey 算子。代码如下:

object NetworkWordCountV2 { 
        


  def main(args: Array[String]) { 
        

    /* * 本地测试时最好指定 hadoop 用户名,否则会默认使用本地电脑的用户名, * 此时在 HDFS 上创建目录时可能会抛出权限不足的异常 */
    System.setProperty("HADOOP_USER_NAME", "root")
      
    val sparkConf = new SparkConf().setAppName("NetworkWordCountV2").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf, Seconds(5))
    /*必须要设置检查点*/
    ssc.checkpoint("hdfs://hadoop001:8020/spark-streaming")
    val lines = ssc.socketTextStream("hadoop001", 9999)
    lines.flatMap(_.split(" ")).map(x => (x, 1))
      .updateStateByKey[Int](updateFunction _)   //updateStateByKey 算子
      .print()

    ssc.start()
    ssc.awaitTermination()
  }

  /** * 累计求和 * * @param currentValues 当前的数据 * @param preValues 之前的数据 * @return 相加后的数据 */
  def updateFunction(currentValues: Seq[Int], preValues: Option[Int]): Option[Int] = { 
        
    val current = currentValues.sum
    val pre = preValues.getOrElse(0)
    Some(current + pre)
  }
}

使用 updateStateByKey 算子,你必须使用 ssc.checkpoint() 设置检查点,这样当使用 updateStateByKey 算子时,它会去检查点中取出上一次保存的信息,并使用自定义的 updateFunction 函数将上一次的数据和本次数据进行相加,然后返回。

2.3 启动测试

在监听端口输入如下测试数据:

[root@hadoop001 ~]# nc -lk 9999
hello world hello spark hive hive hadoop
storm storm flink azkaban
hello world hello spark hive hive hadoop
storm storm flink azkaban

此时控制台输出如下,所有输入都被进行了词频累计:

同时在输出日志中还可以看到检查点操作的相关信息:

# 保存检查点信息
19/05/27 16:21:05 INFO CheckpointWriter: Saving checkpoint for time 1558945265000 ms 
to file 'hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000'

# 删除已经无用的检查点信息
19/05/27 16:21:30 INFO CheckpointWriter: 
Deleting hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000

三、输出操作

3.1 输出API

Spark Streaming 支持以下输出操作:

Output Operation Meaning
() 在运行流应用程序的 driver 节点上打印 DStream 中每个批次的前十个元素。用于开发调试。
(prefix, [suffix]) 将 DStream 的内容保存为文本文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。
(prefix, [suffix]) 将 DStream 的内容序列化为 Java 对象,并保存到 SequenceFiles。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。
(prefix, [suffix]) 将 DStream 的内容保存为 Hadoop 文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。
(func) 最通用的输出方式,它将函数 func 应用于从流生成的每个 RDD。此函数应将每个 RDD 中的数据推送到外部系统,例如将 RDD 保存到文件,或通过网络将其写入数据库。

前面的四个 API 都是直接调用即可,下面主要讲解通用的输出方式 foreachRDD(func),通过该 API 你可以将数据保存到任何你需要的数据源。

3.2 foreachRDD

这里我们使用 Redis 作为客户端,对文章开头示例程序进行改变,把每一次词频统计的结果写入到 Redis,并利用 Redis 的 HINCRBY 命令来进行词频统计。这里需要导入 Jedis 依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

具体实现代码如下:

import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{ 
        Seconds, StreamingContext}
import redis.clients.jedis.Jedis

object NetworkWordCountToRedis { 
        
  
    def main(args: Array[String]) { 
        

    val sparkConf = new SparkConf().setAppName("NetworkWordCountToRedis").setMaster("local[2]")
    val ssc = new StreamingContext(sparkConf, Seconds(5))

    /*创建文本输入流,并进行词频统计*/
    val lines = ssc.socketTextStream("hadoop001", 9999)
    val pairs: DStream[(String, Int)] = lines.flatMap(_.split(" ")).map(x => (x, 1)).reduceByKey(_ + _)
     /*保存数据到 Redis*/
    pairs.foreachRDD { 
         rdd =>
      rdd.foreachPartition { 
         partitionOfRecords =>
        var jedis: Jedis = null
        try { 
        
          jedis = JedisPoolUtil.getConnection
          partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2))
        } catch { 
        
          case ex: Exception =>
            ex.printStackTrace()
        } finally { 
        
          if (jedis != null) jedis.close()
        }
      }
    }
    ssc.start()
    ssc.awaitTermination()
  }
}

其中 JedisPoolUtil 的代码如下:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolUtil { 
        

    /* 声明为 volatile 防止指令重排序 */
    private static volatile JedisPool jedisPool = null;
    private static final String HOST = "localhost";
    private static final int PORT = 6379;

    /* 双重检查实现懒汉式单例 */
    public static Jedis getConnection() { 
        
        if (jedisPool == null) { 
        
            synchronized (JedisPoolUtil.class) { 
        
                if (jedisPool == null) { 
        
                    JedisPoolConfig config = new JedisPoolConfig();
                    config.setMaxTotal(30);
                    config.setMaxIdle(10);
                    jedisPool = new JedisPool(config, HOST, PORT);
                }
            }
        }
        return jedisPool.getResource();
    }
}

3.3 代码说明

这里将上面保存到 Redis 的代码单独抽取出来,并去除异常判断的部分。精简后的代码如下:

pairs.foreachRDD { 
         rdd =>
  rdd.foreachPartition { 
         partitionOfRecords =>
    val jedis = JedisPoolUtil.getConnection
    partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2))
    jedis.close()
  }
}

这里可以看到一共使用了三次循环,分别是循环 RDD,循环分区,循环每条记录,上面我们的代码是在循环分区的时候获取连接,也就是为每一个分区获取一个连接。但是这里大家可能会有疑问:为什么不在循环 RDD 的时候,为每一个 RDD 获取一个连接,这样所需要的连接数会更少。实际上这是不可行的,如果按照这种情况进行改写,如下:

pairs.foreachRDD { 
         rdd =>
    val jedis = JedisPoolUtil.getConnection
    rdd.foreachPartition { 
         partitionOfRecords =>
        partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2))
    }
    jedis.close()
}

此时在执行时候就会抛出 Caused by: java.io.NotSerializableException: redis.clients.jedis.Jedis,这是因为在实际计算时,Spark 会将对 RDD 操作分解为多个 Task,Task 运行在具体的 Worker Node 上。在执行之前,Spark 会对任务进行闭包,之后闭包被序列化并发送给每个 Executor,而 Jedis 显然是不能被序列化的,所以会抛出异常。

第二个需要注意的是 ConnectionPool 最好是一个静态,惰性初始化连接池 。这是因为 Spark 的转换操作本身就是惰性的,且没有数据流时不会触发写出操作,所以出于性能考虑,连接池应该是惰性的,因此上面 JedisPool 在初始化时采用了懒汉式单例进行惰性初始化。

3.4 启动测试

在监听端口输入如下测试数据:

[root@hadoop001 ~]# nc -lk 9999
hello world hello spark hive hive hadoop
storm storm flink azkaban
hello world hello spark hive hive hadoop
storm storm flink azkaban

使用 Redis Manager 查看写入结果 (如下图),可以看到与使用 updateStateByKey 算子得到的计算结果相同。

Spark Streaming 整合 Kafka

一、版本说明

Spark 针对 Kafka 的不同版本,提供了两套整合方案:spark-streaming-kafka-0-8spark-streaming-kafka-0-10,其主要区别如下:

spark-streaming-kafka-0-8 spark-streaming-kafka-0-10
Kafka 版本 0.8.2.1 or higher 0.10.0 or higher
AP 状态 Deprecated从 Spark 2.3.0 版本开始,Kafka 0.8 支持已被弃用 Stable(稳定版)
语言支持 Scala, Java, Python Scala, Java
Receiver DStream Yes No
Direct DStream Yes Yes
SSL / TLS Support No Yes
Offset Commit API(偏移量提交) No Yes
Dynamic Topic Subscription(动态主题订阅) No Yes

本文使用的 Kafka 版本为 kafka_2.12-2.2.0,故采用第二种方式进行整合。

二、项目依赖

项目采用 Maven 进行构建,主要依赖如下:

<properties>
    <scala.version>2.12</scala.version>
</properties>

<dependencies>
    <!-- Spark Streaming-->
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-streaming_${scala.version}</artifactId>
        <version>${spark.version}</version>
    </dependency>
    <!-- Spark Streaming 整合 Kafka 依赖-->
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-streaming-kafka-0-10_${scala.version}</artifactId>
        <version>2.4.3</version>
    </dependency>
</dependencies>

完整源码见本仓库:spark-streaming-kafka

三、整合Kafka

通过调用 KafkaUtils 对象的 createDirectStream 方法来创建输入流,完整代码如下:

import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.{ 
        Seconds, StreamingContext}

/** * spark streaming 整合 kafka */
object KafkaDirectStream { 
        

  def main(args: Array[String]): Unit = { 
        

    val sparkConf = new SparkConf().setAppName("KafkaDirectStream").setMaster("local[2]")
    val streamingContext = new StreamingContext(sparkConf, Seconds(5))

    val kafkaParams = Map[String, Object](
      /* * 指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找其他 broker 的信息。 * 不过建议至少提供两个 broker 的信息作为容错。 */
      "bootstrap.servers" -> "hadoop001:9092",
      /*键的序列化器*/
      "key.deserializer" -> classOf[StringDeserializer],
      /*值的序列化器*/
      "value.deserializer" -> classOf[StringDeserializer],
      /*消费者所在分组的 ID*/
      "group.id" -> "spark-streaming-group",
      /* * 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理: * latest: 在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录) * earliest: 在偏移量无效的情况下,消费者将从起始位置读取分区的记录 */
      "auto.offset.reset" -> "latest",
      /*是否自动提交*/
      "enable.auto.commit" -> (true: java.lang.Boolean)
    )
    
    /*可以同时订阅多个主题*/
    val topics = Array("spark-streaming-topic")
    val stream = KafkaUtils.createDirectStream[String, String](
      streamingContext,
      /*位置策略*/
      PreferConsistent,
      /*订阅主题*/
      Subscribe[String, String](topics, kafkaParams)
    )

    /*打印输入流*/
    stream.map(record => (record.key, record.value)).print()

    streamingContext.start()
    streamingContext.awaitTermination()
  }
}

3.1 ConsumerRecord

这里获得的输入流中每一个 Record 实际上是 ConsumerRecord<K, V> 的实例,其包含了 Record 的所有可用信息,源码如下:

public class ConsumerRecord<K, V> { 
        
    
    public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP;
    public static final int NULL_SIZE = -1;
    public static final int NULL_CHECKSUM = -1;
    
    /*主题名称*/
    private final String topic;
    /*分区编号*/
    private final int partition;
    /*偏移量*/
    private final long offset;
    /*时间戳*/
    private final long timestamp;
    /*时间戳代表的含义*/
    private final TimestampType timestampType;
    /*键序列化器*/
    private final int serializedKeySize;
    /*值序列化器*/
    private final int serializedValueSize;
    /*值序列化器*/
    private final Headers headers;
    /*键*/
    private final K key;
    /*值*/
    private final V value;
    .....   
}

3.2 生产者属性

在示例代码中 kafkaParams 封装了 Kafka 消费者的属性,这些属性和 Spark Streaming 无关,是 Kafka 原生 API 中就有定义的。其中服务器地址、键序列化器和值序列化器是必选的,其他配置是可选的。其余可选的配置项如下:

1. fetch.min.byte

消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker 会等待有足够的可用数据时才会把它返回给消费者。

2. fetch.max.wait.ms

broker 返回给消费者数据的等待时间。

3. max.partition.fetch.bytes

分区返回给消费者的最大字节数。

4. session.timeout.ms

消费者在被认为死亡之前可以与服务器断开连接的时间。

5. auto.offset.reset

该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:

  • latest(默认值) :在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据;
  • earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录。

6. enable.auto.commit

是否自动提交偏移量,默认值是 true,为了避免出现重复数据和数据丢失,可以把它设置为 false。

7. client.id

客户端 id,服务器用来识别消息的来源。

8. max.poll.records

单次调用 poll() 方法能够返回的记录数量。

9. receive.buffer.bytes 和 send.buffer.byte

标签: jl14系列连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台