Apache Flink基础
大数据技术发展
2012年以前,大多数企业的数据仓库主要还是构建在关系型数据库上,例如Oracle、Mysql等数据库之上。但是随着企业数据量的增长,关系型数据库已经无法支撑大规模数据集的存储和分析,这种情况在一线互联网公司尤为明显,也是当时急需要解决的问题。
随着2012年Hadoop技术框架的成熟和稳定,一线互联网公司纷纷使用Hadoop技术栈来构建企业大数据分析平台,随后两年基于大数据的应用如雨后春笋一样涌现,比如千人千面的推荐系统、精准定向程序化交易的广告系统、互联网征信、大数据风控系统。时间到了2015年,Hadoop技术栈已然成为了建设数据仓库的首选项,对盲目跟风的企业来讲,有条件会上Hadoop集群、没有条件创造条件也要上Hadoop集群,那一年我听说过节点数最少的是一家做奢侈品的互联网公司,它们用3个物理机部署了一套数据仓库。
与此同时,随着Hadoop技术在企业大规模的深入应用,人们对Hadoop MapReduce框架越来越无法容忍,因为MapRecude在运行过程中会大量操作磁盘,对于复杂的计算任务来讲,动不动就是几个小时,甚至更长时间。然而大数据领域并没有革命性的框架来解决MapReduce慢的问题,人们只能一边抱怨一边想办法优化MapReduce的性能,然而效果并不是很理想。
直到2015年Spark技术框架的成熟,人们终于找到了替代MapReduce的新选择,这是一个将数据放到内存中计算的新框架,是一个比MapReduce快100倍的计算框架,对于拥有大数据量的企业来讲,真的是久旱逢甘霖,大家一股脑的冲进了Spark的怀抱,至此,大数据数据处理开了Spark时代。
有必要一提的是,Spark除了替代MapReduce以外,还带来了Spark Streaming,专门用来解决流式(实时)计算的问题。虽然当时市场有Apache Storm/Alibaba Jstorm等成熟的流式计算框架,但很快被Spark Streaming淘汰了,个人觉得打败Storm的主要原因就是Spark Streaming提高了数据处理的吞吐量和Spark on yarn的运行方式(Storm需要单独部署一套集群)。
时间到了2018年,Spark迎来了新的挑战者,那就是Apache Flink。Apache Flink与生俱来的流式计算处理能力,大大提高了数据处理的实效性,除了实效性的提升,Apache Flink还实现了exactly-once语义(一条数据只处理一次)、State管理。
作为计算领域最先进的技术框架,Apache Flink一路攻城拔寨,气势如虹。随着2018年年底阿里巴巴收购Flink的母公司,Flink China在中国开始了大规模的Flink技术推广。唾手可得中文文档、深入浅出公开视频、阿里巴巴的最佳实践,加快了Flink技术在中国市场的迅猛落地。
到了2019年的今天,人们出门必谈Flink,如同2015年,那时人们出门必谈Spark。
面对技术的快速迭代,不禁唏嘘,虽然MapReduce拼命的完善自己的生态,但是面对Spark的到来,依然毫无一战之力。同样,即使Spark生态圈已经如此完善,覆盖了离线计算、实时计算、机器学习、图计算等等诸多领域,面对Flink的到来,也在节节败退。
相对MapReduce基于磁盘的计算模式,Spark基于内存的计算方式是革命性的创新;相对Spark批量/微批的计算模式,Flink使用了流式计算的模式贴近了数据产生的本源;在它们各自的时代里,它们都代表了先进的生产力,都是以摧枯拉朽之势,雷霆万钧之力击垮对手。然而面对新的技术革新,它们都是那么弱小,不禁想起了刘慈欣《三体》中的有一句话,毁灭你,与你何干?
为什么是Flink
在大数据处理领域,对数据的处理通常有两种方式,一种是离线处理(批处理),一种是实时处理(流处理)。
离线处理()是指在构建数据仓库的过程中,通过周期性的方式将业务系统的数据同步到大数据平台,然后基于一次同步过来的数据进行数据处理、计算和展现的过程,通常一天一次,一次同步过来的数据会有几个G,甚至达到TB级别。这种处理方式也叫作批处理。
实时处理是指当一条数据产生后,就立刻通过技术的手段对数据进行收集、计算和展现的方式,整个过程是一个低时延的操作。典型的场景就是阿里巴巴双十一的实时大屏。这种方式也叫作流处理。
两种不同的处理方式,也就意味着对同样一个指标的计算会有两个不同的出口,如果两个出口的数据不一致,就会给决策者带来困扰,到底以哪个数据为准。通常的解决方案是当天看实时的数据,次日看离线的数据,为此Apache Storm的作者提出了一套Lambda架构方案来解决这个问题。
下图就是一个典型的Lambda的架构,整个架构分为两个部分,一部分是实时处理(流处理),一部分是离线处理(批处理)。流处理过程中,会将数据收集到Kafka中,然后使用Storm/Spark Streaming的方式对数据进行消费处理,最后将当天的结果进行真实。批处理过程中,会通过ETL技术手段将业务系统的数据拉取到数据仓库中,然后对数据进行批量计算,输出结果数据。
Lambda架构在一定程度上解决了不同计算类型的问题,但是问题也很明显,就是框架太多,增加了平台的复杂度、运维成本。其中最为典型的就是为了使用Storm服务,我们需要脱离于统一的Hadoop Yarn资源调度框架单独部署和运维一套Storm集群。
虽然后来Spark分布式内存处理框架的出现,能够在一套计算框架内完成批处理和流处理的计算,但是由于Spark Streaming是通过微批的方式模拟的流式计算,并不能完美且高效的处理原生的数据流。注:其实数据产生的本质就是一条条真实存在的事件,Spark Streaming是通过一定的时延的情况下对业务数据进行处理,然后得到基于业务数据统计的准确结果。
除了这种违背数据本源的方式,在流计算中还有一个技术难题,就是如何让数据有状态。所谓状态就是计算过程中产生的中间结果,每次新的一条数据被计算都需要基于中间状态结果的基础上进行计算,从而产生最终的结果。一个简单的场景就是,计算每天实时的订单量,在以往的计算中,我们会借助Redis来保存最新的订单数,每来一条数据就操作Redis的API进行incr操作,将中间状态保存在Redis中。这种使用Redis或第三方存储介质保存数据的中间状态的计算方式,大大提高了开发复杂度和多系统之间交互的时间开销,也是在实时计算过程中的一个痛点。
随着Apache Flink计算框架的推出,人们惊喜的发现Flink已经能够很好的应对以上的问题。计算模型方面,Flink是一个纯粹的流式计算框架,能够保准实效性;数据准确性方面,Flink支持exactly-once语义,能够保证数据计算准确性;数据状态管理方面,Flink支持State状态管理,能够减少对第三方系统的依赖。下面是Flink具体的优势点:
1)同时支持高吞吐、低延迟、高性能
2)支持时间时间(Event Time)概念
3)支持有状态的计算
4)支持高度灵活的窗口(Window)操作,
5)支持轻量级分布式快照(Snapshot)实现的容错,保证任务失败之后能够继续按照失败前的状态执行。
6)基于JVM实现独立的内存管理(目前大数据计算框架的趋势)
7)Save Points(保存点),Flink通过Save Points技术将任务执行的快照保存在存储介质上,当任务重启的时候可以直接从事先保存的Save Points恢复原有的计算状态。
毫不夸张的说,Flink代表了当前流计算最高的技术水平,具有先进的架构理念、诸多的优秀特征、完善的编程接口,而Flink也在每一次的Release版本中,不断推出新的特性。正在研发的批流统一的新特性,也是一个非常具有颠覆性的创新。
Flink技术架构
要了解一个系统,一般都是从架构开始。我们关心的问题是:系统部署成功后各个节点都启动了哪些服务,各个服务之间又是怎么交互和协调的。下方是 Flink 集群启动后架构图。
当 Flink 集群启动后,首先会启动一个 JobManger 和一个或多个的 TaskManager。
由 Client 提交任务给 JobManager,JobManager 再调度任务到各个 TaskManager 去执行,然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。
具体的工作职责如下:
1)Client 为提交 Job 的客户端,可以是运行在任何机器上(与 JobManager 环境连通即可)。提交 Job 后,Client 可以结束进程(Streaming的任务),也可以不结束并等待结果返回。
2)JobManager 主要负责调度 Job 并协调 Task 做 checkpoint,职责上很像 Storm 的 Nimbus。从 Client 处接收到 Job 和 JAR 包等资源后,会生成优化后的执行计划,并以 Task 的单元调度到各个 TaskManager 去执行。
3)TaskManager 在启动的时候就设置好了槽位数(Slot),每个 slot 能启动一个 Task,Task 为线程。从 JobManager 处接收需要部署的 Task,部署启动后,与自己的上游建立 Netty 连接,接收数据并处理。
Flink 快速编程
下面是一个scala语言编写的wordcount程序。该程序的功能是获取网络中的一行数据,然后对一行数据进行切割得到句子中的单词,计算每个单词出现的数据。
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, StreamExecutionEnvironment} |
为了完成wordcount程序,我们实现了8个开发步骤。分别是:
1) 初始化执行环境。运行程序第一步就是获取相应的执行环境,执行环境决定了程序执行在本地环境还是集群环境。 getExecutionEnvironment 方法非常智能,能够自己判断是本地环境还是执行环境。同时,我们在创建任务的时候,也可以指定当前程序是流处理还是批处理。通过StreamExecutionEnvironment.getExecutionEnvironment 获取的执行环境是流处理环境,通过ExecutionEnvironment.getExecutionEnvironment 获取的执行环境是批处理环境。
2) 指定数据输入源。准备好执行环境之后,接下来就需要将外部数据引入到Flink程序,本例是从网络中获取数据。在Flink中提供了很多种读取外部数据源的方式,它们有个统一的名字,叫做连接器。目前Flink支持FileSystem、Kafka、Hive、ElasticSearch、Rabbitmq、Hbase、JDBC等等很多主流的连接器。
3) 对句子进行切割。数据接入之后,接下来就是对数据的处理了,本例中使用flatMap对数据进行转换。Flink的Transformation都是通过不同的Operator来实现的,每个Operator内部通过实现Function接口完成数据处理逻辑的定义。
4) 每个单词记为1。
5)按照单词进行GroupBy。
6) 统计每个单词出现的次数。
7) 输出结果。数据经过转换操作之后,形成最终的结果数据集,一般需要将数据集输出到外部的存储介质上,调用addSink方法即可。本例中是将数据输出到控制台。
8) 执行任务。所有的计算逻辑定义好之后,需要显示的调用env.execute()方法来触发应用程序的执行。
其中步骤1和步骤8是固定的模板,用来创建运行环境和提交程序,每个程序都需要配置。步骤2和步骤7也是固定的模板,用来指定每个程序的输入和输出,也就是数据从哪里来到哪里去。步骤3、4、5、6,就是属于每个程序特有的计算逻辑了。
Flink任务提交
Flink的任务执行有两种方式,一种是在本地(开发环境)执行任务,一种是将任务上传到集群中进行运行。
本地运行的方式比较简单,只需要在IDEA中右键运行即可;
集群模式稍微复杂一些,分为两类三种,两类分别是Flink集群模式和Flink on yarn的模式。
其中Flink on Yarn又有两种提交方式,分别是yarn-session和 yarn-cluster。
yarn-session就是在Yarn启动一个Flink集群,所有的Flink任务都在这个集群中运行。
yarn-cluster就没有集群的概念,每个flink应用程序都是一个yarn application,在yarn上可以管理,flink应用程序之间相互独立。
下图就是yarn-session和yarn-cluster的区别,在生产环境中建议使用yarn-cluster的方式。
本地模式
接下来,我们通过本地模式运行下刚才写的wordcount程序。
1)程序之前,现在Linux上开启一个网络服务。
[root@node01 ~]# nc -l 9999 |
2)在IDEA中右键运行我们的程序
3)在Linux上输入一行数据 “hello hello hello hello”
4)观察程序打印结果
Flink集群模式(Standalone)
如果想将程序运行在集群上,需要先将程序打成一个jar包,然后提交任务。
flink run -c com.xes.WordCount wordCount.jar |
Yarn集群模式(on yarn)
如果想将程序运行在集群上,需要先将程序打成一个jar包。然后选择执行的集群模式。
集群模式有两种,一种是yarn-session,一种是yarn-cluster。在生产环境中yarn-cluster是一种被推荐的方式。
它们的提交命令如下
yarn-session | 1)创建Flink服务 bin/yarn-session.sh -n 2 -jm 1024 -tm 1024 (-n是指申请多少个yarn的container;-jm是设置JobManager的JVM内存;-tm 是设置TaskManager的JVM内存) 2)提交任务 flink run -c com.xes.WordCount wordCount.jar |
yarn-cluster | bin/flink run -m yarn-cluster -yn 2 -yjm 1024 -ytm 1024 -c com.xes.WordCount wordCount.jar (-m是指运行模式;-yn是指申请2个container;-yjm是设置JobManager的JVM内存;-ytm是设置TaskManager的JVM内存) |
Flink任务执行
Flink任务提交之后,就会在相应环境开始执行,整个任务执行的过程中会有很明显的四个阶段,分别是生成四张图,按照先后顺序分别是StreamGraph -> JobGraph -> ExecutionGraph -> 物理执行图。
l StreamGraph:是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。
l JobGraph:StreamGraph经过优化后生成了 JobGraph,提交给 JobManager 的数据结构。主要的优化为,将多个符合条件的节点 chain 在一起作为一个节点,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。
l ExecutionGraph:JobManager 根据 JobGraph 生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。
l 物理执行图:JobManager 根据 ExecutionGraph 对 Job 进行调度后,在各个TaskManager 上部署 Task 后形成的“图”,并不是一个具体的数据结构。
StreamGraph其实用来表示应用程序的拓扑结构,它是在客户端生成的,通过StreamGraph我们可以很清楚的知道一个Flink应用程序的计算逻辑。可以通过https://flink.apache.org/visualizer/ 得到一个应用程序的StreamGraph,具体的操作步骤如下:
1)修改代码,改动如下:
//8) 执行任务 |
2)运行wordcount程序,得到拓扑的描述信息
{"nodes":[{"id":1,"type":"Source: Socket Stream","pact":"Data Source","contents":"Source: Socket Stream","parallelism":1},{"id":2,"type":"Flat Map","pact":"Operator","contents":"Flat Map","parallelism":12,"predecessors":[{"id":1,"ship_strategy":"REBALANCE","side":"second"}]},{"id":3,"type":"Map","pact":"Operator","contents":"Map","parallelism":12,"predecessors":[{"id":2,"ship_strategy":"FORWARD","side":"second"}]},{"id":5,"type":"aggregation","pact":"Operator","contents":"aggregation","parallelism":12,"predecessors":[{"id":3,"ship_strategy":"HASH","side":"second"}]},{"id":6,"type":"Sink: Unnamed","pact":"Data Sink","contents":"Sink: Unnamed","parallelism":12,"predecessors":[{"id":5,"ship_strategy":"FORWARD","side":"second"}]}]} |
3)打开网页,并输入拓扑的秒速信息,得到下图。
在WordCount的StreamGraph,我们可以清洗的看到一共有五个操作。
1)读取数据的DataSource,并行度是1
2)对句子进行切割的FlatMap Operator,并行度是12
3)标记每个单词出现一次的Map Operator操作,并行度是12
4)根据单词进行聚合计算的Aggregation操作,并行度是12。该操作完成了单词统计。
5)输出结果的DataSink操作,并行度是12。
StreamGraph生成之后,接下来客户端就会生成JobGraph,JobGraph的作用就是将多个符合条件的节点 chain 在一起作为一个节点。可以简单理解为将多个Operator合并成一个Operator,放在一台机器上进行计算,减少数据在网络中的传输,提高程序性能,有点类似于Storm的LocalOrShuffle分组策略。
客户端生成JobGraph之后,就通过submitJob提交至JobMaster用来生成ExecutionGraph,ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。下图是一个简单的示意。
有了ExecutionGraph,就可以对任务进行执行了,执行任务的本质就是启动一个Thread,并指定Thread的输入和输出,然后不停的从输入中拿出去,并将计算的结果发送给输出。下面是根据Flink源码简化后的Task实现。Task被启动之后,会进入一个while循环,会不停的从inputQueue中获取元素,然后调用用户自定义的userFuncitonProcess方法进行数据处理,并将结果输出到outputQueue队列中。
public class Task extends Thread { |
详细的代码,有兴趣的可以查阅以下两个方法:
org.apache.flink.streaming.runtime.tasks.OneInputStreamTask#run
org.apache.flink.streaming.runtime.io.StreamInputProcessor#processInput