一、版本配置
scala: 2.11.8
spark: 2.3.4
kafka: 2.11-0.10.0.0
二、pom文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.test</groupId> <artifactId>StructuredStreaming</artifactId> <version>1.0-SNAPSHOT</version> <properties> <scala.version>2.11.8</scala.version> <spark.version>2.3.4</spark.version> </properties> <dependencies> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala.version}</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-streaming_2.11</artifactId> <version>${spark.version}</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-sql-kafka-0-10_2.11</artifactId> <version>${spark.version}</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-sql_2.11</artifactId> <version>${spark.version}</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.11</artifactId> <version>${spark.version}</version> </dependency> <dependency> <groupId>net.jpountz.lz4</groupId> <artifactId>lz4</artifactId> <version>1.3.0</version> </dependency> </dependencies> <build> <!--指定打包的位置,默认只打src/main/java目录,且只能打包一个目录--> <sourceDirectory>src/main/scala</sourceDirectory> <testSourceDirectory>src/test/scala</testSourceDirectory> <plugins> <!--指定java版本--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.0</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!--scala依赖插件,为scala提供支持--> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>3.2.0</version> <executions> <execution> <goals> <goal>compile</goal> <goal>testCompile</goal> </goals> <configuration> <args> <arg>-dependencyfile</arg> <arg>${project.build.directory}/.scala_dependencies</arg> </args> </configuration> </execution> </executions> </plugin> <!--把所有jar包集成到一个jar包中--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <!--去掉META-INF文件中可能出现的非法签名文件--> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.test.structuredstreaming.socket.SocketWordCount</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
三、读取kafka的数据
1. 以流的形式读取
1)读取一个topic
val df = spark .readStream .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .option("subscribe", "topic1") .load() df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") .as[(String, String)]
2)读取多个topic
val df = spark .readStream .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .option("subscribe", "topic1,topic2") .load() df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") .as[(String, String)]
3)读取通配符形式的topic组
val df = spark .readStream .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .option("subscribePattern", "topic.*") .load() df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") .as[(String, String)]
2.以批的形式读取
1)设置每个分区的起始和结束值
val df = spark .read .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .option("subscribe", "topic1,topic2") .option("startingOffsets", """{"topic1":{"0":23,"1":-2},"topic2":{"0":-2}}""") .option("endingOffsets", """{"topic1":{"0":50,"1":-1},"topic2":{"0":-1}}""") .load() df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") .as[(String, String)]
2)配置起始和结束的offset值(默认)
val df = spark .read .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .option("subscribePattern", "topic.*") .option("startingOffsets", "earliest") .option("endingOffsets", "latest") .load() df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") .as[(String, String)]
3.Schema信息
读取后的数据的Schema是固定的,包含的列如下:
column | type | 说明 |
key | binary | 信息的key |
value | binary | 信息的value(我自己的数据) |
topic | string | 主题 |
partition | int | 分区 |
offset | long | 偏移值 |
timestamp | long | 时间戳 |
timestampType | int | 类型 |
四、source相关配置
kafka.bootstrap.servers kafka的服务器配置,host:post形式,用逗号进行分割,如host1:9000,host2:9000
startingOffsets offset开始的值,如果是earliest,则从最早的数据开始读;如果是latest,则从最新的数据开始读。默认流是latest,批是earliest
endingOffsets 最大的offset,只在批处理的时候设置,如果是latest则为最新的数据
failOnDataLoss 在流处理时,当数据丢失时(比如topic被删除了,offset在指定的范围之外),查询是否报错,默认为true。这个功能可以当做是一种告警机制,如果对丢失数据不感兴趣,可以设置为false。在批处理时,这个值总是为true。
kafkaConsumer.pollTimeoutMs excutor连接kafka的超时时间,默认是512ms
fetchOffset.numRetries 获取kafka的offset信息时,尝试的次数;默认是3次
fetchOffset.retryIntervalMs 尝试重新读取kafka offset信息时等待的时间,默认是10ms
maxOffsetsPerTrigger 单个批次允许抓取的最大消息条数
五、写入数据到kafka
Apache kafka仅支持“至少一次”的语义,因此,无论是流处理还是批处理,数据都有可能重复。比如,当出现失败的时候,structured streaming会尝试重试,但是不会确定broker那端是否已经处理以及持久化该数据。但是如果query成功,那么可以断定的是,数据至少写入了一次。比较常见的做法是,在后续处理kafka数据时,再进行额外的去重,关于这点,其实structured streaming有专门的解决方案。
保存数据时的schema:
1) key,可选。如果没有填,那么key会当做null,kafka针对null会有专门的处理(待查)。
2) value,必须有
3) topic,可选。(如果配置option里面有topic会覆盖这个字段)
下面是sink输出必须要有的参数:
kafka.bootstrap.servers,kafka的集群地址,host:port格式用逗号分隔。
流处理的数据写入
// 基于配置指定topic val ds = df .selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") .writeStream .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .option("topic", "topic1") .start() // 在字段中包含topic val ds = df .selectExpr("topic", "CAST(key AS STRING)", "CAST(value AS STRING)") .writeStream .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .start()
批处理的数据写入
df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") .write .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .option("topic", "topic1") .save() df.selectExpr("topic", "CAST(key AS STRING)", "CAST(value AS STRING)") .write .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .save()
六、kafka特殊配置
针对Kafka的特殊处理,可以通过DataStreamReader.option进行设置。
关于(详细的kafka配置可以参考consumer的官方文档](http://kafka.apache.org/documentation.html#newconsumerconfigs)
请注意,无法设置以下Kafka参数,并且Kafka源或接收器将引发异常:
1)group.id Kafka source将为每个查询自动创建一个唯一的组id。用户可以设置自动生成的组.id通过可选的源选项groupIdPrefix,默认值为“spark kafka source”。您也可以设置“kafka.group.id“但是,要强制Spark使用特殊组id,请阅读此选项的警告并谨慎使用。
2)auto.offset.reset 设置源选项startingOffsets以指定起始位置。结构化流媒体管理内部消费的补偿,而不是依靠kafka消费者来完成。这将确保在动态订阅新主题/分区时不会丢失任何数据。请注意,startingoffset仅在新的流式查询启动时适用,并且继续将始终从查询停止的位置开始。
3)key.deserializer,value.deserializer,key.serializer,value.serializer 序列化与反序列化,都是ByteArraySerializer
4)enable.auto.commit Kafka源不提交任何偏移量。
5)interceptor.classes Kafka源代码总是以字节数组的形式读取密钥和值。使用ConsumerInterceptor是不安全的,因为它可能会中断查询。
七、代码
package com.test.structuredstreaming.kafka import org.apache.spark.sql.streaming.Trigger import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession} /** * * @author c * @date 2020/8/17 16:54 * */ object kafka_demo2 { def main(args: Array[String]): Unit = { // 准备环境 val spark: SparkSession = SparkSession.builder() .appName("demo03") .master("local[2]") .getOrCreate() // 设置日志级别 spark.sparkContext.setLogLevel("ERROR") // 导入隐式转换 import spark.implicits._ // 读取数据流中的数据 val kafkaDatas: DataFrame = spark.readStream .format("kafka") .option("kafka.bootstrap.servers", "192.168.100.110:9092") // 设置kafka集群 .option("group.id", "2") //消费组 .option("subscribe", "test") // 设置需要读取的主题topic .option("startingOffsets", "latest")// 消费最新的数据 .option("maxOffsetsPerTrigger", 3)//单个批次允许抓取的最大消息条数 .load() // 加载数据 // kafkaDatas 内部数据是kafka数据(key,value) val kafkaDataString: Dataset[(String, String)] = kafkaDatas.selectExpr("CAST(key AS string)", "CAST(value AS string)").as[(String, String)] // 处理,将数据按照空格切分 val word: Dataset[String] = kafkaDataString.flatMap(x => x._2.split(" ")) // 利用DSL语句对数据进行wordcount val wordCount: Dataset[Row] = word.groupBy("value").count().sort($"count".desc) // 输出 wordCount.writeStream .outputMode("complete") // 每次将所有的数据写出 .format("console") // 输出方式,console表示控制台 .trigger(Trigger.ProcessingTime(0))//触发时间间隔,0表示尽可能的快 .start() // 开启任务 .awaitTermination() // 等待程序停止 } }