system: [SYSTEM]

user: 3. 调度架构及策略 3.1. 任务识别 Spark在提交作业(Job)后，会将其划分为多个阶段(Stage)，并根据之间的依赖关系进一步切分为多个任务(Task)， Task是最终调度的基本单元。这一节将从微观的角度分析任务识别的可行性。 首先，当所提交程序调用Spark的Transformation或Action操作接口时，SparkContext会对传入的闭包进行序列化检查，保证其可序列化传输并反序列化执行。清理好的闭包函数将作为初始化参数写入创建的RDD中。此时，Spark即将RDD数据和闭包处理函数绑定，由于RDD是Spark的数据流主线，所以中间无论是执行Stage划分调度还是Task划分调度，都不影响RDD与其闭包的绑定关系。当最终分配到Worker节点上的Executor开始实际执行时，会解析并调用RDD上绑定的闭包执行函数对数据进行迭代操作。 所以从微观角度分析，闭包操作是可以追溯的，因此可以通过在RDD创建时，对传入的闭包进行复杂度分析，得到闭包复杂度并写入RDD，作为最终调度时的参考指标。 定义一：(任务复杂度) 定义 为每个任务的复杂度， 为任务总数。调度策略将依据任务的复杂度决定调度的优先级。 3.2. 集群分层 集群异构性随着硬件更迭和高性能硬件引入将趋于显著，目前Spark调度仅依据CPU核数实现调度，该假设对实际的异构集群并不“公平”。一方面，单一CPU核心的计算能力存在明显差异；另一方面，GPU等高计算性能硬件的计算能力远高于CPU，以核心数进行分配，实际上忽略了每个核心本身的异构性，调度粒度不够细。 所以本文的调度方案首先将集群根据核心的性能进行分层，作为最终调度策略的指标之一。基本分层思路是在集群的不同节点上取单一核心，在其上分别运行多个benchmark作业并记录相应的执行耗时，根据指定的分层数进行划分。 定义二：(节点性能指数) 定义 为节点性能指数， 为节点个数，该指数大小代表对应节点的CPU计算性能，指数越大，说明该CPU性能越强。 定义三：(节点分层级别) 定义 为集群分层数， 为节点计算性能的级别， 的定义如下： (1) 其中，公式中的 是一个大于1的常数，定义为分层指数，这样最终分层结果服从以分层指数为底的指数函数，即越高级别的节点越少，而低级别的节点较多，符合分层的期望。 根据上述两个定义，下面给出集群节点的分层算法。 算法一 (节点分层算法) T[ ]// benchmark任务完成时间， size = N L[ ]//节点层级信息， size = K foreach Worker i 运行benchmark任务 T[i] = 执行时间 end T[ ]按升序排序 T[ ]根据T[ 1 ]归一化 L[ 1 ].add(Worker 1) for (j = 2 to T.size) tmp = 1 level = 0 while (tmp< T[j]) tmp *= α level++ end level = min(level， K) L[level].add(Worker j) end return L[ ] 算法的基本思路如下： 1. 首先遍历Cluster中的所有Worker，并分别运行同一组benchmark任务，统计每个Worker的执行时间； 2. 将得到的结果按升序排序，即约靠前的节点，执行时间越短，相对性能越强； 3. 以最短执行时间结果为标准，对数组初始化，用以后续评估； 4. 先将第一个节点归为 节点，然后依次遍历，并根据定义三得到所有节点对应的分层结果。 根据上述算法，将得到各层级的节点集合，然后可为对应 的节点定义其 ，即相同级别的节点的性能指数相等，依据级别划分性能指数的主要目的是排除偶然性。另外，可将算法一中运行的benchmark进行多次更换得到多个分级结果，得到分级矩阵 ，行代表不同节点，列代表不同benchmark，最终取节点在所有benchmark中分级的众数作为节点最终分级结果。 3.3. 节点检测 前文提及Spark任务调度依据核心数作为分配依据，但根据核心数分配是一种静态思路，核心本身存在忙闲状态，对于在忙的CPU也存在其上队列长度的不同，因此需要动态对CPU使用情况及其上任务队列进行监控，方可实现任务的动态调度优化。 Spark集群是主从式结构，Master和Worker之间利用RPC机制实现通讯，因此可在每个Worker节点上添加一个检测模块，因为要保证Master和Worker的连接，之间会存在心跳(Heartbeat)信息交互，我们可以利用每次心跳信息交互的契机，同步一次当前节点的资源使用情况，Master在本地保存一份Worker节点的资源利用表，作为下一次资源分配时的依据，以实现资源的动态分配。 具体检测的节点参数主要有：队列长度、CPU使用率、节点性能指数三种。单节点上队列长度将影响该节点的完成时间，一般队列越长，其上的预期执行时间越长，则应该尽可能减少其上任务的分配，另外Spark执行时存在依赖，若队列过长，还可能造成其他节点任务依赖于队列中某个任务，导致整个作业的阻塞，因此要检测队列长度从而在调度时避免长队列的出现。而CPU使用率直接反映了节点核心的忙闲，CPU占用过高，说明节点可用的计算资源匮乏，即再分配任务可能需要等待计算资源的释放，造成延迟，所以调度时应可能平衡各节点的CPU占用。性能指数，如前定义二所述，反映了节点的性能，调度时，需要按各节点的承受能力，即性能为其合理分配任务队列，另外，高复杂度的任务也应尽可能交由高性能节点执行，以减少低性能节点负载从而提高整体作业的效率。 Spark上调度结构如图1。 Driver启动Job后，将会创建对应的Task Scheduler作为整个作业的任务调度器，当需要任务分配时，会根据任务所需资源向Master的集群管理器(Cluster Manager)申请运行资源，此时，Cluster Manager会获取所以Worker节点上的可用Executor信息作为其任务的可分配资源池。当任务分配到Executor后，Executor会向Driver注册并通过Heartbeat消息与其保持连接。此时可通过创建新的Heartbeat消息，将CPU状态同步传输给Task Scheduler， Task Scheduler则将其记录并保存，作为其调度指标。 Heartbeat消息交互模型如图2。 具体的交互流程如下： 1. Executor被Master分配给Driver执行Job时，会向Driver发送注册信息，Driver即获取了Executor的基本信息； 2. Executor开始实例化并初始化执行时，会启动一个Heartbeater，如图2所示，同时Heartbeater上回绑定一个Heartbeat Task，即每次心跳时所执行的任务； 3. 每次心跳执行到该任务时，Executor会创建一个Heartbeat消息，该消息包装了所需要向Driver传输的数据，可通过重构消息的结构完成自定义消息的传输； 4. Executor发送消息后，会由Driver的Heartbeat Receiver接收到，并进行消息的解析，将上述的参数保存在Driver中，用于后续调度使用。 3.4. 集群分层调度方案 前三节已经分别从任务、集群和节点三个层次对Spark调度分别进行了分析。本文将综合上述三个因素，实现最终的调度策略。 图1. Spark节点调度结构 图2. Heartbeat消息交互 首先，在任务执行前，将预先执行分层算法(算法一)，对当前的集群环境进行分析，并依据分析结果，将所有节点的性能指标数据写入Spark的配置文件，Spark将会在初始化时读取并存入SparkEnv作为后续调度指标之一。该预执行作业会在以下情况发生时触发： 1. 集群硬件环境发生改变； 2. 分层数配置变化，如：从3层到5层； 3. 无层级信息数据； 4. 配置文件距上次更新时间过长，数据过期。 然后，Spark开始正式启动，到创建RDD时，会解析传入的闭包操作复杂度，将其复杂度 记录到RDD上。Job经过DAG Scheduler和Task Scheduler的划分，开始进行最终资源分配时，将会存在以下调度参数：对于Task，将存在C p ；对于待分配的Executor，存在三元组(P i ， running Tasks.len， CPU Usage)，分别代表该Executor的节点性能参数，其上执行队列的长度，以及Task Scheduler中记录的节点CPU使用情况。算法将在Task Scheduler中引入计算模块，计算每个Executor相对于待调度任务的得分，再结合其复杂度，完成Executor调度。 调度器系统结构如图3。 定义四：(评估函数)定义 为Executor上执行队列长度， 为其CPU使用率。定义 为Executor的当前执行能力指数。 (2) 其中， 、 表示对应的评估指数， 表示执行队列长度阈值， 表示使用率阈值，当Executor的两个指标均超过阈值时，会降低该Executor的分配优先级，主要是防止出现单点密集分配的情况。函数值越小，说明执行能力越强。 定义五：(复杂度转化函数)定义 为复杂度转化函数，将复杂度转化为最优节点计算性能指数 。调度时将会优先分配性能指数大于等于 的节点。若未分配成功，则分配层级从 向下依次选取，直至找到合适的Executor。 图3. 调度策略结构 (3) 其中 表示所定义的性能指标的峰值。 算法二 (节点调度算法) G[ ] //可执行Executor信息 selected = NULL //所选Executor foreach Executor q if (lenq>lenth || Uq>Uth) break G[ ].add({q， f(lenq， Uq)}) end G[ ]按升序排序 for (i = g(Cp) to K) for (j = 1 to G.size) level = get Executor G[j].q level if (level ≥i) selected = Executor G[j].q end end for (i = g(Cp)-1 to 1) for (j = 1 to G.size) level = get Executor G[j].q level if (level ≥i) selected = Executor G[j].q end end if (selected == NULL) selected = Random(Executors) return selected 算法的基本思路如下： 1. 首先遍历所有Executor，排除所有队列长度或CPU使用率超过阈值的Executor，并根据定义四的评估函数，得到当前每个Executor的可执行能力，并将队列升序排序G； 2. 根据定义五的转化函数，计算得到当前复杂度的任务的最优执行层级； 3. 先遍历G，得到大于等于最优执行层级的Executor； 4. 若上一轮遍历没有满足的Executor，则再次遍历，获取小于最优层级的Executor； 5. 若最终依然没有结果，则随机调度。