浅谈 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>[email protected]</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以来经过的毫秒数。