浅谈 YARN 的服务级别协议 (SLA)

本文主要取材并翻译自 Enforcing Application Lifetime SLAs on YARN 以及 Better SLAs via Resource-preemption in YARN’s CapacityScheduler,并结合了一些个人理解。本文假设读者对于 YARN 有基本的了解,并基本以容量调度器为例。

背景

YARN 中的服务级别协议(SLA)定义了应用程序在资源分配、作业优先级和其他指标方面可以期望的服务质量。它确保应用程序在集群中获得必要的资源,并满足其性能要求。SLA 定义了应用程序在运行时需要满足的关键性能指标,如启动时间、执行时间和资源消耗等。在某些场景下,这些指标对于保证业务的正常运行至关重要。

为了满足这些要求,可以采取的几个措施:

  • 预留资源:在提交应用程序时,可以通过设置资源配额来保证应用程序在执行期间获得足够的资源。可以通过预订系统提供的提前预留资源的能力,来确保重要的生产作业能够以可预测的方式运行。
  • 限制执行时间:可以使用 YARN 的任务调度器来设置应用程序的最大执行时间。任务调度器可以检测并终止超过指定执行时间的任务。
  • 作业优先级:YARN 提供了执行 SLA 和根据重要性对应用程序进行优先级排序的机制。容量调度器与公平调度器均提供支持。同时,可以结合资源抢占进行更有效的控制。
  • 监控和告警:设置监控和告警系统,以便在应用程序未满足 SLA 要求时及时发出警报。监控指标可以包括应用程序的启动时间、执行时间、资源使用情况等。
  • 优化调度策略:通过调整 YARN 的调度策略,可以优化资源分配和调度过程,以最大程度地满足应用程序的 SLA 要求。

通过限制应用程序生命周期实现 SLA

应用程序在 YARN 中的生命周期表示应用程序在 YARN 中的整体运行时间。该时间从其启动到完成时间进行计算,包括实际运行时间和等待资源分配的时间。

对于用户和管理员来说,限制 SLA 持续时间的要求是不同的。某些用例中,如果应用程序在特定时间内未完成,则不会对结果产生太多关注。例如,用户可能会运行一个定时的 cron 作业,该作业每 5 分钟返回 CDR 统计信息。假设该定时作业将连续运行,并使用不同的数据集,并且预计 2 分钟或更短就可以完成。如果作业未在预计的时间范围内完成,那么产生的输出对用户可能没有用处。因此,限制应用程序的生命周期可以消除对其运行监控的需求。

管理员可能需要限制特定子队列的应用程序生命周期。在队列在多个部门之间共享的组织中,这个需求非常有帮助。在这种情况下,限制提交到子队列的应用程序的生命周期将确保不同部门的用户希望提交其作业时资源的最佳可用性。

YARN 允许用户和管理员配置应用程序的生命周期,从 Hadoop 2.9 开始就提供了这个功能。

管理员相关配置

capacity-scheduler.xml 中,YARN 允许管理员为子队列设置应用程序的生命周期。

  • yarn.scheduler.capacity.<queue-path>.maximum-application-lifetime: 以秒为单位的应用程序最大生命周期,用于提交到队列中的应用程序。任何小于或等于 0 的值都将被视为禁用。这将是该队列中所有应用程序的硬时间限制。如果配置了正值,那么提交到该队列的任何应用程序在超过配置的生命周期后将被终止。用户还可以在应用程序提交上下文中基于每个应用程序指定生命周期。但是,如果用户生命周期超过队列最大生命周期,用户设置将被覆盖。配置过低的值将导致应用程序提前被终止,此功能仅适用于子队列。
  • yarn.scheduler.capacity.root.<queue-path>.default-application-lifetime: 以秒为单位的应用程序默认生命周期,用于提交到队列中的应用程序。任何小于或等于 0 的值都将被视为禁用。如果用户没有提交具有生命周期值的应用程序,那么将采用此值。默认生命周期不能超过最大生命周期。此功能仅适用于子队列。

用户相关配置

用户可以在作业提交期间或提交后更新应用程序的生命周期,以确保应用程序不会超过配置的应用程序生命周期 SLA。

使用 Java API 设置应用程序生命周期

在应用程序提交期间,用户可以在 ApplicationSubmissionContext 中设置生命周期,即 ApplicationSubmissionContext#setApplicationTimeouts(Map<ApplicationTimeoutType, Long> applicationTimeouts)。目前,YARN 支持一种超时类型,即 LIFETIME,以及以秒为单位的相应超时值。

LIFETIME 超时类型是对应用程序整个生命周期施加的超时限制。它包括实际运行时间以及非运行时间。非运行时间包括调度器分配容器所花费的时间、存储在 RMStateStore 中所需的时间等。

CLI/REST 接口

YARN 提供了 CLI 接口来更新应用程序的生命周期。用户可以更新生命周期,即延长或缩短生命周期的值。

1
yarn application -appId <appId> -updateLifetime <timeout>

更新应用程序生命周期的 REST 接口 URI 为:

1
http://rm-http-address:port/ws/v1/cluster/apps/{appid}/timeout

对该接口使用 PUT 方法,并提供一个 timeout 对象:

1
2
3
4
5
6
7
PUT http://rm-http-address:port/ws/v1/cluster/apps/{appid}/timeout
{
"timeout": {
"type": "LIFETIME",
"expiryTime": "2023-01-01T01:02:03.123+08:00"
}
}

通过资源抢占实现 SLA

为了进一步改进服务级别协议(SLA),YARN 的 Capacity Scheduler 引入了资源抢占的机制。资源抢占是指在集群资源紧缺的情况下,系统根据一定的策略来中断正在运行的任务,将资源分配给更高优先级的任务。通过资源抢占,Capacity Scheduler 可以更好地管理和满足关键应用程序的 SLA。

YARN 的 Capacity Scheduler 本身附带一定的弹性,可以允许队列在集群有空闲资源时使用超出其配置容量的资源。没有资源抢占的情况下,假设队列 A 利用 Capacity Scheduler 的弹性特性使用超出其配置容量的资源。同时,假设另一个队列 B 目前资源不足,需要请求更多资源。队列 B 现在必须等待一段时间,直到队列 A 释放它目前正在使用的资源。由于这个原因,在队列 B 中的应用程序会出现高延迟,我们无法满足在此情况下提交到队列 B 的应用程序的 SLA。

资源抢占是同时尊重弹性和 SLA 的一种方式:当集群中没有未使用的资源,并且一些资源不足的队列请求新的资源时,集群将从消耗超出其配置容量的队列中回收资源。

资源抢占的工作原理

所有与队列相关的信息由 ResourceManager 跟踪。ResourceManager 中的一个名为 PreemptionMonitor 的组件负责在需要时执行资源抢占。它在一个独立的线程中运行,相对较慢地进行遍历,以决定何时重新平衡队列。常规调度本身是在与 PreemptionMonitor 不同的线程中进行的。

假设集群中有 3 个队列,正在运行 4 个应用程序。队列 A 和队列 B 已经使用的资源超过了它们配置的最低容量。队列 C 资源不足,正在请求更多资源。队列 A 中有两个应用程序在运行,队列 B 和队列 C 中分别有一个应用程序。下面,详述一下抢占的步骤:

  1. 从超出使用的队列获取待抢占的容器:PreemptionMonitor 每隔数秒检查队列的状态,并确定需要从每个队列 / 应用程序中收回多少资源,以满足资源不足的队列的需求。结果是得到一个待抢占的容器列表。详细的算法在这里不详述。
  2. 通知 ApplicationMaster 以便其采取行动:ResourceManager 内的 PreemptionMonitor 并不立即杀死标记为待抢占的容器以释放资源,而是通知 ApplicationMaster(AM),以便 AM 在 ResourceManager 做出最终决策之前采取预先的行动。这是 ApplicationMaster 进行第二层检查(类似于调度),确定要释放的适当资源集的方式。
  3. 等待强制终止:在将一些容器添加到待抢占列表后,在经过管理员配置的时间间隔之后,超出容量的队列仍未缩减(通过应用程序的操作),ResourceManager 将强制终止这些容器,以确保满足资源不足队列中应用程序的 SLA 要求。

同时,Capacity Scheduler 支持队列的层次结构概念,资源抢占同样也会考虑多级队列的情况。

对应用程序的影响

ResourceManager 在 AM-RM 心跳中向 ApplicationMaster 发送抢占消息。抢占消息包含即将被抢占的容器列表。PreemptionMessage 可能返回两种不同类型的 “合同(contracts)”:

  • StrictPreemptionContract:包含 ApplicationMaster 绝对应释放的容器列表。AM 除了创建这些容器的工作检查点之外,不能做任何其他操作。
  • PreemptionContract:为 AM 留下了灵活性,可以为这些容器创建检查点,或者释放与 PreemptionContract#getResourceRequests() 中 ResourceRequests 的大小匹配的其他容器。

而对于即将被抢占的容器,通过 PreemptionMessage 中提供的信息,AM 可以更好地处理抢占事件:

  • AM 不需要让 RM 强制杀死标记的容器,而是可以自己杀死属于同一应用程序的其他容器。RM 对特定应用程序的生命周期毫不知情,而 AM 对其容器的特性了解更多。从 AM 的角度来看,可能有些容器比其他容器更重要,它们在进行中的工作可能更进一步。在强制杀死 “重要” 容器之前,AM 可以释放其他 “较廉价” 的容器。
  • 在 RM 触发之前,应用程序可以对标记为抢占的容器进行状态检查点。利用这些检查点的状态,一旦有机会再次获得新的资源,AM 可以恢复被杀死容器执行的工作进度。

如果队列中的应用程序不对抢占消息采取行动,一段时间后容器将被 RM 强制杀死。在这种情况下,RM 将为这些被抢占的容器设置特殊的退出代码(ContainerExitStatus#Preempted),并且 AM 在下一个 AM-RM 心跳中会收到相应的通知。ApplicationMasters 应该特别处理这些被抢占的容器,它们并不真正计入容器进程本身的失败次数。

相关配置

参看 YARN Capacity Scheduler (容量调度器) 不完全指南

未来展望

Apache JIRA YARN-45 描述了对于 YARN 抢占支持的规划总览,在未来可能会添加的功能:

  • YARN-2069: 支持在队列内抢占容器,同时尊重用户限制
  • YARN-2498: 支持在考虑节点标签的情况下预占容器(已实现)

通过预订系统实现 SLA

YARN 的 ReservationSystem 为用户提供了提前预留资源的能力,以确保重要的生产作业能够以可预测的方式运行。ReservationSystem 执行仔细的准入控制并提供绝对数量的资源(而不是集群大小的百分比)的保证。预订既可以是可延展的,也可以具有群体语义 (gang semantics),并且可以具有随时间变化的资源需求。ReservationSystem 是 YARN RM 的一个组件。

典型的预订流程如下:

  1. 用户(或自动化工具)提交预订创建请求,并收到包含 ReservationId 的响应。
  2. 用户(或自动化工具)提交由上一步得到的预订定义语言 (RDL) 和 ReservationId 指定的预订请求。这描述了用户随着时间的推移对资源的需求和时间限制。这可以通过通常的 Client-to-RM 协议或通过 RM 的 REST API 以编程方式完成。如果使用相同的 ReservationId 提交预订,并且 RDL 相同,则不会创建新的预订,并且请求会成功。如果 RDL 不同,则预订将被拒绝,并且请求将不成功。
  3. ReservationSystem 利用 ReservationAgent(图中的 GREE)为计划中的预订找到合理的分配,跟踪当前接受的所有预订和系统中可用资源的数据结构。
  4. SharingPolicy 提供了一种方法来对接受的预订强制执行不变量,并可能会拒绝预订。例如,CapacityOvertimePolicy 允许执行用户可以在他 / 她的所有预订中请求的瞬时最大容量,以及在一段时间内对资源积分的限制,举例来说,用户最多可以预留 50% 的瞬时集群容量,但在任何 24 小时内,他 / 她不能超过平均值的 10%。
  5. 成功验证后,ReservationSystem 会向用户返回一个 ReservationId。
  6. 时机成熟时,名为 PlanFollower 的新组件通过动态创建 / 调整 / 销毁队列将计划状态发布到调度程序。
  7. 然后用户可以通过简单地将 ReservationId 作为 ApplicationSubmissionContext 的一部分将一个(或多个)作业提交到可预订队列。
  8. 调度程序将提供来自创建的特殊队列的容器,以确保尊重资源预订。在预订的限制范围内,用户保证了对资源的访问,在资源共享之上继续进行标准容量 / 公平共享。
  9. 系统包括适应集群容量下降的机制。这包括在可能的情况下通过 “移动” 预订来重新计划,或拒绝先前接受的部分预订(以确保其他预订将收到其全部容量)。

基于 SLA 的 YARN 集群调度展望

以下是一些可能会影响 SLA 的一些因素,需要额外注意:

  1. 为新项目提供了新队列,或者加入到现有队列,计算任务增加
  2. 现有的项目或用户迁移队列
  3. 现有的项目或任务的增长
  4. Ad-Hoc 查询、恶意用户(无论是否主观)
  5. 集群宕机与维护
  6. Pipeline 任务追平、历史数据重跑

其中 1-4 是可以提前规划、计算以及通过合理的监控措施进行规避;5 可以采用高可用、联邦、滚动更新的方式规避;6 除了提前规划与隔离之外,也可能会在一段时间内使用超额的资源,视任务的重要程度大小,可以考虑暂停其他任务为其让出资源,以及可以临时给予队列超额的容量。

在计算资源方面,可以采用试跑的方法。假设单位为 GB-h 和 vCore-h。计算公式为:

1
2
3
4
# 公式 1
SUM(map_slot_seconds + reduce_slot_seconds) * yarn.scheduler.minimum-allocation-mb / 1024 / 60 / 60
# 公式 2
SUM(memory_seconds) / 1024 / 60 / 60, SUM(vcore_seconds) / 60 / 60 FROM rm_app_summary BY app_type

可能对 SLA 有帮助作用的未来展望如下,其中一些功能已经在最新版本的 YARN 实现:

  • YARN-624: Gang Scheduling。调度器在所有任务在允许时都可以同时运行。目前,Application Masters(AM)可以通过保留已获得的容器来近似实现这一点,直到它们获得所需的所有容器。然而,这种方法容易导致死锁,尤其是当不同的 AM 都在等待相同的容器时。
  • YARN-5907: 亲和 / 反亲和设置。AM 应该能够请求容器时进行反亲和(anti-affinity)设置,以确保诸如 Region Servers 不会出现在相同的故障区域。类似地,可能希望指定同一主机或机架的亲和(affinity)设置,而无需指定具体的主机 / 机架。
  • YARN-3409: 节点属性(Node Attribute)。已实现。这将帮助用户根据集群中每个节点的特征有效地利用资源并分配给应用程序。
  • YARN-5139: 全局调度(Global Scheduling)。现有的 YARN 调度是基于节点心跳的。这意味着当前是在每个节点进入调度器并进行心跳时,逐个节点进行调度决策。调度器会尝试选择最佳的应用程序资源请求,以在该节点上启动一个容器。这种逐个节点的处理方式可能导致次优的决策。考虑到未来对复杂资源位置的要求,例如节点约束或亲和 / 反亲和,需要考虑将 YARN 调度器转向全局调度。通过全局调度,YARN 调度器应该能够查看更多的节点,并根据应用程序的要求选择最佳的节点,而不像现有的调度器那样进行逐个节点的处理。
  • YARN-1051: 预订系统。已实现。扩展 YARN RM 来明确处理时间,允许用户在一段时间内 “预留” 容量。这是朝着服务级别协议(SLA)、长期运行的服务、工作流和集群调度的重要一步。
  • YARN-1963: 优先级支持。已实现。在资源紧缺的情况下,根据应用程序的重要性或关键性来分配资源非常有帮助。
  • YARN-2915: 联邦集群。已实现。可以将单个 YARN 集群透明地扩展到数以万计的节点,通过将多个 YARN 独立集群(子集群)进行联邦。在这个联邦环境中运行的应用程序将看到一个庞大的 YARN 集群,并能够在联邦集群的任何节点上进行任务调度。在底层,联邦系统将与子集群的资源管理器进行协商,并向应用程序提供资源。目标是允许一个单独的作业在子集群中无缝地 “跨越”。
  • YARN-3306: 基于队列的策略。调度策略可根据队列进行单独配置。这可以更好地满足不同队列的需求,提高资源利用效率。

Oozie SLA 监控

Apache Oozie 提供了 SLA 监控的功能,在这里进行简要概述。

简介

关键作业可以有与之相关的特定服务级别协议(SLA)要求。这个 SLA 可以以时间为基准,即与作业启动的最大允许时间限制、作业应该在何时结束以及运行的持续时间相关联。Oozie 工作流和协调器允许在应用程序定义的 XML 中定义这些 SLA 限制。通过添加 SLA 监控,Oozie 可以主动监视这些对 SLA 敏感的作业的状态,并发送有关 SLA 是否达标的通知。Oozie 还在 UI 中增加了一个 SLA 选项卡,用户可以查询 SLA 信息,并对作业相对于其 SLA 的表现进行汇总查看。

SLA 追踪

Oozie 允许跟踪 SLA 以满足以下条件:

  • 开始时间
  • 结束时间
  • 作业持续时间

对于每个条件,作业会根据是否达标或未达标进行处理,即:

  • START_MET(开始达标)、START_MISS(开始未达标)
  • END_MET(结束达标)、END_MISS(结束未达标)
  • DURATION_MET(持续时间达标)、DURATION_MISS(持续时间未达标)

对于大多数用户来说,预期结束时间是决定整体 SLA 达标或未达标的最重要标准。因此,作业的 “SLA 状态” 将经历以下四个阶段:

  • Not_Started:作业尚未开始
  • In_Process:作业已开始并正在运行,SLA 正在跟踪
  • Met:由 END_MET 引起
  • Miss:由 END_MISS 引起

除了超过预期的结束时间和 END_MISS(因此最终导致 SLA 未达标)之外,还会在作业未成功结束(例如进入错误状态 Failed/Killed/Error/Timedout)时发生 END_MISS

在应用程序中配置 SLA

为了使作业可跟踪服务级别协议(SLA),只需在工作流应用程序定义中添加标签。如果已经在工作流中使用现有的 SLA 模式(xmlns:sla="uri:oozie:sla:0.1"),无需额外操作即可通过 JMS 消息接收 SLA 通知。新的 SLA 监控框架是向后兼容的,不需要更改应用程序 XML,可以继续通过命令行 API 获取旧记录。然而,旧模式和 API 的使用已被弃用,强烈建议使用新模式。新的 SLA 模式为 "uri:oozie:sla:0.2"。为了使用新的 SLA 模式,需要将工作流 / 协调器模式升级到 0.5,即 "uri:oozie:workflow:0.5"

Workflow SLA 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<workflow-app name="test-wf-job-sla"
xmlns="uri:oozie:workflow:0.5"
xmlns:sla="uri:oozie:sla:0.2">
<start to="grouper"/>
<action name="grouper">
<map-reduce>
<job-tracker>jt</job-tracker>
<name-node>nn</name-node>
<configuration>
<property>
<name>mapred.input.dir</name>
<value>input</value>
</property>
<property>
<name>mapred.output.dir</name>
<value>output</value>
</property>
</configuration>
</map-reduce>
<ok to="end"/>
<error to="end"/>
</action>
<end name="end"/>
<sla:info>
<sla:nominal-time>${nominal_time}</sla:nominal-time>
<sla:should-start>${10 * MINUTES}</sla:should-start>
<sla:should-end>${30 * MINUTES}</sla:should-end>
<sla:max-duration>${30 * MINUTES}</sla:max-duration>
<sla:alert-events>start_miss,end_miss,duration_miss</sla:alert-events>
<sla:alert-contact>joe@example.com</sla:alert-contact>
</sla:info>
</workflow-app>

这个新的 schema 更加简洁且含义明确,摆脱了冗余和未使用的标签:

  • nominal-time:这是用于计算作业 SLA 的相对时间。由于 Oozie 工作流与同步数据依赖关系对齐,一般来说这个名义时间可以用协调器名义时间的值参数化。在独立的工作流中,如果没有同步数据集相关联,也需要指定名义时间,并指定期望工作流在哪个时间运行。
  • should-start:相对于名义时间,这是作业应该开始运行以满足 SLA 的时间(需要指定时间单位)。可选配置。
  • should-end:相对于名义时间,这是作业应该完成以满足 SLA 的时间(需要指定时间单位)。
  • max-duration:这是作业预计运行的最长时间(需要指定时间单位)。可选配置。
  • alert-events:指定需要发送电子邮件警报的事件类型。这个逗号分隔的列表中允许的值是 start_missend_missduration_miss*_met 事件通常可以被认为是低优先级的,因此这些事件的电子邮件警报不是必要的。但是,请注意,这个设置仅适用于通过电子邮件发送的警报,而不适用于通过 JMS 消息发送的警报。在后者的情况下,所有事件都会发送通知,并且用户可以使用所需的选择器来过滤它们。可选配置,并且仅在配置了 alert-contact 时才适用。
  • alert-contact:指定一个逗号分隔的电子邮件地址列表,警报发送到这些地址。可选配置,如果只想在 UI 中查看作业 SLA 历史记录并且不想接收电子邮件警报,则不需要配置它。

注意,所有标签都可以被参数化。相同的 schema 也可以应用于 Workflow-Action 和 Coordinator-Action XML 中,并嵌套在内。

Workflow Action SLA 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<workflow-app name="test-wf-action-sla" xmlns="uri:oozie:workflow:0.5" xmlns:sla="uri:oozie:sla:0.2">
<start to="grouper"/>
<action name="grouper">
...
<ok to="end"/>
<error to="end"/>
<sla:info>
<sla:nominal-time>${nominal_time}</sla:nominal-time>
<sla:should-start>${10 * MINUTES}</sla:should-start>
...
</sla:info>
</action>
<end name="end"/>
</workflow-app>

Coordinator Action SLA 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<coordinator-app name="test-coord-sla" frequency="${coord:days(1)}" freq_timeunit="DAY"
end_of_duration="NONE" start="2013-06-20T08:01Z" end="2013-12-01T08:01Z"
timezone="America/Los_Angeles" xmlns="uri:oozie:coordinator:0.4" xmlns:sla="uri:oozie:sla:0.2">
<action>
<workflow>
<app-path>${wfAppPath}</app-path>
</workflow>
<sla:info>
<sla:nominal-time>${nominal_time}</sla:nominal-time>
...
</sla:info>
</action>
</coordinator-app>

获取 SLA 信息

SLA 信息可以通过以下方式访问:

  1. 通过 Oozie Web UI 的 SLA 选项卡;
  2. 通过配置了 JMS 的消息代理接收即时跟踪的 JMS 消息;
  3. 通过 RESTful API 查询 SLA 摘要。

对于 JMS 通知,必须先设置消息代理,然后 Oozie 会发布消息,并可以挂载订阅者接收这些消息。

在 REST API 中,可以应用以下过滤器来获取 SLA 信息:

  • app_name:应用程序名称;
  • id:工作流作业、工作流 Action 或协调器 Action 的 ID;
  • parent_id:工作流作业、工作流 Action 或协调器 Action 的 Parent ID;
  • nominal_startnominal_end:工作流或协调器的名义时间的开始和结束范围。

当指定时区查询参数时,返回的预期和实际开始 / 结束时间会进行格式化。如果未指定,则返回自 1970 年 1 月 1 日 00:00:00.000 GMT 以来经过的毫秒数。