这是我参与「第四届青训营 」笔记创作活动的的第2天
一 Flink概述
1.1 Apache Flink的诞生背景
大数据:指无法在一定时间内用常规软件工具对其进行获取、存储、管理和处理的数据集合
大数据计算架构发展历史
为什么需要流失计算?
大数据的实时性带来价值更大,比如:
1.监控场景:如果能实时发现业务系统的健康状态,就能提前避免业务故障
2.金融风控:如果实时监测出异常交易的行为,就能及时阻断风险的发生
3.实时推荐:比如在抖音,如果可以根据用户的行为数据发掘用户的兴趣、偏好、就能向用户推荐更感兴趣的内容
大数据实时性的需求,带来大数据计算架构模式的变化
1.2 为什么Apache Flink会脱颖而出
Flink 是一个开源的分布式流式处理框架:
①提供准确的结果,甚至在出现无序或者延迟加载的数据的情况下。
②它是状态化的容错的,同时在维护一次完整的的应用状态时,能无缝修复错误。
③大规模运行,在上千个节点运行时有很好的吞吐量和低延迟。
二 Flink整体架构
2.1 Flink分层架构
1.SDK层:SQL/Table、DataStream、Python
2.执行引擎层(Runtime层):Runtime层提供了支持Flink计算的全部核心实现,比如支持分布式Stream处理、JobGraph到ExecutionGraph的映射、调度等等,为上层API层提供基础服务。
3.状态存储层:负责存储算子的状态信息
4.资源调度层:该层主要涉及了Flink的部署模式,Flink支持多种部署模式:本地、集群(Standalone/YARN)、云(GCE/EC2)。Standalone部署模式与Spark类似
2.2 Flink总体架构
flink运行时有两种类型的进程组成:一个JobManager和若干个TaskManager,也是典型的主从架构。 高可用(HA)设置中可能有多个 JobManager,其中一个始终是 leader,其他的则是 standby。
| 角色 | 职责 |
|---|---|
| Jobmanager | 管理集群的计算资源、job的管理与调度执行、checkpoint的协调 |
| taskmanager | 提供计算资源供job运行 |
| Client | 解析job为JobGraph对象,然后提交到Jobmanager运行,并监控Job运行的状态 |
客户将flink jar提交到client端,client将job解析为JobGraph实例,然后将jar和JobGraph一起通过RPC提交到jobmanager,提交成功后Jobmanager返回JobClient给client,用于job的通讯,可以用来获取job的状态。jobmanager将job拆分成不同的task并提交到TaskManagr开始作业的运行。
三 Flink架构优化
① 服务架构的优化
客户端服务化:
下图介绍了一条SQL怎么在客户端一步一步变为JobGraph,最终提交给JM:
在改动之前,每次接受一个query时会启动一个新的JVM进程来进行作业的编译。其中JVM的启动、Class的加载、代码的动态编译 ( 如Optimizer模块由于需要通过Janino动态编译进行cost计算 ) 等操作都非常耗时 ( 需要约3~5s )。因此,我们将客户端进行服务化,将整个Client做成Service,当接收到用户的query时,无需重复各项加载工作,可将延时降低至100ms 左右。
自定义CollectionTableSink:
这部分优化,源于OLAP的一个特性:OLAP会将最终计算结果发给客户端,通过JobManager转发给Client。假如某个query的结果数据量很大,会让JobManager OOM ( OutOfMemory );如果同时执行多个query,也会相互影响。因此,我们从新实现了一个CollectionTableSink,限制数据的条数和数据大小,避免出现OOM,保证多个Query同时运行时的稳定性。
调度优化:
在Batch模式下的调度存在以下问题:
- 使用Lazy_from_sources模式调度,会导致整体运行时间较长,也可能造成死锁。
注:调度死锁是指在资源有限的情况下,多个Job同时运行时,如果多个Job都只申请到了部分资源并没有剩余资源可以申请,导致Job没法继续执行,新的Job也没法提交 - RM ( Resource Manager ) 按OnDemand方式分配Slot需求,也会造成死锁
- RM以单线程同步模式向TM ( Transaction Manager ) 分配Slot请求,会造成等待时间更长。
针对上述问题,我们提出了以下几点改动:
- 采用Eager调度模式 ( 确保所有的资源都申请到后才开始运行 )
- 使用FIFO ( 先进先出队 ) 模式申请资源 ( 确保当前Job的资源分配结束后才开始下一个Job的资源分配 )
- 将单线程同步模式改为多线程异步模式,减少任务启动时间和执行时间
② 针对source的优化
在ROLAP的执行场景中,所有数据都是通过扫描原始数据表后进行处理;因此,基于Source的读取性能非常关键,直接影响Job的执行效率。
Project&Filter下堆:
像Parquet这类的列存文件格式,支持按需读取相所需列,同时支持RowGroup级别的过滤。利用该特性,可以将Project和Filter下推到TableSource,从而只需要扫描Query中涉及的字段和满足条件的RowGroup,大大提升读取效率。
Aggregate下堆:
这个优化也是充分利用了TableSource的特性:例如Parquet文件的metadata中已经存储了每个RowGroup的统计信息 ( 如 max、min等 ),因此在做max、min这类聚合统计时,可直接读取metadata信息,而不需要先读取所有原始数据再计算。
③ 在没有统计信息场景下做的优化
消除CrossJoin:
CrossJoin是没有任何Join条件,将Join的两张表的数据做笛卡尔积,导致Join的结果膨胀非常厉害,这类Join应该尽量避免。我们对含有CrossJoin的Plan进行改写:将有join条件的表格先做join ( 通常会因为一些数据Join不上而减少数据 ),从而提高执行效率。这是一个确定性的改写,即使在没有统计信息的情况下,也可以使用该优化。