转载学习:字节跳动 Flink 基于 Slot 的资源管理实践 仅供自己学习使用。
众所周知,Flink 在提交和运行 Flink 作业时,需要配置 Flink 资源信息,包括 TaskManager 的数量,每个 TaskManager 的 CPU 数、内存大小以及 Slot 数量。TaskManager 的数量,每个 TaskManager 的 CPU 数、内存大小都比较容易理解,主要是配置启动的计算进程数以及每个进程绑定的物理资源大小。
那么 Slot 是什么?为什么需要在 Flink 作业启动时配置?
一言以蔽之,Slot 是 Flink 集群管理资源的最小单位,也是 Flink 作业申请和释放资源的单位。本文主要分析 Flink 基于 Slot 的资源管理、作业资源申请以及释放流程。
阅读提示:字节跳动内部目前主要使用 Flink-1.11 版本,所以本文分析的 Flink Slot 资源管理实现部分内容将基于此版本展开。
Flink 作业在运行过程中,整个 Flink 集群其实分为四个角色节点,分别为 Dispatcher、JobMaster、ResourceManager 以及 TaskManager,其中 Dispatcher、JobMaster 以及 ResourceManager 在同一个进程内启动和执行。
Dispatcher 接收各类查询请求,例如作业的各类 Metrics 等;
JobMaster 是作业的 AM,管理作业的执行状态;
ResourceManager 管理 Flink 集群的资源和资源分配;
TaskManager 管理 Flink 计算任务的执行。
Flink 作业被提交到资源管理器 (Yarn/K8s) 后,资源管理器根据作业所需的资源配置 (多少个 TaskManager,每个 TaskManager 分配多少 CPU / 内存) 为作业分配资源,并启动对应数量的 TaskManager 进程。
TaskManager 进程启动后,向 ResourceManager 节点注册信息,其中最关键的信息就是 Slot。
TaskManager 根据配置的每个 TaskManager 的 Slot 数,向 ResourceManager 汇报 Slot,而在 ResourceManager 节点内维护和管理所有的 Slot 列表。我们可以简单地将 Slot 理解为资源槽,这个资源槽会在 TaskManager 上跟作业具体的计算任务关联。
Slot 是一个逻辑概念,它不会跟具体的 CPU 核数绑定,例如一个 TaskManager 绑定了 N 个 CPU 和配置了 M 个 Slot,N 和 M 之间没有任何关联。
但是 Slot 跟内存资源相关,我们知道 TaskManager 启动时会指定进程的总内存大小,这块的内存会被分为堆内内存、堆外内存,其中堆外内存又被分为 Managed Memory 和 Direct Memory,对具体内存划分有兴趣的小伙伴可以通过 Flink 内存模型详细了解。
这里我们要说的是 Managed Memory,这部分内存不会预先分配,但是会按照 Slot 划分大小。简单来讲,就是将 TaskManager 中 Managed Memory 总大小除以 Slot 的数量,就是每个 Slot 可以使用的 Managed Memory 大小。
TaskManager 的每个 Slot 关联多个计算任务,每个计算任务由独立的 Java 线程执行,所以多个计算线程会跟一个 Slot 关联,也就是多个计算线程会共享一个 Managed Memory 内存。
上文提到,TaskManager 根据配置的 Slot 数量,会向 ResourceManager 汇报它上面的 Slot 数据。ResourceManager 节点在内部维护 TaskManager 列表,每个 TaskManager 分别有哪些 Slot 以及目前空闲的 Slot 集合。
Flink 集群中的每个 Flink 作业会有一个 JobMaster 节点,JobMaster 节点将 Flink 作业解析成物理执行计划,向 ResourceManager 申请 Slot 资源,同时管理作业中每个计算任务的执行状态。当一个作业提交到 Flink 集群后,Slot 资源总体申请流程如下图所示。
TaskManager 向 ResourceManager 汇报 Slot 资源数据;
JobMaster 向 ResourceManager 发起 Slot 资源申请;
ResourceManager 根据 Free Slots 集合为 Slot 申请分配 Slot,然后向 TaskManager 发起资源确认;
TaskManager 在进程内将指定 Slot 标记为指定作业使用,然后向 JobMaster 汇报 Slot 资源分配信息;
JobMaster 接收到 Slot 资源分配信息后,将 Slot 资源跟作业中的计算任务关联;
JobMaster 将计算任务描述信息发送到 Slot 所属的 TaskManager,部署计算任务。
ResourceManager 在 Slot Pool Manager 中管理和维护 TaskManager 以及对应的 Slot 集合,每个 Slot 有三种状态:FREE、PENDING 和 ALLOCATED。
ResourceManager 处理 Slot 申请是一个异步过程,ResourceManager 接收到 Slot 申请后会先将请求放入到 Pending 列表中,然后给这些请求分配 Slot,最后向 TaskManager 发送请求确认资源申请,完成确认后会更新分配的 Slot 状态。主要流程和操作如下图所示。
JobMaster 申请资源也是一个异步申请加回调确认的过程,主要通过 Slot Pool 管理和实现资源申请。
Slot Pool 中管理 4 类 Slot 相关数据结构,分别为 waitingForResourceManager
列表结构、pendingRequests
列表结构、AvailableSlots 结构和 AllocatedSlots 结构。
1、waitingForResourceManager 数据
Flink 作业的 JobMaster 根据每个计算任务,生成一个 Slot 申请请求,并放入到一个 waitingForResourceManager 的请求列表内。这里需要注意的是会有 Slot 共享的问题,如果多个计算任务共享同一个 Slot,那么这些计算任务只会生成一个 Slot 申请请求。
每个 Slot 请求会生成唯一的 AllocationID,该 ID 会由 ResourceManager 发送给 TaskManager,并最终返回给 JobMaster。
当 JobMaster 跟 ResourceManager 建立连接时,从 waitingForResourceManager 中遍历获取每个 Slot 请求,然后逐个向 ResourceManager 发送 Slot 申请,同时将每个 Slot Request 放入到 Pending Request 列表中。
2、PendingRequests 数据
在上文我们提到,JobMaster 向 ResourceManager 发送申请 Slot 请求后,由于 ResourceManager 的异步申请机制,ResourceManager 并不会直接返回申请到的 Slot 数据,所以 JobMaster 会将 Slot 请求放入到 Pending Request 等待回调。
当 JobMaster 接收到 TaskManager 的 offerSlots 请求获取到申请的 Slot 信息时,才真正完成 Slot 的申请,在 offerSlots 请求中包含分配给该作业的 Slot 列表。JobMaster 会遍历每个 offer Slot,执行每个 Slot 的分配操作,具体可以分为以下几个步骤:
根据上文中每个请求的 AllocationID 从 pendingRequests 中移除指定的 Request;
通过异步回调,将 Slot 分配给指定的计算任务 (多个计算任务共享,则会分配多个计算任务);
在 AllocatedSlots 数据结构中增加分配的 Slot 信息。
3、AllocatedSlots 和 AvailableSlots 数据
AllocatedSlots 存放已经被分配给计算任务的 Slot 信息列表,
AvailableSlots 存放还未被分配给计算任务的 Slot 信息列表。
由于存在超时重新申请等异常情况,例如 JobMaster 申请 Slot 超时重新发起申请请求,所以存在 TaskManager 向 JobMaster 返回的 offerSlots 根据 AllocationID 在 pendingRequests 中找不到对应请求,或者在 LazyFromSource 过程中上游计算任务执行完成需要释放 Slot 等情况,所以会将这些未被分配的 Slot 放入到 AvailableSlots 中。
当作业需要新的 Slot 分配给指定的计算任务时,会优先从 AvailableSlots 中查找 Slot 资源,只有未找到才会向 ResourceManager 发起请求。
在 AvailableSlots 中的每个 Slot 会带有一个时间戳,后台线程会定时检查 AvailableSlots 中的每个 Slot,如果时间戳和当前时间超过一定阈值,该 Slot 会被主动释放掉,避免资源泄漏。
上述流程主要基于 1.11 版本分析 Slot 资源申请流程,在最新发布的 1.14 版本中 Flink 实现了 Declarative 资源申请,总体流程也是走 JM->RM->TM,但是具体实现有比较大的简化。
TaskManager 中有两个数据结构跟作业申请资源相关:TaskSlotTable 和 JobTable。TaskSlotTable 管理 TaskManager 中的 Slot 以及跟计算任务之间的关系
,主要包含以下几类数据:
TaskManager 中的 Slot 数量;
taskSlots,根据 Slot 索引号管理该 Slot 的状态 (TaskSlot),TaskSlot 里包含该 Slot 的计算任务列表等数据;
allocatedSlots,根据 AllocationID 管理该 Slot 的状态 (TaskSlot);
taskSlotMappings,根据计算任务的 ID (ExecutionAttemptID) 管理计算任务和 TaskSlot;
slotsPerJob,根据 JobID 管理属于该 Job 的 AllocationID 集合。
JobTable 管理和 JobMaster 的连接信息,当 TaskManager 获取到指定作业的 Slot 申请时,根据 JobMaster 的地址跟 JobMaster 创建连接,向 JobMaster 注册,并将连接信息保存到 JobTable 中。
TaskManager 在接收到 ResourceManager 发送过来的 Slot 申请后,会对 Slot 申请进行处理并更新 TaskSlotTable,在这个过程中会将 Slot 申请加入到定时检查中,释放超时未分配成功的 Slot 资源。具体流程如下图所示。
这里比较重要的一个流程是接收到指定作业的 Slot 申请后,会跟作业创建连接,然后将 TaskManager 注册到 JobMaster,JobMaster 接收到注册信息后会跟 TaskManager 创建连接和心跳监控,TM 和 JM 的心跳只监控连通性,相关流程如下图所示。
JobMaster 将 Slot 资源分配给计算任务后,生成计算任务的部署信息,部署信息里包含作业信息、计算任务信息、上下游 Shuffle 信息以及计算任务所部署的 Slot 索引号信息等,然后 JobMaster 将部署信息发送给指定的 TaskManager。
TaskManager 接收到计算任务部署信息后,对计算任务进行校验、部署和执行,这个过程涉及到 TaskSlotTable 以及 JobTable 操作,具体流程如下图所示。
ACTIVE:正在被指定的作业使用;
ALLOCATED:创建时的初始状态,为某个作业创建,但是还没被使用;
RELEASING:正在被释放中。
在 TaskSlot 创建时,会初始化一个 MemoryManager,管理 Slot 中所有计算任务申请和释放 Managed Memory,共用 TaskSlot 的所有计算任务共享 MemoryManager,TaskSlot 管理了所有在上面运行的 Task 列表。
TaskManager 中的计算任务完成计算后,会释放该计算任务申请和使用的资源,涉及到 Slot 相关的主要以下几个操作:
完成上述操作后,会向 JobMaster 发送计算任务更新。JobMaster 收集到所有计算任务的更新消息后,完成作业执行并跟 ResourceManager 断开连接,然后遍历申请到的 Slot 并向指定的 TaskManager 发送资源释放请求。
TaskManager 接受到 Slot 释放请求后,会从 TaskSlotTable 移除指定的 Slot 信息并向 ResourceManager 释放 Slot 信息,如果作业所有 Slot 都被删除,会关闭跟 JobMaster 的连接。TaskManager 处理的总体流程如下图所示。
ResourceManager 接收到指定 Slot 释放请求后,会从资源申请列表中查找与该 Slot 匹配的申请请求并处理,若申请列表没有请求则将 Slot 放入到空闲 Slot 列表中。
开源社区在 1.11 版本之后,对资源申请和释放流程具体的代码实现做过比较大的重构和优化。在资源管理和流程实现上,主要是支持细粒度资源管理 (FLIP-56: Dynamic Slot Allocation - Apache Flink - Apache Software Foundation) 和声明式资源申请 (FLIP-138: Declarative Resource management - Apache Flink - Apache Software Foundation)。
在上文我们提到,TaskManager 会将 ManagedMemory 会按照里面的 Slot 进行等分,这会带来资源浪费。
作业的每个计算节点都可以设置不同的并发度,所以每个 Slot 内部执行的计算任务类型和数量有可能是不同的。这意味着每个 Slot 的计算任务所需的内存资源会存在比较大的不同,比如有些 Slot 有 Join 等计算任务,有些 Slot 没有。
原先按照 Slot 平均划分内存大小的方式会造成资源浪费,为了提升资源使用率,从 Flink 1.14 版本开始支持细粒度资源申请
。
JobMaster 向 ResourceManager 申请 Slot 时,会向 ResourceManager 指定资源数量,包括 CPU、内存等。ResourceManager 根据当前维护的资源列表,为作业分配指定资源的 Slot,同时向指定的 TaskManager 发送请求。TaskManager 接收到带有资源信息的 Slot 申请后,会创建 Slot,并向 JobMaster 确认资源申请。
所以从 Slot 总体申请流程上,新版本跟原先的处理是相同的,这块主要是支持在 Slot 申请时带上相应的资源信息,ResourceManager 会根据管理的 TaskManager 剩余资源信息为计算任务分配 TaskManager。
在上面的 Slot 申请流程中,一个 Flink 作业会申请若干个 Slot 资源,但是在申请过程中是按照单个 Slot 独立申请的。Flink 作业,特别是流式作业,通常需要完成所有所需的 Slot 资源申请后,作业才能正常运行。
所以在 Flink 新版本中支持了声明式资源申请,JobMaster 向 ResourceManager 申请资源时,会将所需的多个 Slot 打包成一个 Batch,向 ResourceManager 发起资源申请。
ResourceManager 接收到作业的多个 Slot 申请后,会处理 SlotManager 中管理的资源,然后根据 Slot 逐个向指定的 TaskManager 发起资源请求。
每个作业所需的 Slot 数量,目前是在 JobMaster 资源申请时进行打包处理,后续可能会根据 JobGraph 执行计划中每个计算节点的并发度直接计算。
总体上来讲,Flink 整个资源管理、申请和分配围绕 Slot 展开,同时每个 TaskManager 中的 Slot 数量决定了作业在该 TaskManager 中运行的并发计算任务数量。
本篇文章主要介绍了 Slot 对资源分配、释放以及计算执行的影响,希望可以帮助大家更好地决策每个 TaskManager 中的 Slot 数量,对 Flink 作业进行调优。
目前,字节跳动流式计算团队同步支持的火山引擎流式计算 Flink 版正在公测中,支持云中立模式,支持公共云、 混合云 及多云部署,全面贴合企业上云策略,欢迎申请试用:www.volcengine.com/product/flink