You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

398 lines
24 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 34Spark + Kafka流计算中的“万金油”
你好,我是吴磊。
在前面的几讲中咱们不止一次提到就数据源来说Kafka是Structured Streaming最重要的Source之一。在工业级的生产系统中Kafka与Spark这对组合最为常见。因此掌握Kafka与Spark的集成对于想从事流计算方向的同学来说是至关重要的。
今天这一讲咱们就来结合实例说一说Spark与Kafka这对“万金油”组合如何使用。随着业务飞速发展各家公司的集群规模都是有增无减。在集群规模暴涨的情况下资源利用率逐渐成为大家越来越关注的焦点。毕竟不管是自建的Data center还是公有云每台机器都是真金白银的投入。
## 实例:资源利用率实时计算
咱们今天的实例就和资源利用率的实时计算有关。具体来说我们首先需要搜集集群中每台机器的资源CPU、内存利用率并将其写入Kafka。然后我们使用Spark的Structured Streaming来消费Kafka数据流并对资源利用率数据做初步的分析与聚合。最后再通过Structured Streaming将聚合结果打印到Console、并写回到Kafka如下图所示。
![图片](https://static001.geekbang.org/resource/image/29/45/2968c988151de68798233229842c7e45.jpg?wh=1920x576 "资源利用率实时计算流程图")
一般来说在工业级应用中上图中的每一个圆角矩形在部署上都是独立的。绿色矩形代表待监测的服务器集群蓝色矩形表示独立部署的Kafka集群而红色的Spark集群也是独立部署的。所谓独立部署它指的是集群之间不共享机器资源如下图所示。
![图片](https://static001.geekbang.org/resource/image/8c/c7/8cba5a71a01c64a1dae52b13fc5144c7.jpg?wh=1920x970 "独立部署示意图")
如果你手头上没有这样的部署环境,也不用担心。要完成资源利用率实时计算的实例,咱们不必非要依赖独立部署的分布式集群。实际上,仅在单机环境中,你就可以复现今天的实例。
## 课程安排
今天这一讲涉及的内容比较多,在正式开始课程之前,咱们不妨先梳理一下课程内容,让你做到心中有数。
![图片](https://static001.geekbang.org/resource/image/29/45/2968c988151de68798233229842c7e45.jpg?wh=1920x576 "资源利用率实时计算流程图")
对于上图的1、2、3、4这四个步骤我们会结合代码实现分别讲解如下这四个环节
1.生成CPU与内存消耗数据流写入Kafka
2.Structured Streaming消费Kafka数据并做初步聚合
3.Structured Streaming将计算结果打印到终端
4.Structured Streaming将计算结果写回Kafka以备后用。
除此之外为了照顾不熟悉Kafka的同学咱们还会对Kafka的安装、Topic创建与消费、以及Kafka的基本概念做一个简单的梳理。
## 速读Kafka的架构与运行机制
在完成前面交代的计算环节之前我们需要了解Kafka都提供了哪些核心功能。
在大数据的流计算生态中Kafka是应用最为广泛的消息中间件Messaging Queue。消息中间件的核心功能有以下三点。
1.连接消息生产者与消息消费者;
2.缓存生产者生产的消息(或者说事件);
3.有能力让消费者以最低延迟访问到消息。
所谓消息生产者它指的是事件或消息的来源与渠道。在我们的例子中待监测集群就是生产者。集群中的机器源源不断地生产资源利用率消息。相应地消息的消费者它指的是访问并处理消息的系统。显然在这一讲的例子中消费者是Spark。Structured Streaming读取并处理Kafka中的资源利用率消息对其进行聚合、汇总。
经过前面的分析,我们不难发现,消息中间件的存在,让生产者与消费者这两个系统之间,天然地享有如下三方面的收益。
* 解耦:双方无需感知对方的存在,二者除了消息本身以外,再无交集;
* 异步:双方都可以按照自己的“节奏”和“步调”,来生产或是消费消息,而不必受制于对方的处理能力;
* 削峰:当消费者订阅了多个生产者的消息,且多个生产者同时生成大量消息时,得益于异步模式,消费者可以灵活地消费并处理消息,从而避免计算资源被撑爆的隐患。
好啦了解了Kafka的核心功能与特性之后接下来我们说一说Kafka的系统架构。与大多数主从架构的大数据组件如HDFS、YARN、Spark、Presto、Flink等等不同Kafka为无主架构。也就是说在Kafka集群中没有Master这样一个角色来维护全局的数据状态。
集群中的每台Server被称为Kafka BrokerBroker的职责在于存储生产者生产的消息并为消费者提供数据访问。Broker与Broker之间都是相互独立的彼此不存在任何的依赖关系。
如果就这么平铺直叙去介绍Kafka架构的话难免让你昏昏欲睡所以我们上图解。配合示意图解释Kafka中的关键概念会更加直观易懂。
![图片](https://static001.geekbang.org/resource/image/9e/1e/9e60c6667b7ec728281575e5b19b081e.jpg?wh=1920x967 "Kafka架构示意图")
刚刚说过Kafka为无主架构它依赖ZooKeeper来存储并维护全局元信息。所谓元信息它指的是消息在Kafka集群中的分布与状态。在逻辑上消息隶属于一个又一个的Topic也就是消息的话题或是主题。在上面的示意图中蓝色圆角矩形所代表的消息全部隶属于Topic A而绿色圆角矩形则隶属于Topic B。
而在资源利用率的实例中我们会创建两个Topic一个是CPU利用率cpu-monitor另一个是内存利用率mem-monitor。**生产者在向Kafka写入消息的时候需要明确指明消息隶属于哪一个Topic**。比方说关于CPU的监控数据应当发往cpu-monitor而对于内存的监控数据则应该发往mem-monitor。
为了平衡不同Broker之间的工作负载在物理上同一个Topic中的消息以分区、也就是Partition为粒度进行存储示意图中的圆角矩形代表的正是一个个数据分区。在Kafka中一个分区实际上就是磁盘上的一个文件目录。而消息则依序存储在分区目录的文件中。
为了提供数据访问的高可用HAHigh Availability在生产者把消息写入主分区Leader之后Kafka会把消息同步到多个分区副本Follower示意图中的步骤1与步骤2演示了这个过程。
一般来说消费者默认会从主分区拉取并消费数据如图中的步骤3所示。**而当主分区出现故障、导致数据不可用时Kafka就会从剩余的分区副本中选拔出一个新的主分区来对外提供服务这个过程又称作“选主”**。
好啦到此为止Kafka的基础功能和运行机制我们就讲完了尽管这些介绍不足以覆盖Kafka的全貌但是对于初学者来说这些概念足以帮我们进军实战做好Kafka与Spark的集成。
## Kafka与Spark集成
接下来咱们就来围绕着“资源利用率实时计算”这个例子手把手地带你实现Kafka与Spark的集成过程。首先第一步我们先来准备Kafka环境。
### Kafka环境准备
要配置Kafka环境我们只需要简单的三个步骤即可
1.安装ZooKeeper、安装Kafka启动ZooKeeper
2.修改Kafka配置文件server.properties设置ZooKeeper相关配置项
3.启动Kafka创建Topic。
首先,咱们从 [ZooKeeper官网](https://archive.apache.org/dist/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz)与 [Kafka官网](https://archive.apache.org/dist/kafka/2.8.0/kafka_2.12-2.8.0.tgz),分别下载二者的安装包。然后,依次解压安装包、并配置相关环境变量即可,如下表所示。
```shell
// 下载ZooKeeper安装包
wget https://archive.apache.org/dist/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz
// 下载Kafka安装包
wget https://archive.apache.org/dist/kafka/2.8.0/kafka_2.12-2.8.0.tgz
 
// 把ZooKeeper解压并安装到指定目录
tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz -C /opt/zookeeper
// 把Kafka解压并安装到指定目录
tar -zxvf kafka_2.12-2.8.0.tgz -C /opt/kafka
 
// 编辑环境变量
vi ~/.bash_profile
/** 输入如下内容到文件中
export ZOOKEEPER_HOME=/opt/zookeeper/apache-zookeeper-3.7.0-bin
export KAFKA_HOME=/opt/kafka/kafka_2.12-2.8.0
export PATH=$PATH:$ZOOKEEPER_HOME/bin:$KAFKA_HOME/bin
*/
 
// 启动ZooKeeper
zkServer.sh start
```
接下来我们打开Kafka配置目录下也即$KAFKA\_HOME/config的server.properties文件将其中的配置项zookeeper.connect设置为“hostname:2181”也就是主机名加端口号。
如果你把ZooKeeper和Kafka安装到同一个节点那么hostname可以写localhost。而如果是分布式部署hostname要写ZooKeeper所在的安装节点。一般来说ZooKeeper默认使用2181端口来提供服务这里我们使用默认端口即可。
配置文件设置完毕之后我们就可以使用如下命令在多个节点启动Kafka Broker。
```shell
kafka-server-start.sh -daemon $KAFKA_HOME/config/server.properties
```
Kafka启动之后咱们就来创建刚刚提到的两个Topiccpu-monitor和mem-monitor它们分别用来存储CPU利用率消息与内存利用率消息。
```shell
kafka-topics.sh --zookeeper hostname:2181/kafka --create
--topic cpu-monitor
--replication-factor 3
--partitions 1
 
kafka-topics.sh --zookeeper hostname:2181/kafka --create
--topic mem-monitor
--replication-factor 3 
--partitions 1
```
怎么样是不是很简单要创建Topic只要指定ZooKeeper服务地址、Topic名字和副本数量即可。不过这里需要特别注意的是**副本数量也就是replication-factor不能超过集群中的Broker数量**。所以如果你是本地部署的话也就是所有服务都部署到一台节点那么这里的replication-factor应该设置为1。
好啦到此为止Kafka环境安装、配置完毕。下一步我们就该让生产者去生产资源利用率消息并把消息源源不断地注入Kafka集群了。
### 消息的生产
在咱们的实例中我们要做的是监测集群中每台机器的资源利用率。因此我们需要这些机器每隔一段时间就把CPU和内存利用率发送出来。而要做到这一点咱们只需要完成一下两个两个必要步骤
1.每台节点从本机收集CPU与内存使用数据
2.把收集到的数据按照固定间隔发送给Kafka集群。
由于消息生产这部分代码比较长而我们的重点是学习Kafka与Spark的集成因此这里咱们只给出这两个步骤所涉及的关键代码片段。完整的代码实现你可以从[这里](https://github.com/wulei-bj-cn/learn-spark/tree/main/chapter34)进行下载。
```scala
import java.lang.management.ManagementFactory
import java.lang.reflect.Modifier
 
def getUsage(mothedName: String): Any = {
// 获取操作系统Java Bean
val operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean
// 获取操作系统对象中声明过的方法
    for (method <- operatingSystemMXBean.getClass.getDeclaredMethods) {
      method.setAccessible(true)
// 判断是否为我们需要的方法名
      if (method.getName.startsWith(mothedName) && Modifier.isPublic(method.getModifiers)) {
// 调用并执行方法获取指定资源CPU或内存的利用率
        return method.invoke(operatingSystemMXBean)
      }
    }
    throw new Exception(s"Can not reflect method: ${mothedName}")
  }
 
// 获取CPU利用率
def getCPUUsage(): String = {
    var usage = 0.0
try{
// 调用getUsage方法传入"getSystemCpuLoad"参数获取CPU利用率
      usage = getUsage("getSystemCpuLoad").asInstanceOf[Double] * 100
    } catch {
      case e: Exception => throw e
    }
    usage.toString
  }
 
// 获取内存利用率
def getMemoryUsage(): String = {
    var freeMemory = 0L
    var totalMemory = 0L
    var usage = 0.0
try{
// 调用getUsage方法传入相关内存参数获取内存利用率
      freeMemory = getUsage("getFreePhysicalMemorySize").asInstanceOf[Long]
      totalMemory = getUsage("getTotalPhysicalMemorySize").asInstanceOf[Long]
// 用总内存,减去空闲内存,获取当前内存用量
      usage = (totalMemory - freeMemory.doubleValue) / totalMemory * 100
    } catch {
      case e: Exception => throw e
    }
    usage.toString
  }
```
利用Java的反射机制获取资源利用率
上面的代码用来获取CPU与内存利用率。在这段代码中最核心的部分是利用Java的反射机制来获取操作系统对象的各个公有方法然后通过调用这些公有方法来完成资源利用率的获取。
不过看到这你可能会说“我并不了解Java的反射机制上面的代码看不太懂。”这也没关系只要你能结合注释把上述代码的计算逻辑搞清楚即可。获取到资源利用率的数据之后接下来我们就可以把它们发送给Kafka了。
```scala
import org.apache.kafka.clients.producer.{Callback, KafkaProducer, ProducerConfig, ProducerRecord}
import org.apache.kafka.common.serialization.StringSerializer
 
// 初始化属性信息
  def initConfig(clientID: String): Properties = {
    val props = new Properties
val brokerList = "localhost:9092"
// 指定Kafka集群Broker列表
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList)
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer].getName)
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer].getName)
    props.put(ProducerConfig.CLIENT_ID_CONFIG, clientID)
    props
  }
 
val clientID = "usage.monitor.client"
    val cpuTopic = "cpu-monitor"
    val memTopic = "mem-monitor"
 
// 定义属性其中包括Kafka集群信息、序列化方法等等
val props = initConfig(clientID)
// 定义Kafka Producer对象用于发送消息
val producer = new KafkaProducer[String, String](props)
// 回调函数,可暂时忽略
    val usageCallback = _
 
    while (true) {
      var cpuUsage = new String
      var memoryUsage = new String
// 调用之前定义的函数获取CPU、内存利用率
      cpuUsage = getCPUUsage()
      memoryUsage = getMemoryUsage()
     
// 为CPU Topic生成Kafka消息
      val cpuRecord = new ProducerRecord[String, String](cpuTopic, clientID, cpuUsage)
// 为Memory Topic生成Kafka消息
      val memRecord = new ProducerRecord[String, String](memTopic, clientID, memoryUsage)
// 向Kafka集群发送CPU利用率消息
      producer.send(cpuRecord, usageCallback)
// 向Kafka集群发送内存利用率消息
      producer.send(memRecord, usageCallback)
// 设置发送间隔2秒
      Thread.sleep(2000)
    }
```
从上面的代码中,我们不难发现,其中的关键步骤有三步:
* 定义Kafka Producer对象其中需要我们在属性信息中指明Kafka集群相关信息
* 调用之前定义的函数getCPUUsage、getMemoryUsage获取CPU与内存资源利用率
* 把资源利用率封装为消息并发送给对应的Topic。
好啦到此为止生产端的事情我们就全部做完啦。在待监测的集群中每隔两秒钟每台机器都会向Kafka集群的cpu-monitor和mem-monitor这两个Topic发送即时消息。Kafka接收到这些消息之后会把它们落盘到相应的分区中等待着下游也就是Spark的消费。
### 消息的消费
接下来终于要轮到Structured Streaming闪亮登场了。在流计算模块的[第一讲](https://time.geekbang.org/column/article/446691)我们就提到Structured Streaming支持多种SourceSocket、File、Kafka而在这些Source中Kafka的应用最为广泛。在用法上相比其他Source从Kafka接收并消费数据并没有什么两样咱们依然是依赖“万能”的readStream API如下表所示。
```scala
import org.apache.spark.sql.DataFrame
 
// 依然是依赖readStream API
val dfCPU:DataFrame = spark.readStream
// format要明确指定Kafka
.format("kafka")
// 指定Kafka集群Broker地址多个Broker用逗号隔开
.option("kafka.bootstrap.servers", "hostname1:9092,hostname2:9092,hostname3:9092")
// 订阅相关的Topic这里以cpu-monitor为例
.option("subscribe", "cpu-monitor")
.load()
```
对于readStream API的用法想必你早已烂熟于心了上面的代码你应该会觉得看上去很眼熟。这里需要我们特别注意的主要有三点
* format中需要明确指定Kafka
* 为kafka.bootstrap.servers键值指定Kafka集群Broker多个Broker之间以逗号分隔
* 为subscribe键值指定需要消费的Topic名明确Structured Streaming要消费的Topic。
挥完上面的“三板斧”之后我们就得到了用于承载CPU利用率消息的DataFrame。有了DataFrame我们就可以利用Spark SQL提供的能力去做各式各样的数据处理。再者结合Structured Streaming框架特有的Window和Watermark机制我们还能以时间窗口为粒度做计数统计同时决定“多迟”的消息我们将不再处理。
不过在此之前咱们不妨先来直观看下代码感受一下存在Kafka中的消息长什么样子。
```scala
import org.apache.spark.sql.streaming.{OutputMode, Trigger}
import scala.concurrent.duration._
 
dfCPU.writeStream
.outputMode("Complete")
// 以Console为Sink
.format("console")
// 每10秒钟触发一次Micro-batch
.trigger(Trigger.ProcessingTime(10.seconds))
.start()
.awaitTermination()
```
利用上述代码通过终端我们可以直接观察到Structured Streaming获取的Kafka消息从而对亟待处理的消息建立一个感性的认知如下图所示。
![图片](https://static001.geekbang.org/resource/image/44/8a/44ae590e0172409d7605d347fbac918a.jpg?wh=1920x583 "CPU利用率消息示例")
在上面的数据中除了Key、Value以外其他信息都是消息的元信息也即消息所属Topic、所在分区、消息的偏移地址、录入Kafka的时间等等。
在咱们的实例中Key对应的是发送资源利用率数据的服务器节点而Value则是具体的CPU或是内存利用率。初步熟悉了消息的Schema与构成之后接下来咱们就可以有的放矢地去处理这些实时的数据流了。
对于这些每两秒钟就产生的资源利用率数据假设我们仅关心它们在一定时间内比如10秒钟的平均值那么我们就可以结合Trigger与聚合计算来做到这一点代码如下所示。
```scala
import org.apache.spark.sql.types.StringType
 
dfCPU
.withColumn("clientName", $"key".cast(StringType))
.withColumn("cpuUsage", $"value".cast(StringType))
// 按照服务器做分组
.groupBy($"clientName")
// 求取均值
.agg(avg($"cpuUsage").cast(StringType).alias("avgCPUUsage"))
.writeStream
.outputMode("Complete")
// 以Console为Sink
.format("console")
// 每10秒触发一次Micro-batch
.trigger(Trigger.ProcessingTime(10.seconds))
.start()
.awaitTermination()
```
可以看到我们利用Fixed interval trigger每隔10秒创建一个Micro-batch。然后在一个Micro-batch中我们按照发送消息的服务器做分组并计算CPU利用率平均值。最后将统计结果打印到终端如下图所示。
![图片](https://static001.geekbang.org/resource/image/eb/ee/eb18aa02064583dd681d2c5b09cea1ee.jpg?wh=1920x1222 "统计CPU利用率均值")
### 再次写入Kafka
实际上除了把结果打印到终端外我们还可以把它写回Kafka。我们知道Structured Streaming支持种类丰富的Sink除了常用于测试的Console以外还支持File、Kafka、Foreach(Batch)等等。要把数据写回Kafka也不难我们只需在writeStream API中指定format为Kafka并设置相关选项即可如下表所示。
```scala
dfCPU
.withColumn("key", $"key".cast(StringType))
.withColumn("value", $"value".cast(StringType))
.groupBy($"key")
.agg(avg($"value").cast(StringType).alias("value"))
.writeStream
.outputMode("Complete")
// 指定Sink为Kafka
.format("kafka")
// 设置Kafka集群信息本例中只有localhost一个Kafka Broker
.option("kafka.bootstrap.servers", "localhost:9092")
// 指定待写入的Kafka Topic需事先创建好Topiccpu-monitor-agg-result
.option("topic", "cpu-monitor-agg-result")
// 指定WAL Checkpoint目录地址
.option("checkpointLocation", "/tmp/checkpoint")
.trigger(Trigger.ProcessingTime(10.seconds))
.start()
.awaitTermination()
```
我们首先指定Sink为Kafka然后通过option选项分别设置Kafka集群信息、待写入的Topic名字以及WAL Checkpoint目录。将上述代码敲入spark-shellStructured Streaming会每隔10秒钟就从Kafka拉取原始的利用率信息Topiccpu-monitor然后按照服务器做分组聚合最终再把聚合结果写回到KafkaTopiccpu-monitor-agg-result
这里有两点需要特别注意,一个是**读取与写入的Topic要分开以免造成逻辑与数据上的混乱**。再者,细心的你可能已经发现,**写回Kafka的数据在Schema上必须用“key”和“value”这两个固定的字段**而不能再像写入Console时可以灵活地定义类似于“clientName”和“avgCPUUsage”这样的字段名关于这一点还需要你特别关注。
## 重点回顾
好啦到此为止我手把手地带你实现了Kafka与Spark的集成完成了图中涉及的每一个环节也即从消息的生产、到写入Kafka再到消息的消费与处理并最终写回Kafka。
![图片](https://static001.geekbang.org/resource/image/29/45/2968c988151de68798233229842c7e45.jpg?wh=1920x576 "资源利用率实时计算流程图")
今天的内容比较多你除了需要掌握集成中的每一个环节与用法外还需要了解一些有关Kafka的基本概念与特性。Kafka是应用最为广泛的消息中间件Messaging Queue它的核心功能有三个
1.连接消息生产者与消息消费者;
2.缓存生产者生产的消息(或者说事件);
3.有能力让消费者以最低延迟访问到消息。
对于Kafka的一些基本概念你无需死记硬背在需要的时候回顾后面这张架构图即可。这张图中清楚地标记了Kafka的基础概念以及消息生产、缓存与消费的简易流程。
![图片](https://static001.geekbang.org/resource/image/9e/1e/9e60c6667b7ec728281575e5b19b081e.jpg?wh=1920x967 "Kafka架构示意图")
而对于Kafka与Spark两者的集成不管是Structured Streaming通过readStream API消费Kafka消息还是使用writeStream API将计算结果写入Kafka你只需要记住如下几点即可轻松地搭建这对“万金油”组合。
* 在format函数中指定Kafka为Source或Sink
* 在option选项中为kafka.bootstrap.servers键值指定Kafka集群Broker
* 在option选项中设置subscribe或是topic指定读取或是写入的Kafka Topic。
## 每课一练
请你结合本讲中CPU利用率的代码针对内存利用率完成示意图中的各个环节也即内存利用率消息的生产、写入Kafka步骤1、消息的消费与计算步骤2、3聚合结果再次写入Kafka步骤4
![图片](https://static001.geekbang.org/resource/image/29/45/2968c988151de68798233229842c7e45.jpg?wh=1920x576)
欢迎你把今天这讲内容转发给更多同事、朋友跟他一起动手试验一下Spark + Kafka的实例我再留言区等你分享。