这是我参与「第四届青训营 」笔记创作活动的第1天。
一、为什么选择Flink进行流式计算
1、大数据解决方案发展史
1)Hadoop:分布式、Map-Reduce、离线计算
2)Spark:批处理、流处理(Spark Streaming 解决方案:把数据切成微批,比如每2秒一个批)、SQL高阶API、内存迭代计算
3)Flink:流计算、实时、流批一体、Streaming(流)/Batch(批) SQL
2、为什么需要流式计算
大数据的实时性可以给一些业务带来更大的价值,比如根据用户行为数据实时推荐内容、监测业务系统的健康状态、监测异常交易行为。
批式计算可以简单理解为“需要等一批数据到齐之后,才开始处理”。由于批式计算不是实时计算,因此也称它为“离线计算”。批式计算处理的数据集是静态数据集。计算时长以小时、天为单位。
由于大数据实时性的需求,大数据计算架构模式也从批式计算转变为流式计算。
流式计算可以理解为数据(水)通过水管源源不断流向水龙头,在水管中先对数据进行基本的处理,处理后的数据流向业务下游,直到业务处理完成。数据源不断更新,流式计算作业24小时不断运行。
3、为什么选择Flink
Everything is a stream. 对于有边界的数据集(比如批数据集)、无边界的数据集(比如消息队列)都能在Flink里被抽象成流。得益于这种底层抽象,使得Flink能够处理各种数据。而Flink的SQL支持,能够促进普及使用,降低业务维护成本。
Flink能够保证每个数据被处理一次,延迟性在毫秒级(Spark Streaming延迟在秒级),能够吞吐的数据量大,提供了内置的对于状态一致性的处理,重启后可以完整恢复中间状态。
1)流式计算框架对比
2)Flink社区的开源生态
Flink的开源生态优秀。Flink是个计算框架,本身没有数据存储的能力,但是能够和大多数数据存储引擎(消息队列:Kafka、RocketMQ、RabbitMQ;Key-Value型:Redis;离线数据源:HDFS、Hive;OLAP相关:Clikhouse)进行比较好的集成。
二、Flink架构
1、Flink的分层架构
1)SDK层
①SQL相关的API ②DataStream(JAVA)相关的API ③Python相关的API
2)执行引擎层(Runtime层)
①翻译:将SDK层的描述翻译为DAG(有向无环图),用于描述数据处理的逻辑。
②调度:将DAG转化为分布式环境下的TASK,将TASK分配到不同的worker节点(也叫TaskManager,TM)执行,TASK之间通过Shuffle Service 进行数据交换
3)状态存储层
负责存储算子的状态信息。如果有状态相关的处理,可将状态数据存储在Flink State Backend里。
4)资源管理层
支持将Flink部署在不同环境中,如Linux、Windows、K8S、Yarn...
2、Flink整体架构
1)一个Flink集群,主要包含JM、TM两个核心组件
①JobManager(JM):负责整个任务的协调工作,包括:调度Task;触发协调Tsak做Checkpoint;协调容错恢复...
②TaskManager(TM):负责执行一个DataFlow Graph的各个Task以及data streams的buffer(缓冲)和数据交换
代码写在Client端,将用户代码转换为Dataflow graph(可理解为DAG逻辑执行图),传给JobManager。JM将逻辑执行图转化为物理执行图,并将物理执行图相关的Task分配到TM端。
2)JobManger的架构与职责
Dispatcher接收Client端提交的job,为这些job拉起JobMaster,如果JobMaster挂掉之后,Dispatcher可以恢复作业。
JobMaster负责管理一个job的整个生命周期,会向ResourceManager申请slot(插槽,一个插槽能放定量的task)。
ResourceManager负责slot资源的管理和调度。收到JobMaster的slot申请之后,会调用(假设运行在K8S上)K8S的API去拉起一些资源(TM)。
TM被拉起后,会向ResourceManager发起注册。JobMaster收到ResourceManager批准的资源后,把Task部署在对应的worker节点(也叫TM)上。
3、Flink作业示例
流式的wordCount:从kafka中读取一个实时数据流,每10秒统计一次单词出现次数,输出到下游系统里。DataStream实现代码如下:
/*Source*/
DataStream<String> lines = env.addSource(new FlinkKafkaConsumer<>(...));
/*
1、如果想要创建一个Flink作业,必须先声明环境变量env。比如我想从卡夫卡中读一个数据源,所以用“env.addSource()”。
2、FlinkKafkaConsumer是用户使用Kafka作为Source进行编程的入口,用户可以指明卡夫卡的地址/topic是哪个,它就会从卡夫卡中读取(也叫消费)相关的数据。
3、DataStream<String>的“String”意味着处理后的格式是String。
*/
/*Transformation解析*/
DataStream<Event> envents = lines.map((line) -> parse(line));
/*
1、map代表对数据集进行一对一的解析处理
2、map((line) -> parse(line))代表每读一行,就对这行调用parse
3、parse可以按空格拆分单词
4、<Event>:单词拆分为Event的格式
*/
/*Transformation*/
DataStream<Statistics> stats = events
keyBy(event -> event.id)//逻辑地将一个流拆分成不相连的分区,每个分区包含相同key的元素。在内部通过哈希分区实现的。
timeWindow(Time.seconds(10))//设计了一个10s的窗口
apply(new MyWindowAggregationFunction());//实现统计单词频次的功能
/*Sink*/
stats.addSink(new BucketingSink(path));//把数据写入下游系统
1)业务逻辑图(condensed view)
2)业务执行图(parallelized view)
假设sink算子的并发配置为1,其余算子的并发为2。
map后的一个单词需要使用keyBy去做哈希,根据单词对应的哈希值,有可能传到keyBy[1]或者keyBy[2]。
3)业务执行优化
为了更高效地分布式执行,Flink会尽可能地将不同的operator链接(chain)在一起形成Task。这样每个Task可以在一个线程中执行,内部叫做OperatorChain,如下图的source和map算子可以chain在一起。
比如Source[1]之后可以直接做map[1],不需要像keyBy一样在[1]、[2]间进行hash shuffle,因此可以将Source和map链接(chain)在一起。也就是说,在物理执行上,Source[1]读完数据后,map[1]紧接着做解析,在一个线程中串行执行。
相比原本Source[1]一个线程、map[1]一个线程,减少了线程的切换、数据的序列化反序列化操作。如果结合Flink执行机制的话,chain能够减少TM在数据缓冲区的交换,减少了延迟。
最后将Task调度到具体TM中的slot执行,一个slot只能运行同一个Task的subTask(比如Source和map是subtask)。
一个TM(可以理解为一个进程)里有几个slot(可以理解为几个线程)是用户可以自定义的,每个slot是一个单独的线程(Thread)在执行。
三、个人总结
重点需要了解:
1、数据流是如何在Flink 客户端、JobManager、TaskManager之间流动的
2、JobManager、TaskManager的架构
3、OperatorChain的优点