第一节:Flink理论简介与基本实现
- 尚硅谷视频资料:www.bilibili.com/video/BV1qy…
- 部分资料引自:blog.csdn.net/weixin_4148…
1、Flink 是什么
• Apache Flink is a framework and distributed processing engine for stateful computations over unbounded and bounded data streams
• Apache Flink 是一个框架和分布式处理引擎,用于对无界和有界数据流进行状态计算【流处理框架】
2、Flink企业应用
3、为什么选择Flink
流数据(在自然环境中,数据的产生原本就是流式的,比如聊天,即持续不断的产生的,没有固定的时间段)更真实地反映了我们的生活方式
- 传统的数据架构是基于有限数据集的【批处理,数据先累积成一批,一起处理,成为一个数据集dataset】,但是在数据攒成一批的过程需要时间的等待,实时性不高
- 我们的目标
- 低延迟(毫秒级的延迟)
- 高吞吐(分布式的集群扩展)
- 结果的准确性和良好的容错性【来一个就处理一个,但是有乱序问题,比如本来网络就有延迟,经过传输就先处理了前面的数据】,容错性[一个节点挂,全部挂,于是索引节点重新回滚到起始点,重新处理,但是代价太高,让他直接回滚到离故障发生最近的一个状态]
因此Flink就能解决以上问题。
4、哪些行业需要处理流数据
- 电商和市场营销
- 实时数据报表(用ETL + Hive生成报表数据,Hive处理时间长)、广告投放、业务流程需要
- 物联网(IOT)
- 传感器实时数据采集和显示
- 实时报警
- 交通运输业
- 电信业
- 基站流量调配
- 银行和金融业
- 实时结算和通知推送,实时检测异常行为
5、传统数据处理架构
5.1、事务处理架构
CRM: 客户资源管理系统
类似于流式处理,来一个事件,立即做出响应【实时性很好】,适用于 关系型数据库,数据量较小【数据量大,牵扯到连表查询之类的不适用】的情况
用Web程序来举例,用户点击了网页上的按钮后,就发生一次点击事件,会向服务器发起请求,后台立即做出响应,从数据库中查询对应的信息,进行计算,得到的结果,可能再存回数据库中,最后给用户做一个页面响应
比如用户登录,用户填写用户名和密码之后,点击注册,后台做出响应,去数据库中查询,验证该用户是否存在,如果不存在就把用户信息经过一些加密处理,再保存到数据库当中,最后给用户反馈,注册成功
5.2、分析处理架构
将数据从业务数据库复制到数仓,再进行利用数据处理引擎进行分析和查询
类似于批处理,攒上一批数据,一起处理【处理大量数据,做到高并发】
因为数据量太大,数据来源多种多样,可能来自于mysql,sqlserver,orcale等多个关系型数据库,和一些非结构化的数据,比如前端埋点数据,可能为JSON或其他格式,一个单纯的关系型数据库根本无法处理
但是如上图所示,不能实现数据实时性处理的效果从,所以如果对实时性要求不高,就采用批处理的方式,存放到数据仓库中进行汇总之后,再进行分析,如果要做到低延迟实时分析,就要使用流式处理的方式。
6、流处理的演变
6.1、有状态的流式处理
如何做到既能处理大量数据下的高并发又能做到实时性处理分析呢?
- 来一条数据处理一个
- 数据不要保存到数据库,将数据保存到本地内存,将他存成一个本地状态,根据本地状态进行计算处理
- 做集群进行高并发的扩展
像一个管道,数据从一边流入,进行处理后,从另一边流出,但后边如果想继续使用,也可以拿出来消费
在前面的事物处理流程中,需要数据时,就从数据库中查询,此处和此过程相似,如果要做一些复杂的计算,需要一些其他信息,就要有一个地方存储信息,这里保存在Local state(本地状态中),访问和计算会很快,但本地状态占用内存,而且可能会丢失,如果出现故障怎么恢复?
所以这里使用checkpoint操作,把Local State周期性的保存到远程的存储空间(Remote Storage)中去,出现故障直接从远程空间恢复到原来的状态即可。(Remote Storage可能是文件系统,数据库等),因此可以做到低延迟、高吞吐、良好的容错性,当数据量大时,可以做成分布式,但是分布式可能存在网络延迟带来的乱序问题,怎么解决?
- 采用两套系统,流处理保证数据的实时性,批处理保证数据的准确性
- 当有数据输入时,来一条处理一条,隔一段时间,采用批处理的方式,处理一批数据,保证结果的准确性
- 所以用户这边看到的情况是,数据实时显示,但隔一段时间后,数据可能还会变化
- 这就是后面的lambda 架构
6.2、lambda架构
用两套系统,同时保证低延迟和结果准确
这种架构虽然综合了流处理和批处理,支撑了数据行业的早期发展,但是它也有一些致命缺点,并在大数据3.0时代越来越不适应数据分析业务的需求,它需要同时开发和维护两套系统,工作量翻倍
6.3、Flink
而flink使用一套API解决了以上所有问题
7、Flink的主要特点
7.1、Event-driven:事件驱动
事件驱动的应用程序
事件驱动的应用程序是一种有状态的应用程序,它从一个或多个事件流中摄取事件,并通过**触发计算、状态更新或外部操作(根据从远程存储得到的结果、状态)**对传入的事件做出反应。
事件驱动的应用程序是传统事务处理架构的演变,具有分离的计算和数据存储层。在此架构中,应用程序从远程事务数据库读取数据并将数据保存到远程事务数据库。
相比之下,事件驱动的应用程序基于有状态的流处理应用程序。在此设计中,数据和计算位于同一位置,从而产生本地(内存或磁盘)数据访问。容错是通过定期将检查点写入远程持久存储来实现的。下图描述了传统应用架构与事件驱动应用的区别。
事件驱动的应用程序有哪些优势?
事件驱动的应用程序不是查询远程数据库,而是在本地访问其数据,从而在吞吐量和延迟方面产生更好的性能。远程持久存储的定期检查点可以异步和增量完成。因此,检查点对常规事件处理的影响非常小。然而,事件驱动的应用程序设计提供的好处不仅仅是本地数据访问。在分层架构中,多个应用程序共享同一个数据库是很常见的。因此,需要协调数据库的任何更改,例如由于应用程序更新或扩展服务而更改数据布局。由于每个事件驱动的应用程序都负责自己的数据,因此更改数据表示或扩展应用程序需要较少的协调。
ingest vt. 摄取;咽下;吸收;接待
periodically adv. 定期地;周期性地;偶尔;间歇
asynchronous adj. [电] 异步的;不同时的;不同期的
7.2、基于流的世界观【批流一体】
在 Flink 的世界观中,一切都是由流组成的,离线数据是有界的流;实时数据是一个没有界限的流:这就是所谓的有界流和无界流
7.3、分层API
- 越顶层越抽象,表达含义越简明,使用越方便
- 越底层越具体,表达能力越丰富,使用越灵活
- 离线的是datasetapi
7.4、Flink 的其它特点
- 支持事件时间(event-time)和处理时间(processing-time)
- 精确一次(exactly-once)的状态一致性保证
- 低延迟,每秒处理数百万个事件,毫秒级延迟
- 与众多常用存储系统的连接
- 高可用,动态扩展,实现7*24小时全天候运行
8、Flink vs Spark Streaming
流(stream)和微批(micro-batching)
Spark Streaming也是用来做实时数据处理的,术语“microbatch”经常用于描述批次小和/或以小间隔处理的情况。即使处理可能每隔几分钟发生一次,数据仍然一次处理一批【思想:批次足够小,实时性就更好】,性能也不高。Spark Streaming是设计用于支持微批处理的系统的一个例子。
数据模型
- spark 采用 RDD 模型,spark streaming 的 DStream 实际上也就是一组组小批数据 RDD 的集合
- flink 基本数据模型是数据流,以及事件(Event)序列
运行时架构
- spark 是批计算,将 DAG 划分为不同的 stage,一个完成后才可以计算下一个
- flink 是标准的流执行模式,一个事件在一个节点处理完后可以直接发往下一个节点进行处理
9、Flink入门案例之批处理与流处理实现word count统计
9.1、批处理实现word count【离线数据集】
1、创建Maven工程,引入相关依赖
<!--该artifactId后面为scala版本号-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_2.12</artifactId>
<version>1.10.1</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>1.10.1</version>
</dependency>
2、music.txt内容
Will I be getting through
Now that I must try to leave it all behind
Did you see what you have done to me
3、Flink批处理实现word count
- 代码
package com.lemon.demo;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.DataSet;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.Collector;
/**
* @author lemon
* @create 2021-12-27 15:50
* TO:一把青梅换了酒钱
*/
public class WorldCount {
public static void main(String[] args) throws Exception {
// 1.创建执行环境,类似spark的上下文
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// 2.从文件中读取数据
String inputPath = "D:\\workspace_ideal\\Flinkexer\\src\\main\\resources\\music.txt";
DataSet<String> dataSet = env.readTextFile(inputPath);
// 3.按空格对数据集中的每一条数据(一行就是一条)进行分词。分成一个一个的
// map之后进行flat扁平化(打散),转换为 word,count 的二元组形式 如<hello,1>进行输出
DataSet<Tuple2<String,Integer>> resultSet = dataSet.flatMap(new MyFlatMapper())
//数据分组,可以按照字段名或字段位置,这里传入0代表使用tuple2第一个位置的数据进行分组
.groupBy(0)
//求和,这里传入1代表使用tuple2第二个位置的数据进行求和
.sum(1);
// 4.输出结果
resultSet.print();
}
// 自定义flatMapFunction接口实现类,该接口为函数式接口
// flatMapFunction需要泛型<T,O>,T为输入数据类型,O为输出数据类型,Tuple2是flink提供的元祖类型
// Tuple2需要泛型<T0,T1>,这里T0 T1根据想要的输出结果格式进行自定义
private static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String,Integer>> {
/***
* @param value 输入的数据
* @param collector 收集器, 将需要返回的数据(输出数据)收集起来
* @throws Exception
*/
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
// 1.按空格分词
String[] words = value.split(" ");
// 2.将每一个word封装为二元组
for (String word : words) {
Tuple2<String,Integer> tuple2 = new Tuple2<>(word,1);
//3、使用out收集器调用collect方法收集想要进行输出的数据
out.collect(tuple2);
}
}
}
}
2. 结果:可以看到输出的是最终统计结果。
(must,1)
(what,1)
(you,2)
(see,1)
(Did,1)
(behind,1)
(done,1)
(to,2)
(be,1)
(I,2)
(Now,1)
(getting,1)
(leave,1)
(me,1)
(through,1)
(Will,1)
(all,1)
(have,1)
(it,1)
(that,1)
(try,1)
3、补充:
-
env.readTextFile(inputPath);本身得到的是DataSource,DataSource继承了Operator,实现了Dataset。所以这里面所有的方法调用都是datasetAPI -
dataSet.flatMap(new MyFlatMapper()),调用flatMap方法就是为了将输入数据进行打散,再根据里面的参数定义得到相应数据输出形式,这里面传入的参数是一个实现了FlatMapFunction接口,并且重写了flatMap方法的类,这个flatMap方法没有返回值,value就是传过来的数据,通过Collector收集器输出对应数据
-
FlatMapFunction里面的flatMap方法
void flatMap(T value,collector<O> out) throws Exception{}
9.2、流处理实现word count【实时数据集】
- 代码
package com.lemon.demo;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
/**
* @author lemon
* @create 2021-12-27 16:12
* TO:一把青梅换了酒钱
*/
public class WordCountStream {
public static void main(String[] args) throws Exception {
// 1.创建流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 从文件中获取数据
String inputPath = "D:\\workspace_ideal\\Flinkexer\\src\\main\\resources\\music.txt";
DataStream<String> dataStream = env.readTextFile(inputPath);
// 3.基于数据流进行计算
DataStream<Tuple2<String,Integer>> resultStream = dataStream.flatMap(new MyFlatMapper())
.keyBy(0) //keyBy类似于group by,分区与key有关(hashcode)
.sum(1);
// 4.输出结果
resultStream.print();
// 5.启动流任务,来一个处理一个,所以上面先将所有数据处理流程任务完成,然后数据一个一个的进行
env.execute();
}
private static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String,Integer>> {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
// 1.按空格分词
String[] words = value.split(" ");
// 2.将word封装为二元组
for (String word : words) {
Tuple2<String,Integer> tuple2 = new Tuple2<>(word,1);
// 使用out收集器收集数据
out.collect(tuple2);
}
}
}
}
- 结果:
2> (behind,1)
4> (Did,1)
5> (you,1)
8> (must,1)
5> (you,2)
6> (to,1)
6> (be,1)
7> (see,1)
5> (that,1)
1> (it,1)
6> (Now,1)
4> (what,1)
6> (to,2)
4> (me,1)
7> (have,1)
7> (getting,1)
7> (through,1)
3> (done,1)
7> (leave,1)
3> (Will,1)
3> (I,1)
3> (I,2)
3> (try,1)
3> (all,1)
补充:
有数据来就会直接处理,不等数据堆叠到一定数量级,注意这里的keyBy这里不像批处理的groupBy,即所有数据统一处理,而是用流处理的keyBy,每一个数据都按照key值进行hash计算,进行类似分区的操作,来一个数据就处理一次,所有中间过程都有输出!
流处理会存储计算状态,会输出中间过程,所以会发现有些单词出现了叠加,原因就是FLink的流处理保存了每一次的状态,前面的数字代表并行执行线程的并行度,默认是电脑CPU核数【本地环境单机运行没有办法实现分布式下的Flink,所以应该是并行执行过程中线程的编号,可以认为是真实生产环境当中的分区编号】,可以通过env.setParallelism改变,
这里env.execute();之前的代码,可以理解为是在定义任务,只有执行env.execute()后,Flink才把前面的代码片段当作一个任务整体(每个线程根据这个任务操作,并行处理流数据)。
env.readTextFile(inputPath);本身得到的是DataStreamSource,DataStreamSource继承了SingleOutputStreamOperate,它继承了DataStream.所以这里面所有的方法调用都是dataStreamAPI[最核心的]
9.3、基于netcat流数据源的word count
上面的问题: 文件里面的数据相当于是一个有界流,真实的流数据应该是无界的真正意义上的流式数据,源源不断的来,那么就要牵扯到Kafka消息队列组件传输数据了【有一个数据就消费一个】,但是这里没有演示复杂情况,这里使用netcat(nc)工具来演示流式数据的产生,在liunx自带的(centos要安装)。
上面的是开发环境,真实环境是在生产环境下,起一个flink集群,部署起来后在生产集群上进行作业的提交
1、启动nc(这里演示的是windows上的nc),执行 nc -l -p 9999 一直监听9999端口等待socket连接(开启netcat程序,作为socket服务端,发送数据。Linux对应nc -lk 9999)
2、先执行下面程序建立两者的通信连接,然后cmd窗口发数据演示,输入后回车即会将该条消息发送到客户端。会发现同一个字符串,前面输出的编号是一样的,因为key => hashcode,同一个key的hash值固定,分配给相对应的线程处理。
代码
package com.jiam.demo.flink;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
// 获取Socket传过来的数据进行word count
public class SocketWordCountStream {
public static void main(String[] args) throws Exception {
// 1.创建流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 从Socket文本流获取数据,监听本机9999端口,获取socket连接
DataStream<String> inputDataStream = env.socketTextStream("localhost",9999);
// 3.基于数据流进行计算
// 3.1 map之后进行flat扁平化,转换为 word,count 的形式 如<hello,1>
DataStream<Tuple2<String,Integer>> resultStream = inputDataStream.flatMap(new MyFlatMapper())
// 3.2 keyBy类似于group by
.keyBy(0)
.sum(1);
// 4.输出结果
resultStream.print();
// 5.启动流任务
env.execute();
}
private static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String,Integer>> {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
// 1.按空格分词
String[] words = value.split(" ");
// 2.将word封装为二元组
for (String word : words) {
Tuple2<String,Integer> tuple2 = new Tuple2<>(word,1);
// 使用out收集器收集数据
out.collect(tuple2);
}
}
}
}
结果: 这才是真正意义上的流式数据,但是也有问题,可以发现程序中主机和端口号写死了
改进:
1、程序的启动配置 run-》edit configurations… ,这样的话我们就能在程序启动的时候,通过main方法获取相应参数[args]
2、程序改进如下:
package com.lemon.demo;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
/**
* @author lemon
* @create 2021-12-27 16:38
* TO:一把青梅换了酒钱
*/
public class SocketWordCountStream {
public static void main(String[] args) throws Exception {
// 1.创建流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 从Socket文本流获取数据
// main 方法是可以在程序启动时传递参数的
// Flink提供了parameterTool工具可以从程序启动参数中提取配置项
ParameterTool parameterTool = ParameterTool.fromArgs(args);
String host = parameterTool.get("host");
int port = parameterTool.getInt("port");
DataStream<String> inputDataStream = env.socketTextStream(host,port);
// 3.基于数据流进行计算
DataStream<Tuple2<String,Integer>> resultStream = inputDataStream.flatMap(new MyFlatMapper())
.keyBy(0)
.sum(1);
// 4.输出结果
resultStream.print();
// 5.启动流任务
env.execute();
}
private static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String,Integer>> {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
// 1.按空格分词
String[] words = value.split(" ");
// 2.将word封装为二元组
for (String word : words) {
Tuple2<String,Integer> tuple2 = new Tuple2<>(word,1);
// 使用out收集器收集数据
out.collect(tuple2);
}
}
}
}
结果:
当然,我们可以用java编写一个socket程序模拟nc,以便发送数据,供flink程序使用。 如下所示,代码来自:blog.csdn.net/weixin_4148…
package com.jiam.demo.flink;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.*;
import java.util.Scanner;
// 编写word count
public class SocketForWordCount {
public static void main(String[] args) throws IOException {
// TCP通信,服务端口9999
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("开始等待连接...");
Socket accept = serverSocket.accept();
System.out.println("连接成功...");
// 封装udp并发送数据
Scanner sc = new Scanner(System.in);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
try {
String line = "";
System.out.println("请输入单词串,各单词以空格分割.");
while (!"#".equals(line = sc.nextLine())) {
writer.write(line);
writer.newLine();
writer.flush();
System.out.println("发送:" + line);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
accept.close();
}
}
}
先启动Socket程序,再启动Flink程序。 这里Socket的编写要注意加上writer.newLine() —— 即换行,否则一是会导致只有输入"#"后之前发送的消息才能被word count 程序接收,二是会产生TCP粘包问题。