流/批/OLAP 一体的 Flink 引擎介绍(课中)

394 阅读20分钟

这是我参与「第四届青训营 」笔记创作活动的的第3天

Flink概述

Apache Flink的诞生背景

何为大数据?

大数据指无法在一定时间内用常规软件工具对其进行获取,存储,管理和处理的数据集合

4v特性

  • Volumes 海量化(规模大)
  • Variety 多样化(结构化半结构化数据)
  • Velocity 快速化(数据产生速度快)
  • Value 价值化(价值密度虽然低,但是整体价值高)

大数据计算架构发展历程

史前~06HadoopSparkFlink
传统数仓,Oracle,单机,黑箱分布式,MapReduce,离线计算批处理,流处理,SQL高阶API,内存迭代计算流计算,实时,更快速,流批一体,Streaming,BatchSQL

从Hadoop到Spark,由于Map与Reduce之间数据需要落盘,处理性能较差,所以Spark中提出了内存迭代计算;其次Hadoop中的MapReduce抽象了两个算子Map,Reduce,整体提供的接口较为原始,Spark中提供了SQL高阶API,能够表达更多语义,使任务表达更为简单。 所以,Spark逐渐取代了Hadoop中的MapReduce计算框架。

从Spark到Flink,由于业内对于数据的实时性有了更高的要求,Flink是一个流式计算框架,并且可以流批都支持SQL使用,所以流式计算的活都被Flink包了,为什么需要流式计算呢?

为什么需要流式计算?

联想日常生活中场景,抖音,多多总能根据我们的实施浏览发掘我们的兴趣爱好,带来实施推荐;下单时,如果检测到异常交易行为,也能及时阻断风险发生;对于一些后台系统的健康状态,也能实时监控,避免业务故障。

从前以批式计算为主,如同分组加密,需要等待一批数据到齐才开始处理,所以又称为离线计算,是一种非实时的计算,处理静态数据集,以小时甚至天为周期。

大数据实时性的需求,带来了大数据计算架构模式的变化,流式计算应运而生,特点是实时计算,快速低延迟,无边界,动态,无限流(unbounded data stream),流式计算的作业持续不断地进行(7 * 24h不间断),流批一体

为什么是Apache Flink

image.png

  • streaming model:Flink都是Native的stream计算框架
  • stateful:在曾经的storm上,中间计算的结果会选择存进一个外部的存储中比如mysql,redis,需要时,再把数据load回来;而Spark Streaming和Flink引擎自带状态处理功能

计算语义

一个执行流/事件处理应用的流处理引擎通常允许用户制定一个可靠性模式或者处理语义,来标示引擎会为应用图的实体之间的数据处理提供什么样的保证。由于你总是会遇到网络、机器这些会导致数据丢失的故障,因而这些保证是有意义的。有三种模型/标签,at-most-once、at-least-once以及exactly-once,通常被用来描述流处理引擎应该为应用提供的数据处理语义。

At-most-once:尽力而为(best effort),数据或事件被保证只会被应用中的所有算子最多护理一次,这就意味着对于流处理应用完全处理它之前丢失的数据,也不会有额外的重发。

At-least-once:保证应用图中的所有算子都会至少被处理一次,这通常意味着当事件在被应用完全处理之前就丢失的话,会被从source开始重发replayed(retransmitted),如图

image.png

Exactly-once:流处理引擎中一个著名的且经常被广泛讨论的特征是它们的处理语义,而“exactly-once”是其中最受欢迎的,同时也有很多引擎声称它们提供“exactly-once”处理语义。是什么呢?倘若发生各种故障,事件也会被确保只会被应用中的所有算子“恰好(精确)”处理一次。有两种典型实现机制

  • 分布式快照checkpointing
  • at-least-once的事件投递加上消息去重

在这种机制中,流处理应用中的每一个算子的所有状态都会周期性地checkpointed。倘若系统发生了故障,每一个算子的所有状态都会回滚到最近的全局一致的检查点处。在回滚过程中,所有的处理都会暂停。Sources也会根据最近的检查点重置到正确到offset。整个流处理应用基本上倒回到最近的一致性状态,处理也可以从这个状态重新开始。如图

image.png

在上图中,流处理应用T1时在正常地工作,同时状态也被checkpointed。T2时,算子处理一个输入数据时失败了。此时,S = 4的状态已经保存到了持久化存储当中,而S = 12的状态仍然位于算子的内存当中。为了解决这个不一致,T3时processing graph倒回到S = 4的状态并“重放”流中的每一个状态直到最新的状态,并处理每一个数据。最终结果是虽然某些数据被处理了多次,但是无论执行了多少次回滚,结果状态依然是相同的。用来实现“exactly-once”的另一种方法是在每一个算子的基础上,将at-least-once的事件投递与事件去重相结合。使用这种方法的引擎会重放失败的事件以进一步尝试进行处理,并在每一个算子上,在事件进入到用户定义的逻辑之前删除重复的事件。这一机制需要为每一个算子维护一份事务日志(transaction log)来记录哪些事件已经处理过了。如图

image.png

source:# 我们谈论的Exactly once到底是什么?

Apache flink is a framework and a distributed processing engine for stateful computations over unbounded and bounded data streams.Flink has been designed to run in all common cluster environments, perform computations as in-memory speed and at any scale.

  • Exactly-Once精确一次的计算语义
  • 状态容错checkpoint
  • dataflow编程模型
  • window等高阶需求支持友好
  • 流批一体,基于无边界和有边界数据集

Apache Flink开源生态

image.png

本身没有存储能力,但是生态完善,kafka,rocketMQ,rabbitMQ三种消息队列等等;

部署支持standalone,还可以yarn部署,k8s部署

系统框架

  • Gelly(graph):图计算
  • Flink ML:支持机器学习
  • Stateful Function:针对状态存储

Flink整体架构

Flink分层架构

image.png

SDK层

Flink的SDK层主要有三类

  • SQL/Table:方便用户使用SQL做一些Flink流式计算处理
  • DataStream:有时候用SQL难以表达,可以用Java的API
  • pyFlink:解决机器学习相关场景

执行引擎层(Runtime层)

提供了统一的DAG的API,将SDK层无论是哪一种描述转化成统一的抽象DAG图,用来描述数据处理的PipeLine,不管是流还是批,都会转换成DAG图,进入调度层;调度层再把DAG转化成分布式环境下的Task,将不同的Task分发到不同的worker节点,Task会产生数据交换(不同机器间),通过shuffle service传输数据

Flink的执行引擎基于流处理实现

  • 支持分布式流处理
  • 从作业图(JobGraph)到执行图(ExecutionGraph)的映射、调度等
  • 为上层的API层提供基础服务
  • 构建新的组件或算子

状态存储层

负责存储算子的状态信息(写入本地磁盘filesystem/rocketDB)

资源调度层

资源管理框架(standalone,yarn,K8S),目前Flink可以支持部署在多种环境

Flink总体架构

image.png

一个Flink集群,主要包含以下两个核心组件:JM,TM

  • Client端,用于与JM建立连接,进行Flink任务的提交,Client会将Flink任务组装成一个JobGraph并进行提交,一个JobGraph是一个Flink Dataflow,其中包含一个Flink程序的JobID,Job名称,配置信息,一组JobVetex等。
  • JobManager,Flink系统协调者,负责整个任务协调工作,包括调度task,触发协调task做checkpoint,协调容错恢复等,接收job任务并调度job的多个task执行。同时负责job信息的收集和管理TaskManger。
  • TaskManger:负责执行计算的Worker,同时进行所在节点的资源管理(包括内存,cpu,网络),启动时向JobManager汇报资源信息,执行一个dataflow graph的各个task以及data streams的buffer和数据交换

Flink的项目代码经过处理转换成逻辑执行图dataflow graph(用户数据处理逻辑Pipeline的内容转换为一个DAG图),Client将该图提交给JM;JM将逻辑图转换为一个具体的物理执行图,JM根据具体的物理执行图的调度把对应的Task调度到不同的TM中。

JobManager

image.png

  • Dispatcher:接收Client端提交的Job作业,拉起JobManager来执行作业,并在JobMaster挂掉之后恢复作业
  • JobMaster:管理一个Job的整个生命周期,JM会向RM申请一些资源(request slot插槽,一个插槽放一个task,另外的task放在其它的slot中),并将task调度到对应TaskManager上
  • ResourceManager:负责slot资源的管理和调度,TaskManager拉起之后会向RM注册请求slot资源,RM收到注册请求之后,将2个slot的资源给JobMaster,JM再去做Task的分配,将Task部署到对应的TM节点上

Flink作业示例

参考Flink控制任务调度:作业链与处理槽共享组(slot-sharing-group)

image.png

流式的wordcount示例,从kafka中读取了一个实时数据流,每10s统计一次单词出现次数,以上是DataStream实现代码

source -> transformation(map -> keyby(),window(),apply())-> sink

业务逻辑转化成了streaming dataflow graph,如上图下半部分所示

在分布式处理中,我们要注意并发度的设置,假设作业的sink算子并发度配置为1,其余算子并发度为2,紧接着会将上面的streaming dataflow graph转化为parallel dataflow(Execution graph)

Flink 中的程序本质上是并行和分布式的。在执行过程中,一个流有一个或多个流分区,每个算子都有一个或多个算子子任务。算子子任务相互独立,在不同的线程中执行,可能在不同的机器或容器上执行。

算子子任务的数量是该特定算子的并行度。 同一程序的不同运算符可能具有不同级别的并行度。

image.png

对于分布式执行,Flink将 operator 子任务chain在一起形成 tasks。每个tasks由一个线程执行,内部叫做OperatorChain。将运算符链接到任务中是一种有用的优化:它减少了线程到线程切换和缓冲的开销,并在降低延迟的同时提高了整体吞吐量。链接行为可以配置。

下图中的示例数据流使用五个子任务执行,因此使用五个并行线程,source和map算子可以chain在一起。

image.png

最后将上面的Task调度到TaskManager中的slot执行,一个slot只能运行一个task的subTask。

image.png

可以看出上图一个TM中有三个slot,每个进程都有三个线程在单独执行,每个slot由一个单独的线程执行。在TM中,slot之间的资源比如CPU是不会隔离的,内存有一部分会被隔离,但也不完全,比如说heap的内存没有被严格隔离。

从调度来看,上一张图,source,map,1,source,map,2,keyby1,keyby2,sink1最终通过JM调度,至Task Slot上。

shuffle,slot sharing group没讲

如何通过调整默认行为以及控制作业链与作业分配(处理槽共享组)来提高应用的性能。

其实这两个概念我们可以看作:资源共享链与资源共享组。当我们编写完一个Flink程序,从Client开始执行->JobManager->TaskManager->Slot启动并执行Task的过程中,会对我们提交的执行计划进行优化,其中有两个比较重要的优化过程是:任务链与处理槽共享组,前者是对执行效率的优化,后者是对内存资源的优化。

Flink如何做到流批一体

为什么需要流批一体

讲白了就是需求不同,既有对秒为单位的数据统计需要,也需要一个更宏观的统计信息,单位从分钟,小时甚至天。

image.png

  • 实时:原始数据导入Kafka,Flink基于其中的数据做相关的ETL清洗或者数据统计(直播间数据统计,点赞量播放量统计...),处理完后,将数据导入服务层(ES/Clickhouse/ByteSQL),最后向客户端提供展示
  • 离线:通常有可能数据是从kafka落到Hive中,再通过Hive或者Spark做离线处理,数据导至某个服务层,给用户提供服务。

上述架构的一些坏处:

  • 人力成本高:流批两套系统,相同逻辑需要开发两遍
  • 数据链路冗余:本身计算内容是一致的,由于两套链路,相同逻辑需要运行两遍,产生一定的资源浪费
  • 数据口径不一致:两套系统,两套算子,两套UDF(用户自定义函数),通常会产生不同程度的误差,这些误差会给业务方带来非常大的困扰。

流批一体的挑战

流式计算批式计算
unbounded data streambounded data stream
低延迟,业务感知运行中的情况实时性要求不高,只关注最终结果产出时间

Flink如何做到流批一体

为什么可以做到流批一体呢?

批示计算是流式计算的特例,everything is streams,bounded data stream(batch data)也是一种特殊的数据流。因此,可以用一套引擎架构来解决上述两种情况,只不过需要针对不同场景支持相应的扩展性,并允许做到不同的优化策略

image.png

调度方面,既能做流的调度,又能做批的调度,因为可以统一两种计算模式调度模型;流和批的shuffle service因为业务场景的差异也是不同的,但是这里支持插件化的shuffle service,所以Flink也是支持批的计算场景的。

怎么做到的?

主要从以下几个模块做到流批一体的:

  • SQL层(支持有边界和无边界的数据集输入和处理)
  • DataStream API层统一,流批都可以使用DataStream API来开发(SQL难以解决的,java,Scala实现)
  • Scheduler层架构统一,支持流批场景
  • Failover Recovery层架构统一,支持流批场景(容错)
  • Shuffle Service层架构统一,流批场景选择不同的Shuffle Service

Scheduler层

Scheduler主要负责将作业的DAG转化为在分布式环境中可以执行的Task

在早期版本中,Flink支持了两种调度模式EAGER和LAZY

模式场景特点
EAGERStream场景申请一个作业所需要的全部资源,然后同时调度这个作业全部的Task,所有的Task之间采取PipeLine的方式进行通信
LAZYBatch作业先调度上游,等待上游产生的数据或结束后(数据写入磁盘,释放上游的资源),用上游处理完的资源再调度下游,类似Spark的Stage执行模式

image.png

  • 对于EAGER模式,只有12个资源一起全调度起来了,集群得有足够的资源,这样才算流作业run起来了;
  • 而对于LAZY模式,最小调度一个Task即可,集群有1个Slot资源可以运行;

这也能体会到如果是无限流,LAZY模式是无法解决的,每个slot资源一直被使用永远可能不会被释放,所以只有所有task都调度起来,才算run起来一个流作业。

在最新的Flink调度机制中,提出了一个Pipeline Region

image.png

不难看出,需要资源越少(LAZY),效果越不好,需要资源越多(EAGER),效果也越好(类似于PR曲线),因为所有的Task都在同时运行,而且数据无需落盘。

  • 由PipeLine的数据交换方式连接的Task构成为一个PipeLine Region,如果一个流全部是无限数据集,就可以认为整个流是一个PipeLine Region整体,这个PipeLine Region作业必须全部调度起来
  • 本质上,不管是流作业还是批作业,都按照PipeLine Region粒度来申请资源和调度任务。PipeLine Region就是一个抽象,为了在调度层面同时处理这两种场景

对于刚才的DAG逻辑图,分别对应批和流是ALL_EDGES_BLOCKING和ALL_EDGES_PIPELINED

  • ALL_EDGES_BLOCKING:所有Task之间的数据交换都是BLOCKING模式,比如A->B过程中数据需要落盘,所以总共分为12个PIPELINE REGION
  • ALL_EDGES_PIPELINED:所有Task之间的数据交换都是PIPELINE模式,中间通过PIPELINE连接,所以总共只有1个PIPELINE REGION,整个作业作为1个PIPELINE REGION调度,当然是需要12个Task的资源

这个改进就是通过对流批不同处理模式的模型做了抽象,做到了在一个调度器里同时对流批做调度,做到了调度层的统一。

Shuffle Service层

讲了半天,什么是Shuffle?

它是在分布式计算中,用来连接上下游数据交换的过程,实际上,分布式计算中所有涉及到上下游数据的传递的过程都算作shuffle,也就是刚才每个Task之间的side都经历了shuffle。

  1. 基于文件的Pull Based Shuffle,比如Spark或MR,他的特点是具有较高的容错性,适合较大规模的批处理作业,由于是基于文件的,其容错性和稳定性会更好一些
  2. 基于Pipeline的Push Based Shuffle,比如Flink,storm,presto等,它的特点是低延迟和高性能,必须快啊直接存在内存里面,正因为如此shuffle数据不会落盘,如果是batch任务的话,就需要重新开始任务

流批shuffle之间的差异

数据的
生命周期shuffle数据与Task是绑定的,如果Task销毁了,那么shuffle数据也没了批作业shuffle数据与Task是解耦的,批的shuffle数据
存储介质内存(为了velocity和real-time),这一点也决定了生命周期磁盘(因为数据volume很大,并且得保证fault tolerance)
部署方式由于是Pipeline的传递模式,shuffle服务和计算节点部署在一起,可以减少网络开销,从而降低延迟latency批作业则不同,除了传统的数据落盘,还有remote shuffle service,不完全写入本地磁盘,远程写入多份的好处就是防止宕机

Flink对于流批提供的两种shuffle,虽然streaming和batch shuffle在具体策略上存在一定的差异,但本质上都是为了对数据进行re-partition,因此不同的shuffle之间是存在一定共性。 所以Flink的目标是提供一套统一的shuffle框架,既能满足不同shuffle在策略上的定制,同时还能避免在共性需求上进行重复开发。

image.png

为了统一Flink在streaming和batch模式下的shuffle结构,Flink实现了一个Pluggable的shuffle service框架,抽象出一些公共模块。

Flink开源社区支持:

  • Netty Shuffle Service(内置):支持pipeline和blocking,为Flink默认的shuffle service策略,通过Netty连接,直接把数据传至下游
  • Remote Shuffle Service:既支持pipeline又支持blocking,不过对于pipeline模式,走remote反而性能会下降(因为网络传输存在的延迟latency),所以该种策略主要还是用在batch的blocking场景

Flink架构优化

OLAP业务场景(交互式分析)

在抖音的一些推广活动中,运营同学需要对一些实时产出的结果数据做一些实时多维分析,来帮助后面活动的决策。

image.png

OLAP要求高并发查询,查询时长也有要求,这是区别于流批的一个点

image.png

一套解决?

前面说到,批示计算中的有界数据集(批式数据)是一种特殊的流;现在,我们说OLAP计算是一种特殊的批式计算,只是对并发和实时性要求也很高,其他与普通批式作业没有两样。

因此理论上,我们可以用一套引擎架构来解决上述三种场景,只不过需要对不同场景支持相应的扩展性,并允许做不同的优化策略。

Flink从流式计算出发,需要想支持Batch和OLAP场景,就需要解决如下问题:

image.png

如何支持OLAP场景

Flink做OLAP的优势

  • 引擎统一
    • 降低学习成本
    • 提升开发效率
    • 提高维护效率
  • 既有优势
    • 内存计算
    • Code-gen
    • Pipeline Shuffle
    • Session模式的MPP架构
  • 生态支持
    • 跨数据源查询支持
    • TPC-DS基准测试性能强

Flink OLAP场景的挑战

  • 秒级和毫秒级的小作业
  • 作业频繁启停,资源碎片
  • 对于latency和QPS的要求

Flink OLAP架构现状

image.png

  • Client:提交SQL Query
  • Gateway:接收Client提交的SQL Query,对SQL进行语法解析和查询优化,生成Flink作业执行计划,提交给Session集群
  • Session Cluster:执行作业调度及计算,并返回结果。提前把集群创建出来,一台机器作为JobManager,另外十台机器作为TaskManager,因为集群已经创建好了,所以作业提交过来不用再去新创建TM,申请资源(等待YARN,K8S),这样资源申请就节省下来了。专门解决OLAP场景。

Flink在OLAP架构的问题与设想

架构与功能模块:

  1. JobManager与ResourceManager在一个进程内启动,无法对JobManager进行水平扩展。JM是维护一个作业的提交和分发,而RM是负责资源相关工作,OLAP需要支持高并发,在一个集群内,期望对JM做扩展,比如说JM有3个,RM有1个,这样的话可以对JM某一个组件做扩展,来支撑更大的QPS
  2. Gateway与Flink Session Cluster互相独立,无法进行统一管理

作业管理和部署模块:

  1. JM处理和调度作业时,负责的功能较多(JM在解析完作业后,会生成物理执行计划,物理执行计划最小单位为计算任务,JM管理了这个作业的所有的计算任务,比如说TM两两之间的连接都是由JM来管理的),导致单作业处理时间长,占用了过多的内存,影响高并发情况下的性能
  2. TM部署计算任务时,任务初始化部分耗时严重(与流批不同,OLAP需要频繁初始化),消耗大量CPU

资源管理及计算任务调度:

  1. 资源申请及资源释放流程链路过长
  2. Slot作为资源管理单元,JM管理Slot资源,导致JM无法感知到TM维度的资源分布,在JM端不易做基于负载的优化,资源管理完全依赖于RM

其它:

  1. 作业心跳与Failover机制,并不适合AP这种秒级或者毫秒级计算场景
  2. AP目前使用Batch算子进行计算,这些算子初始化比较耗时(比如聚合操作申请内存)