一、案例引入

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

<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

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

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

3.1 StreamingContext

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

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

3.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/*

关于高级数据源的整合单独整理至:Spark Streaming整合FlumeSpark Streaming整合Kafka

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

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

3.1 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-basis

参考资料

Spark官方文档:http://spark.apache.org/docs/latest/streaming-programming-guide.html

更多大数据系列文章可以参见个人 GitHub 开源项目: 程序员大数据入门指南

Spark学习之路(十四)—— Spark Streaming 基本操作的更多相关文章

  1. Spark学习之路 (四)Spark的广播变量和累加器

    一.概述 在spark程序中,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本.这些变量会被复制到每台机器上 ...

  2. Spark学习之路(四)—— RDD常用算子详解

    一.Transformation spark常用的Transformation算子如下表: Transformation算子 Meaning(含义) map(func) 对原RDD中每个元素运用 fu ...

  3. Spark学习之路 (四)Spark的广播变量和累加器[转]

    概述 在spark程序中,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本.这些变量会被复制到每台机器上,并 ...

  4. 学习之路十四:客户端调用WCF服务的几种方法小议

    最近项目中接触了一点WCF的知识,也就是怎么调用WCF服务,上网查了一些资料,很快就搞出来,可是不符合头的要求,主要有以下几个方面: ①WCF的地址会变动,地址虽变,但是里面的逻辑不变! ②不要引用W ...

  5. 嵌入式Linux驱动学习之路(十四)按键驱动-同步、互斥、阻塞

    目的:同一个时刻,只能有一个应用程序打开我们的驱动程序. ①原子操作: v = ATOMIC_INIT( i )  定义原子变量v并初始化为i atomic_read(v)        返回原子变量 ...

  6. zigbee学习之路(十四):基于协议栈的无线数据传输

    一.前言 上次实验,我们介绍了zigbee原理的应用与使用,进行了基于zigbee的串口发送协议,但是上个实验并没有实现数据的收发.在这个实验中,我们要进行zigbee的接受和发送实验. 二.实验功能 ...

  7. IOS学习之路十四(用TableView做的新闻客户端展示页面)

    最近做的也个项目,要做一个IOS的新闻展示view(有图有文字,不用UIwebview,因为数据是用webservice解析的到的json数据),自己一直没有头绪,可后来听一个学长说可以用listvi ...

  8. [转]Spark学习之路 (三)Spark之RDD

    Spark学习之路 (三)Spark之RDD   https://www.cnblogs.com/qingyunzong/p/8899715.html 目录 一.RDD的概述 1.1 什么是RDD? ...

  9. Linux学习总结(十四)—— 查看CPU信息

    文章首发于[博客园-陈树义],点击跳转到原文Linux学习总结(十四)-- 查看CPU信息. Linux学习总结(十四)-- 查看CPU信息 商用服务器CPU最常用的是 Intel Xeon 系列,该 ...

  10. 我的MYSQL学习心得(十四) 备份和恢复

    我的MYSQL学习心得(十四) 备份和恢复 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得(四) ...

随机推荐

  1. android高仿人人网

    经过几个月的努力,终于基本完成了人人API拥有的所有功能,界面采用仿照人人梦想版5.13制作,其中资源文件也采用人人的APK文件资源,完成的功能及知识点如下: 1.通过三种动画仿照出人人引导页的放大切 ...

  2. ValueStack和OGNL达到Struts2形式的数据存储原理

    (1)最近学习struts相框,我们在快乐struts强大.为了便于使用转发,但不了解详细的内部数据存储: (2)网上找了很多关于struts数据存储的原理,但我还没有找到一个具体的解释,本书上找到了 ...

  3. matlab 工具函数 —— normalize(归一化数据)

    function x = normalize(x, mu, sigma) x = bsxfun(@minus, x, mu); x = bsxfun(@rdivide, x, sigma); end ...

  4. C# 有道API翻译 查询单词详细信息

    原文:C# 有道API翻译 查询单词详细信息 有道云官方文档 有道云翻译API简介:http://ai.youdao.com/docs/doc-trans-api.s#p01 有道云C#Demo : ...

  5. Symfony——如何使用Assetic实现资源管理

    1. 安装和启用 从Symfony 2.8开始,Assetic不再包含在Symfony Standard Edition中.在使用其任何功能之前,请在您的项目中安装执行此控制台命令的 AsseticB ...

  6. .net core service && angular项目 iis发布

    项目结构 .net core 后端服务站点 angular 前端页面站点 项目模板来自于abp或者52abp .net core 后端服务站点发布到IIS 发布报错 .Net Core使用IIS部署出 ...

  7. 协程在Web服务器中的应用(配的图还不错)

    协程(纤程,微线程)这个概念早就有之,各家互联网公司也都有研究,但在国内各大论坛和大会热起来,还是今年的事. 最近参与讨论开放平台建设和架构设计过程中,有同事提到了使用协程代替线程,能够很大幅度的提高 ...

  8. DB First EF中的存储过程、函数、视图

    视图约等于表(属性)存储过程变为方法,方法中调用存储过程 EF可以调用存储过程,DB First的流程是刷新模型,获取存储过程,调用参考:http://blog.csdn.net/sudazf/art ...

  9. 搜索服务器Elasticsearch

    基本 ElasticSearch是一个基于Lucene的搜索服务器.它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口.Elasticsearch是用Java开发的,并作为Ap ...

  10. jQuery省市联动

    <!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"> ...