1. 基本概念
Why-分布式计算发展史
为什么需要流式计算,为什么需要Flink,是需要从分布式计算的历史开始说。
随着大数据时代到来,单机的计算已经不能满足数据计算的需求,将多个计算机组成集群去处理一个问题的方案成为主流,即分布式计算。而分布式系统的发展也伴随批处理向流处理的演进过程
MapReduce
首先是MapReduce的分布式计算编程范式,它通过将一个问题拆分为多个子问题,并在多个机器上求解,同时机器间进行数据交换和数据合并,最终输出结果。基于这种模式诞生了Hadoop框架,是较早的批处理框架。集团内产品对应ODPS,我们平时使用Hive SQL其底层执行基于MapReduce
其对应的过程大致如下:
- split分片。首先一个任务由client提交给JobTracker组件,JobTracker指定TaskTracker完成对应任务。数据源一般是HDFS,他会把HDFS上存储的block(基本单位)合并并分发给不同的map task做为任务的数据源,这也是“分而治之”的分
- map映射。主要是是将数据映射成key-value的数据结构,在map task中数据将会经历下面几个过程:
- 每个map task都有一个内存缓冲区(一般100MB),数据会先转化为key-value数据结构并存在内存中,接着通过对应的Partitioner策略对数据进行分组(默认hash后为reduce task的数量取膜),用于决定数据最终由哪个reduce task处理
- 当数据量超过内存80%后会溢出(spill),此时新写入的使用剩余的20%内存,溢出的数据会以文件形式写入磁盘。
- 溢写程序执行的过程他会对key做排序(sort),期间为了减少传输/存储成本会根据Combine的策略合并(combine)部分数据,类似提前reduce
- 当多次溢出形成了多个溢出文件,需要将这些文件归并在一起形成一个大文件,这个过程是合并(Merge),最终根据不同partition将文件拆分发给对应的reduce task
- reduce归约。reduce task任务执行前会通过HTTP请求不断地拉取不同map task产生的属于自己分区的结果文件,同时做类似溢写的操作将内存的数据跟磁盘数据做merge,其中也伴随combine,reduce的shuffle动作在最终文件形成之时结束。后续则是reducer执行并将结果输出到HDFS上
我们可以看到Hadoop的MapReduce的过程中存在一些问题:
- 过多的IO操作。MapReduce通常需要将计算的中间结果写入磁盘,然后还要读取磁盘,从而导致了频繁的磁盘IO,增加了时延
- 默认的排序。MapReduce的Shuffle存在默认的排序机制,主要因为key排序后有利于combine/reduce的操作、外排序操作更节省内存、输出文件有序利于随机访问和用户查询等原因。这也导致MapReduce在Shuffle时需要花费大量时间进行外排序(为什么排序)
这些问题随着实时性场景的增多逐渐暴露出来,往往数据都要隔日产生,一开始大家并没有在意,直到Spark的出现
Spark
针对MR这些问题诞生了Spark、Tez等DAG框架,其初衷是改良Hadoop的MapReduce编程模型和执行速度。与MapReduce相比Spark有了这些优势:
- 充分利用内存。Spark将分布式数据对象抽象成RDD(Resilient Distributed Datasets 弹性分布式数据集),他这种数据结构,便于数据的分区、容错和pipline。基于这种模型可以使用内存保存shuffle过程中产生的中间数据,对于被频繁使用/复杂计算的RDD可以cache在内存反复使用而不像MapReduce落盘,极大提升了计算速度,减少IO开销
- 任务阶段细分。根据任务的复杂程度,Spark将任务拆分成多个阶段(Stage)组成一个有向无环图(DAG Directed Acyclic Graph),相比MapReduce计算模型更加灵活:阶段的划分依赖shuffle类型算子比如join,如果只是转化类的函数则可以归为一个阶段,而MapReduce相当于融合多个阶段,从而加重了一次操作的工作量
Hadoop MapReduce 简单粗暴地根据 shuffle 将大数据计算分成 Map 和 Reduce 两个阶段,然后就算完事了。而 Spark 更细腻一点,将前一个的 Reduce 和后一个的 Map 连接起来,当作一个阶段持续计算,形成一个更加优雅、高效的计算模型,虽然其本质依然是 Map 和 Reduce。
但是这种多个计算阶段依赖执行的方案可以有效减少对 HDFS 的访问,减少作业的调度执行次数,因此执行速度也更快
因为这些特征,Spark在有界数据批处理方面一直处于统治地位。但是现实中,数据往往是源源不断产生的,划分一部分历史数据进行批量处理往往没法满足实时的需求。有没有方式可以无需针对整个数据集操作,而对通过系统传输的每个数据项执行操作呢,流处理应运而生。
尽管Spark后期也尝试引入扩展Spark Streaming,希望将批处理粒度变小改为微批处理,但其核心还是批处理的思想
Flink
想较于批处理处理速度更快的流处理框架逐渐盛行。Storm可以以极低的延迟处理数据,成为了流处理的先驱。但是Core Storm无法保证消息的处理顺序,且消息提供了“至少一次”的处理保证,这意味着消息可以重复处理,对于一些要求数据正确性的业务场景是不能接受的。
Flink框架的出现解决了这些问题,并具有以下优势:
- 基于流处理延迟更低。通过来一条数据处理一条数据的方式,相较于Spark等批处理框架拥有更强的实时性。其计算模型基于Dataflow,表现为类似Spark的DAG但是有向无环图的顶点是算子(Operator)
- 容错实现消息Exactly-once语义。Flink基于Chandy-Lamport算法,它会把分布式的每一个节点的状态保存到分布式文件系统里面作为Checkpoint(检查点),当作业失败遍回滚以达到Exactly-once的语义,保证消息仅消费一次。如下图,状态保存在本地内存,多余则溢出磁盘,Flink通过定期和异步地对本地状态进行持久化存储来保证故障场景下精确一次的状态一致性
- 数据有序。数据的乱序及迟到方面,Flink通过引入Event Time、WaterMark、Trigger 等机制,解决了这些特有问题。
总结
三种编程范式:
- OLTP类型,请求+响应,响应时间最快,不擅长处理大数据量
- 批处理,间隔一段时间,一次性处理大量数据,类似scheduler的作业任务,一般要几十分钟到几小时,响应慢
- 流式处理,响应速度介于前两者,事件触发/微批量处理保证持续的响应,流是无边界/有序的数据集
流式计算两种类型:
- 微批处理(Micro-batch processing),如Spark。这种类型处理认为流处理是基于微批处理的,即它把流处理看作是批处理的一种特殊形式。微批处理每次接收到一个时间间隔的数据才会去处理,所以天生很难在实时性上有所提升。
- 流处理(Native Streaming),如Flink、Storm。在数据源上与Spark的RDD不同的是,Stream代表一个数据流而不是静态数据的集合,基于事件持续的响应,比如日志、binlog、点击流等。
三种主流流式计算框架区别:
功能指标 |
Storm |
Spark |
Flink |
处理模式 |
native |
micro-batching |
native |
消息保障 |
至少一次 |
有且一次 |
有且一次 |
实时性 |
低延迟,亚秒级 |
高延迟,秒级 |
低延迟,亚秒级 |
吞吐量 |
低 |
高 |
高(Storm3-5倍) |
流量控制 |
不支持 |
支持 |
支持 |
容错方式 |
record ack |
rdd based check point |
check point |
What-什么是Flink
Flink是什么,官方的定义是在无边界和有边界数据流上进行有状态的计算的框架和分布式处理引擎
Apache Flink is a framework and distributed processing engine for stateful computations over unbounded and bounded data streams.
无边界和有边界:数据在系统中是实时源源不断产生的,有边界在Flink中指的如下有限流覆盖的一块系统历史数据的集合,它的数量在一开始就是确认的,一旦确认不可改变;而无边界数据是持续产生的数据,输入是无限的没有终止时间,需要以特定的时间顺序处理
有状态计算:所谓的有状态计算就是要结合历史信息进行的计算。例如对于反欺诈行为的识别,要根据用户在近几分钟之内的行为做出判断。一旦出现异常,就需要重新执行流计算任务,但重新处理所有的原始数据是不现实的,而Flink的容错机制和State能够使Flink的流计算作业恢复到近期的一个时间点,从这个时间点开始执行流计算任务,这无疑能够大大降低大规模任务失败恢复的成本。
Where-Flink应用场景
Flink的应用场景:
事件驱动型应用
传统事务分层架构,会监听事件并在读写公共的事务数据库再去触发相应动作,计算和存储是分离的;而基于Flink的事件驱动型应用,通过监听事件的日志并写入/更新状态到应用本地,同时触发动作/输出新事件给下游,期间定期向远程持久化存储的checkpoint,这种方式的计算和存储是不分离的。
对于那些不在意中间过程的事件型驱动适合这种模式,而像平时业务型应用还是建议采用传统架构。具体应用如下:
- 异常检测系统:反欺诈监测、异常监控、基于规则的报警等
- 业务流程:业务统计、实时推荐等,参考https://ata.alibaba-inc.com/articles/142577
数据分析应用
区分传统的批处理,将数据批量导入再统一分析存在一定的延迟且存在数据有界性,而流处理的引入可以根据事件的产生实时更新,在交易中HSF依赖健康分析就是Flink这个场景的应用,实时关联应用上下游的调用情况分析各个服务间的强弱依赖关系,进而做出预警措施。
具体应用有:
- 应用质量监控
- 用户/应用行为分析
- 大规模图分析
数据管道应用
通过Flink的SQL接口可以实现实时的ETL工作,将上游数据进行处理后输出下游
应用有:
- 电商平台实时查询索引构建
- 持续ETL
原理
什么是流?
流可以理解为上面讲到的不断产生的无边界的数据,而Flink则是处理这种数据的流处理框架。那么数据流在Flink中是什么形式存在的,又如何流转和变化,最终结果是怎么输出的
Dataflow模型
Flink的流模型参考了Dataflow模型,它是一套准确可靠的关于流处理的解决方案。在Dataflow模型提出以前,流处理常被认为是一种不可靠但低延迟的处理方式,需要配合类似于MapReduce的准确但高延迟的批处理框架才能得到一个可靠的结果(Lambda架构)。
背景:
起初,Dataflow模型是为了解决Google的广告变现问题而设计的。因为广告主需要实时的知道自己投放的广告播放、观看情况等指标从而更好的进行决策,但是批处理框架Mapreduce、Spark等无法满足时延的要求(因为它们需要等待所有的数据成为一个批次后才会开始处理),(当时)新生的流处理框架Aurora、Niagara等还没经受大规模生产环境的考验,饱经考验的流处理框架Storm、Samza却没有“恰好一次”的准确性保障(在广告投放时,如果播放量算多一次,意味广告主的亏损,导致对平台的不信任,而少算一次则是平台的亏损,平台方很难接受),DStreaming(Spark1.X)无法处理事件时间,只有基于记录数或基于数据处理时间的窗口,Lambda架构过于复杂且可维护性低,最契合的Flink在当时并未成熟。最后Google只能基于MillWheel重新审视流的概念设计出Dataflow模型和Google Cloud Dataflow框架,并最终影响了Spark 2.x和Flink的发展,也促使了Apache Beam项目的开源。
Dataflow由三个部分组成:
- Source(数据源):负责获取输入数据。
- Transformation(数据处理):对数据进行处理加工,通常对应着多个算子。
- Sink(数据汇):负责输出数据。
Flink程序执行时,由流和转换操作映射到streaming dataflows,每个数据流有1个或多个 source,有一个或多个sink,这个数据流最终形成一个DAG。
上述的是一个逻辑图,实际在执行Dataflow程序的时候需要将他转换为物理Dataflow图。
首先Flink本身是分布式的,在实际执行时一个Stream(流)有多个Stream Partition(流分区),一个operator(算子)也会有多个operator subtasks(子算子任务),他可以让不同算子子任务在不同机器上执行以达到分布式能力。一个算子并行任务的个数叫做算子的并行度。并行度可以通过程序中重写来修改
对于两个算子间流的流转来说有两种模式:
- One-to-one(一对一模式)保留分区和顺序,数据是对其的,如上图source和map间,类似Spark的窄依赖。ps:相同并发度的One-to-One的两个算子会优化成一个task由一个线程执行,同时将多个Operator Subtask串起来组成一个Operator Chain(任务链),这样可以减少线程切换/时延并增加吞吐量。
- Redistributing(重分配模式),分区会改变,如上图的map和keyBy/window及keyBy/window和sink,类似Spark的宽依赖shuffle
比如上述Flink程序逻辑,对应输入source并行度为2则会有两个source的算子子任务,map和keyBy等算子并行度也是2,而最终输出的Sink并行度为1意味着结果最终汇聚在一个节点输出,最终Parallel Dataflows图如下:
进一步,Flink的Dataflow执行按层级分为4层:
- StreamingGraph:是根据用户通过Stream API编写的代码生成的初始流程图,用于表示程序的拓扑结构。
- JobGraph:StreamGraph经过优化后生成了JobGraph,提交给JobManager的数据结构。主要的优化为将多个符合条件的节点链接在一起作为一个节点(Operator Chains 任务链)后放在一个作业中执行,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。
- ExecutionGraph:JobManager根据JobGraph生成ExecutionGraph,ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。
- 物理执行图:JobManager根据ExecutionGraph对任务进行调度后,在各个TaskManager上部署作业后形成的“图”,并不是一个具体的数据结构。
State状态
与批计算相比,State是流计算特有的,批计算没有failover机制,要么成功,要么重新计算。
上面说到数据流以单个记录维度流转在Flink搭建的算子DAG中,当数据到达某些算子节点时会转换成另外的格式流转到下一个算子,但是有的算子是需要做聚合计算或模式匹配的,我们不能来一个数据就将历史数据重做一次,所以保存原来计算结果在原来结果上做增量计算就很有必要。Flink中的持久化模型就是State,实现为RocksDB本地文件+异步HDFS持久化,后改为基于Niagara的分布式存储
当我们考虑一下使用批处理系统来分析一个*数据集时,会发现状态的重要性显而易见。在现代流处理器兴起之前,处理*数据集的一个通常做法是将输入的事件攒成微批,然后交由批处理器来处理。当一个任务结束时,计算结果将被持久化,而所有的运算符状态就丢失了。一旦一个任务在计算下一个微批次的数据时,这个任务是无法访问上一个任务的状态的(都丢掉了)。这个问题通常使用将状态代理到外部系统(例如数据库)的方法来解决。相反,在一个连续不间断运行的流处理任务中,事件的状态是一直存在的,我们可以将状态暴露出来作为编程模型中的一等公民。当然,我们的确可以使用外部系统来管理流的状态,即使这个解决方案会带来额外的延迟。
State分为两类:
- KeyedState:这里面的key是我们在SQL语句中对应的GroupBy/PartitioneBy里面的字段,key的值就是groupby/PartitionBy字段组成的Row的字节数组,每一个key都有一个属于自己的State,key与key之间的State是不可见的;
- OperatorState:Blink内部的Source Connector的实现中就会用OperatorState来记录source数据读取的offset。
以一次双流JOIN的过程来理解State:参考 https://ata.alibaba-inc.com/articles/109200
双流INNER JOIN两边事件都会存储到State里面,如上,事件流按照标号先后流入到join节点,我们假设右边流比较快,先流入了3个事件,3个事件会存储到state中,但因为左边还没有数据,所有右边前3个事件流入时候,没有join结果流出,当左边第一个事件序号为4的流入时候,先存储左边state,再与右边已经流入的3个事件进行join,join的结果如图 三行结果会流入到下游节点sink。当第5号事件流入时候,也会和左边第4号事件进行join,流出一条jion结果到下游节点。ps:对于left join会增加一个撤回动作,针对已经发到下游但是右表内容传null的情况。
State扩容机制:
- KeyedState:Flink会根据最大并行度将key划分到对应Keygroup中,一个Keygroup包含多个key。根据目前的并行度,每个operator均匀地获取多个Keygroup。当扩容的时候Keygroup重新划分,每个operator获得的Keygroup将重新分配
- OperatorState:由于没有key的分配问题,OperatorState的扩容会直接从List中划分。比如Metaq有5个分区,source算子从1个并发度增长到2个,那么会是一个存3个分区信息一个存2个分区信息
常见算子
算子 |
实现 |
备注 |
map: |
无状态算子。输入一个元素,然后返回一个元素 |
|
flatmap |
无状态算子。 输入一个元素,可以返回零个,一个或者多个元素 |
|
filter |
对流进行过滤,符合条件的数据会被留下 |
|
join |
有状态算子。两边事件存储在State上,数据结构为Map<JoinKey, Map<rowData, count>其中count记录相同事件个数 |
https://ata.alibaba-inc.com/articles/109200 数据关联相关都会用到双流JOIN |
group by |
有状态算子。通常会维护一个TreeMap内存结构用于存储TopN数据,以及MapState结构用于failover后恢复 |
https://ata.alibaba-inc.com/articles/94592 可以做TopN |
时间概念
流处理跟批处理很大的不同是他需要引入时间的概念,因为数据的产生是*且连续的,我们需要给予他们秩序,Flink中增加了一个新的时间维度用于管理流上的事件,通过时间的概念打通流和批,但同时给数据处理也带来了复杂性。
Flink定义了三种数据时间概念:
- Event Time 事件时间:该事件被创建的时间,通常用时间戳表示,由生产服务来附加,Flink通过 timestamp assigners 来获得时间戳。可以用于解决消息乱序的时候一致性问题
- Processing Time 处理时间:是基于时间操作的operator的机器本地时间。Flink默认使用的时间就是处理时间,但是在大多数情况下都会使用事件时间
- Ingestion time 摄入时间:流入Flink source operator的时间
Watermarks
引入时间主要为了解决几个问题,首先是数据乱序延迟到达的问题,在分布式系统下很常见,Flink主要通过水位线机制解决。
水位线(Watermarks)是全局进度的度量标准。系统可以确信在一个时间点之后,不会有早于这个时间点发生的事件到来了。本质上,水位线提供了一个逻辑时钟,这个逻辑时钟告诉系统当前的事件时间。当一个运算符接收到含有时间T的水位线时,这个运算符会认为早于时间T的发生的事件已经全部都到达了。对于事件时间窗口和乱序事件的处理,水位线非常重要。运算符一旦接收到水位线,运算符会认为一段时间内发生的所有事件都已经观察到,可以触发针对这段时间内所有事件的计算了
ps:对于多个输入源的算子,Watermarks的传递上会选择所有输入源中目前达到的最低值传递给下一个算子
水位线提供了一种结果可信度和延时之间的妥协。激进的水位线设置可以保证低延迟,但结果的准确性不够;如果水位线设置的过于宽松,计算的结果准确性会很高,但可能会增加流处理程序不必要的延时。通过在数据源上设置 WATERMARK wk1 FOR event_time as withOffset(event_time, 5000)来控制延迟范围
对于一些场景对延迟和速度重视大于准确性时可以使用处理时间作为标准,这时候就不需要感知乱序/迟到,只在机器时间满足时去触发计算即可。
Window
为了能够得到例如中值之类的统计结果,我们需要给定一个边界,Flink引入Window(窗口)机制用于处理有界的数据,这里窗口可以是基于数据属性(Count Window 最近100个事件)也可以基于时间(Time Window,如过去24小时)。主要下面种窗口类型:
- 翻滚窗口(Tumbling Window,无重叠),通过groupbyTUMBLE(accessTime, INTERVAL '2' MINUTE)实现
- 滚动窗口(Sliding Window,有重叠)
- 会话窗口(Session Window,活动间隙)
系统容错
流计算容错一致性保证有三种,分别是:
- Exactly-once,是指每条 event 会且只会对 state 产生一次影响,这里的“一次”并非端到端的严格一次,而是指在 Flink 内部只处理一次,不包括 source和 sink 的处理。
- At-least-once,是指每条 event 会对 state 产生最少一次影响,也就是存在重复处理的可能。
- At-most-once,是指每条 event 会对 state 产生最多一次影响,就是状态可能会在出错时丢失。
Checkpointing检查点
Flink中基于异步轻量级的分布式快照技术提供了Checkpoints容错机制,通过它的恢复机制保证了Exactly once状态一致性。
Checkpoints可以将同一时间点作业/算子的状态数据全局统一快照处理,包括前面提到的算子状态和键值分区状态。当发生了故障后,Flink会将所有任务的状态恢复至最后一次Checkpoint中的状态,并从那里重新开始执行。这跟MySQL之类的checkpoint不大一样,Mysql会通过redolog增量存储恢复时重做,但是Flink则是从checkpoint点重新计算,因为它存储了输入流上一次消费的位置。
其过程有就像Watermarks的传递一样,Flink会在流上定期产生一个barrier(屏障)。barrier 是一个轻量的,用于标记stream顺序的数据结构。barrier被插入到数据流中,作为数据流的一部分和数据一起向下流动,过程如下:
- barrier 由source节点发出;
- barrier会将流上event切分到不同的checkpoint中;
- 汇聚到当前节点的多流的barrier要对齐(At least once不需要对齐);
- barrier对齐之后会进行Checkpointing,生成snapshot,快照保存到StateBackend中;
- 完成snapshot之后向下游发出barrier,继续直到Sink节点;
以上是Flink系统内Exactly-once语义的实现。如果想实现端到端(Soruce到Sink)的Exactly-once语义,需要外部Source和Sink的支持,比如Source要支持精准的offset,Sink要支持两阶段提交
retract回撤
运行架构
集群架构
Flink整体架构图如下:
- 作业管理器(JobManager):主要作用主要是将客户端提过来的任务执行拓扑图,生成相应的物理执行计划并提交给TaskManager去执行。任务执行前作业管理器会向资源管理器(ResourceManager)请求执行任务必要的资源,也就是任务管理器(TaskManager)上的插槽(slot),一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager上。同时在运行过程中,作业管理器会负责所有需要*协调的操作,比如说检查点(checkpoints)的协调。作为主进程(Master Process),JobManager控制着单个应用程序的执行,也就是每个应用都由一个不同的JobManager管理
- 资源管理器(ResourceManager):主要负责管理任务管理器(TaskManager)的插槽(slot),TaskManger插槽是Flink中定义的处理资源单元。Flink为不同的环境和资源管理工具提供了不同资源管理器,比如YARN、Mesos、K8s,以及standalone部署。当作业管理器申请插槽资源时,ResourceManager会将有空闲插槽的TaskManager分配给作业管理器。如果ResourceManager没有足够的插槽来满足作业管理器的请求,它还可以向资源提供平台发起会话,以提供启动TaskManager进程的容器。另外,ResourceManager还负责终止空闲的TaskManager,释放计算资源。
- 任务管理器(TaskManager):是Flink中的工作进程。通常在Flink中会有多个TaskManager运行,每一个TaskManager都包含了一定数量的插槽(slots)。插槽的数量限制了TaskManager能够执行的任务数量。启动之后,TaskManager会向资源管理器注册它的插槽;收到资源管理器的指令后,TaskManager就会将一个或者多个插槽提供给作业管理器调用。作业管理器就可以向插槽分配任务(tasks)来执行了。在执行过程中,一个TaskManager可以跟其它运行同一应用程序的TaskManager交换数据。
- 分发器(Dispatcher):当一个应用被提交执行时,分发器就会启动并将应用移交给一个作业管理器。Dispatcher也会启动一个Web UI,用来方便地展示和监控作业执行的信息。Dispatcher在架构中可能并不是必需的。
高可用方面,如果TaskManager挂了,JobManager会监测到错误并向ResourceManager申请新的资源,如果没有足够资源将没发重启。对于JobManager来说,它会将元数据信息存储在外部存储,而在Zookeeper集群中保存一份指向存储的指针,当JobManager宕机会先取消任务,然后会有新的JobManager启动并接替它的工作。
任务执行
如上图所示,每个Worker (TaskManager) 是一个JVM进程,可以在单独的线程中执行一个或多个子任务。为了控制一个TaskManager接受多少任务,一个TaskManager有至少一个Task Slot(任务槽)。TaskManager会划分内存分配给各个Task Slot,由于在同一个JVM他们会共享TCP连接、心跳消息、CPU资源。
流控
Flink的流量控制基于信任度:
接收任务授予发送任务一些“信任度”(credit),也就是为了接收其数据而保留的网络缓冲区数。当发送者收到一个信任度通知,它就会按照被授予的信任度,发送尽可能多的缓冲数据,并且同时发送目前积压数据的大小——也就是已填满并准备发送的网络缓冲的数量。接收者用保留的缓冲区处理发来的数据,并对发送者传来的积压量进行综合考量,为其所有连接的发送者确定下一个信用度授权的优先级。
基于信用度的流控制可以减少延迟,因为发送者可以在接收者有足够的资源接受数据时立即发送数据。此外,在数据倾斜的情况下,这样分配网络资源是一种很有效的机制,因为信用度是根据发送者积压数据量的规模授予的。因此,基于信用的流量控制是Flink实现高吞吐量和低延迟的重要组成部分。
使用
Flink的API分层如下,我们经常开发中使用的是它最高层抽象的API,即声明式编程 -- SQL