作者:宋辛童(花名:五藏) Apache Flink PMC Member & Committer
整理:陈政羽(Apache Flink Contributor,Apache Flink China 社区志愿者)
1.14这个新版本一共有35个比较重要的新特性以及一些优化工作,目前已经有26个工作完成,5个任务不确定是否能准时完成,有4个特性放到后续版本完成。

在这个版本在历史当中囊括的优化和新增功能点其实并不算非常的多,其实大家通过观察发版节奏可以发现通常发布1-2个大版本后都会发布一个变化改动稍微少一点的版本,主要目的是把一些特性更加稳定下来。所以我们1.14版本的定位就是这样的一个定位,我们称之为质量改进和维护的一个版本。这个版本预计8.16停止新特性开发,大概9月中能够和大家正式见面,大家有兴趣可以关注以下链接去跟踪功能发布进度
Flink流批一体其实从1.9版本开始大家就受到持续的关注,它作为社区RoadMap重要组成部分,随着大数据不断推进的实时化。但是传统的离线的需求并不会给实时任务完全取代,还会是长期存在的一个状态。按照以往流批独立技术方案的痛点,维护两套系统,两套开发人员,两套数据链路处理相似内容带来维护的风险性和冗余,同时有可能是流批使用的不是同一套数据处理系统,引擎本身差异可能存在数据口径不一致的问题,从而导致业务数据存在一定的误差。所以Flink社区定制的目标是实时离线一体化这个技术路线,这个比较重要的技术趋势和方向。
Flink在过去的几个版本当中流批一体完成了非常多的一个工作,在目前引擎层面来看,API 算子执行层面上做到流批同一套机制运行。在任务具体的执行模式上会有2种不同执行模式。对于无限的数据流我们统一采用了流的执行模式,流的执行模式指的是所有是通过一个Pipeline模式去连接的,流的执行模式是上游和下游数据是同时运行的,随着上游不断产出数据,下游不断消费数据。这种称为全Pipeline的执行方式,它可以通过eventTime表示数据什么时候产生的;通过watermark得知目前哪个时间点数据已经到达了;通过state 来维护计算中间状态;通过checkpoint 做容错的处理。如下图是不同的执行模式

对于有限的数据集有2种模式,我们可以把它看成一个有限的数据流去做处理,也可以把它看成批的执行模式。批的执行模式虽然有eventTime,但是对于watermark来说只有正无穷。如果基于数据的state排序后,它在任务的调度和shuffle上会有更多的选择。流批的执行它们2者是有区别的,例如批的执行模式会有落盘的中间过程,只有当前面任务执行完成,下游的任务才会触发,这个容错机制是通过suffle进入容错。这2者各自有各自的执行优势:对于流的执行模式来说,它没有落盘的压力,容错是基于数据的分段,通过不断对数据进行打点checkpoint去保证断点恢复,然而在批处理上,因为是要经过shuffle落盘的,所以对磁盘会有压力,但是因为我数据是经过排序的,所以对批来说可能后续的计算效率会有一定的提升,同时在执行时候我们是经过分段去执行任务的,无需同时执行。容错计算方面是根据stage进行容错,这两种各自优劣进行不同场景进行选择。
Flink1.14优化点主要是针对在流的执行模式下,如何去处理有限数据集。之前处理无限数据集和有限数据集最大区别是任务可能结束的问题。我们来看看下图

在流checkpoint机制,对于一个无限流,它的所有checkpoint是由source节点进行触发的,由source节点发送checkpoint Barrier ,当checkpoint Barrier流过整个作业时候,同时这个checkpoint会存储当前作业所有的state状态。在有限流的checkpoint机制中,我们的task是有可能提早完成任务结束的。上游的Task有可能先处理完任务提早退出了,但是下游的task还是在执行中。在同一个stage不同并发下,有可能数据量不一致导致部分任务提早完成了。这种情况下,后续如果进行checkpoint
我们引入了JobManager动态根据当前任务的执行情况,去明确checkpoint Barrier 是从哪里开始触发的一个机制。同时我们在部分任务结束后,后续的checkpoint只会保存 仍然在运行task所对应的stage,通过这种方式我们能够让任务执行完成后 还可以继续做checkpoint ,在有限流执行当中更好的保障

Task结束后二阶段提交
我们在部分sink使用上,例如下图的Kafka Sink上,涉及到 Task 需要依靠checkpoint机制,进行二阶段提交,从而保证数据的Exactly-once 一致性

具体而言可以这样说:在checkpoint过程中,每个算子只会进行准备提交的操作。比如数据会提交到外部的临时存储目录下,所有任务都完成这次checkpoint之后会收到一个信号,才会执行真正的commit。它会把所有分布式的临时文件一次性以事务的方式提交到外部系统。这种算法在当前有限流的情况下,作业结束后并不能保证有 Checkpoint,那么最后一部分数据如何提交?在1.14中我们让task数据处理完成后,Task 等待 Checkpoint 完成后才可以正式的退出,这样可以针对有限流可能一些任务结束的改进
目前Flink触发checkpoint是依靠barrier在算子进行流通,当算子遇barrier随着算子一直往下游进行发送,当下游遇到barrier的时候就会进行快照操作,然后再把barrier往下游继续发送。对于多路的情况我们会把barrier进行对齐,把先到barrier的这一路数据暂时性的block,等到两路 barrier 都到了之后我们才做快照,最后才会去继续往下继续发送barrier。

现有的CheckPoint机制很明显存在以下问题:
针对这些痛点Flink在最近几个版本一直在持续的优化,Unaligned Checkpoint 就是其中一个机制。barrier算子在到达input buffer最前面的时候,我们就会开始触发CheckPoint操作。它会立刻把barrier传到算子的Out Put Buffer的最前面,相当于会立刻被下游的算子所读取到。通过这种方式可以使得barrier 不受到数据阻塞,解决反压时候无法进行Checkpoint。当我们把barrier发下去后,我们需要做一个短暂的暂停,暂停时候我们会把算子的State 和 input output buffer 中的数据进行一个标记,以方便后续随时准备上传。对于多路情况会一直等到另外一路barrier到达之前数据,全部进行标注。通过这种方式整个在做Checkpoint的时候,也不需要对barrier进行对齐,唯一需要做的停顿就是在整个过程中对所有buffer和state标注的这样一个过程。这种方式可以很好的解决了反压时无法做出 checkpoint 和 Barrier 对齐阻塞数据影响性能处理

这个主要是用于减少Checkpoint间隔,如左图1所示,在Incremental Checkpoint 当中,先让算子写入state 的 changelog。写完后才把变化真正数据写入到StateTable上。state 的 changelog不断的向外部进行持久的存储化。在这个过程中我们其实无需等待整个StateTable去做一个持久化操作,我们只需要保证对应的Checkpoint这一部分的changelog能够持久化完成,就可以开始做下一次checkpoint。StateTable是以一个周期性的方式,独立的去对外做持续化的一个过程。

这两个过程进行拆分后,我们就有了从以前需要做全量持久化(Per Checkpoint)变成 增量持久化 (Per Checkpoint)+ 后台周期性 全量持久化,从而达到同样容错的一个效果。在这个过程中我每一次checkpoint需要做持久化的数据量减少了,从而我做checkpoint的间隔能够大幅度减少。其实在RocksDB也是能支持 Incremental Checkpoint 。但是有两个问题,第一个问题是RocksDB的Incremental Checkpoint 是依赖它自己本身的一些实现,当中会存在一些数据压缩,压缩所消耗的时间以及压缩效果具有不确定性,这个是和我们的数据相关的;第二个问题是只能针对特定的StateBackend来使用,目前在做Generalized Incremental Checkpoint 实际上能保证是对于StateBackend无关的,从运行时的机制来保证了一个比较稳定、更小的Checkpoint间隔。
目前 Unaligned Checkpoint 是在Flink1.13就已经发布了,在1.14版本主要是针对bug的修复和补充,Generalized Incremental Checkpoint 目前社区还在做最后的冲刺,是比较有希望在1.14中和大家见面。
这2个构建过程在之前的版本都有o(n^2)的时间复杂度,主要问题需要对于每个下游节点去遍历每一个上游节点的情况。例如去遍历每一个上游是不是一个Pipeline 边连接的关系,或者我要去遍历它的每一个上游生成对应的Result Partition 信息。我们通过引入group概念,假设我们已知上下游2个任务的连接方式是out to out,那我们相当于把所有Pipeline Region信息 或者 Result Partition 信息以Group的形式进行组合,这样只需知道下游对于的是上游的哪一个group就可以,通过一个简单的wordcount测试对比优化前后的性能如下表格
| 执行模式 | 并发度 | 优化前 | 优化后 | |
|---|---|---|---|---|
| 构建 Pipeline Region | 流 | 8k x 8k | 3s 441ms | 22ms |
| 16k x 16k | 14s 319ms | 107ms | ||
| 批 | 8k x 8k | 8s 941ms | 124ms | |
| 16k x 16k | 34s 484ms | 308ms | ||
| 任务部署 | 流 | 8k x 8k | 32s 611ms | 6s 480ms |
| 16k x 16k | 129s 408ms | 19s 051ms |
从表格中可以看到构建速度具有大幅度提升,构建Pipeline Region 的性能从秒级提升至毫秒级别。任务部署我们是从第一个任务开始部署到所有任务开始运行的状态,我们这边只统计了流,因为批需要上游结束后才能结束调度。整体时间来看,整个任务初始化,调度等流程减少到分钟级的消耗
细粒度资源管理在历史比较多的版本我们一直在做,在Flink1.14我们终于可以把这一部分API暴露出来在DataSteam提供给用户使用了。用户可以在DataStream中自定义SlotSharingGroup的划分情况,如下图所示可以这样去定义Slot的资源划分。通过这样实现了支持 DataStream API,自定义 SSG 划分方式以及资源配置 TaskManager 动态资源扣减

对于每一个Slot可以通过比较细粒度的配置,通过这样我们在Runtime上会自动根据用户资源配置进行动态的资源切割,切割后如图下

这样做的好处而不会像之前那样会有固定资源的Slot,而是做资源的动态扣减,通过这样的方式希望能够达到更加精细的资源管理和资源的使用率
Window Table-Valued Function 支持更多算子与窗口类型 ,可以看如下表格对比
| Tumble | Hop | Cumulate | Session | |
|---|---|---|---|---|
| Aggregate | 1.13 | 1.13 | 1.13 | 1.14 |
| TopN | 1.13 | 1.13 | 1.13 | |
| Join | 1.14 | 1.14 | 1.14 | |
| Deduplicate | 1.14 | 1.14 | 1.14 |
从表格中可以看出对于原有的三个窗口类型进行加强,同时新增Session窗口支持Aggregate的操作
Table API 支持声明式注册 Source / Sink 功能对齐 SQL DDL 如图下所示,同时支持FLIP-27新的Source接口。new Source 替代旧的 connect() 接口

全新代码生成器解决了大家在生成代码超过Java最长代码限制,新的代码生成器会对代码进行拆解,彻底解决代码超长问题;同时我们移除Flink Planner,新版本中 Blink Planner 成为Flink Planner的唯一实现
在之前版本中有先后执行的UDF可以看到图左边,在Java上面有java的Operator,先去把数据发给python下面的udf去进行执行,执行后又发回给Java传送给下游的Operator,最后向python这种跨进程的传输去处理,这样就会导致存在很多次冗余的数据传输。

在1.14版本中通过改进可以看到右图,可以把他们连接在一起,只需要一个来回的Java和Python进行数据通信,通过减少传输数据次数能够达到性能比较好的一个改进
在以往本地执行实际是在python的进程中去运行我们客户端程序,提交java进程启动一个迷你集群去执行java部分代码。Java部分代码也会和生产环境部分的一样,去启动一个新的python进程去执行对应的python udf,从图下可以看出其实我们在本地调试中是没有必要存在的

所以我们支持lookback模式可以让java的opt直接把udf运行在python client所相同的进程里面,通过这种方式避免了我们额外启动进程所带来一个额外的开销,最重要是在本地调试中我们可以在同一个进程之内能够更好利用一些工具进行debug,这个是对开发者体验上的一个提升
通过今天讲解Flink1.14的主要新特性介绍,首先我们先介绍了目前社区在批流一体上的工作,通过介绍批流不同的执行模式和JM节点任务触发的优化改进更好的去兼容批作业。然后通过分析现有的CheckPoint机制痛点,在新版本中如何改进;以及在大规模作业调度优化和细粒度的资源管理上面如何做到对性能优化;最后介绍了TableSQL API 和 Pyhton上相关的性能优化。欢迎各位后续继续关注发版的一些最新动态以及我们在后续的Release过程中的一些其他技术分享和专题
]]>文章介绍:Flink1.13版本于最近发布了,里面有比较多新的Feature和特性,今天就由我和徐榜江老师带着大家一起去探寻这些新特性,还有一些改进。徐榜江老师目前就职于阿里巴巴 Flink-SQL引擎团队,主要负责社区的SQL引擎模块开发。这篇文章一共会分为4个部分,首先我们会先给大家介绍Flink-SQL在1.13版本上面整体的一个改动,还有一些核心Feature的解读和重要改进,最后就是总结以及Flink1.14一些功能提前和大家剧透。
作者:徐榜江 (Apache Flink PMC)整理:陈政羽(Apache Flink Contributor,Apache Flink China 社区志愿者)

Flink-SQL 1.13是一个社区大版本,解决的issue在1000+以上,通过图中我们可以看到解决的问题大部分是关于Table-SQL模块,一共400多个issue占了37%左右,主要是围绕了其中的5个Flip进行展开,稍后文章我们也会根据这5个进行描述,它们分别是
下面我们来通过逐个Feature进行解读
在腾讯、阿里、字节等内部已经有这个功能,这次社区在Flink1.13我们推出了TVF的相关支持和相关优化。下面将从 Window TVF 语法、近实时累计计算场景、 Window 性能优化、多维数据分析进行解剖这个新功能
在1.13 前,是一个特殊的GroupWindowFunction
1 | SELECT |
在1.13时候我们对它进行了Table-Valued Function的语法标准化
1 | SELECT window_start,window_end,window_time,SUM(price) |
通过上面的观察,我们可以发现TVF 无需一定要跟在GROUP BY 语法后面,在Window TVF 基于关系代数 ,使得更加标准化。划分窗口只需要TVF,无需再次进行GROUP BY的相关操作;TVF扩展性和表达能力更强,可以自定义TVF(例如topn)


以上例子就是TVF做一个窗口划分,只需要把数据划分到窗口无需聚合,如果后续需要聚合只需要GROPBY即可。对于批的用户操作是很自然的一件事,而不需要像1.13之前做一定需要一个特殊的GROUP Function
目前WINDOW TVF 支持TUMBLE,HOP WINDOW;新增了CUMULATE WINDOW,SESSION WINDOW 预计在1.14支持

以图里面一个宽度为单位,第一个window统计一个宽度的数据,第二个window是想统计第一+第二个宽度的数据,第三个window想统计 1 2 3 宽度的数据。这个就是累积计算场景UV。例如:UV大盘曲线:每隔10分钟统计一次当天累积用户uv。在Flink1.13之前,我们需要做这个场景我们一般做法如下
1 | INSERT INTO cumulative_uv |
把时间戳取出按照GROUP BY 取出来,然后再做聚合操作,在里面按照10分钟进行截取,这样达到近似计算的场景
Flink1.13前做法:弊端 逐条计算,追逆数据时候,如果在生产和消费速度相同时候,就会如上图 曲线会比较平稳,但是生产和消费速度不匹配的时候就会跳变。
在Flink1.13可以改变我们的做法,当我们拥有了cumulate windows 时候 我们可以修改为下面的语法,每条数据精确分到每个window里面,例如我们是按照event_time进行划分的时候就会
1 | INSERT INTO cumulative_uv |
最终实现效果如下图

内存优化:通过内存预分配,缓存 window 的数据,通过 window watermark 触发计算,通过申请一些buffer避免高频的访问state
切片优化:将 window 切片,尽可能复用已计算结果,如 hopwindow,cumulate window。计算过的window数据无需再次计算,对切片进行重复利用数据
算子优化:window 支持,local-global 优化;同时支持count(distinct) 自动解热点优化
迟到数据:支持迟到数据计算到后续分片, 保证数据准确性
通过开源 Benchmark (Nexmark) 测试,普适性能有 2x 提升,在 count(distinct) 场景会有更好的性能提升

语法的标准化带来了更多的灵活性和扩展性,它可以直接在window窗口函数上面进行多维分析,如下图所示,可以直接进行GROUPING SETS、ROLLUP、CUBE的计算,如果是在1.13之前的版本,我们可能需要对这些进行单独的编写SQL后,再做union的一些聚合才能获得结果。类似这种多维分析的场景,可以直接在window-tvf上面实现

支持WINDOW TOP-N

时区问题可以归纳为3个主要原因:
| 时间函数 | Flink 1.13之前 | Flink1.13 |
|---|---|---|
| CURRENT_TIMESTAMP | 返回类型: TIMESTAMP UTC+0时区: 2021-05-22 01:40:52 UTC+8时区: 2021-05-22 01:40:52 | 返回类型: TIMESTAMP_LTZ UTC+0时区: 2021-05-22 01:40:52 UTC+8时区: 2021-05-22 09:40:52 |
| PROCTIME() | 返回类型: TIMESTAMP PROCTIME UTC+0时区: 2021-05-22 01:40:52 UTC+8时区: 2021-05-22 01:40:52 | 返回类型: TIMESTAMP_LTZ PROCTIME UTC+0时区: 2021-05-22 01:40:52 UTC+8时区: 2021-05-22 09:40:52 |
针对TIMESTAMP类型没有携带时区问题,我们推出了TIMESTAMP_LTZ 类型,LTZ是Local Time Zone的缩写,我们可以通过下面的表格来对比和TIMESTAMP两者的对比
| 数据类型 | 缩写 | 含义 |
|---|---|---|
| TIMESTAMP (p) WITHOUT TIME ZONE | TIMESTAMP (p) | 用于描述年, 月, 日, 小时, 分钟, 秒 和 小数秒 TIMESTAMP 可以通过一个字符串来指定 |
| TIMESTAMP (p) WITH LOCAL TIME | TIMESTAMP_LTZ (p) | 用于描述时间线上的绝对时间点,类似System.currentTimeMillis() 没有字符串表达形式 在计算和可视化时, 使用 session 中配置 的时区。 |
TIMESTAMP_LTZ 区别于之前我们使用TIMESTAMP,它是表示绝对时间的含义,通过对比我们可以发现,如果我们配置使用TIMESTAMP类型,他可以是字符串类型的。不管是从英国还是中国来说来对比这个值,其实都是一样的;但是对于TIMSTAMP_TLZ来说,它的来源就是一个Long值,在不同的时区去观察这个数据是不一样的,这样更加符合用户在实际生产上面一些需求。
订正 PROCTIME() 函数
当我们有了TIMESTAMP_LTZ 这个类型的时候,我们对PROCTIME()类型做了纠正,在1.13之前它总是返回UTC的TIMESTAMP,我们现在进行了纠正,把返回类型变为了TIMESTAMP_LTZ。PROCTIME除了表示函数之外,PROCTIME也可以表示时间属性的标记,下图我们通过创建这些时间类型的一张demo表可以看到类型发生的变化

订正 CURRENT_TIMESTAMP/CURRENT_TIME/CURRENT_DATE/NOW() 函数
这些函数在不同时区下出来的值是会发生变化的,例如在英国UTC时区时候是凌晨2点,但是如果你设置了时区是UTC+8的时候,时间是在早上的10点,不同时区的实际时间会发生变化,效果如下图:

解决 processing time window 时区问题
PROCTIME可以表示一个时间属性,我们基于PROCTIME的WINDOW操作,在Flink1.13之前如果我们需要做按天的window操作,进行按天WINDOW你需要手动解决时区问题,去做一些8小时的偏移然后再减回去。在FLIP-162解决了这个问题,现在用户使用的时候十分简单,PROCTIME直接声明了,结果是本地的时区。例如下图案例,英国时区的window_end 和 中国时区 的window_end会发生变化
1 | FLINK SQL> CREATE TABLE MyTable( |
我们通过设置不同的时区去对比发现实际window聚合的时间区间会有所变化

订正 Streaming 和 Batch 模式下函数取值方式
时间函数其实在流和批上面表现的形式会有所区别,主要这次修正是让用户更加符合实际的使用习惯。例如一下函数,在流模式中是per-record计算(在流模式下,是逐条数据的时间),在batch模式是query-start计算,(例如我们在使用一些离线计算引擎,hive 就是每一个批作业实际运行的时间)
Streaming 模式 per-record 计算,Batch 模式在 query-start 计算:
Stream 和 Batch 模式都是 per-record 计算:
EVENT_TIME 在Flink1.13也支持了定义在TIMESTAMP列上,相当于EVENT_TIME现在目前支持定义在TIMESTAMP和TIMESTAMP_
LTZ上面。
当你上游源数据包含了字符串的时间(如:2021-4-15 14:00:00)这样的场景,直接声明为TIMESTAMP然后把EVENT_TIME直接定义在上面即可,WINDOW窗口在计算的时候会基于你的字符串进行切分,最终会符合你实际想要的预想结果;
当你上游数据源的打点时间是属于long值,表示是一个绝对时间的含义。Flink1.13你可以把EVENT_TIME定义在TIMESTAMP上面,然后通过转换为TIMESTAMP_LTZ类型在window上面做一些聚合,在不同时区上面看到的值就是不一样的,自动的解决了8小时的时区便宜问题,无需人工干预在SQL语句查询层面做语法的修改
小提示:Flink-SQL标准里面的进行订正,在各位进行版本的时候需要留意作业逻辑中是否包含此类函数,避免升级后业务受到影响
对于国外夏令时,以前在做相关窗口计算操作是十分困难的一件事,Flink 支持在 TIMESTAMP_LTZ 列上定义时间属性, Flink SQL 在 window 处理时结合 TIMESTAMP 和 TIMESTAMP_LTZ, 优雅地支持了夏令时。主要是针对海外的业务统计场景会比较友好

在洛杉矶时区,[2021-03-14 00:00, 2021-03-14 00:04] 窗口会收集 3 个小时的数据
在非夏令时区,[2021-03-14 00:00, 2021-03-14 00:04] 窗口会收集 4 个小时的数据
这个主要是做了Hive语法的兼容性增强,首先支持了Hive的一些常用DML和DQL语法,这里列举部分
Hive dialect 支持 Hive 常用语法,hive有十分多内置函数,Hive dialect 需要配合 HiveCatalog 和 Hive Module 一起使用,Hive Module 提供了 Hive 所有内置函数,加载后可以直接访问
1 | FLINK SQL> CREATE CATALOG myhive WITH ('type'='hive'); --setup HiveCatalog |
与此同时,我们还可以通过Hive dialect 创建/删除 Catalog 函数以及一些自己自定义的一些函数,对用户使用起来会更加方便
1 | FLINK SQL> SHOW FUNCTIONS; |
在Flink1.13之前,大家觉得就是Flink SQL Client就是周边的一个小工具,在FLIP-163进行重要改进:
通过-i的参数,提前把DDL一次性加载初始化,方便初始化表的多个DDL语句,无需再多次使用command命令逐条发送,通过替代以前yaml方式去创建表
1 | ./sql-client.sh -i inin.sql |
-f 参数,其中SQL文件支持DML(insert into)语句
1 | ./sql-client.sh -i inin.sql -f sqlfile |
支持更多实用的配置
支持STATEMENT SET
1 | FLINK SQL> BEGIN STATEMENT SET; |
有可能我们一个查询不止写到一个sink里面,我需要输出到多个sink,一个sink写jdbc 一个sink写到hbase;在1.13之前需要启动2个query去完成这个作业,然后1.13我们可以把这些放到一个statement里面以一个作业的方式去执行,能够做到 source的复用,节约资源
虽然SQL大大降低了我们使用实时计算的一些使用门槛,但是TABLE和SQL以前我们在ds和table之间的转换比较不方便,对于一些底层封装我们上层sql用户无法直接拿到,例如访问state去做操作,flip-136就是解决这个问题的。
1 | Table table = tableEnv.fromDataStream( |
1 | //DATASTREAM 转 Table |
Flink1.14 主要有以下这几点的规划:
通过上面的文章的介绍,我们可以知道1.13 SQL主要就是围绕着这5部分去展开探讨的:
最后还分享了关于Flink1.14 SQL 上面的一些未来规划,看完文章的小伙伴相信大家对Flink SQL 在这个版本中变化有了深刻的了解,在实践过程中大家可以多多关注这些新的改动和变化带来业务层面上面的便捷。
]]>今天就由来自阿里的两位专家,分别是宋辛童和郭旸泽给我们带来Flink在1.12上资源管理新特性的讲解,大家聊一聊关于内存管理以及资源调度相关的一些进展。 议题主要分为2部分,Flink在1.12版本上的高效内存管理和资源调度相关变化将由宋辛童老师讲解,关于扩展资源框架相关问题由郭旸泽老师给大家介绍,关于如何扩展资源框架和以及如何使用GPU进行Flink计算,最后还跟我们讨论了社区在资源管理方面未来的一些规划。
作者:宋辛童(Apache Flink Contributor,阿里巴巴技术专家) 、郭旸泽 (Apache Flink Contributor,阿里巴巴高级开发工程师)
整理:陈政羽(Apache Flink Contributor,Apache Flink China 社区志愿者)
大家可能都有感觉,在我们运行Flink作业时候,该如何配置Flink的一些内存配置选项,我们到底如何去管理它使得Flink作业能够更加高效稳定的运行。在Flink1.10在Flink1.11分别引入了新的内存模型,如下图

从图中我们把我们常常需要关注的内存模块我们圈了起来。因为我们80~90%的用户只需要关心的是这一小部分,它才是真正用于任务执行和具体的作业相关的模块,需要每次去不断调整的部分。其他的大部分是Flink框架的内存,我们认为80到90%的情况可能不需要进行调整,如果一旦出现了问题,在社区的文档中也会很好的帮助大家解决这些问题。哪怕只有图中的4项内存需要我们注意,大家还不得不面临的一个问题:我的一个作业到底需要多少内存才能满足实际生产需求?这里面会存在一些问题:这些内存指标到底是什么?我的作业是否因为内存不足影响了我的作业性能导致吞吐量无法提升?作业是否存在资源浪费的现象?
针对这样的一个问题,我们社区在1.12版本当中给大家提供了一个全新的关于叫manager和task manager的UI(如图所示)

它能够通过网页直观的把每一项监控指标我们的配置值是多少,实际的使用情况是怎么样的,对应到了我们的内存模型当中,对应到哪一项全部都直观的展示给大家,有了这样的一个展示之后,我们可以很清楚的了解到作业的运行情况到底是怎么样的,我们应该去进行如何的调整,然后再配合社区的文档,我们对于每一项的内存具体用哪些配置参数可以来进行调整,通过这种方式的话,希望能够方便到大家对作业的内存管理能够有一个更好的了解。如果大家想对这一项有兴趣可以参考FFA 2020的相关视频。
接下来想重点跟大家聊一下的是关于Flink的托管内存,托管内存实际上是Flink特有的一种本地内存,它不受JVM的管理和 GC管理,而是由Flink自行进行管理的这样一类的内存。
这种内存管理主要体现在两个方面,一个方面是它会进行一个slot级别的,我们叫 slot级别的预算规划。它可以保证你在作业运行过程中不会因为内存不足,造成某些算子或者任务无法运行;同时也不会因为说预留了过多的内存没有使用造成这样的一个资源浪费,这是所谓的slot级别的资源规划。
同时Flink会去保证当你的任务运行结束的时候,它能够准确的把这些内存释放出来,这样Task Manager在用来给新的任务进行执行的时候,内存一定是可用的,这个是一个Flink管理内存的这种方式。托管内存有一个很重要的特性,我们叫做适应性。什么叫做适应性?指的是算子对于内存的需求是一个动态可调整的,算子不会因为说给予任务过多的内存,但是实际不需要这么多从而造成一个资源使用上的浪费,它会在一定的合理范围内,不管多少内存都会进行合理的分配;它也不会说给的内存相对比较少导致整个作业无法运行,只是可能在相对比较少的时候会受到一些限制,例如通过频繁的落盘保证作业的运行,这样会导致性能受到一定的影响。以上这就是我们所说的资源适应性。
针对托管内存,目前Flink有以下几个场景是使用的
Flink对于management memory的管理,它的预算的管理主要是分为两个阶段,首先第一个阶段是在作业的 job graph编译的阶段,在这个阶段需要主要去搞清楚的是三个问题
第一个问题是:slot当中到底有哪些算子或者任务会同时执行。这个关系到说我在一个查询作业中如何对内存进行规划,是否还有其他的任务需要使用management memory从而把相应的内存留出来。 如果是流式的作业当中,这个问题是比较简单的因为我们需要所有的算子同时的执行,才能保证上游产出的数据能给下游及时的消费掉,这个数据才能够在整个job grep当中流动起来。 但是如果我们是在批处理的一些场景当中,实际上我们会存在两种数据shuffle的模式,一种是pipeline的模式,这种pipeline的模式跟流式是一样的,也就是我们前面听到的blound stream的这种处理方式,我同样是需要上游和下游的算子同时运行,然后上游随时产出,下游随时消费。

另外一种的话是我们所谓的 batch的 blocking的这种方式,他要求上游把数据全部产出,并且落盘结束之后,下游才能开始读数据。这两种实际上会影响到哪些任务可以同时执行。我们目前在flink当中,他做的是根据作业拓扑图当中的这样的一个边的类型(如图上)。我们划分出了一个我们定义了一个概念叫做pipelined region,也就是全部都由pipeline的边锁连通起来的这样一个子图,我们把这个子图识别出来,用来判断哪些task会同时执行,这个是我们如何回答第一个问题,哪些算子会同时执行。
然后接下来我们搞清楚的第二个问题就是slot当中我们到底有哪些使用场景?我们刚才介绍了三种manage memory的使用场景。在这个阶段,对于流式作业,我们可能会出现的是像 Python UDF以及State Operator。这个阶段当中我们需要注意的是,我们这里并不能肯定 State Operator是否一定会用到management memory,因为这是跟它的状态类型看的类型是相关的,如果它使用了 RocksDB State Operator,它是需要使用的manage memory的,但是如果它是使用Heap State Backend,它并不需要,但是作业在编译的阶段是并不知道状态的类型,所以这里是需要去注意的。
然后对于batch的作业的话,我们除了需要搞清楚有哪些使用地方之外,我们还需要去搞清楚一件事情。 我们前面提到过batch的operator,它在使用management memory是一种算子独享的方式,而不是以slot为单位去进行共享。我们需要知道不同的算子到底谁应该分配多少内存,这个事情目前是由目前是由flink的 计划作业来自动的来进行一个设置的,Flink作业编译的阶段主要完成的这几个工作。

第一个步骤是我们根据State Backend的类型去判断是否有 RocksDB。如图上所示,同样的刚才我们比如说这样的一个slot,还有ABC三个算字,B跟C分别用到了Python,C用到了State for的 Operator,这种情况下,如果是在heap里边看的情况下,我们走上面的分支,我们整个slot当中只有一种在使用,就是Python。下面的话我们会存在两种使用方式,其中一个是RocksDB State Backend,有了这样的一个第一步的判断之后,第二步我们会去决定根据一个用户的配置,去决定不同使用方式之间怎么样去共享slot的management memory。
在我们这个Steaming的例子当中,我们定义的是说相当于Python的权重是30%,然后State Backend的权重是70%。在这样的一个情况下,如果我只有 Python的情况下,当然 Python的部分是使用100%的内存(Streaming的Heap State Backend分支);而对于第二种情况(Streaming的RocksDB State Backend分支),B、C的这两个Operator共用30%的内存用于 Python的 UDF,另外C在独享70%的内存用于 RocksDB State Backend。最后Flink会根据 Task manager的资源配置,一个slot当中到底有多少manager memory来决定每个operator实际可以用的内存的数量。

批处理的情况下跟流的情况有两个不同的地方,首先它不需要去判断State Backend的类型了,这是一个简化; 其次对于batch的算子,我们刚才说每一个算子它有自己独享的这样一个资源的这样一个预算,这种情况下我们会去根据使用率算出不同的使用场景需要多少的Shared之后,我还要把比例进一步的细分到每个Operator。
| 配置参数 | 默认值 | 备注 | |
|---|---|---|---|
| 大小 | taskmanager.memory.managed.size | / | 绝对大小 |
| 权重 | taskmanager.memory.managed.fraction | 0.4 | 相对大小(占用Flink)总内存比例 |
| taskmanager.memory.managed.consumer-weight | DATAPROC:70,PYTHON:30 | 多种用途并存时候分配权重 |
这个图表当中展示了我们需要的上面是 manager,memory大小有两种配置方式,一种是绝对值的这种配置方式,还有一种是作为 Task Manager总内存的一个相对值的这样一个配置方式。taskmanager.memory.managed.consumer-weight是一个新加的配置项,它的数据类型是一个map的类型,也就是说我们在这里面实际上是给了一个key冒号value,然后逗号再加上下一组key冒号value的这样的一个数据这样的结构。这里面我们目前支持两种 consumer的 key,一个是DATAPROC, DATAPROC既包含了流处理当中的状态后端State Backend的内存,也包含了批处理当中的 Batch Operator,然后另外一种是Python。
部分资源调度相关的Feature是其他版本或者邮件列表里面大家询问较多的,这里我们也做对应的介绍

Flink在1.12支持了最大slot数的一个限制(slotmanager.number-of-slots.max),在之前我们也有提到过对于流式作业我们要求所有的operator同时执行起来,才能够保证数据的顺畅的运行。在这种情况下,作业的并发度决定了我们的任务需要多少个slot和资源去执行作业,但是对于批处理其实并不是这样的,批处理作业往往可以有一个很大的并发度,但实际并不需要这么多的资源,批处理用很少的资源,跑完前面的任务腾出Slot给后续的任务使用。通过这种串行的方式去执行任务能避免YARN/K8s 集群的资源过多的占用。目前这个参数支持在yarn/mesos/native k8使用
在我们实际生产中有可能程序的错误,网络的抖动,硬件的故障等问题造成TaskManager无法连接,甚至TaskManager直接挂掉。我们在日志中常见的就是TaskManagerLost这样的一个报错。对于这种情况就是需要进行作业重启,在重启的过程中需要重新申请资源和重启TaskManager进程,这种性能消耗代价是非常高昂的。对于稳定性要求相对比较高的作业,Flink1.12提供了这样的一个新的 feature,能够支持在Flink集群当中始终持有少量的冗余的TaskManager,这些冗余的TaskManager可以用于在单点故障的时候快速的去恢复,而不需要等待一个重新的资源申请的这样一个过程。
通过配置slotmanager.redundant-taskmanager-num 可以实现冗余TaskManager。这里所谓的冗余TaskManager并不是完完全全有两个TaskManager是空负载运行的,而是说相比于我所需要的总共的资源数量,会多出两个TaskManager,任务可能是相对比较均匀的分布在上面,在能够在利用空闲TaskManager同时,也能够达到一个相对比较好的负载。 在一旦发生故障的时候,我可以去先把任务快速的调度到现有的还存活的TaskManager当中,然后再去进行一个新一轮的资源申请。目前这个参数支持在yarn/mesos/native k8使用
任务平铺问题主要出现在Flink Standalone模式下或者是比较旧版本的k8s模式部署下的。在这种模式下因为事先定义好了有多少个TaskManager,每个TaskManager上有多少slot,这样就会导致经常会出现一个调度不均的问题,可能部分manager放的任务很满,有的放的比较松散。在1.11的版本当中引入了这样一个参数cluster.evenly-spread-out-slots,这样的参数能够控制它,去进行一个相对比较均衡的这样一个调度。

注意:这个参数我们只针对的是Standalone模式,因为在yarn跟k8s的模式下,我们实际上是根据你作业的需求来决定我要起多少task manager的,所以是先有了需求再有TaskManager,而不是先有task manager,再有 slot的调度需求。 在每次调度任务的时候,实际上我只能看到当前注册上来的那一个TaskManager,Flink没办法全局的知道说后面还有多少TaskManager会注册上来,这也是我们为什么很多人在问的一个问题,就是为什么特性打开了之后好像并没有起到一个很好的效果,这是第一件事情。
第二个需要注意的点是这里面我们只能决定每一个TaskManager上有多少空闲slot,然而并不能够决定每个operator有不同的并发数,Flink并不能决定说每个operator是否在TaskManager上是一个均匀的分布,因为在flink的资源调度逻辑当中,在整个slot的allocation这一层是完全看不到task的。这2个地方是需要大家注意的
近几年随着人工智能领域的不断发展,深度学习模型也已经被应用在了各种各样的生产场景中,比较典型的应用场景像是推荐系统、广告推送以及一些智能的风险控制。支持AI以及支持这些深度学习模型或者机器学习算法,一直以来都是Apache Flink社区的长期目标规划之一。 针对这些目标,目前已经有了一些第三方的工作,那么目前阿里巴巴去开源,一个是Flink AI Extended的项目,那么它是一个基于Flink的深度学习扩展框架,目前它里面支持了TensorFlow和PyTouch这些常用的机器学习框架,那么有了它以后,你就在Flink执行这些框架的运行。
那么另一个就是Alink,它是一个基于Flink的通用算法平台,那么里面也内置了一很多常用的机器学习算法,但是以上两个都是从功能性上对Flink进行一些扩展,那么从算力或者说资源的角度上来说,不管是深度学习模型还是机器学习算法,它通常来讲都是我整个作业中的计算瓶颈所在,而GPU则是这个领域被广泛使用的,用来加速这一过程的这么一种资源的设备,那么对于Flink这种对实质性要求比较高的作业,支持机器去加入对GPU的支持就尤为关键了。
那么何为加入对GPU的支持?首先从这个调度上来讲,那么目前Flink只支持用户去配置CPU,内存这两个维度的资源。如果Flink要加入这一部分的支持,那么我们首先需要允许用户配置我每个TaskManager上面有几个GPU资源,而且部署在yarn或者k8s上时,我还要将这些资源需求进行一个转发。 有了资源以后,第二步就是将GPU的信息传递给算子,那么用户自定义的这些算子需要在运行时获取当前环境,也就是说它 task所执行的 TaskManager中可以使用的这些GPU资源的信息。
以上两个针对所有扩展资源实际上都是通用的,而针对GPU资源来讲,它有一个特殊的资源隔离需求,GPU的显存资源只支持独占使用,那么如果我们多个TaskManager进程跑在了同一台物理机上的时候,我们需要保证每个GPU只能有一个TM去独占,那么我们的扩展资源框架就是针对调度,还有信息传递这两个通用需求,它抽象出来一个比较高层的框架,任除了不只是GPU任何扩展资源都可以插件的形式来加入我们扩展资源框架。 关于扩展资源框架的具体的实现细节社区的FlIP-108有详细描述,接下来我们从用户的角度上讲一下如何使用扩展资源框架。
使用资源框架我们可以分为以下这3个步骤:首先为该扩展资源设置相关配置,然后为所需的扩展资源准备扩展资源框架中的插件,最后在算子中,从RuntimeContext来获取扩展资源的信息并使用这些资源
1 | # 定义扩展资源名称,“gpu” |
首先以GPU为例来说一下配置,那么第一个配置就是 external-resources,那么它的值是一个列表,里面包含了你所有去需要的扩展资源的名称,这个名称是大家可以指自己指定的,不一定叫 GPU。但是这个名称之后,你所有扩展资源相关的配置都会以这个名称作为前缀,我们在这里就定义了一个叫做GPU的扩展资源。那么接下来 amount参数设置就是定义了我每个TaskManager上有多少个GPU这种扩展资源,如果你的Flink部署在YARN或者K8s上,你还需要去配置你这种扩展资源。最后如果你为扩展资源准备了插件的话,你需要把插件的工厂类的类名进行配置。
根据不同的部署模式去准备不同的插件,如果你是部署一个StandAlone集群的话,那么 GPU资源是需要有你的集群管理员去保证的。也就是说你TM进程起的那些物理机上需要实际有 GPU的设备,那么如果你是执行在YARN模式的话,你需要保证你的Hadoop版本在2.10以及3.1以上,这样的话它才支持一个GPU的调度,并且需要进行 Resource Type的相关配置。如果你是执行在K8s集群的话,需要保证你集群的版本在1.10以上,这样他才支持体 Device plugin机制。
不管是哪个厂商的显卡,我们都需要去安装对应的device plugin,在确保我们TaskManager上有GPU资源以后,我们下一步就是获取 GPU资源的信息,那么你需要准备这么插件,这个插件主要需要实现两个接口,一个是ExternalResourceDriver,一个是ExternalResourceDriverFactory,代码如下
1 | public interface ExternalResourceDriverFactory { |
那么ExternalResourceDriver会在每个TaskManager上进行启动,然后框架会调用它的retrieve resource,从而来获取我当前 TaskManager上可见的或者说可以使用的这些扩展资源的信息。那么factory就是 driver的一个工厂类了,他在TaskManager初始化的时候会被用来初始化所有你定义的扩展资源的这些driver。 目前这个插件的核心的功能就是在运行时去获取扩展资源的信息,现在Flink中内置了针对GPU的插件,那么它里边的实现原理也很简单,首先它通过执行一个叫做Discovery的脚本来获取 GPU的信息,那么目前GPU的信息里只包含 GPU设备的index。
那么详细来讲一下 Discovery script,首先我们是给大家提供了一个默认脚本的,但是用户也可以去自定义去实现一个脚本,并通过一个路径的配置来指定它。 那么如果你要自定义实现的话,那么你需要遵守脚本的协议。
首先我们的driver会在调脚本的时候将GPU的数量作为第一个参数进行输入,那么之后是接着用户自定义的参数列表,那么如果这个脚本就是执行正常且输出符合你的预期,你需要把 GPU的index列表以逗号分割的形式输出到标准输出;那么如果你脚本执行出错了,或者说你认为结果不符合预期,那么你需要脚本已非零值进行退出。需要注意这也会引发就是TaskManager的初始化失败,那么你的所有标准输出以及错误都会被输出到日志里。
Flink提供的默认脚本是通过 vdissmi工具来获取当前的机器中可用的GPU数量以及index,它只会返回就是所需数量,例如作业只需要两块GPU,而它发现了三块的话,它会只会截取到前两块,但是如果说你的他发现了这批数量不满足要求的话,他就会以非零值退出。 具体的实现大家感兴趣的话,也可以去flink项目中的plugins/external-resource-gpu/目录下去查看具体实现。
如果在StandAlone模式下,我们还需要保证各个TaskManager进程之间的对GPU的独占的访问,因此默认脚本也提供了一个协调模式,那么你可以在用户自定义参数列表里加入Enable Coordination Mode来启动协调模式,启动以后它会通过一个全局的文件锁来实现GPU信息的同步,以此来协调同一台机器上多个TaskManager进程对GPU资源的使用。
下面我们根据前面所说的做一个机器学习界的HelloWorld,这个主要是通过手写数字进行识别数据集的一个操作

通过一个预先训练好的DNN网络,输入以后经过一层的全连接网络来得到一个10位的向量,然后经过一个MaxIndexOf的这么一个操作,最终得到我们的识别结果。
我们的运行环境是在一台有两块GPU的机器上,我们起一个StandAlone集群,然后上面有两个TaskManager进程,那么我们需要保证 TaskManagerr进程分别使用其中一块GPU来不冲突进行计算,这就是通过我们默认脚本的 Coordination Mode来实现的。
这里简单的介绍一下它的核心类,MNISTClassifier
1 | class MNISTClassifier extends RichMapFunction<List<Float>, Integer> { |
首先从open方法中的RunTimeContext来获取我们的GPU信息,那么从中选出第一块GPU,用它来初始化机器学习的类库。
map方法中 ,它的输入就是我们手写图片的矩阵。把这个矩阵以及我们之间训练好的模型矩阵都放入GPU,利用库进行一个矩阵乘法的运算,然后把结果从GPU进行一个取出,在本地做一个MaxIndexOf的操作,最终就能得到我们的识别结果了。
在算子中使用GPU
1 | class MNISTClassifier extends RichMapFunction<List<Float>, Integer> { |
运行时使用GPU进行计算

具体案例演示流程可以前往观看视频或者参考Github上面的链接动手尝试
目前关于资源方面的计划主要有两个方面,首先是被动资源模式,它是指Flink可以根据运行时的可用资源来决定整个job以及各个算子的并发度,而且当可用资源就是发生变化的时候,会根据变化自动的对并发度进行一个调整,这个Feature主要是为了解决我们平常flink跑在k8s或者yarn上的任务,如果资源不够导致作业无法启动执行,有了被动资源模式可以在有限的资源情况下去处理数据。
另一个比较重要的工作就是细粒度的资源管理,那么它允许用户可以为任务指定不同的资源需求,这样在调度的时候就会使用不同规格的TaskManager以及slot做一个TaskManager Slot关于资源的异构。那么它主要是为了对资源的利用效率进行一个优化,尤其是复杂场景的资源利用效率的提升。
通过文章的介绍,相信大家对Flink内存管理有了更加清晰的认知。首先从本地内存、Job Graph 编译阶段、执行阶段来解答每个流程的内存管理以及内存分配细节,通过新的参数配置控制TaskManager的内存分配;然后从大家平时遇到资源调度相关问题,包括最大Slot数使用,如何进行TaskManager进行容错,任务如何通过任务平铺均摊任务资源;最后在机器学习和深度学习领域常常用到GPU进行加速计算,通过解释Flink在1.12版本如何使用扩展资源框架和演示Demo给我们展示了资源扩展的使用,最后针对资源利用率方面提出2个社区未来正在做的计划,包括被动资源模式和细粒度的资源管理。
[1] Accelerating your workload with GPU and other external resources
]]>