ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Slurm主调度循环拆解:PENDING作业为何进不了节点

Slurm主调度循环拆解:PENDING作业为何进不了节点 我见过太多人把集群排队慢的锅甩给Slurm结果深挖下去发现是没搞懂主调度main scheduling loop到底在按什么规则分配资源。你在squeue里看到一大堆PENDING作业节点load却低得可怜这种资源明明有空、作业就是进不去的诡异局面基本都指向对主调度的理解偏差。这一篇我打算把Slurm的调度主链路掰开揉碎地讲清楚调度循环如何被触发、作业从队列到节点的完整决策链、以及它和优先级、回填、抢占之间的关系最后用实际排查经验收尾帮你看懂调度日志和REASON少踩几个坑。这篇文章适合两类人一类是刚接手HPC集群的新人管理员天天被用户追着问作业为什么还不跑另一类是用Slurm做平台的开发在配置QOS、分区、节点限制时被各种参数之间的隐形约束绕晕。搞清楚主调度后面理解回填和抢占才有地基。我会用最贴近现场操作的视角来写参数和命令都可以直接拿到你的集群上验证。1. 先把主调度到底干了什么讲清楚1.1 一次调度的执行路径事件到落地的四个阶段很多人以为Slurm的调度是一个后台服务在不停地看有哪些作业在排队这个理解太宽了。实际上slurmctld里一次完整的主调度是从事件触发到资源分配落盘的闭环大致可以拆成四个阶段。第一阶段是触发。调度不会每秒无脑跑一遍它是由事件驱动的。常见触发事件包括新作业提交、作业结束释放资源、节点状态变化比如节点从维护状态恢复、reservation窗口开启、还有管理员手动执行scontrol reconfigure等等。这些事件会把这个调度请求挂到slurmctld的事件循环里等待被处理。第二阶段是扫描。slurmctld拿到调度机会后会从pending队列里按优先级顺序取出候选作业。这里有个关键点Slurm不是一次把所有pending作业全部拿出来一起决策而是逐个处理。它先取出优先级最高的作业尝试给它分配资源如果成功再继续处理下一个如果失败默认情况下这个作业会被留在队列里再往下看其他作业。这种先来后到的处理顺序就是主调度循环的基本工作方式。第三阶段是决策这也是核心。对每个作业slurmctld会调用select插件常见的是select/cons_tres的select_nodes和select_partitions等函数来完成节点过滤、资源匹配、分区准入这一连串判断。我在第2部分会专门详细拆解这个决策树。第四阶段是落地。一旦分配成功slurmctld会把分配结果写回作业记录更新节点上的TRES占用然后给对应的slurmd发送消息作业状态从PENDING变为CONFIGURING。后面slurmd去创建cgroup、拉起任务进程就是另外一套逻辑了不再属于主调度这一步。也就是说主调度只负责决定谁能去哪台机器而不是真的把任务搬到节点上干重活的是slurmd和srun。1.2 slurmctld为什么把调度做成单线操作这点我要专门拎出来说因为很多人不理解为什么大集群调度会慢更不理解为什么调度变慢时squeue的响应也会变卡。slurmctld虽然是多线程的很多RPC请求可以并行处理但调度决策的主路径是在核心事件线程中串行执行的。换句话说同一时刻只会有一个调度过程在跑它跑的时候会占用slurmctld的主事件循环。如果这一轮调度要处理的作业数量很大或者每个作业要遍历的节点数量很大这轮调度就会阻塞其他操作包括用户执行squeue时发来的查询请求。这个设计有它的合理性调度的本质是修改全局资源状态如果多个线程同时改那就得上大量锁锁冲突带来的问题比单线程串行还要难搞。Slurm选择单线程串行调度换来的是状态一致性上的简单可靠。代价就是调度器同一时刻只能干一件事如果一轮调度太重整个集群的管理面都会暂时变慢。所以你在调优时核心不是让调度跑得更快而是让每一轮调度变得轻。这正好引出下面这两个周期参数。1.3 两个周期参数sched_interval与sched_min_interval在slurm.conf的SchedulerParameters里有两个参数经常被人搞混一个是sched_interval一个是sched_min_interval。sched_interval控制的是周期性调度的频率默认是1000000微秒1秒。也就是说即使没有任何事件触发slurmctld也会每隔一个周期主动跑一次调度逻辑防止有作业长期卡在队列里没人管。sched_min_interval设置的是两次调度之间的最小时间间隔默认是2000000微秒2秒。这个参数的作用是限流即使有很多事件连续触发真正的调度执行也不会比这个频率更高。设想一下如果同时有几百个作业提交事件刷得飞快没有这个下限的话slurmctld会一直忙于调度其他RPC基本就瘫痪了。实操建议如果你们的集群以大批量作业为主2秒的默认值完全够用。但如果有很多交互式作业用户提交后希望一两秒内启动你可以把sched_min_interval调低比如改成500000微秒0.5秒调度响应会变得灵敏很多。代价就是slurmctld的CPU占用会上升尤其是作业和节点数量大的集群需要盯一下负载。改完配置后执行scontrol reconfigure就可以生效不需要重启整个slurmctld服务。2. 建分区、选节点、选分区主调度的三步决策2.1 候选节点筛选IDLE状态与资源量只是第一道门槛主调度在处理某个作业时第一步动作可以理解成筛简历——先把候选节点范围快速缩小把明显不合适的直接淘汰掉。这一步在select插件里有专门的逻辑核心入口是select_nodes。第一道门槛是节点状态。正常情况下节点必须处于IDLE状态作业才能被分配上去。如果节点被DRAIN、DRAINING、NOT_RESPONDING或者已经有别的作业在跑ALLOCATED在主调度看来就直接排除。这里有个细节MIXED状态表示节点上部分资源被占用这种节点如果还有空闲资源主调度是允许新作业去填充的前提是作业请求的资源量不大于剩余量。第二道门槛是资源量。作业在提交时声明的CPU数量、内存大小、GPU卡数、其他GRES资源比如FPGA、MIC都必须小于等于节点的可用量。这一步看起来简单但实际踩坑很多因为CPU、内存、GRES是三维匹配不是某一项满足就行。例如一个作业请求了16核、64G内存节点虽然有32核但可用内存只剩32G照样会被拒。第三道门槛是约束条件。作业通过--constraintxxx声明的节点特性Feature、通过--exclude避免的节点列表、通过--nodelist指定的特定节点组都会在这一步参与过滤。很多作业排队排到天荒地老原因就是约束条件写得过死候选节点被筛得没剩几个。2.2 分区准入QOS与限制条件决定了作业能不能进来通过节点筛选之后下一个关键判断是这个作业到底能不能在目标分区里运行这一步对应select_partitions相关的准入逻辑。注意和很多人想象的不同分区准入不只是看分区是否存在它要过好几道关卡。首先是访问控制。分区可以配置AllowAccounts、DenyAccounts、AllowUsers、DenyUsers。如果提交作业的用户不在这分区的允许列表里作业根本进不来。这个很直接不多说。其次是分区级别的资源限制。每个分区都有MaxNodes、MaxTime、State等属性。作业请求的节点数量超过分区的MaxNodes或者作业声明的运行时长超过分区的MaxTime都会导致调度失败。这里有个常见的坑有些管理员在分区里设置了MaxTime08:00:00用户提交时写了--time12:00:00它不是提示超时而是让作业在这个分区里永远无法被调度。你如果不看分区配置只看squeue的REASON很可能会一头雾水。再就是QOS这把大刀。QOSQuality of Service是Slurm里做配额和优先级控制的核心。一个作业能使用的资源量上限是由它关联的QOS决定的。比如QOS设置了MaxCpuPerUserLimit80意味着这个QOS下的所有用户作业加起来最多不能超过80个CPU核心。一旦达到限制后续作业即使节点完全空闲也会被主调度拒之门外REASON会显示成QOSMaxCpuPerUserLimit这类字样。这里我想强调一点QOS限制和分区限制经常是共同作用的。一个作业提交时带了--partitiongpu --qoshigh它必须同时满足gpu分区的节点资源约束和high这个QOS的配额限制任何一边卡住作业就起不来。2.3 节点选择Slurm为什么倾向于把作业打包放置等节点和分区都筛完剩下的问题就变成在一堆满足条件的候选节点里具体把作业放到哪些节点上Slurm的默认策略是优先把作业集中放置packing也就是尽量使用最少数量的节点。比如一个作业要32个CPU核心每个节点有64核那主调度会倾向于把这个作业全部放到一个节点上而不是拆到两个节点更不会平均散列到所有空闲节点。很多新手第一次看到这种分配不均的情况会误以为调度器负载均衡有bug其实不是。把作业pack到尽量少的节点上有两个明显好处一是减少节点碎片让剩余节点更容易被大作业使用二是减少节点间的通信开销对MPI这类对网络敏感的作业特别重要。当然节点选择不是光看资源够不够。在cons_tres里还会考虑拓扑信息。如果启用了topology/tree这种插件调度器会选择和作业已有资源网络距离更近的节点尽量让一个作业的节点落在同一个交换机下避免跨交换机通信造成性能损失。还有个细节在多个节点都满足条件时Slurm会倾向于选择当前负载相对低的节点。这个负载看的是这个节点上已经分配出去的资源量或者正在运行的作业数不是看机器load average。主调度希望避免每次都把新作业压在同一个节点上导致热点。2.4 从PENDING到RUNNING主调度负责哪一段搞清楚了决策逻辑再看作业状态机就很容易理解。一张表可以直接说明问题作业状态含义谁在处理PENDING作业已入队等待调度器分配资源slurmctld主调度CONFIGURING资源已分配正在通知slurmd准备任务环境slurmdRUNNING用户任务已经在节点上运行srun/slurmdCOMPLETING任务结束正在做收尾清理slurmd主调度的职责范围就是PENDING到CONFIGURING这一小段。一旦slurmctld把分配结果发给slurmd作业状态变为CONFIGURING后面slurmd创建cgroup、绑定GPU、拉起来任务进程主调度就不再干预。所以你在排查作业为什么还没跑这个问题时要把注意力放在PENDING阶段一旦它进入了CONFIGURING说明调度已经成功后面是slurmd的事情。这里有一个非常容易踩的坑CONFIGURING状态长时间不动。这种情况往往是slurmd和slurmctld之间的通信出了问题或者slurmd在准备任务环境时卡住比如GPU显存没释放、cgroup目录残留、共享存储挂载超时。很多人以为是调度器故障squeue一看却显示CONFIGURING其实主调度早就干完活了。3. 主调度之外的三股力量优先级、回填与抢占3.1 优先级是排队号却不由主调度来算主调度扫描pending作业时不是按提交时间一个一个来而是按照优先级从高到低处理。那么优先级是谁算出来的答案是priority/multifactor插件它独立于主调度存在会周期性地为所有pending作业计算数值。multifactor的优先级取决于多个因素作业等待时间Age、用户的公平共享份额Fairshare、分区权重Partition、QOS权重QOS、作业大小JobSize等。每个因素都有对应的权重参数比如PriorityWeightAge、PriorityWeightFairshare、PriorityWeightQOS。普通用户没必要深究公式但管理员一定要清楚主调度是按这个现成的分数排序的它不负责计算公平性只负责按分数执行。有个实际现象可以解释很多人的困惑为什么刚提交的小作业反而比等了很久的大作业先跑因为小作业在Fairshare上可能得分更高或者它的QOS权重更高。这种看起来插队的情况其实是优先级机制的预设效果不是主调度在乱来。3.2 回填是主调度的填空优化器我们先说一个没有回填的Slurm会怎样。假设集群上有两个大作业正在跑第三个大作业在排队它的启动条件依赖其中一个大作业释放资源。与此同时集群上有一些零零散散的空闲CPU核足够跑一个小作业。在主调度的默认逻辑里因为那个大作业拿不到全部资源而它在队列中优先级最高后面的小作业根本不会被轮到。结果是大批零散资源被白白浪费小作业跟着大作业一起等。回填Backfill机制就是为了解决这个问题。启用sched/backfill之后主调度在处理完这个暂时无法启动的大作业时不会直接结束这一轮而是启动一个回填窗口它会不断往后看队列里有哪个低优先级小作业能在当前空闲碎片上跑完并且在这个大作业预期启动时间之前结束不会耽误它。如果满足条件这个小作业就被允许先行运行。这里要注意一个容易混淆的点回填不是主调度之后的另一个调度器它是sched/backfill这个调度器内部的一个增强功能。也就是说你现在用的调度器如果写成SchedulerTypesched/backfill那它的主体里面已经包含了主调度逻辑和回填逻辑主调度管大作业能不能启动回填管小作业能不能趁机偷跑。回填相关的参数非常多比如bf_window控制回填窗口长度默认在分钟到小时级别bf_resolution控制回填扫描的时间粒度。这些留到下一篇单独讲这里你只需要建立主调度回填是上下层关系的认知。3.3 抢占是最后手段但很多集群根本没开优先级高的作业来了但低优先级作业占着资源不放怎么办理论上可以抢占Preemption但实际上很多生产集群根本不会启用抢占宁可直接让高优先级作业排队等。抢占不是主调度的默认行为要满足几个前提配置了PreemptTypepreempt/qos或者preempt/partition同时作业关联的QOS或分区设置了对应的PreemptMode。比如一个QOS设置了PreemptModeREQUEUE那它就可以把低优先级QOS的作业重新放入队列把资源抢过来给高优先级作业用。现在我要强调一个细节抢占动作本身虽然由调度器触发但它不像回填那样是主调度循环的一部分而是走一套独立的抢占处理逻辑。slurmctld根据抢占配置确定需要杀掉或挂起哪些作业然后向对应节点的slurmd发信号slurmd再执行挂起SUSPEND、重新排队REQUEUE或取消CANCEL。这些动作也不是调度主路径顺手做的而是异步处理的。所以你在排查环境里看到作业消失了别想当然认为是slurmctld直接杀了进程真正操作进程的是节点上的slurmd。3.4 把完整调度流程串起来看拿一个例子把整个流程走一遍。用户提交了作业A指定--partitiongpu --gresgpu:2 --time02:00:00。事件触发srun提交请求到达slurmctld触发调度事件。slurmctld把作业A加入pending队列等优先级插件给出优先级分数。主调度循环被唤醒从队首取出最高优先级作业假设是A。准入检查A请求了GPU分区gpu分区的AllowUsers里包含该用户QOS配额没用完A请求的2小时也在分区的MaxTime之内通过。节点筛选select_nodes在gpu分区里找空闲GPU节点检查节点状态、GPU卡数量、剩余内存、Feature约束。有节点状态DRAIN排除有节点虽然IDLE但GPU卡只剩1张不满足2卡要求排除最终锁定一个满足条件的节点。分配落盘slurmctld把分配结果写回作业记录向目标节点slurmd推送任务信息作业状态转为CONFIGURING。如果第5步没找到合适节点A继续保持PENDING此时回填机制启动看看有没有低优先级小作业B可以暂时使用空闲碎片资源同时不耽误A在预期时间启动。B被放行。整个流程里主调度只负责第3到第6步回填在第7步介入抢占则是在另一个场景里异步发生的。理清这个边界以后看调度日志就不会一头雾水。4. 主调度问题排查实录与优化建议4.1 排查三板斧squeue、scontrol、slurmctld日志遇到作业排队不启动我一般按三条线同时查。第一条线是看squeue。不要只看默认输出推荐用自定义格式squeue -o %.10i %.9P %.12u %.20j %.8T %.10M %.9l %.6D %.16R这个格式会列出作业ID、分区、用户、作业名、状态、运行时间、时限、节点数和REASON。重点看T状态和RREASON。如果状态是PENDINGREASON就是排查的核心线索。PENDING时间超过预期说明调度器一直在为它寻找资源但条件始终不满足。第二条线是看scontrol show job它能给出更细的信息。例如作业实际请求的节点列表、资源量、是否指定了约束条件、QOS是哪个、分区里有没有匹配的节点。命令如下scontrol show job 12345输出里要重点看这部分JobState、Reason、MinNodes、NumNodes、NumCPUs、TRES、Partition、QOS、Command。很多时候squeue只显示一个笼统的Resources但scontrol能告诉你这个资源请求到底是卡在GPU上、内存上还是Feature上。第三条线是看slurmctld日志。日志可以在slurm.conf里通过SlurmctldLogFile配置用systemd管理的也可以journalctl -u slurmctld查看。默认情况日志信息量不大如果要做问题分析可以把SlurmctldDebug临时调到debug或者debug2执行scontrol reconfigure后复现一次排队情况再从日志里搜索作业ID和sched相关关键字能看到主调度在处理这个作业时的详细决策过程。4.2 squeue的REASON速查表REASON含义排查方向Priority有更高优先级的作业在排队看优先级插件配置确认Fairshare/QOS权重Resources当前没有足够资源满足作业请求用scontrol查看作业资源请求量和节点空闲量做对比Dependency等待依赖作业结束检查--dependency条件BadConstraints作业的节点约束无法满足确认Feature写法和节点实际配置NodesReqFail用户指定的节点不可用检查--nodelist里的节点状态QOSMaxCpuPerUserLimit当前用户在该QOS下CPU配额已用完检查QOS的配额配置AssocGrpCPUMinutesLimit账号或用户的CPU分钟数配额耗尽检查account限制Reservation等待预留窗口开始确认reservation时间PartitionTimeLimit作业请求时长超过分区限制调整--time或分区MaxTimeJobHeldByUser作业被用户手动挂起用户执行scontrol release这张表不可能覆盖全部REASON但覆盖了90%以上的日常场景。遇到没见过的直接去Slurm的squeue手册页里找每个REASON都有对应说明比网上支离破碎的问答靠谱得多。4.3 典型案例复盘节点空闲作业却排队有一次我排查一个环境用户说GPU作业排了三个小时还没走squeue显示ReasonResources我第一反应是GPU资源不足。但scontrol show node一看明明有两块A100处于IDLE作业也只要一块为什么调度不上顺着scontrol show job看了作业详情发现了两个隐蔽问题。第一这个作业虽然是GPU作业但它同时请求了--mem64G。目标节点虽然GPU空闲但节点上被其他作业占用的内存很多可用内存不足64G。主调度做的是多维资源同时匹配GPU满足还不够内存也必须满足于是作业被拒。第二这个作业还带了一个--constraintib也就是要求节点具有高速网络特性。IDLE的GPU节点里有的没有配这个Feature有的虽然配了但内存不满足。两个约束条件交集处理完候选节点就只剩零个。整个排查下来不是Slurm不调度而是资源和约束之间出现了隐形不匹配。这个案例的教训特别典型看到ReasonResources一定不要只盯着一项资源看要同时看CPU、内存、GRES以及所有约束条件的交集。我的习惯是先跑一次scontrol show job把作业的资源请求全部抄下来再和scontrol show node的输出对照缺哪一项都能快速定位。4.4 调度性能优化让主调度循环轻装上阵调度慢不是靠堆硬件解决的要减少每一轮调度的工作量。这里分享几个我实测有效的方向。第一合理控制pending作业数量。如果队列里长期躺着几万个pending作业主调度每一轮都要从队首开始逐个尝试哪怕大多数作业因为依赖关系完全无法启动调度器也要花时间处理。用QOS的MaxSubmitJobsPerUser和MaxJobsPerUser限制用户提交量能有效防止队列无限膨胀。第二打开SchedulerParameters里的bf_continue之类选项让回填计算可以分片执行避免一次回填扫描把主事件循环卡死太久。回填本身是CPU密集操作bf_continue配合合适的bf_window、bf_resolution可以让调度器在多个周期之间分摊计算量这是大集群调优的常用手段。第三关注节点状态健康度。如果集群里有一批节点长期处于DRAIN或NOT_RESPONDING调度器每次扫描时都要跳过它们。这些节点占用的扫描时间虽然不多但架不住数量多。定期清理失联节点、修复DRAIN节点不仅对调度性能有帮助也能减少用户的困惑。第四Slurm版本升级本身就是一种优化。近几个大版本对调度器做了不少改进比如更高效的数据结构、更细粒度的锁、更快的节点过滤逻辑。如果你还在用20.02之前的版本可以考虑升级调度性能的改善体感很明显。最后再分享一个我的习惯改任何一个调度相关参数之前先把当前SchedulerParameters、PriorityType、PreemptType记下来改完参数后用scontrol reconfigure热加载观察一段时间再决定是否固化。不要一次性堆一堆优化参数出了问题都不知道是哪一个引入的。我个人在实际维护中最深的一个感受是主调度本身很少出bug绝大多数调度异常都是配置自洽性问题。QOS优先级配得再高配额不给够高优先级作业照样卡在配额限制上节点Feature写得再详细用户不按约束提交匹配度就是零。与其一味加大调度频率不如把分区、QOS、节点约束这套准入逻辑梳理干净让主调度在清晰的规则里做决策比什么调参都有效。下一篇我计划把sched/backfill的回填参数和调优案例单独展开讲那是让集群吞吐量上一个台阶的关键到时候见。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进