Flink核心知识笔记整理

        本文内容主要从《Flink内核原理与实现》一书中摘录总结成文,可以让我们以最快的速度回顾Flink相关的核心知识点。文章成文以常见的领域模块组织。

系统逻辑

  1. 用户接口DataStream底层都要转换为Transformation。
  2. 水印Watermark是为了解决数据乱序的问题导致结果计算不准确。数据进入系统后,会因为各种网络和处理原因,导致顺序错乱。此时就有可能需要重新等待整理一番才能进行下一步处理,这带来了计算的延迟。但是又不能无限的等待下去,需要一个机制来保证特定时间后一定会触发窗口进行计算。这个最大延迟计算时间就是水印了
  3. flink有物理类型和逻辑类型两种类型系统。物理类型面向开发者,逻辑类型是描述物理类型的类型系统,能够对物理类型进行序列化和反序列化

作业

  1. 作业运行时由多个Task组成,一个Task包含一个或多个算子,一个算子就是一个计算步骤。具体的计算由算子总包装的Function或用户自定义函数UDF来执行
  2. 一个作业的数据计算会分成多个Task去执行,那么就需要对数据进行切分,然后交给不同的Task去执行。这种数据分区有多种切分策略。比如轮换和广播。广播是指复制同样的数据给下游每个分区。
  3. 开发人员开发的flink作业jar文件并不是flink可以直接执行的,需要经过转换之后才能提交给集群。flink客户端通过反射启动jar中的函数,生成StreamGraph,将JobGraph提交给集群。集群收到后,将JobGraph翻译成ExecutionGraph,然后开始调度执行,启动成功之后开始消费数据。
  4. 作业提交有两种模式,即Session模式和Per-Job模式。前者作业共享集群资源,通过HTTp提交。后者 是一个作业一个集群,相互隔离,这样也会导致集群资源进程频繁创建和关闭,因为创建一个集群TaskManager需要经历创建一个新容器,下载flink 节点镜像并启动、注册等一系列操作。
  5. session模式下,flink客户端通过http向Dispatcher提交JobGraph。收到JobGraph后,为作业创建一个JobMaster,将工作交给JobMaster构建ExecutionGraph。JobMaster向ResourceManager申请资源,开始调度ExecutionGraph执行。ResourceManager收到JobMaster的资源请求,如果当前有空闲Slot则将其分配给JobMaster,否则将向容器资源管理器(Yarn、K8s等)申请创建TaskManager(将资源请求加入等待队列,并通过心跳向容器资源管理Yarn等申请新的容器资源来启动TaskManager进程)。然后ResourceManager从HDFS加载flink jar及配置文件等在新容器中启动TaskManager进程。TaskManager启动后会向ResourceManager注册自己,并把自己的Slot资源情况汇报给ResourceManager。ResourceManager从等待队列中取出Slot请求,向TaskManager确认资源可用情况,并告知TaskManager将Slot分配给了那个JobManager。TaskManager向JobMaster提供Slot,JobMaster调度Task到TaskManager的此Slot上执行。
  6. Per-Job模式下JobGraph和集群的资源需求一起提交给资源管理器,其他过程和上面Session模式一样。
  7. 批处理接口DataSet和流处理接口DataStream在底层转换到JobGraph时达到了统一。对于流处理接口首先要将DataStream API的调用转换为Transformation,然后经过StreamGraph->JobGraph->ExecutionGraph 3层转换,最后经过Flink的调度执行,在Flink集群中启动计算任务,形成一个物理执行图。对于批处理接口首先将DataSet API的调用转换为OptimizedPlan,然后转换为JobGraph,此时就和上面流处理接口上达到了统一。
  8. StreamGraph由节点StreamNode和边StreamEdge构成的图结构,在作业提交给集群之前由客户端构建完成。节点可以看成是算子,边用来连接两个 节点,包含了旁路输出、分区器、字段筛选输出等信息。
  9. JobGraph核心对象有JobVertex、JobEdge、IntermediateDataSet。JobVertex的输入是JobEdge,输出是IntermediateDataSet,它包含一个或多个算子。JobEdge是连接JobVertex和IntermediateDataSet的边。IntermediateDataSet是JobVertex的输出中间结果数据集。
  10. JobGraph是在客户端就要转换好的,然后才会提交到服务端,服务端JobMaster需要进一步将JobGraph进行转换为ExecutionGraph。这样才能真正开始进行作业调度执行了。ExecutionGraph拥有和JobGraph相似的结构,ExecutionJobVertex可以简单理解为一Ttask,如果需要并行则会有多个。
  11. 执行模式是作业执行是采用流水线Pipeline还是批Batch模式。流计算采用推送模式。
  12. 作业执行核心对象组件有Task、ResultPartition&ResultSubPartition、输入网关InputGate&InputChannel

任务

  1. 任务Task会被横向和纵向拆分。横向拆分是指任务的并行度,即产生多个实例并行运行任务。纵向拆分是把一个任务在输入-处理-输出过程中分解成多个步骤子任务完成。拆分过程构成了一个有向无环图DAG。
  2. 系统运行时如果需要改变任务并行度,则相应算子状态中的数据也要做重分布调整。算子状态中的ListState并行度改变时,会将并发上的每个List都取出,然后合并到一个新的List,再重新均匀分配给新的Task。而UnionListState会将原来的List拼接起来,然后不做划分,直接交给用户。广播状态BroadcastState就不会有什么变化,新Task也会得到相同的状态。
  3. Task本身是一个Runnable对象,有run方法可以在线程池中被运行的。里面通过反射机制实例化业务逻辑执行。
  4. StreamTask是算子的执行容器,而Task又在其之上,Task会反射实例化StreamTask的子类,并调用invoke方法执行业务逻辑
  5. Task的输入有两种,一种是从上游Task获取数据,使用InputGate作为底层读取数据组件。二种是从外部获取数据。输入有单流和双流两个接口。
  6. 结果分区ResultPartition用来表示作业单个Task产生的数据,是一组Buffer实例,由多个结果子分区ResultSubPartition组成用来进一步切分ResultPartition。切分成多少个子分区取决于下游任务的并行度和数据分发模式。下游任务请求上游任务产生的结果数据,是请求的子分区,有远程和本地请求两种。
  7. 有限数据集BoundData,它定义存储和读取中间计算结果数据。它有3中实现方案,①使用FileChannel写入和读取数据文件。②使用FileChannel写入数据文件,但使用内存映射读取数据文件。③使用内存映射写入和读取数据文件。内存映射mmap适用于大文件且固定大小读写,而FileChannel适用于小文件读写。
  8. 输入网关InputGate是Task输入数据的封装组件,和JobEdge一一对应。其对于于上游的结果分区ResultPartition。InputGate中的InputChannel对应于结果子分区ResultSubPartition
  9. Flink作业真正执行起来之后,会在物理上构成Task相互连接的数据流转DGA图。这是JobMaster实现规划好的。
  10. 上游Task将处理结果发送给下游Task,根据需要有单播和广播两种情形。单播就是一个数据只能被下游一个Task消费一次,如果下游Task是并行的,则将结果数据采用轮询方案分别放入下游Task对应的结果子分区ResultSubPartition。广播就是下游Task即便是并行的,结果数据也要发给每一个Task使用,如果采用单播那种方案给下游每个并行的Task的子结果分区分发数据,则十分浪费内存空间,改进方案就是只将数据存入第一个结果子分区,然后由下游并行的Task自己取或主动推送给他们。然后对于广播记录和广播事件来说,因为本身数据量不大,所以采用向所有结果子分区写入数据的方案。
  11. flink数据传送给下游主要有三种,计算数据StreamRecord、水印WaterMark、延时标记LeteceyMark行为。
  12. Flink数据传递有三种情形。①本地线程内交换。一般是一个Task内有多个顺序执行的算子链OperatorChain之间上下游传递数据,无需序列化和反序列化,直接使用java对象传递即可。②本地线程之间交换。不同Task之间上下游传递数据,使用一个共享的Buffer缓冲数据,上下游Task之间使用wait/notify all机制等待通知获取数据,需要序列化和反序列化。③跨网络的数据传递。位于不同节点上或同一个节点上的不同JVM上的上下游Task之间传递数据,使用Push/Pull方式,底层需要使用Netty传输
  13. 在Push模式中,Task执行结果序列化后先放入一个缓冲池,然后会写入下游对应的ResultSubPartition。由单独的一个线程定期100ms向下游flush一次数据,相比一条数据就立即推送而言,大大提高了吞吐量,虽然有了一定的延时。

算子

  1. 算子Operator包含生命周期管理、状态与容错管理、数据处理3个方面的关键行为。分单流输入和双流输入算子,单流就是只有一个输入源,一般的算子都是这种类型。双流就是两个输入源,常见于Group、Join操作
  1. 算子Operator生命周期阶段:setup->open->close->dispose。setup包括初始化环境、时间服务、注册监控等。open包括状态初始化,完后才会执行Function进行数据处理。close是数据处理完关闭算子,确保将所有缓冲数据向下游发送。dispose进行算子占用的资源释放
  2. 算子Operator状态与容错管理:提供状态存储,触发检查点时保存状态快照,并异步保存到外部的分布式存储。作业失败的时候负责从保存的快照中恢复状态
  3. 异步算子Operator目的是解决执行逻辑要与外部系统交互时网络延迟所导致的系统瓶颈问题。比如连续对外发送请求查询某用户信息时,不需要等待上一次查询结果返回就可以发送下一个请求。在算子输出模式上也设计了顺序输出和无序输出两种方式,后者性能显然更高,顺序输出就需要后返回的请求结果等待前面的请求结果后再输出,算子内部会维护一个队列。
  4. 窗口Window算子Operator负责将进入系统的数据流按照某个方式进行切分,放入不同的窗口中,创建新窗口或合并旧窗口。一个元素可以被放入多个窗口中。
  5. 水印WaterMark一般由数据源算子产生,向下游传递。窗口算子收到上游多个水印时选择其中较小的作为当前新的水印并发送给下游
  6. 一个算子Operator中可以有多个定时器服务,通过名称区分。内部使用两个优先队列分别维护事件时间和处理时间的定时器。定时器触发计算执行是根据水印的时间确定的,水印时间过期了就要触发执行
  7. flink使用自己定义的优先级队列来管理定时器。有基于java堆内存的和Cache+RocksDB实现的优先级队列两种实现。Cache保存队列的前N个元素,其余保存在RocksDB中。两种方案中堆内存性能高,但是容纳的数据量较小。后者反之。
  8. 流计算中因为是无限时长的,所以算子的状态不会自动清理,随着时间可能越积越多,需要我们通过API控制状态里的数据过期时间。状态的过期数据通过下次被访问时校验是否过期及清理工作,快照时也会触发。这种机制类似Redis的过期清理策略
  9. 为了更高效的实现分布式执行,flink会尽可能的将多个算子融合在一起形成一个算子链OperatorChain。一个算子链在同一个Task线程内执行。算子链中的算子之间直接本地方法调用串行执行。这样可以减少线程切换开销,减少消息数据的序列化和反序列化,减少网络IO,提高整体吞吐量。不过算子融合在一起的策略也是有条件的,不然就变成了单机系统了,期间需要评估代价
  10. 数据交换模式是指上下游算子之间如何交换数据。根据执行模式的不同而不同。一共有4中模式。块blocking和pipelined,blocking是上游处理完所有数据之后,才会交给下游进行处理,可以被消费多次,可以被并发消费,等待调度器销毁,适用于批处理。pipelined是上游处理结果数据只能被1个下游消费者消费1次,然后就销毁,适用于流批处理
  11. 定时器服务TimerService是在算子Operator中提供定时器的管理行为,包含定时器的注册和删除。定时器分为事件时间和处理时间两种。定时服务管理器使用Map结构管理所有的定时器服务。key就是定时器服务的名称
  12. 状态State用来保存中间计算结果或者缓存数据。根据是否需要保存中间结果分为有状态和无状态,有状态需要考虑备份和恢复这种容错机制,这就是State的作用。按照是否有key划分为主键状态keyedState和算子状态OperatorState。前者State和流上的每个key绑定,一对一的关系。后者和特定算子绑定,一个算子实例绑定一个状态。广播状态BroadcastState,就是来自一个流的数据需要广播到所有下游任务,在算子本地存储,在处理另一个流的时候依赖于广播的数据。

函数

  1. 每个窗口Window都有自己的触发器Trigger,触发器上有一个定时器用于决定何时触发计算和清除,是一种延时处理机制。分别有基于计数、事件时间和处理时间触发机制
  1. 触发器Trigger触发时,窗口中的元素会交给过滤器Evictor(如果定义了的话),用来遍历窗口中的元素,可以用来在计算前对窗口中的元素做某种过滤。将剩下的合法元素交给函数进行计算。分别有计数、时间、阈值过滤器几种类型。窗口中超过一定数量或时间范围或阈值后的元素会被舍弃
  2. 函数Function是算子Operator内具体负责执行的逻辑代码。
  3. 双流Join函数Function实现有即时和延迟两种。即时Join是先创建一个State对象,接受到输入流1事件后更新State,接受到输入流2的事件后遍历State,根据Join条件进行匹配,并发送结果到下游。延迟Join是分别为两个输入流创建各自的State对象,可以连续接受多个输入事件的数据暂存起来。创建一个定时器,等待定时时间到达后触发两个State数据集合的Join计算。将匹配后的结果发送到下游。后者设计显然大大提高了吞吐量,但是损失了实时性。
  4. 检查点函数Function就是支持函数级别的保存和恢复的函数。为了实现函数级别的State管理而设计。函数的状态也可以快照备份到外部存储设备
  5. 窗口Window函数Function是窗口内负责具体执行逻辑计算的。分为增量和全量计算函数,增量是每个数据到达后立即计算,窗口只保留计算后的中间结果,而全量计算是先缓存窗口中的所有数据,然后触发执行计算,需要占用额外更多的空间。
  6. 会话窗口Window实现时,为每一个事件都创建一个新窗口,然后判断是否需要与前序窗口进行合并依据间隔时间。原因是有些事件到达总不是那么及时,原来划分到不同的会话窗口的事件数据,因为迟来的新事件,而使得其不再满足划分不同窗口的时间间隔条件需要合并之。窗口合并总是合并到前序窗口,删除后来的窗口,State也是如此。对于窗口中的触发器合并,是先删除所有的,然后在创建新的触发器。

其他

  1. 物理类型是开发阶段使用的java类型,运行时需要将物理类型转换为逻辑类型,就需要使用类型提取机制实现。java中使用反射获取从字节码中获取提取类型。对于泛型类型提取则比较困难,这是由于java编译成字节码时进行了类型擦除。比如List会变成List父类类型。为了使类型信息不丢失,flink使用TypeHit接口在开发时用于保留类型信息。以便在转换为逻辑类型时可以直接获取真实的泛型类型。
  2. 调度器是flink作业执行的核心组件,管理作业执行的所有相关过程,包括JobGraph转换到ExecutionGraph,作业的发布、取消、停止。作业Task的发布、取消、停止,资源的申请与释放及失败处理。
  3. 调度模式也是因流处理作业和批处理作业不同而采取不同的模式。和概念调度策略类似。
  4. 调度策略是根据流和批的处理不同而使用不同的策略。流作业调度前需要一次性获取所有需要的Slot,而批处理作业则可以分阶段调度执行,上一个阶段执行完毕,数据可消费的时候,开始调度下游的执行。

集群模块

  1. 典型的Master-Slave架构。包括ResourcManager、JobManager/JobMaster、TaskManager三种角色的节点。

ResourcManager

  1. ResourcManager管理集群资源申请,比如启动新的容器运行TaskManager,然后会把自己持有的slot信息注册到它那里。同时管理TaskManager上的Slot资源被JobMaster申请和释放。它是和资源管理框架Yarn、k8s、mesos集成的。向他们申请和释放资源

JobMaster

  1. JobMaster负责作业的生命周期管理,作业状态共有9种,包括创建Created、运行Running Man、重试中Restarting、取消中Cancelling、已取消Canceled、已挂起Suspended、已完成Finished、失败中Failing、已失败Failed。为了进行有效的状态之间转换管理,采用状态机思路设计。
  2. 现在的JobManager的含义是一个具体的进程,是flink集群的Master节点。旧版的很多作业管理功能转移到了新版的JobMaster去了。它可以包含多个JobMaster线程以及ResourcManager和派发任务的Dispatcher组件。
  3. JobMaster是具体某个作业的管理者,取代了以前的JobManager一些职责。多个作业就会在JobManager进程中通过JobManagerRunner产生多个JobMaster线程,作业就这样通过一个小集群来运行。而此时JobMaster就是当前作业的Master了。这样也就做到了作业之间隔离运行。它负责产生JobGraph->ExecutionGraph可执行任务。执行任务前会先去ResourcManager申请可用的Slot,如果slot不足ResourcManager就会去yarn、k8s等资源管理中心申请新容器启动TaskManager以便获得充足的slot。获取足够slot后便可以开始将任务分解交给这些TaskManager去执行。
  4. JobMaster在批处理中使用InputSplit为计算任务分配待计算的数据分片。
  5. JobMaster向所有TaskManager发出取消作业的指令,TaskManager执行task的取消操作,并进行相关内存资源的清理。当所有清理作业完成后向JobMaster发出通知,最终JobMaster停止,向ResourceManager归还所有的slot资源退出作业。

TaskManager

  1. TaskManager就是作业的slave,负责执行作业的具体计算任务,TaskManager提供实际的Slot资源。TaskManager进程在容器中启动后会去ResourcManager注册自己持有的slot资源,同时保持心跳反馈状态信息给ResourcManager。一个TaskManager至少有一个Slot
  2. Flink通过槽Slot这个概念将所有资源进行精细化管理,提高效率。每个Slot就是一个线程资源,附加一定的内存空间。一个Slot可以执行多个Task作业,但同一时刻只能执行一个,串行执行一组Task。类似线程线程池里的线程。某些场景下为了提高资源(内存、TCP连接、心跳)的利用率,将它们共享给当前TaskManager进程中拥有的所有Slot,而不用严格划分。这样不同Slot中的Task可以共享数据集和数据结构。对于并行任务,同一种类型的Task不能分配到同一个Slot中,这样就会变成串行执行,显然无法提高并行度了。
  3. TaskManager负责Task的生命周期管理。并将状态的变化通知到JobMaster。实际上作业的状态就是下属Task们状态的汇总。Task状态种类和作业类似,同样采用状态机进行设计管理

心跳机制

  1. JobMaster、ResourcManager、TaskManager相互之间保持心跳。

选举机制

  1. 支持三种选主服务,①基于ZK选举。②没有ZK时的选举。③本地调试模式下的选举。生产环境建议使用基于ZK的
  2. 在HA模式下集群节点对JobMaster识别使用Akka的Fenced消息类型,防止集群选举的脑裂问题。该消息类型带有JobMaster的ID,新产生的JobMaster的ID被集群节点接受后,节点将忽略其他节点发送的JobMaster ID信息,因为旧的JobMaster可能因为假死导致失去了JobMaster地位,但是后面又活过来了,旧JobMaster如果继续给其他节点发送命令就会被忽略掉。

元数据

  1. ZK主要存储一些集群的节点leader选举信息,resourceManager和JobMaster等。不存储集群各种元数据信息。

存储模块

内存

  1. 底层使用了MMAP读写大文件。
  2. 1.9版本之前的Flink的Row使用的是java的对象数组(Object[])来存储一行数据。在数据计算过程中,需要消耗大量的CPU来序列化和反序列化Row对象。
  3. 1.9版本Blink引入了二进制的内存行式存储和列式存储两种数据组织形式,两种存储结构都比较紧凑。
  4. 1.9版本Blink中BinaryRow是行式存储的实现类,直接使用MemorySegment来存储和计算,计算过程直接对二进制数据结构进行操作,避免了序列化和反序列开销。(备注:理论上来说,普通的java数字类型加减乘除运算底层也是位运算,但是由于flink这里的类型都是自定义的二进制存储,所以你要重新实现一遍位运算。否则你只能先把自定义的二进制类型转换为java类型后再用java的语法操作运算,这样就多了一层类型转换的开销了)
  5. 使用基于内存的列式存储。目前仅实现了堆上的列式存储,堆外内存还不被支持。默认一个列式存储单元可以存储2048个行的该列数据。这样可以充分利用CPU将内存中数据预加载至CPU缓存时,总是加载相邻的内存块中的数据。大大提高了缓存命中率。在经常按列统计类的分析型应用中非常有用,避免了浪费过多的CPU时钟周期。
  6. Java对象存储到MemorySegment,中间需要转化为StreamRecord对象,然后再序列化成二进制的字节数组存入MemorySegment。每个Task都有属于自己的本地缓冲池LocalBufferPool,里面有若干个MemorySegment。Task将结果存入MemorySegment,如果一个装不下就会拆分使用多个存储。
  7. 为了提高内存管理效率,部分使用堆外内存,自主内存管理。数据以二进制(byte[])存储在内存中,相当于它将普通的java对象再做了一次类型转换,转成字节数组形式。理论上来说java对象最终也是以二进制存储在内存这块硬件上的。区别就是flink使用了自己的二进制转换数据对象的规则,所以都需要借助于byte[]来重新编码。这一切都是为了降低数据存储空间成本而设计的。为了增加数据存储密度,放弃使用java对象表示(对象头就占了不少空间),改为序列化为二进制字节数据,便于通过快速移位操作,无需反序列化成java对象再操作。
  8. RecordSerializer是序列化器,支持跨内存段将目标对象序列化成二进制的字节数据放入MemorySegment。其实现是借助于一个中间缓冲区缓存序列化后的数据,然后再根据指针位置写入内存段。RecordDeserializer是其反序列化器
  9. flink使用堆外自主内存管理的原因是java堆内存存在一些缺陷。比如有效数据密度较低,由于都是使用对象实现基础数据类型,导致实际内存占用加大。垃圾回收在大数据量场景下可能导致频繁的FullGC,从而导致系统假死,由此可能节点失去心跳。内存不够用导致OOM。CPU难以命中缓存,由于缓存局部性原理CPU总是能快速读取临近内存块的数据,而java堆中的数据是比较随意分布的,因此逻辑上相邻的数据,实际上CPU读取时需要随机检索内存块加载数据至高速缓冲区。
  10. 为了对堆外和堆内内存有效管理,防止发生OOM等异常,flink实行严格的内存预配置,系统启动前就配置好内存,运行期间不再调整。自主进行内存淘汰等。
  11. 内存管理器MemoryManager是flink中管理托管内存的组件,其管理的内存只使用堆外内存。批处理中一般用于排序、Hash表和中间结果保存。流计算中作为RocksDBStateBackend的内存。内存管理器实现自主内存释放管理,实现了若干个内存垃圾回收机制。内存使用完或Task停止执行时回收至资源池。
  12. 为了提升内存使用效率,采用预分配内存机制-内存段(MemorySegment),类似memcached。节点启动就申请并划分好了内存区域,直接申请使用和归还即可,有专门的类负责管理
  13. flink内存管理模拟了操作系统内存管理,将内存划分为内存段和内存页。内存段是分配的最小单元。内存段可以上堆上内存(默认32k),也可以是堆 外直接内存。对象序列化成二进制byte数组后存入。使用时同样需要反序列化。内存页包含多个内存段,解决了某些数据较大,需要跨段保存的问题,所以内存页大小并不固定。内存页接口也是flink内部存取处理的入口,而不是直接操作内存段
  14. java对象的有效信息被序列化为二进制数据流,保存在预分配的内存段上,内存段是内存分配的最小单元,固定长度为32k。(备注:一个内存段是进程虚拟内存中一块连续的地址空间,虚拟内存是以页Page为单位的,大小默认为4kb。一个页是可以被完整的映射到物理内存中的一个页,这个页里面的数据才是真实的物理内存上的连续,不同的页就不一定会映射到连续的物理内存中了。所以很明显一个flink的内存段相当于需要8个物理页才能容纳,大概率是无法做到真正物理内存上的连续,但是可以做到虚拟地址空间上的连续)
  15. Task算子处理数据完毕,将结果交给下游的时候,需要使用缓冲(Buffer)。每个TaskManager只有一个网络缓冲池(NetworkBufferPool),不过这个缓冲池和底层socket缓冲没关系。每个Task有一个本地缓冲池(LocalBufferPool),是从网络缓冲池申请而来。本地缓冲池有若干网络缓冲器(NetworkBuffer),每个网络缓冲包装了一个内存段。网络缓冲使用引用计数方式管理,引用计数为0,则表示可以释放缓冲。

磁盘

  1. flink是计算引擎,本身不不提供存储数据能力,需要从外部数据源连接获取。同时计算完的结果,也需要输出存储到外部设备。包括kafka、rabbitmq、redis、akka、netty、es、hdfs、Cassandra、hbase
  2. flink一般不需要将业务处理数据持久化到外部磁盘设备,但是在其容错-恢复机制中需要使用磁盘持久化一些检查点数据以便恢复作业。比如算子中的State需要持久化

网络模块

  1. 底层使用akka通信框架,akka底层使用Netty。Flink底层有RPC层的抽象,也就是说支持未来多种rpc框架的实现,目前仅有akka一种实现。
  2. 批处理Batch模式下游Task获取数据采用pull拉模式,有利于节点根据自身负载来处理数据量。类似kafka消费者。其他MQ都是采用push推模式,可能造成消费者消息积压。
  3. flink1.5之前TaskManager之间不管有多少Task在运行都是只有一条TCP连接的,底层采用Netty多路复用提高效率,且无流控。
  4. 上下游Task采用Push方式向下游推送数据时,如果下游Task处理速度较慢,待处理数据就会在下游缓冲区堆积,直到缓冲区塞满耗尽,然后产生各种异常,甚至导致上游Task也无法继续处理数据,这个过程叫反压/背压。更严重的是还会影响其他Task之间的通信,因为都是共享缓冲区的。
  5. 流处理Stream模式下上游Task主动向下游Task推送PUSH模式。为了解决下游消费能力不足导致数据积压影响链路节点上的其他Task工作问题,使用基于信用积分的流控机制
  6. flink1.5后引入信用流控机制。即上下游Task之间发送接受数据靠相互告知自己的承载能力上游做流量控制。每个Task发送和接受数据都使用独占的缓冲区,这样就不会影响其他Task工作。另外增加一个公共的浮动缓冲区Buffer池,如果某个Task的缓冲区满了,且其上游待发送的数据又比较多时,可以申请更多一些的Buffer。对flink这种大多采用Push方式交换数据的系统很有效,此外kafka是采用Pull方式拉取消息,没有这么复杂的流控。
  7. 改进后上下游Task发送接收数据都是有专属每个Task的缓冲队列,有利于增加系统吞吐量且避免个别链路拥塞导致真个集群拥塞瘫痪。节点之间只有一条TCP链接,所有Task都是共享的。

序列化

  1. flink内置了大量对应物理类型的逻辑类型,负责序列化和反序列化物理类型。如果是用户自定义函数,则需要自己提供序列化和反序列化实现,否则flink默认使用kryo组件序列化它。kryo是可以序列化任何java对象的,就是效率比较低。
  2. 使用缓冲池MemorySegment交换数据需要进行序列化和反序列化操作。Java对象存储到MemorySegment,中间需要转化为StreamRecord对象,然后再序列化成二进制的字节数据存入MemorySegment。RecordSerializer是序列化器

容错模块

  1. 采用checkpoint检查点机制暂存中断的任务快照,保证后续任务能够从最近的一次检查点恢复。es、kafka中都有类似机制。尤其对于流式任务尤为重要,总不能让一个做了几天几个月的任务因为异常中断而重新做一次吧。全局检查点机制设计分布式事务协同问题,flink采用两阶段提交方式,尽可能的减少错误发生的概率。
  2. 保存点savepoint是基于检查点机制而来,用来做任务的完整状态备份,方便进行系统升级以及集群转移恢复用的。集群重启后可以从保存点恢复运行状态,保障以前运行的成果不需要重新运行。保存点实现机制相当于一个Map,key就是算子的ID,value就是算子的状态State对象。
  3. 状态State要具备一定的容错能力,比如算子线程异常退出,所以需要实现本次存储,以便后续恢复。有纯内存、内存+文件以及RocksDB三种方案。纯内存方案不适用于生产环境,适用于开发测试环境,面对整个节点或进程宕机就无能为力,仅能应付某个任务局部中断异常的情况。后两种适用于长周期大规模数据及生产环境,因为数据被持久化到磁盘了。
  4. 状态除了上述本地内存或磁盘持久化外,为了应对节点宕机场景,还需要将状态持久化到外部可靠存储设备中,比如HDFS。有全量和增量两种策略,内存或内存+文件备份方案支持全量策略,但是不支持增量策略。他们是算子通过单独启动一个线程异步持久化到HDFS的,为了不影响当前状态的持续更新,使用复制写入策略即CopyOnWriteSateTable来保证线程安全。RocksDb支持增量和全量两种策略,全量使用自身快照机制保证线程安全。而增量使用基于LSM-Tree算法的KV存储写入方案,通过顺序append写入更新部分,每次写入生成一个个小文件,后续将小文件按照key来进行合并,最终新值不断覆盖旧值。这个机制类似HBase的数据写入。
  5. 使用轻量级的异步快照机制,具体来说就是对于流式计算,系统周期性的从作业的数据源头注入一条特殊的屏障Barrier消息,该消息之间严格保证顺序性,并虽然数据的处理传递给所有下游的算子,算子收到该数据消息后执行本地状态State的备份(外部存储器)。为了让快照备份具有严格事务性,JobMaster会使用一个叫做检查点协调器CheckpointCoordinator组件负责协调flink算子的State分布式快照。算子备份完本地快照后会通知该协调器,等收到所有算子都通知完成当前检查点的屏障的备份后认为本次快照成功。Barrier机制很好解决了流计算中的快照问题
  6. 屏障Barrier在遇到一个有着双路输入的算子时,其Barrier就会一分为二,由于是异步处理,可能会导致算子收到下一个周期的Barrier了,都还没收到上一个周期的另外一个输入通道的Barrier。为了解决这个问题,采用输入通道Barrier对齐机制,就是两个输入通道如果只收到其中一个,则不会阻塞当前通道的数据,只是将其缓存起来,等到另外一个通道的Barrier过来后再进行处理,这样就能严格保证两个Barrier周期之间的数据顺序性。
  7. 比如你从kafka中持续读取数据进行处理,如果发生了异常,作业恢复时,就会从数据源算子快照那里恢复未处理完的kafka数据,因为快照备份时也保存了kafka消费的偏移量。异常期间kafka的数据会堆积起来,不妨碍作业恢复后继续处理。所以一般流计算最好按数据的事件时间做统计处理,不然如果按处理时间此时作业恢复后会短时间内处理大量积压的数据。
  8. 屏障Barrier作为一个检查点消息,其设计为递增的,由JobMaster发放。JobMaster监控检查点收集算子的反馈消息时也有超时等待时间,不能无限期等待下去,超时异常都会写检查点失败。
  9. 在错误发生时,首先会尝试对作业进行局部恢复,如果无法恢复则会将整个作业进行重启,从保存的快照中恢复。
  10. Task是作业执行的最小单位,恢复从有两个策略。一个是若Task发生异常,则重启左右Task,恢复成本高,但是恢复作业一致性好。二个是分区恢复策略,若Task发生异常,则重启该分区的所有Task,恢复成本低,但实现逻辑复杂。首先需要对作业的Task们进行分区划分,分区有多重策略比如横向、纵向以及上下游依赖关系等。
  11. 当task发生错误,TaskManager会通过RPC通知JobManager,JobManager将对应的Excution的状态转换为FAILED并触发失败策略。如果错误是可恢复的,则JobManager会调度相关Task重启,否则升级为整个作业重新开始执行。
  12. JobMaster故障了会被TaskManager检测到或直接收到ZK的宕机通知,此时TaskManager会将该Job ID的作业取消执行,但保持其Slot一段时间等待连接新的JobMaster,ResourcManager检测到其失败后,只是通知其尝试重新连接一次看能否行,如果不行最后还是通过重新选举新的节点作为JobMaster,这个过程不用担心旧JobMaster上拥有的配置信息、作业执行状态和数据丢失,因为这些信息都有持久化到hdfs和ZK中。
  13. ResourcManager负责资源的管理,其在内存中维护的很多信息没有持久化,一旦宕机只能重新选举新ResourcManager并重新收集信息到内存。其宕机都会被JobMaster和TaskManager检测到,重新选举后会通知他们重新连接
  14. TaskManager故障了会被JobMaster检测出来,将该TaskManager的Slot资源移除,并尝试恢复作业。被ResourcManager检测到后会通知JobMaster并启动一个新的TaskManager,这是在Yarn、K8s等资源集群下才支持的,如果是自己搭建的固定节点集群就不行。

事务

  1. 如何保证系统数据处理的事务性也是十分重要的。flink依赖底层的检查点机制实现有限的事务属性,确保端到端严格执行一次。对于上游source算子作业处理异常,可以从本地检查点备份的数据中恢复上次读取的数据或位置,然后重新进行读取即可。但是对于下游的sink算子就不那么容易保证了,比如作业有两个sink输出算子,一个已经写入外部存储设备了,另一个写失败了需要回滚,此时没法让外部设备也跟着一起回滚。如果此时重新恢复作业执行一次,外部设备就会写入重复的数据。为了解决这个问题,就要根据外设具体情况而定,有些MQ支持事务消息就比较好办,如果不支持,则需要其支持数据写入幂等性。此外flink也支持扩展开发,使sink与外部设备事务关联起来。flink的检查点确认事务也采用两阶段提交机制,预提交阶段各个算子只将自身状态保存到临时文件中,JobMaster收到所有算子预提交应答后才会通知他们commit,否则就会发出检查点事务失败回滚通知。然后算子将临时文件中的检查点信息写入正式文件持久化。2PC本质上也是最终一致性方案,尽量减少中间过程导致造成的异常影响,没法实现严格的单机数据库事务效果。

数据一致性

其他模块

  1. 流是无界持续不断的数据,批是有界数据集。批处理可以看成是流处理的一个特殊场景,流中的一个窗口Window。此时对于窗口中的数据来说就是一个有限的固定集合,对它的处理就是一个普通的批处理了。
  1. 窗口Window可以按计数和时间以及会话等方式切分。时间窗口和计数窗口又可以分为滚动窗口和滑动窗口,滚动窗口中数据没有重合的,但是滑动窗口中下一个窗口和上一个窗口中就可能存在重合的数据。会话窗口是当超过一段时间还没有收到新的数据,则视该窗口结束。它无法事先定义好窗口的长度和时间等,窗口之间的元素更不会出现重合
  2. 数据有三种时间记录。分别是数据从外部系统产生时间称为事件时间,进入处理系统时称为摄取时间,被算子处理时称为处理时间。
  3. 用户接口提供API层和应用框架层。API层是核心接口,应用框架层是基于API层面向特定场景,提供简单易用的接口
  1. 提供本地调试模式和远程集群模式。本地调试在单机上启动一个伪集群,通过线程模拟多个集群节点。本地调试好的程序提交到生产环境的集群模式即可。
  2. 各类资源、任务、角色都需要一个唯一ID标识。flink采用分布式ID方式快速生成
  3. 使用第三方的SQL解析优化组件Calcite组件,构建Flink SQL API模块
  4. Table api 和Sql api最终都被转换为Operation,再到Transformation可实际执行的算子和计划
  5. Flink SQL的优化类似于关系型数据库,有基于规则和基于代价两种方式。
  6. 使用了Janjno组件作为底层嵌入式java编译器技术。它可以在运行时对内存中的java源码(程序生成的)文本进行编译并加载到虚拟机中使用。该技术广泛应用于运行时生成函数、算子、表达式等。通过此类技术由此可以看出,java是一种扩展性和灵活度很强的语言。包括反射技术、ASCII字节码生成修改技术、编译时动态生成字节码技术等高级技巧。他们广泛应用于各类java框架中。
  7. flink将实现流批一体化处理,目前还没有完全实现,还是分开的API操作。Spark强调批处理,Flink合并流批一体化,脱颖而出
  8. flink sql实现流批一体化语义,即可以处理传统关系数据库这种有限集合的批处理,更支持持续无界的流处理。使用动态表(Dynamic Table)将流转换为表,这样就能和批处理的关系表兼容了。传统关系数据库的SQL查询是针对静态的有限集合,且能最终得到一个固定结果的过程。而流数据处理就不符合这点了,参与查询计算的数据是无限无界的,且查询结果也是不断更新变化的。
  9. 流的SQL查询是连续查询的,将数据流映射成动态表,相比于传统关系数据库表是静态有限集合且存放到磁盘,则动态表都是相反的。
  10. 不过也需要注意一点就是并非所有传统SQL查询都适合流上查询,因为有些查询中间结果代价太高,可能导致内存溢出错误。比如连接查询join。一般批处理使用sql是问题不大的

你可能感兴趣的:(读书笔记,java,大数据,flink,分布式)