流式计算新贵Kafka Stream
一 What is Kafka Stream
Kafka Streams。Apache Kafka 开源项目的一个组成部分。是一个功能强大,易于使用的库。用于在 Kafka 上构建高可分布式、拓展性,容错的应用程序。
二 Features
- 功能强大
- 高扩展性,弹性,容错
- 轻量级
- 无需专门的集群
- 一个库,而不是框架
- 完全集成
- 100%的 Kafka 0.10.0 版本兼容
- 易于集成到现有的应用程序
- 实时性
- 毫秒级延迟
- 并非微批处理
- 窗口允许乱序数据
- 允许迟到数据
三 Why need KafkaStream
- 当前已经有非常多的流式处理系统,最知名且应用最多的开源流式处理系统有 Spark Streaming 和 Apache Storm。Apache Storm 发展多年,应用广泛,提供记录级别的处理能力,当前也支持 SQL on Stream。而 Spark Streaming 基于 Apache Spark,可以非常方便与图计算,SQL 处理等集成,功能强大,对于熟悉其它 Spark 应用开发的用户而言使用门槛低。另外,目前主流的 Hadoop 发行版,如 Cloudera 和 Hortonworks,都集成了 Apache Storm 和 Apache Spark,使得部署更容易。
- 既然 Apache Spark 与 Apache Storm 拥用如此多的优势,那为何还需要 Kafka Stream 呢?
- 主要有如下原因。
- 第一,Spark 和 Storm 都是流式处理框架,而 Kafka Stream 提供的是一个基于 Kafka 的流式处理类库。框架要求开发者按照特定的方式去开发逻辑部分,供框架调用。开发者很难了解框架的具体运行方式,从而使得调试成本高,并且使用受限。而 Kafka Stream 作为流式处理类库,直接提供具体的类给开发者调用,整个应用的运行方式主要由开发者控制,方便使用和调试。
- 第二,虽然 Cloudera 与 Hortonworks 方便了 Storm 和 Spark 的部署,但是这些框架的部署仍然相对复杂。而 Kafka Stream 作为类库,可以非常方便的嵌入应用程序中,它对应用的打包和部署基本没有任何要求。
- 第三,就流式处理系统而言,基本都支持 Kafka 作为数据源。例如 Storm 具有专门的kafka-spout,而 Spark 也提供专门的 spark-streaming-kafka 模块。事实上,Kafka 基本上是主流的流式处理系统的标准数据源。换言之,大部分流式系统中都已部署了 Kafka,此时使用Kafka Stream 的成本非常低。
- 第四,==使用 Storm 或 Spark Streaming 时,需要为框架本身的进程预留资源,==如 Storm的 supervisor 和 Spark on YARN 的 node manager。即使对于应用实例而言,框架本身也会占用部分资源,如 Spark Streaming 需要为 shuffle 和 storage 预留内存。但是 Kafka 作为类库不占用系统资源。
- 第五,由于Kafka 本身提供数据持久化,因此 Kafka Stream 提供滚动部署和滚动升级以及重新计算的能力。
- 第六,由于 Kafka Consumer Rebalance 机制,Kafka Stream 可以在线动态调整并行度。
- 第一,Spark 和 Storm 都是流式处理框架,而 Kafka Stream 提供的是一个基于 Kafka 的流式处理类库。框架要求开发者按照特定的方式去开发逻辑部分,供框架调用。开发者很难了解框架的具体运行方式,从而使得调试成本高,并且使用受限。而 Kafka Stream 作为流式处理类库,直接提供具体的类给开发者调用,整个应用的运行方式主要由开发者控制,方便使用和调试。
四 Kafka Stream案例
1、Maven工程添加依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>2.0.0</version>
</dependency>
2、案例一:实现topic之间的流传输
- Linux创建输入流topic
//创建传入流的topic
[root@hadoopwei ~]# kafka-topics.sh --create --zookeeper 192.168.198.201:2181 --topic mystreamin --partitions 1 --replication-factor 1
Created topic "mystreamin".
- IDEA创建流处理并执行
package cn.kgc.kb09.kafkaStream;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
/**
* @author: Zhuuu_ZZ
* @Date 2020/12/15
* @Description:
*/
public class MyStream {
public static void main(String[] args) {
Properties prop =new Properties();
prop.put(StreamsConfig.APPLICATION_ID_CONFIG,"mystream");
prop.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.198.201:9092");
prop.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
prop.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG,Serdes.String().getClass());
//创建流构造器
StreamsBuilder builder=new StreamsBuilder();
//构建好builder 将mystreamin topic中的数据写入到mystreamout topic中
builder.stream("mystreamin").to("mystreamout");
final Topology topo=builder.build();
final KafkaStreams streams = new KafkaStreams(topo, prop);
final CountDownLatch latch=new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread("stream"){
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
}
}
- Linux生产消息进入流输入topic并消费流输出topic
//生产消息
[root@hadoopwei ~]# kafka-console-producer.sh --topic mystreamin --broker-list 127.0.0.1:9092
//消费消息
[root@hadoopwei ~]# kafka-console-producer.sh --topic mystreamout --bootstrap-server127.0.0.1:9092
3、案例二:利用Kafka流实现wordcount
- Linux创建流输入topic
//创建传入流的topic
[root@hadoopwei ~]# kafka-topics.sh --create --zookeeper 192.168.198.201:2181 --topic wordcount_input --partitions 1 --replication-factor 1
Created topic "wordcount_input".
- IDEA自定义流处理过程
package cn.kgc.kb09.kafkaStream;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.*;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
/**
* @author: Zhuuu_ZZ
* @Date 2020/12/15
* @Description:
*/
public class WordCountStream {
public static void main(String[] args) {
Properties prop=new Properties();
prop.put(StreamsConfig.APPLICATION_ID_CONFIG,"wordcount");
prop.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.198.201:9092");
prop.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG,"3000");
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest"); //earliest latest none
prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
prop.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
prop.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG,Serdes.String().getClass());
//创建流构造器
//wordcount-input
//hello world
//hello java
StreamsBuilder builder=new StreamsBuilder();
KTable<String,Long> count = builder.stream("wordcount_input") //从kafka中一条一条的取数据
.flatMapValues( //返回压扁后的数据
(value) -> { //对数据进行按空格切割,返回List集合
String[] split = value.toString().split(" ");
List<String> strings = Arrays.asList(split);
return strings;
// return null;
}
) //key:null value:hello,null world,null hello,null java
.map((k, v) -> {
return new KeyValue<String, String>(v, "1");
}).groupByKey().count();
count.toStream().foreach((k,v)->{
System.out.println("key:"+k+" "+"value:"+v);
});
count.toStream().map((x,y)->{
return new KeyValue<String,String>(x,y.toString());
})
.to("wordcount_out");
final Topology topo=builder.build();
final KafkaStreams streams = new KafkaStreams(topo, prop);
final CountDownLatch latch=new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread("stream"){
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
}
}
- Linux生产消息进入流输入topic并消费流输出topic
//生产消息
[root@hadoopwei ~]# kafka-console-producer.sh --topic wordcount_input --broker-list 127.0.0.1:9092
>
//消费消息
[root@hadoopwei ~]# kafka-console-producer.sh --topic wordcount_out --bootstrap-server127.0.0.1:9092
4、案例三:利用Kafka流实现对输入数字的求和
- Linux创建输入流topic
//创建传入流的topic
[root@hadoopwei ~]# kafka-topics.sh --create --zookeeper 192.168.198.201:2181 --topic sumin --partitions 1 --replication-factor 1
Created topic "sumin".
- IDEA自定义流处理过程并运行
package cn.kgc.kb09.kafkaStream;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.*;
import org.apache.kafka.streams.kstream.KTable;
import java.util.*;
import java.util.concurrent.CountDownLatch;
/**
* @author: Zhuuu_ZZ
* @Date 2020/12/15
* @Description:
*/
public class SumStream {
public static void main(String[] args) {
Properties prop = new Properties();
prop.put(StreamsConfig.APPLICATION_ID_CONFIG, "sum");
prop.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.198.201:9092");
prop.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, "3000");
prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); //earliest latest none
prop.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
prop.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
//创建流构造器
StreamsBuilder builder = new StreamsBuilder();
KTable<String, String> sum = builder.stream("sumin") //从kafka中一条一条的取数据
.map((x, y) -> {
return new KeyValue<>("1", y.toString());
}).groupByKey()
.reduce((x, y) -> {//必须输入两个数字后才输出和,因为一开始内部store-state没有缓存,等两个数字求出和sum后会存入到x为一个内部store-state,然后下个数字进来为y,和x相加
// int i = Integer.parseInt(x)+ Integer.parseInt(y);
// return String.valueOf(i);
Interger i=Integer.parseInt(x)+ Integer.parseInt(y);
return i.toString();
});
sum.toStream().foreach((x,y)-> System.out.println(y));
sum.toStream().to("sumout");
final Topology topo = builder.build();
final KafkaStreams streams = new KafkaStreams(topo, prop);
final CountDownLatch latch = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread("stream") {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
}
}
- Linux生产消息进入流输入topic并消费流输出topic
//生产消息
[root@hadoopwei ~]# kafka-console-producer.sh --topic sumin --broker-list 127.0.0.1:9092
>
//消费消息
[root@hadoopwei ~]# kafka-console-producer.sh --topic sumout --bootstrap-server127.0.0.1:9092
5、案列四:Kafka Stream实现不同窗口的流处理
- Linux创建输入流topic
//创建传入流的topic
[root@hadoopwei ~]# kafka-topics.sh --create --zookeeper 192.168.198.201:2181 --topic windowdemo --partitions 1 --replication-factor 1
Created topic "windowdemo".
- IDEA实现不同窗口的流处理
package cn.kgc.kb09.kafkaStream;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.*;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import org.apache.kafka.streams.kstream.SessionWindows;
import org.apache.kafka.streams.kstream.TimeWindows;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
/**
* @author: Zhuuu_ZZ
* @Date 2020/12/16
* @Description:
*/
public class WindowStream {
public static void main(String[] args) {
Properties prop = new Properties();
//不同的窗口流不能使用相同的应用ID
prop.put(StreamsConfig.APPLICATION_ID_CONFIG, "sessionwindow");
prop.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.198.201:9092");
prop.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 3000);
prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); //earliest latest none
prop.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
prop.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
//创建流构造器
StreamsBuilder builder = new StreamsBuilder();
KStream<Object, Object> source = builder.stream("windowdemo");
source.flatMapValues(value-> Arrays.asList(value.toString().split("\\s+")))
.map((x,y)->{
return new KeyValue<String,String>(y,"1");
}).groupByKey()
//以下所有窗口的时间均可通过下方参数调设
//Tumbling Time Window(窗口为20秒,20秒内有效)
// .windowedBy(TimeWindows.of(Duration.ofSeconds(20).toMillis()))
//Hopping Time Window(窗口为5秒,每次移动2秒,所以若5秒内只输入一次会出现5/2+1=3次)
// .windowedBy(TimeWindows.of(Duration.ofSeconds(5).toMillis()).advanceBy(Duration.ofSeconds(2).toMillis()))
//Session Time Window(20秒内只要输入Session就有效,距离下一次输入超过20秒Session失效,所有从重新从0开始)
.windowedBy(SessionWindows.with(Duration.ofSeconds(20).toMillis()))
.count().toStream().foreach((x,y)->{
System.out.println("x:"+x+" y:"+y);
});
final Topology topo = builder.build();
final KafkaStreams streams = new KafkaStreams(topo, prop);
final CountDownLatch latch = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread("stream") {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
}
}
注意:
- ERROR:
-
Exception in thread “sum-a3bbe4d0-4cc9-4812-a7a0-e650a8a60c9f-StreamThread-1” java.lang.IllegalArgumentException: Window endMs time cannot be smaller than window startMs time.
-
数组越界
-
- 解决方案:
- 大概率是窗口ID一致,请修改
prop.put(StreamsConfig.APPLICATION_ID_CONFIG, "sessionwindow");
的参数。
- 大概率是窗口ID一致,请修改
6、案列五:利用Kafka Stream实现数据清洗
(1)需求:Flume将数据加载进入Kafka的流输入topic后,利用Kafka Stream将数据清洗后写入流输出topic。
(2)实现:
- 创建流输入topic
//创建传入流的topic
[root@hadoopwei ~]# kafka-topics.sh --create --zookeeper 192.168.198.201:2181 --topic user_friends_raw --partitions 1 --replication-factor 1
Created topic "user_friends_raw".
- 创建Flume Agent配置文件并执行
vi userfriend-flume-kafka.conf
user_friend.sources=userFriendSource
user_friend.channels=userFriendChannel
user_friend.sinks=userFriendSink
user_friend.sources.userFriendSource.type=spooldir
user_friend.sources.userFriendSource.spoolDir=/opt/flume160/conf/jobkb09/dataSourceFile/userFriend
user_friend.sources.userFriendSource.deserializer=LINE
user_friend.sources.userFriendSource.deserializer.maxLineLength=320000
user_friend.sources.userFriendSource.includePattern=userFriend_[0-9]{4}-[0-9]{2}-[0-9]{2}.csv
user_friend.sources.userFriendSource.interceptors=head_filter
user_friend.sources.userFriendSource.interceptors.head_filter.type=regex_filter
user_friend.sources.userFriendSource.interceptors.head_filter.regex=^user,friends*
user_friend.sources.userFriendSource.interceptors.head_filter.excludeEvents=true
user_friend.channels.userFriendChannel.type=file
user_friend.channels.userFriendChannel.checkpointDir=/opt/flume160/conf/jobkb09/checkPointFile/userFriend
user_friend.channels.userFriendChannel.dataDirs=/opt/flume160/conf/jobkb09/dataChannelFile/userFriend
user_friend.sinks.userFriendSink.type=org.apache.flume.sink.kafka.KafkaSink
user_friend.sinks.userFriendSink.batchSize=640
user_friend.sinks.userFriendSink.brokerList=192.168.198.201:9092
user_friend.sinks.userFriendSink.topic=user_friends_raw
user_friend.sources.userFriendSource.channels=userFriendChannel
user_friend.sinks.userFriendSink.channel=userFriendChannel
//执行配置文件
[root@hadoopwei jobkb09]# flume-ng agent -n user_friend -c /opt/flume160/conf/ -f /opt/flume160/conf/jobkb09/userfriend-flume-kafka.conf -Dflume.root.logger=INFO,console
//将数据文件导入Source源文件夹下
[root@hadoopwei jobkb09]# cp /opt/flume160/conf/jobkb09/tmp/user_friends.csv /opt/flume160/conf/jobkb09/dataSourceFile/userFriend/userFriend_2020-12-16.csv
- 此时数据已写入流输入Topic中,编写IDEA流处理过程
package cn.kgc.kb09.kafkaStream;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
/**
* @author: Zhuuu_ZZ
* @Date 2020/12/16
* @Description:
*/
public class UserFriendStream {
public static void main(String[] args) {
Properties prop = new Properties();
prop.put(StreamsConfig.APPLICATION_ID_CONFIG, "userfriendapp");
prop.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.198.201:9092");
prop.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
prop.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
StreamsBuilder builder = new StreamsBuilder();
//流处理过程
builder.stream("user_friends_raw").flatMap((k,v)->{
List<KeyValue<String,String>> list=new ArrayList();
String[] info = v.toString().split(",");
if(info.length==2) {
String[] friends = info[1].split("\\s+");
if (info[0].trim().length() > 0) {
for (String friend : friends) {
System.out.println(info[0] + " " + friend);
list.add(new KeyValue<String, String>(null, info[0] + "," + friend));
}
}
}
return list;
}).to("user_friends");
final Topology topo = builder.build();
final KafkaStreams streams = new KafkaStreams(topo, prop);
final CountDownLatch latch = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread("stream") {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(0);
}
}
- Linux消费流输出Topic信息
[root@hadoopwei jobkb09]# kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092 --topic user_friends --from-beginning
五 Kafka设计解析&数据可靠性深度解读
参考:
链接:Kafka设计解析(七)Kafka Stream
链接: kafka数据可靠性深度解读.