做高性能计算集群管理的朋友应该都有这种体会:LSF用得好不好,很大程度上就看队列设计得好不好。很多人最开始接触LSF,都是从bsub提交作业开始,觉得无非就是加一个-q参数选个队列。但真正上了生产环境,你就会发现,事情远远没有这么简单——不同业务组的作业挤在同一个队列里,高优先级任务排队排到天荒地老,低优先级任务又占着宝贵的GPU资源不放,两边都在催你,调度器却像没脾气的老好人一样谁都不得罪。这篇文章我就围绕LSF调度实战,重点聊聊怎么用bsub命令配合多队列设计,把资源抢占和公平调度这两件看似矛盾的事情同时搞定。
我会先从多队列的规划和业务场景讲起,然后落到bsub的常用参数和队列映射,再展开抢占(Preemption)和公平共享(Fairshare)的具体配置逻辑,最后用一个从零搭建的完整案例串起来。适合HPC管理员、集群运维工程师,以及那些被bjobs里排队作业搞到头大的科研支持同学参考。
1. 先从业务场景说起:为什么必须做多队列
1.1 单队列带来的混乱
很多小团队初期只有一条队列,比如默认的normal。看起来简单省事,但一旦人数超过五个,作业类型开始差异变大,问题就全冒出来了。我见过的最典型场景是这样的:基因测序分析组跑一个需要72小时的比对任务,把128核全占了;另一边算法组正在赶一个模型验收,就差8核做推理,结果排队排了三个小时。两边都有道理,调度器却没有任何办法判断谁更急,只能按提交时间排序,先来的先跑。
这种时候,你作为管理员站在中间很尴尬。不能直接bkill人家的作业,但也不能对紧急需求视而不见。其实锅不在bsub,也不在科研人员,而在队列设计——你根本没有把“需要响应急性任务”和“需要稳定跑长任务”这两种SLA分开。这就是多队列存在的核心意义:用不同的队列承载不同优先级、不同时长、不同资源要求的作业,让调度器代替你做出选择。
1.2 多队列到底解决什么问题
多队列本质上是把资源管理和业务策略做了一层抽象。你可以按照业务流程划分,也可以按照资源类型划分,还可以按照用户组划分。比较常见的做法是分成四类:
| 队列名 | 优先级 | 典型用途 | 特点 |
|---|---|---|---|
| urgent | 最高 | 线上紧急修复、老板要的结果、产品验收 | 可抢占低优先级作业,响应极快 |
| normal | 中 | 日常运行任务、常规测试 | 资源充足时优先于低队列 |
| batch | 较低 | 大批量计算、非紧急分析 | 资源空闲时利用剩余容量 |
| idle | 最低 | 运维维护任务、备份、一次性清理脚本 | 随时可被抢占,只能填缝 |
这四类队列对应的就是四种完全不同的用户预期。urgent里的作业需要秒级调度、立刻开工;batch里的作业能接受排队几个小时;idle里的作业则根本不在意什么时候跑完,只要别影响别人就行。把它们分到不同队列之后,你在bsub层面只要引导用户选对队列,后面的调度行为就交给LSF去处理。
1.3 队列设计的三大原则
我在实际规划队列时始终遵循三个原则。第一是“队列即SLA”,每个队列必须明确回答三个问题:能占多少资源、能跑多久、能不能被打断。没有明确答案的队列宁可不开。第二是“宁多勿缺”,也就是先建队列,再根据使用情况收敛。一开始就把所有用户塞进两条队列,以后改配置的成本远高于新增一条队列的成本。第三是“默认队列要保守”,默认队列建议设置成中低优先级,而不是最高优先级,这样即使用户懒得思考提交到哪,也不会因为默认选错而把生产集群资源全部吃掉。
把这个原则落到lsb.queues配置文件里,实际上就是调整队列的PRIORITY、NICE、PREEMPTION、PREEMPTABLE这些参数。队列规划清楚之后,bsub命令才真正有了发挥空间。
2. bsub命令核心用法:从提交到切换
2.1 常用参数速查
bsub是整个LSF操作的起点。很多人只用了-q和-o,其实它在多队列调度中能做的事情非常多。我整理了一份高频参数对照,这些参数在多队列场景下基本都会用到。
| 参数 | 作用 | 示例 |
|---|---|---|
-q | 指定提交队列 | bsub -q urgent |
-n | 申请CPU核数 | bsub -n 8 |
-R | 资源选择表达式 | bsub -R "span[hosts=1] rusage[mem=16G]" |
-M | 内存使用上限 | bsub -M 32G |
-W | 运行时长上限 | bsub -W 04:00 |
-J | 作业名称 | bsub -J "test_0422" |
-o / -e | 标准输出/错误文件 | bsub -o %J.out -e %J.err |
-P | 项目计费标签 | bsub -P bio_project |
-G | 用户组 | bsub -G research_group |
-w | 作业依赖条件 | bsub -w "done(12345)" |
-i | 指定输入文件 | bsub -i input.txt |
-sp | 调整作业调度优先级 | bsub -sp 50 |
-q指定的是队列,-sp指定的是作业在队列内的调度优先级,两者是不同的维度,别混淆。-sp取值范围一般在0到100左右,数值越大优先级越高,但也必须在所属队列允许的范围内,不能越权。
2.2 通过bsub让作业正确落队
让作业落队不是简单的“选个队列”就结束。我习惯在提交命令里同时声明资源需求、时长上限和项目名称,这样调度器才能根据队列策略做出最优决定。比如一个需要4核8G内存、最长跑两小时的批量任务,可以写成:
bsub -q batch \ -n 4 \ -R "rusage[mem=8G]" \ -M 8G \ -W 02:00 \ -J "batch_align" \ -o "%J_align.out" \ -e "%J_align.err" \ /opt/pipeline/run_align.py-W参数在低优先级队列里特别关键。因为LSF在做调度决策时,除了看优先级,还会看作业的运行时长和现有资源使用情况。一个明确表明自己“只跑两小时”的作业,调度器更容易把它放到资源窗口里。这就是为什么我建议团队在低优先级队列中强制要求-W,否则调度器面对一个不知道什么时候结束的作业,只能保守处理。
2.3 队列之间如何切换:bswitch
作业已经提交到了batch队列,但用户突然说这个结果下午就要,怎么办?不需要bkill掉重提,直接使用bswitch把作业切换到更高优先级队列。
bswitch -q urgent 123456执行之后作业的队列归属会变为urgent,如果本身还没开始运行,就会按照urgent队列的策略重新参与调度。这个操作比bkill再bsub安全得多,因为作业的JOB ID、输出文件路径都不会变化,用户不需要去翻日志重新找文件。不过要注意,bswitch是管理操作,建议收窄权限,不要开放给所有用户,否则大家都在排队的时候切来切去,秩序就乱了。
2.4 用bqueues和bjobs随时体检
只会提交作业还不够,你得学会看调度器在想什么。bqueues -l normal能看到队列的详细配置和当前作业分布;bjobs -l 123456能看到作业为什么排队、满足了哪些条件、差什么资源。我排查调度问题时有几个固定动作:先bqueues看队列负载,再bjobs -p看排队原因,最后bhosts看集群整体资源。这三板斧下来,90%的问题都能定位。
3. 让紧急作业“插队”:资源抢占的配置与触发逻辑
3.1 抢占到底是怎么发生的
先说清楚一个概念:LSF里“抢占”不是bsub提交作业时附带的动作,而是调度策略的产物。当你往urgent队列提交作业,但集群里没有空闲资源时,LSF会检查是否存在“可被抢占”的低优先级作业。如果存在,并且高优先级作业的资源需求比低优先级作业的资源需求“更紧急”,调度器就会暂停或者终止低优先级作业,把资源腾出来给高优先级作业。
整个触发过程是LSF调度器周期性的调度循环自动完成的,不需要管理员手动介入。但是抢占能否发生,完全依赖队列定义里的PREEMPTION和PREEMPTABLE参数。这两个参数不配置,PRIORITY再高,也只是排队排前面,不会打断正在运行的作业。
3.2 队列级抢占配置示例
下面是一个典型的队列定义片段,我省略了无关参数,重点看抢占相关的配置:
Begin Queue QUEUE_NAME = urgent PRIORITY = 130 NICE = 99 PREEMPTION = HOST RC_INTERVAL = 120 End Queue Begin Queue QUEUE_NAME = batch PRIORITY = 20 NICE = 10 PREEMPTABLE = Y End Queue配置里urgent队列设置了PREEMPTION = HOST,意思是这个队列的作业可以抢占其他队列作业所占用的主机资源。batch队列设置了PREEMPTABLE = Y,表示它愿意被更高优先级的作业抢占。NICE参数影响的是运行中作业被抢占后的挂起优先级,NICE值越大,作业在资源竞争时越容易被暂停。
3.3 抢占粒度:整个任务还是部分资源
PREEMPTION = HOST是最粗粒度的抢占模式:高优先级作业需要的主机,如果正被低优先级作业占用,直接把低优先级作业挂起或者终止。但我实际经验里,这种粗粒度配置往往会造成一种浪费——一个高优先级作业只需要2核,却把一个占满64核的低优先级作业全部打断了。
更精细的做法是细化资源选择表达式。比如urgent队列设置为只需要2核的时候,尽量找一个还有2核空闲的主机,而不是优先抢占整个节点。这个优化不在PREEMPTION参数本身,而在bsub提交时的-R表达式。我在urgent队列的提交模板里通常这样写:
-R "select[type==LINUX64 && mem>=8G] span[hosts=1] rusage[mem=8G]"这样调度器就知道你需要的是一个8G内存和相应CPU的“空洞”,而不是整个节点。优先级调度负责保证你能插队,资源表达式负责保证你不滥用插队权限。
3.4 被抢占的作业会怎样
这一点每个用户都应该知道:作业被抢占,不代表一定就失败了。如果低优先级作业使用了-W限制运行时长,并且配置了检查点机制,抢占发生时作业会被挂起,等待高优先级作业跑完之后恢复运行;如果已经触发了终止条件,那么作业就是真的结束,后续需要重新提交。所以在日常规范里,我会把“抢占接受度”写进队列说明:batch队列用户可以预期自己的作业可能被暂停,但不应预期作业数据会丢失。对于不能容忍中断的任务,务必提交到normal队列而不是batch。
4. 不饿死任何一方:公平调度(Fairshare)实战
4.1 为什么抢占之外还需要公平
抢占解决的是“紧急任务快速插队”的问题,但如果永远插队,低优先级用户会完全失去生产资源。想象一下研究所有三个课题组,A组每次提交作业都走urgent,日子一长,B组和C组可能连续一周都分不到资源。这种时候,光按队列优先级调度是不够的,你需要引入公平调度机制。
LSF的Fairshare机制通过追踪LSF用户和用户组的资源使用历史,动态调整每个用户组的作业调度优先级。用得越多的用户,它的动态优先级会逐渐降低;用得少的用户,调度器会倾向于让他们获得资源。这样既能保留抢占能力,又不至于让某一方长期饿死。
4.2 配置共享组
我最近在一次生产环境实践里,利用LSF的公平共享配置解决了这个问题。首先要将用户划分进共享组。配置文件加上类似这样的定义:
Begin Fairshare FAIRSHARE_GROUP /groupA USERS = user1,user2 FAIRSHARE_GROUP /groupB USERS = user3,user4,user5 FAIRSHARE_GROUP /groupC USERS = user6 End Fairshare然后队列里开启Fairshare:
Begin Queue QUEUE_NAME = normal FAIRSHARE = Y PRIORITY = 50 SLOT_LIMIT = 64 End QueueFAIRSHARE = Y表示这条队列参与公平共享调度。当两个作业同时排队时,LSF会参考共享组的已用资源份额,优先调度使用量较低的组。这里的“已用份额”不是瞬时值,而是基于历史,所以即使当前没有作业在跑,之前跑得太多,也会影响后续调度。
4.3 动态优先级:让历史用量起作用
配置完Fairshare不是一劳永逸,你还要理解LSF是怎么计算“谁用得多”的。核心是衰减机制。系统会定期把每个用户组的资源使用记录乘以一个衰减因子,比如0.9或0.95,让历史的用量影响随时间逐步减弱。这个衰减速度可以通过调度参数调节。周期太短,公平性不明显;周期太长,用户会觉得自己怎么跑都被压制。我通常建议从默认值附近开始,运行两周之后根据作业完成情况微调。
假设A组今天用掉了800核时,B组只用掉200核时,在下次调度时,A组的调度优先级会自动降低,B组的作业即使后提交,也有机会被优先调度。这就实现了“不用管理员介入也能保护弱势用户组”的效果。
4.4 公平调度不适合哪些场景
公平调度并不是万能的。如果业务上有绝对等级关系,比如“生产任务永远要优先于研发任务”,那单纯依靠Fairshare是不够的,必须配合队列优先级和抢占策略。另外,Fairshare针对的是排队中的作业,不处理已经跑起来的长任务。一个已经运行了五天的作业,不会因为用了太多配额被自动杀掉。所以对于要严格限制资源消耗的场景,需要搭配bsub的-W时限限制或者LSF的RLIMIT能力。
5. 从零搭建一套抢占+公平调度体系:完整案例
5.1 需求背景与总体思路
我最近帮一个生物信息团队重建集群调度体系,需求非常典型。团队有21个用户,分为三个课题组:肿瘤分析组、基因组组装组和数据管理组。肿瘤分析组经常有临床样本要赶时间,必须能抢占;基因组组装组任务量大但是允许排队;数据管理组跑一些索引、备份脚本,可以随时被抢占。整个集群有96核,其中GPU队列4卡。
我的设计思路是:三条业务队列对应三个组的日常任务,再加一条urgent队列作为全局紧急通道。urgent队列可以抢占batch队列任务,但不能随意抢占normal队列。三个组在normal队列中开Fairshare,防止某个组长期霸占。
5.2 队列参数落盘
lsb.queues里我写了这样一套核心配置:
Begin Queue QUEUE_NAME = urgent PRIORITY = 120 NICE = 99 PREEMPTION = HOST RUN_LIMIT = 04:00 End Queue Begin Queue QUEUE_NAME = normal PRIORITY = 50 NICE = 20 FAIRSHARE = Y PREEMPTABLE = N End Queue Begin Queue QUEUE_NAME = batch PRIORITY = 10 NICE = 0 PREEMPTABLE = Y End QueueRUN_LIMIT限制了urgent队列作业最长跑4小时,防止有人假装紧急然后无限占据资源。PREEMPTABLE = N能保证normal队列的作业不会被urgent抢占,因为normal队列的用户都是正规做分析的人群,一旦被反复打断,会影响整个项目的交付进度。而batch队列设置为可抢占,正好利用了它的低优先级特性。
5.3 Fairshare共享组配置
公平调度这部分我按课题组划分了共享权重,因为数据管理组日常任务简单,权重给低了反而没必要。三组权重配比为5:3:2,这能反映业务上的资源投入预期,不是完全平均主义。配置大概长这样:
Begin Fairshare FAIRSHARE_GROUP /tumor WEIGHT = 5 USERS = user1,user2,user3,user4,user5,user6,user7 FAIRSHARE_GROUP /genome WEIGHT = 3 USERS = user8,...,user15 FAIRSHARE_GROUP /data WEIGHT = 2 USERS = user16,...,user21 End Fairshare权重值是相对值,并不是按人头比例直接切资源。它影响的是调度器对每个组“应该分到多少资源”的参考依据。权重5的组并不是能多拿5倍CPU,而是表示它们在生产价值上权重更高。
5.4 用户侧的标准提交模板
光在服务端配置还不够,用户侧的bsub模板也得跟上。我给三组分别做了提交脚本模板。肿瘤组的紧急任务模板如下:
#!/bin/bash #BSUB -q urgent #BSUB -n 16 #BSUB -R "rusage[mem=32G]" #BSUB -M 32G #BSUB -W 04:00 #BSUB -J urgent_tumor #BSUB -o %J.out #BSUB -e %J.err python /data/pipeline/run_tumor.py普通组的模板则是:
#!/bin/bash #BSUB -q normal #BSUB -n 32 #BSUB -R "rusage[mem=64G]" #BSUB -M 64G #BSUB -W 24:00 #BSUB -J normal_genome #BSUB -o %J.out #BSUB -e %J.err python /data/pipeline/run_assembly.py低优先级组的模板:
#!/bin/bash #BSUB -q batch #BSUB -n 8 #BSUB -R "rusage[mem=16G]" #BSUB -M 16G #BSUB -W 48:00 #BSUB -J batch_stats #BSUB -o %J.out #BSUB -e %J.err python /data/pipeline/run_stats.py模板写好后,直接在bsub < job_script提交即可,不用再手敲参数,错误率低很多。
5.5 效果验证过程
配置完成后,我分了三步验证。第一步,提交一个64核的batch作业,让它占满大部分资源;第二步,提交一个16核的urgent作业,观察调度器是否自动挂起batch作业;第三步,连续向normal队列提交来自三组的作业,各提交10份,观察调度优先级变化。
实测结果是:urgent作业提交40秒内开始运行,被抢占的batch作业自动挂起,等urgent作业跑完再恢复运行;三个组在normal队列里的排队等待时间分布也随使用量发生了变化,数据管理组因为权重低,在高峰期等待稍久,但不够成阻断。整体达到预期。
6. 常见问题与排障:查漏补缺
6.1 抢占没有生效
这是最常被问的问题。配置了PREEMPTION,但高优先级作业一直排队,低优先级作业照跑。排查时先确认低优先级队列有没有PREEMPTABLE = Y。只设置了高队列的PREEMPTION,没有让低队列“允许被抢占”,调度器是不能主动动手的。这是最容易被忽略的一环。
还需要确认资源维度是否匹配。如果高优先级作业需要的是GPU,而低优先级作业占满的是CPU,那也不会发生抢占,因为资源类型不一样,LSF不会因为CPU空闲就不抢GPU给你。这个很好理解:需求不匹配,无从谈起。
6.2 Fairshare配置了却没有动态优先级
Fairshare不生效的常见原因是FAIRSHARE参数没有在队列里打开。我见过很多人只配置了lsb.fairshare文件,队列配置里却漏了FAIRSHARE = Y,整个机制完全没进入调度流程。另一个原因是,如果提交作业时使用了-P project但项目没有接入共享组计算,也会使部分作业不参与Fairshare,这时候要检查项目的归属关系。
6.3 作业被抢占后结果丢失
很多用户跑了两天的分析任务,突然被urgent作业抢占,程序直接终止,输出文件只有一半。这种问题只能靠检查点机制来缓解,batch队列声明为可抢占,那用户必须接受中断风险。我通常在队列说明里写得明明白白,并建议用户在脚本里增加断点续跑机制。同时我们可以用bsub -k这类参数来指定作业完成时的通知,配合脚本判断结果完整性。
6.4 队列排队的“假象”
有时候看bqueues队列里有20个作业在排队,但单独查每个作业的排队原因时,你会发现并不是因为资源不足,而是因为用户提交时写了-w依赖条件,等待前一个作业完成。这种情况跟调度策略没有任何关系,别去调整队列参数,否则可能越调越糟。
6.5 逐步调优的建议
调度体系的搭建不是一次性工作。我第一次上线这套体系后,持续观察了两周,才根据实际数据做了三轮微调。第一轮调整了NICE值,因为发现低优先级作业被挂起后,系统空闲资源利用不够充分;第二轮调整了Fairshare的衰减周期,让历史用量对调度的影响更明显;第三轮修改了urgent队列的RUN_LIMIT,从12小时缩短到4小时,因为发现有人把紧急队列当长期队列用。每一步改动之前,我都会先用bparams导出当前配置,备份好后仔细核对再改。
写在最后的小经验
这套多队列加抢占加公平调度的体系,我前前后后在不同集群上踩了很多坑才稳定下来。要说最深刻的一条经验,就是不要迷信任何一个单一策略。抢占能解决紧急任务,但用多了会伤害公平;Fairshare能维护平衡,但没法应对真正的突发情况;多队列给了你灵活性,但也要求你把业务SLA想得足够清楚。真正稳定的调度体系,是这些机制共同作用的结果。最后再分享一个实用技巧:每次调整完lsb.queues或者Fairshare配置后,记得用lsadmin reconfig和badmin reconfig重新加载配置,别整个重启集群,否则所有运行中的作业都会跟着遭殃。配置生效后,再跑几个小作业测试一下,确认行为和预期一致再放量,这个习惯能帮你省下大量的夜间排障时间。