问题导读
1.FairScheduler瓶颈有哪些?
2.为何切换CapacityScheduler?
3.如何引入CSQueueStore?
01.集群现状
唯品会线上Yarn集群每天运行着50多万的Spark、Hive、Flink等任务,提交的作业量每月以2%-8%的速度在增长,我们数据团队需要给业务方提供一个稳定可靠的计算平台,同时尽可能的提高集群的资源利用率,以业务和技术驱动为基石来提升Yarn集群的调度性能,获取最大收益比。
02.队列设计模式
基于对于资源更好的掌控,我们选择采用层级队列组织方法,将队列构成一个树形结构,根队列叫做ROOT,代表集群全部资源,一个队列可以配置多个分支队列,其资源之和不超过父队。
我们将分支队列和部门项目进行一一映射,且每个项目都有对应的分支队列,分支队列的资源就是项目可使用的资源;同时给项目内任务设定优先级(P0,P1,P2等),映射到不同叶子队列。如图所示,ROOT下有三个队列QA,QB,QC,对应三个项目A,B,C,项目的任务分为P0、P1、P2,同样映射到队列QP0、QP1、QP2中。
图:部门和项目的队列映射
叶子队列反映的是每个项目中不同优先级的作业,这样形成部门级别分支队列控制集群资源导向,叶子队列对部门资源进行资源倾斜的设计思路,而此设计思路有一个特点就是叶子节点的队列名字是一样的,这也反映在我们选择的Scheduler实现上面。
03.FairScheduler vs CapacityScheduler
由于历史原因,我们一直使用的FailScheduler,业务方通过客户端将作业提交到Yarn集群,Yarn集群统一进行资源管理和任务调度,按照作业指定队列获得对应的资源,从而启动任务执行,通过和底层存储进行交互,完成计算返回给业务方结果。
该Scheduler设计目标是为所有的应用分配公平的资源,主要的特点包括:
▲支持多用户多队列和资源共享,采用树形结构的队列层次,使用固定内存大小和核数来定义队列资源;
▲默认是基于内存的公平调度策略FairSharePolicy,也可以配置为包括内存和cpu的调度,采用Ghodsi等开发的主资源公平算法
DominantResourceFairnessPolicy;
▲资源的分配基于公平共享原则,可为队列配置最小资源和设置最大资源限制;
▲通过配置文件可限制每个用户、队列的运行app的数量。
但是就目前而言,社区更侧重发展CapacityScheduler。CapacityScheduler是Yahoo开发的适合共享环境的调度器,随着版本的迭代,对调度的吞吐量做了众多改进,如多线程异步调度,GlobalScheduler等。相对FairScheduler,它在资源管控、优先级配比等方面更便于管理,对此做了以下简单总结:
其中很重要的一点是在任务提交时,FairScheduler需要叶子队列路径,而CapacityScheduler需要叶子队列名称,我们队列设计模式与FairScheduler提交方式是一致的,和CapacityScheduler是不兼容的。此外针对两者的对比,网上总结了很多,我们摘出它们主要的异同点:
04.瓶颈与选择
FairScheduler的瓶颈
在不断的实践运行中,我们发现FairScheduler的问题不断放大,比如基于Fair的调度方式采用的是真实资源配置,随着集群机器的资源变化和业务的调整,我们就需要相应的调整Scheduler配置文件。为此我们还专门写了一个工具用于从一个比例生成FairScheduler的配置文件,实在低效。特别是我们后续准备开启NodeLabel和手动超配资源后,需要快速的调度资源,这个Scheduler调整和维护更是不可忍受的。
总结我们面临的一些瓶颈:
▲ Scheduler资源调整频繁,维护成本变大;
▲部门项目和队列映射混乱,无法准确计算实际资源使用;
▲频繁调整Scheduler,导致资源配置失真;
▲个别队列资源倾斜严重,不能精细管理。
CapacityScheduler切换的思路
在经过讨论后我们决定切换使用CapacityScheduler方式,但是切换过程发现目前我们的队列设计中:叶子队列重复和CapacityScheduler是完全冲突的,也就是说如果我们要切换Capacity,就必须推倒我们的队列设计模式,让业务调度层面完全重构,这个代价无疑是巨大的。
我们内部讨论认为最好能够有一个方案既能使用CapacityScheduler,但又能兼容目前的叶子队列设计模式。在这个过程中我们意外发现了一个社区正在开发的功能:Capacity Scheduler兼容Fair Scheduler 队列设计模式[YARN-7621,YARN-9879]。我们研究这个功能发现这种方式对用户来说是透明的,也是兼容部门队列映射的,需要做的是修改CapacityScheduler中涉及的所有队列提交方式,将叶子队列提交替换为全路径提交,采用和FairScheduler一致的队列提交方式,这样做出的改动主要涉及Resourcemanager,用户作业提交方式不用做任何改变。
综合考虑后我们决定选择采用社区的这个正在开发的功能,保持对用户透明,采用CapacityScheduler兼容FairScheduler的方式来统一队列的提交方式。
05.Capacity Scheduler Support Fair Scheduler
我们对YARN-7621,YARN-9879进行了详细的研究,其中YARN-7621支持使用队列路径来提交应用,YARN-9879支持Scheduler中存在同名队列,这两点符合我们现有需要的调度逻辑,能够做到兼容FairScheduler的调度,我们总结兼容方案核心要点如下:
▲不应将新行为强加给老用户,不修改业务方原有的队列使用方式,尽可能的减少业务影响;
▲兼容Fairschelder队列的任务提交方式,队列名称只能在其直接父队列下唯一,或者换句话说到队列的路径必须是唯一的;
▲由于叶子名称可以不受约束,队列不再是唯一的,队列应能被其完整路径引用。
但是该功能目前还有很多不完善以及没有完成的部分,主要集中在针对队列路径的更改,替换成绝对路径的调用方法:getQueuePath。进而我们针对这部分做了大量的工作。
06.getQueuePath
既然主要的思路就是把对getQueueName的使用全部进行更换,替换成getQueuePath,完成对Queue路径到绝对Queue路径的切换。首先我们来看看在整个App生命周期中哪些地方会使用到队列相对路径。我们都知道资源的调度和管理由ResourceManager来负责,具体的按照队列来进行资源隔离和使用,保证提交的任务都能按照设定资源配置进行运行,那么我们来看看RM如何使用CapacityScheduler来为提交上来的任务进行队列匹配和资源调度的。
ResourceManager服务启动时首先会初始化Sheduler,期间会加载capacity-scheduler.xml,从root节点开始递归解析配置设计的层级队列,将分支队列的信息保存在queues中。每一次递归,会处理一个节点队列,获取对应的资源配置、父队列和子队列的信息等,保存在CSQueue中并加入到queues中,同时对LeafQueue进行检查保证在全局唯一。当作业提交到RM时,对提交队列进行验证并获取对应资源,完成后续任务调度执行。
图:使用QueueName解析和提交任务
引入CSQueueStore
默认Scheduler解析的队列保存在CapacitySchedulerQueueManager类的Map里面,为了方便解析和保存全路径队列,引入了CSQueueStore类,来保存LeafQueue队列全路径、队列的上下文信息,和对应的路径节点的映射。
图:使用QueuePath解析和提交任务
CSQueueStore
- public class CSQueueStore {
- private final Map<String, CSQueue> fullNameQueues = new HashMap<>();
- private final Map<String, Set<String>> shortNameToLongNames = new HashMap<>();
- private final Map<String, CSQueue> getMap = new HashMap<>();
-
- Map<String, CSQueue> getFullNameQueues() {
- return ImmutableMap.copyOf(fullNameQueues);
- }
-
- public Collection<CSQueue> getQueues() {
- try {
- modificationLock.readLock().lock();
- return ImmutableList.copyOf(fullNameQueues.values());
- } finally {
- modificationLock.readLock().unlock();
- }
- }
- }
复制代码
具体替换部分
本次的替换其中涉及类主要有:AbstractCSQueue,LeafQueue,ParentQueue,CapacityScheduler,CapacitySchedulerQueueManager CSQueueStore,CapacitySchedulerConfigValidator,QueueMapping,核心思想就是将getQueueName()的逻辑替换为getQueuePath()。
getQueuePath
- String fullName = queue.getQueuePath();
- fullNameQueues.put(fullName, queue);
复制代码
这里改动的部分很多,就不再一一展开,简单介绍一个调度核心需要修改的例子。
为了支持加载同名的叶子队列,例如root.a.q1,root.b.q1,root.c.q1的这种场景,在进行初始化解析Queue时,会保存队列全路径fullQueueName,同时去除LeafQueue唯一性判断,将具体队列信息保存在CSQueue类里面,并在每一次的递归中将当前的队列加入到CSQueueStore类中,最终完成所有队列的解析,部分代码如下:
获取全路径的队列
- static CSQueue parseQueue(*){
- CSQueueStore queues,
- ...
- String fullQueueName = (parent == null) ? queueName : (parent.getQueuePath() + "." + queueName);
- String[] childQueueNames = conf.getQueues(fullQueueName);
- ......
- List<CSQueue> childQueues = new ArrayList<>();
- for (String childQueueName : childQueueNames) {
- CSQueue childQueue = parseQueue(csContext, conf, queue, childQueueName,
- queues, oldQueues, hook);
- childQueues.add(childQueue);
- }
- parentQueue.setChildQueues(childQueues);
- ......
- ueues.add(queue);
- }
复制代码
默认队列
在此前FairScheduler中,如果我们没有定义任何队列,所有的应用将会放在一个default队列中,能够对用户设置错误进行兜底,而在CapacityScheduler中并没有这个选项。为了兼容用户的使用习惯,我们也在CapacityScheduler中实现了这个机制。
- public String getFallbackQueue() {
- return get(FALLBACK_QUEUE);
- }
-
- public void setFallbackQueue(String fallbackQueue) {
- set(FALLBACK_QUEUE, fallbackQueue);
- }
-
- CSQueue queue = getQueue(queueName);
- // Check if the queue is a plan queue
- if (queue == null) {
- if (conf.getFallbackQueue() == null) {
- return queueName;
- } else {
- RMApp rmApp = rmContext.getRMApps().get(applicationId);
- rmApp.setQueue(conf.getFallbackQueue());
- return conf.getFallbackQueue();
- }
复制代码
整体功能经过我们测试上线并没有发现重大的调度问题,线上集群使用CapacityScheduler调度也已经正常运行半年。
通过本次兼容FairScheduler,重新梳理业务方使用部门项目队列,解决各个项目混用队列吃大锅饭的问题,也更方便在业务层面对资源需求进行统计管理。同时依据CapacityScheduler的优势,通过调整Scheduler和开启Node Label,对高优先级任务进行资源倾斜以及资源隔离,提高了集群资源利用率,也满足了多样的计算需求。
07.思考
这里可以看到为了能够兼容业务层面的队列设计思路,我们花了更多的时间在Yarn层面进行兼容,这不一定是所有场景下的最优方案,但却是我们目前场景下最优的选择。目前在很多场景还有队列相对路径没有修改彻底的问题,例如:
▲老的Web UI的Scheduler 队列对应的Applications不能正确显示;
▲新的UIV2 Queue页面上面调用仍然使用的相对路径,所以展示信息有误;
▲Yarn Move App 也使用的是相对路径,所以进行Move就会报错。
上述线上生产遇到的问题,还需要进行优化修复,完善这个功能进而共享社区,帮助大家解决具有同样场景的问题。