Apache Spark是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。
Apache Spark掀开了内存计算的先河,以内存作为赌注,赢得了内存计算的飞速发展。但是在其火热的同时,开发人员发现,在Spark中,计算框架普遍存在的缺点和不足依然没有完全解决,而这些问题随着5G时代的来临以及决策者对实时数据分析结果的迫切需要而凸显的更加明显:
l 数据精准一次性处理(Exactly-Once)
l 乱序数据,迟到数据
l 低延迟,高吞吐,准确性
l 容错性(挂了怎么办)
Apache Flink是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。在Spark火热的同时,也默默地发展自己,并尝试着解决其他计算框架的问题。
慢慢地,随着这些问题的解决,Flink慢慢被绝大数程序员所熟知并进行大力推广,阿里公司在2015年改进Flink,并创建了内部分支Blink,目前服务于阿里集团内部搜索、推荐、广告和蚂蚁等大量核心实时业务。
[(2019年)1.9之后Blink跟Flink合并]
——最大区别:计算模型不一样;Spark是伪实时,微批处理,flink就是真正的流处理
Spark 和 Flink 一开始都拥有着同一个梦想,他们都希望能够用同一个技术把流处理和批处理统一起来,但他们走了完全不一样的两条路前者是以批处理的技术为根本,并尝试在批处理之上支持流计算;后者则认为流计算技术是最基本的,在流计算的基础之上支持批处理。正因为这种架构上的不同,今后二者在能做的事情上会有一些细微的区别。比如在低延迟场景,Spark 基于微批处理的方式需要同步会有额外开销,因此无法在延迟上做到极致。在大数据处理的低延迟场景,Flink 已经有非常大的优势。
Spark和Flink的主要差别就在于计算模型不同。Spark采用了微批处理模型,而Flink采用了基于操作符的连续流模型。因此,对Apache Spark和Apache Flink的选择实际上变成了计算模型的选择,而这种选择需要在延迟、吞吐量和可靠性等多个方面进行权衡。
--批流针对计算模型,离线实时针对计算延迟
批处理的特点是有界、持久、大量,非常适合需要访问全套记录才能完成的计算工作,一般用于需要长时间运行的离线统计。在Spark的世界观中,一切都是由批次组成的,离线数据是Ø 一个大批次,而实时数据是由一个一个无限的小批次组成的。
流处理
特点是无界、实时, 无需针对整个数据集执行操作,而是对通过系统传输的每个数据项执行操作,一般用于延迟小的实时统计。在Flink的世界观中,一切都是由流组成的,离线数据是有界限的流,实时数据是一个没有界限的流,
无界数据流【有头无尾】:
无界数据流有开始但是没有结束,必须在获取流数据后立即处理。对于无界数据流我们无法等待所有数据都到达,因为输入是无界的,并且在任何时间点都不会完成。处理无界数据通常要求以特定顺序(例如事件发生的顺序)获取数据,以便能够推断结果完整性。
有界数据流【有头有尾】:
有界数据流有明确定义的开始和结束,可以在执行任何计算之前通过获取指定范围内所有数据来处理有界流,处理有界流不需要有序获取,因为可以对在指定范围内的有界数据集进行排序后再处理,有界流的处理也称为批处理。【flink认为批处理是有界数据流】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BzwtUdMw-1606664305872)(https://i.loli.net/2020/11/29/zVZ2bIa5rNoPivs.jpg)]
如果企业中非要技术选型从Spark和Flink这两个主流框架中选择一个来进行流数据处理,我们推荐使用Flink,主(显而)要(易见)的原因为:
Flink灵活的窗口
Exactly Once语义保证
这两个原因可以大大的解放程序员, 加快编程效率, 把本来需要程序员花大力气手动完成的工作交给框架完成。
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.AggregateOperator;
import org.apache.flink.api.java.operators.DataSource;
import org.apache.flink.api.java.operators.FlatMapOperator;
import org.apache.flink.api.java.operators.UnsortedGrouping;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.Collector;
/**
* 批处理
*/
public class Flink01_WordCount_Batch {
public static void main(String[] args) throws Exception {
//1.初始化执行环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
//2.读取数据
DataSource<String> fileDS = env.readTextFile("input/word.txt");
//3.处理数据
//3.1 压平:切分。转换成kv形式
FlatMapOperator<String, Tuple2<String, Integer>> wordAndDonDS = fileDS.flatMap(new MyFlatmapFunction());
//3.2 按照word分组
UnsortedGrouping<Tuple2<String, Integer>> wordAndOneGroup = wordAndDonDS.groupBy(0);
//3.3聚合
AggregateOperator<Tuple2<String, Integer>> resultDS = wordAndOneGroup.sum(1);
//4.输出结果
resultDS.print();
//5.执行(批处理不用)
}
public static class MyFlatmapFunction implements FlatMapFunction<String,Tuple2<String,Integer>>{
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
// 1.切分
String[] words = value.split(" ");
for (String word : words) {
// 2.转换成元组
Tuple2<String, Integer> tuple2 = Tuple2.of(word, 1);
// 3.通过采集器,往下游传输
out.collect(tuple2);
}
}
}
}
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
/**
* 有界流处理(有头有尾)
*/
public class Flink02_WordCount_BoundStream {
public static void main(String[] args) throws Exception {
//1.创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//2.读数据
DataStreamSource<String> inputDS = env.readTextFile("/opt/module/data/word.txt");//虚拟机路径
//3.处理数据
// 3.1 压平操作:切分、转换成(word,1)形式
SingleOutputStreamOperator<Tuple2<String, Integer>> wordAndOneDS = inputDS.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
//切分
String[] words = value.split(" ");
for (String word : words) {
out.collect(Tuple2.of(word,1));
}
}
});
//3.2按照word分组
KeyedStream<Tuple2<String, Integer>, Tuple> wordAndOneKS = wordAndOneDS.keyBy(0);
//3.3组内求和
SingleOutputStreamOperator<Tuple2<String, Integer>> resultDS =wordAndOneKS.sum(1);
//4.输出
resultDS.print();
//5.执行
env.execute();
}
}
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import javax.xml.transform.Result;
public class Flink03_WordCount_BoundStream {
/**
* 无界流处理(有头无尾)
*/
public static void main(String[] args) throws Exception {
//1.创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//2.读数据
DataStreamSource<String> socketDS = env.socketTextStream("hadoop102", 9999);
//3.处理数据
//3.1 压平、切分、转换
SingleOutputStreamOperator<Tuple2<String, Integer>> wordAndOneDS = socketDS
.flatMap((FlatMapFunction<String, Tuple2<String, Integer>>) (line, out) -> {
String[] words = line.split(" ");
for (String word : words) {
out.collect(Tuple2.of(word,1));
}
}).returns(new TypeHint<Tuple2<String, Integer>>() {
@Override
public TypeInformation<Tuple2<String, Integer>> getTypeInfo() {
return super.getTypeInfo();
}
});
//3.2 按照word分组
KeyedStream<Tuple2<String, Integer>, Tuple> wordAndOneKS = wordAndOneDS.keyBy(0);
//3.3 求和
SingleOutputStreamOperator<Tuple2<String, Integer>> resultDS = wordAndOneKS.sum(1);
//4.输出
resultDS.print();
//5.执行
env.execute();
}
}
--多个类可以打在一个jar包中,执行jar包的时候可以指定执行jar包中的哪个类 --class或者-c
记得数据源需要分发在集群上
不想放到本地,可以将数据放到hdfs上
--回顾spaek的jar包提交,spark-submit
# spark-submit [options] jar包 jar包中类的参数
/opt/module/spark-local/bin/spark-submit
--master local #通过本地服务将job提交到集群上
--class com.atguigu.spark.WordCount1 #调用jar包的哪个类
/home/atguigu/wc.jar
/home/atguigu/input
bin/flink run
-m hadoop102:6123
-c Flink02_WordCount_BoundStream #类名
/opt/module/data/flink-wc.jar #jar包路径
fs.default 文件系统是这个路径9820
--到该目录下来查看 /opt/module/hadoop/etc/hadoop/core-site.xml 文件
<!-- 指定NameNode的地址 -->
<property>
<name>fs.defaultFS</name>
<value>hdfs://hadoop102:8020
</property>
--修改conf/flink-conf.yaml配置文件
# Line79
high-availability: zookeeper
# Line88
high-availability.storageDir: hdfs://hadoop102:8020/flink/ha/
# Line94
high-availability.zookeeper.quorum: hadoop102:2282,hadoop103:2282,hadoop104:2282
--修改conf/master配置文件
hadoop102:8081
hadoop103:8081
--修改zoo.cfg配置文件
#Line 32 防止和外部ZK冲突
clientPort=2282
#Line 35
server.88=hadoop102:2888:3888
server.89=hadoop103:2888:3888
server.90=hadoop104:2888:3888
# server . 用哪个节点的ID =hadoop102: 内部通信端口 :选举端口
--分发配置文件
#启动flink自带的zk
bin/start-zookeeper-quorum.sh
#启动集群
bin/start-cluster.sh
export HADOOP_CLASSPATH=`hadoop classpath`
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
bin/flink run
-m yarn-cluster
-c Flink02_WordCount_BoundStream #类名
/opt/module/data/flink-wc.jar #jar包路径
bin/flink run -m yarn-cluster -cFlink03_WordCount_BoundStream /opt/module/data/flink-wc.jar
在上面的应用程序提交时,一个Job会对应一个yarn-session集群,每提交一个作业会根据自身的情况,都会单独向yarn申请资源,直到作业执行完成,一个作业的失败与否并不会影响下一个作业的正常提交和运行。独享Dispatcher和ResourceManager,按需接受资源申请;适合规模大长时间运行的作业
这种方式每次提交都会创建一个新的flink集群,任务之间互相独立,互不影响,方便管理。任务执行完成之后创建的集群也会消失。
yarn先启动一个集群,任务往这个集群提交,
在规模小执行时间短的作业执行时,频繁的申请资源并不是一个好的选择,所以Flink还提供了一种可以事先申请一定资源,然后在这个资源中并行执行多个作业的集群方式。
在yarn中初始化一个flink集群,开辟指定的资源,以后提交任务都向这里提交。这个flink集群会常驻在yarn集群中,除非手工停止。
Session-Cluster集群模式和Per-Job-Cluster不一样的是需要事先创建Yarn应用后再提交Flink应用程序。
Yarn会按需动态分配TaskManager个数
bin/yarn-session.sh -d -s 2 -jm 1024 -tm 1024 -nm test
-s(–slots) | 每个TaskManager的slot数量 |
---|---|
-jm | JobManager的内存(单位MB) |
-tm | 每个Taskmanager容器的内存(单位MB) |
-nm | yarn 的appName |
-qu | 指定yarn的队列 |
-d【detach】 | 后台执行 |
Flink Runtime 层的主要架构如下图所示,它展示了一个 Flink 集群的基本结构。整体来说,它采用了标准 master-slave 的结构,master负责管理整个集群中的资源和作业;TaskManager 则是 Slave,负责提供具体的资源并实际执行作业
负责接收用户提供的作业,并且负责为这个新提交的作业启动一个新的 JobManager 组件
负责资源的管理,在整个 Flink 集群中只有一个 ResourceManager
负责管理作业的执行,在一个 Flink 集群中可能有多个作业同时执行,每个作业 都有自己的 JobManager 组件
主要负责执行具体的task任务,从JobManager处接收需要部署的 Task,部署 启 动后,与自己的上游建立连接,接收数据并处理。
集群管理器,比如Standalone、YARN、K8s等,就是前面我们学习的不同环境
提交Job的客户端,可以是运行在任何机器上(与 JobManager 环境连通即可)。提交Job后,Client可以结束进程(Streaming的任务),也可以不结束并等待结果返回
1.用来执行task的
2.slots能够共享【申请的时候富余的那个可以共享】
3.是对于内存的划分
Flink中每一个worker(TaskManager)都是一个JVM进程,它可能会在独立的线程上执行一个Task。为了控制一个worker能接收多少个task,worker通过Task Slot来进行控制(一个worker至少有一个Task Slot)。
这里的Slot如何来理解呢?很多的文章中经常会和Spark框架进行类比,将Slot类比为Core,其实简单这么类比是可以的,可实际上,可以考虑下,当Spark申请资源后,这个Core执行任务时有可能是空闲的,但是这个时候Spark并不能将这个空闲下来的Core共享给其他Job使用,所以这里的Core是Job内部共享使用的。接下来我们再回想一下,之前在Yarn Session-Cluster模式时,其实是可以并行执行多个Job的,那如果申请两个Slot,而执行Job时,只用到了一个,剩下的一个怎么办?那我们自认而然就会想到可以将这个Slot给并行的其他Job,对吗?所以Flink中的Slot和Spark中的Core还是有很大区别的。
每个task slot表示TaskManager拥有资源的一个固定大小的子集。假如一个TaskManager有三个slot,那么它会将其管理的内存分成三份给各个slot。资源slot化意味着一个task将不需要跟来自其他job的task竞争被管理的内存,取而代之的是它将拥有一定数量的内存储备。需要注意的是,这里不会涉及到CPU的隔离,slot目前仅仅用来隔离task的受管理的内存。【solt不是对cpu的划分,而是对内存的划分】
'Spark的并行度由分区数决定'
'Flink每一步都能手动条件中并行度'
可以在代码中指定
提交参数指定 (- p)
配置文件指定(默认是1)
还可以对每个算子设置并行度
--优先顺序:
算子(代码)> 执行环境(代码) > 提交参数 > 配置文件
--并行度与slots的关系:
Slots必须大于每一个并行度,即大于兄弟最多的那一家【slots>= 算子的最大并行度】 整个作业的并行度由并行度最大的算子决定 整个作业的task总数就是没个算子的并行度之和
1.Subtask某一个算子的一个并行任务
2.Flink的task是由不同算子的subtask放在一起组成
3.Task是静态的,并行度是动态的
在学习Spark RDD时,无论是读取内存中的数据,或读取文件数据,都会接触一个叫并行度的概念,并且在RDD的算子中也可以动态改变并行度,通过学习,咱们应该知道Spark中的并行度最终体现为分区,而分区又意味着Task。所以Spark 计算中Task的数量是可以通过并行度推算出来的。这个大家没有的问题的话,那就好办了,为什么?因为Flink的并行度的作用和Spark中并行度的作用的一样的。最后都可以表现为任务的并行执行。幸福感满满的。
虽然Spark中的并行度和Flink的并行度的原理,作用基本一致,但是由于模型选择的问题,所以使用上依然有些细微的区别:
Spark的并行度设置后需要调用特殊的算子(repartition)或特殊的操作(shuffle)才能进行改变,比如调用flatMap算子后再调用repartition改变分区。
Flink的并行度设置可以在任何算子后使用,并且为了方便,也可以设置全局并行度
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
但是需要注意,某些数据源数据的采集是无法改变并行度,如Socket
一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism),一般情况下,一个流程序的并行度,可以认为就是其所有算子中最大的并行度。一个程序中,不同的算子可能具有不同的并行度。
Stream在算子之间传输数据的形式可以是one-to-one(forwarding)的模式也可以是redistributing的模式,具体是哪一种形式,取决于算子的种类。
Ø One-to-one:类似于spark中的窄依赖
stream(比如在source和map operator之间)维护着分区以及元素的顺序。那意味着flatmap 算子的子任务看到的元素的个数以及顺序跟source 算子的子任务生产的元素的个数、顺序相同,map、fliter、flatMap等算子都是one-to-one的对应关系。
Ø Redistributing(重分配):类似于spark中的宽依赖
stream(map()跟keyBy/window之间或者keyBy/window跟sink之间)的分区会发生改变。每一个算子的子任务依据所选择的transformation发送数据到不同的目标任务。例如,keyBy()基于hashCode重分区、broadcast和rebalance(是按照轮循再平衡,本来可以一对一,但由于你调整了算子并行度)会随机重新分区,这些算子都会引起redistribute过程,而redistribute过程就类似于Spark中的shuffle过程。
keyBy作用于hash这个过程中
--不同算子的子任务满足某种关系之后串在一起,组成一个task
Flink执行时,由于并行度的设置,可以将同一个Job不同算子的subtask放在同一块内存中进行处理,那么这样在执行时就可以合并成一个完整的task进行处理,而不是独立的子任务,
这样就减少了子任务(SubTask)之间调度和数据传递的性能损耗
1.算子之间是one-to-one的关系。
2.并且并行度是一样的。
在Flink执行计算时,多个算子的subTask到底能不能组成一个Task是不确定的。比如读取并行度为1的数据源,但是map映射时使用并行度2,那么这样map算子就存在两个subtask,可以数据源读取时只有一个subtask,那么就会导致其中一个subtask无法链接成task,就需要在其他slot中执行。所以在这种情况下,到底哪些subtask可以组合,哪些subtask不能组合,就需要动态调整,这就需要用到一种任务链的操作进行设置。
任务链必须满足两个条件:one-to-one的数据传输并且并行度相同
.startNewChain(); .disableChaining();
由Flink程序直接映射成的数据流图是StreamGraph,也被称为逻辑流图,因为它们表示的是计算逻辑的高级视图, 简单理解就是将整个流计算的执行过程用图形表示出来,这样更直观,更便于理解,所有用于表示程序的拓扑结构
虽然更便于理解,但是和真正执行还有差别的,因为到底什么样的subtask组合成一个完整的task,task之间如何将多个符合条件的节点 chain 在一起作为一个节点,这些还是不能直观的展示给我们,所以为了直观地观察一个流处理程序的执行,Flink还需要将逻辑流图转换为作业图 JobGraph,提交给 JobManager
JobManager 根据JobGraph生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构
在哪生成传递给谁:
我们来看看当一个应用提交执行时,Flink的各个组件是如何交互协作的:
图 任务提交和组件交互流程
上图是从一个较为高层级的视角,来看应用中各组件的交互协作。如果部署的集群环境不同(例如YARN,Mesos,Kubernetes,Standalone等),其中一些步骤可以被省略,或是有些组件会运行在同一个JVM进程中。
具体地,如果我们将Flink集群部署到YARN上,那么就会有如下的提交流程:
1) Flink任务提交后,Client向HDFS上传Flink的Jar包和配置
2) 向Yarn ResourceManager提交任务,ResourceManager分配Container资源
3) 通知对应的NodeManager启动ApplicationMaster,ApplicationMaster启动后加载Flink的Jar包和配置构建环境,然后启动JobManager
4) ApplicationMaster向ResourceManager申请资源启动TaskManager
5) ResourceManager分配Container资源后,由ApplicationMaster通知资源所在节点的NodeManager启动TaskManager
6) NodeManager加载Flink的Jar包和配置构建环境并启动TaskManager
7) TaskManager启动后向JobManager发送心跳包,并等待JobManager向其分配任务。