LSF在数据中心和高性能计算集群里的地位,不需要我多说。真正让运维头疼的从来不是bsub怎么提交作业,而是集群一忙起来,生产任务被长周期任务堵死,老板催结果,业务方互相甩锅,这时候才意识到:多队列的资源抢占和公平调度,不是搭几套队列就能糊弄过去的。我接手过的集群不少,几乎每个团队对调度的诉求都惊人地相似——高优作业要能“挤进去”,但低优作业也不能饿死。这两个目标天然存在张力,本文就用LSF最核心的bsub命令和配套的队列配置,把这条平衡木怎么走完完整整讲清楚。
1. 先把调度问题说透:抢占和公平不是同一个维度的事
1.1 一个典型的混乱场景:生产作业被长任务堵死
想象这样一个画面:周五下午,数据分析组往集群里丢了200个基因比对任务,平均跑8小时。与此同时,生产环境的核心计算任务也在排队,但队列前面的长作业把CPU占得满满当当。你打开bjobs一看,生产任务的排队时长已经超过5小时,业务方电话一个接一个。这时候哪怕你把生产任务的优先级调到100,它还是在排队,因为正在跑的作业不会被新提交的几个任务自动挤掉。
很多人第一次接触LSF时,脑子里默认了一个错误的模型:认为高优先级队列的任务跑起来后,会自动让低优先级队列的任务让路。实际上LSF的默认行为非常保守,没有主动抢占机制时,正在运行的作业根本不会收到任何干扰信号。你调再高的优先级,它也只影响排队顺序,不影响已经在跑的作业。这就是生产事故的源头——你这边升了优先级,那边该堵还是堵。
1.2 LSF队列的本质:不是“分区”,而是一套策略容器
要理解抢占和公平,必须先打破“队列就是分区”的思维定式。很多用过Slurm或PBS的工程师常把队列类比成不同的资源池,但在LSF里,队列(queue)更像一个装载调度策略的容器。它声明了哪些用户能用、优先级多高、能占多少CPU和内存、作业允许跑多久、作业被抢占后怎么处理,等等。同一个队列里的作业可以跑在完全不同的主机上,不同队列的作业也能共享同一批机器。
所以真正决定“多队列抢占与公平调度”效果的,不是建了几个队列,而是每个队列身上挂着的策略参数。你在lsb.queues里定义队列时,如果只写了QNAME和PRIORITY,那这个队列其实和默认队列没有本质区别。后面我会详细拆解每个关键参数的意义,这里先记住一个总原则:队列是策略集合,资源和优先级只是策略的输入项。
1.3 抢占与公平的对立统一:为什么两者必须同时考虑
高优先级任务抢资源,靠的是PREEMPTION机制;低优先级任务不被饿死,靠的是FAIRSHARE和队列权重。前者像“特权通道”,后者像“最低生活保障”。只做抢占不做公平,结果是低优作业永远起不来,集群有效利用率反而下降——因为大家都在提交高优作业,最终所有人的优先级一起通胀。只做公平不做抢占,生产任务的关键时效性又无法保证。这两件事必须放在同一个配置体系里联动设计。
这个道理听着简单,但实际配置时大部分人容易走极端。我见过一个团队把所有队列的PREEMPTION开到了最高档,结果低优作业每跑20分钟就被挂起一次,检查点机制又没做好,最后低优作业全在跑重复工作,总吞吐量掉了30%。所以不要上来就调抢占参数,先得把整个多队列模型想清楚。
2. 队列结构设计:金字塔模型与预留容量
2.1 三级队列划分:生产、试跑、批量的典型配置
实战里我习惯把队列设计成三层金字塔结构。顶层是生产队列,承载时效性最强的任务,优先级最高,允许抢占低层作业,但队列本身的并发数要控制住,防止生产作业自己互相踩踏。中间层是试跑和短任务队列,适用于代码调试、小规模测试,优先级适中,不抢占但也尽量不被频繁打断。底层是批量计算队列,跑大规模、长周期、可断点的作业,优先级最低,被抢占后自动挂起或重排队。
下面是一份我常用的lsb.queues配置骨架:
Begin Queue QUEUE=production PRIORITY=400 NICE=20 USERS=prod_user_group HOSTS=all PREEMPTION=SUSPEND RES_REQ=span[hosts=1] rusage[mem=4096] RESUME_COND=rusage[mem=3072] RUNLIMIT=720 DESCRIPTION=Production compute tasks End Queue Begin Queue QUEUE=normal PRIORITY=200 NICE=10 HOSTS=all RES_REQ=rusage[mem=2048] RUNLIMIT=1440 DESCRIPTION=Interactive short jobs End Queue Begin Queue QUEUE=batch PRIORITY=50 NICE=0 HOSTS=all PREEMPTION=REQUEUE RES_REQ=rusage[mem=1024] RUNLIMIT=4320 DESCRIPTION=Long-running batch jobs End Queue这里有几个关键点。PRIORITY决定了排队顺序,差值拉开到倍数级时,高优作业才能稳定排在前面。NICE用于影响Linux内核级别的CPU份额,生产队列给20,批量队列给0,意思是即便批量作业在跑,生产队列的任务在同机器上也更容易抢到CPU时间片。这一点常被忽略,但实践中很有效果。
2.2 SPEC限制:防止高优作业吃掉全部资源
高优队列最危险的地方在于,它会无限抢占,直到整个集群只剩它在跑。为了避免这个问题,必须给每个队列设置资源使用上限。LSF里最实用的手段是QMAX和QLIMIT。
QMAX限制队列的最大作业槽位数,防止同时运行的作业数失控。QLIMIT限制队列每用户或每项目最大槽位数,避免单一用户填满队列。
我的经验是,生产队列的QMAX不要超过集群总槽位的30%。你可能会问:高优队列不应该能无限伸展吗?实际生产中,高优作业往往对特定软件许可证或数据宿主有依赖,并发上去了反而互相等IO,吞吐量不会线性增长。留出配额给低优作业,对生产任务本身也是一种保护。
2.3 预留槽位:给“紧急任务”留后门
除了QMAX之外,绑定主机和预留槽位也是一个有效做法。可以单独用一个高优队列绑定几台专用机器,并在其上设置所有其他队列不可用。这样紧急任务永远有“硬预留”的资源可跑,完全不需要抢占,零干扰。抢占机制再快也需要几秒到几分钟的响应时间,而硬预留是实时可用的。
Begin ResourceMap RESOURCE/GLOBAL_HOSTS = pool_prod RESOURCE/GLOBAL_HOSTS = pool_batch End ResourceMap配合队列的HOSTS字段将队列绑定到对应资源池。不过硬预留会闲置资源,所以只适合业务量波动大的场景,集群长期满载的团队不建议大量预留。
3. bsub提交背后的调度链路:提交参数如何影响排队、抢占和公平
3.1 基础提交参数:队列、项目、应用名
bsub看起来就是交个作业,但它往调度器传的每个字段都会参与决策。三个最基础的参数是:
bsub -q production -P M123 -app dowork ./run.sh这里-q指定队列,-P指定项目账号,-app指定应用配置文件。项目账号是fairshare计算的最小单位之一,如果你不指定,LSF会默认归到当前用户的名下,fairshare统计就会混乱。应用配置(lsb.applications)里可以定义CPU、内存、临时目录等环境,也会影响调度器对作业资源需求的判断。
按我踩过的坑,最容易被忽略的是-P。很多用户从脚本模板复制命令,压根不知道有项目账号这回事,结果fairshare统计出来的全是各用户根目录下的“no_project”,优先级计算失去意义。所以如果你要搞多队列公平调度,第一步不是调参数,而是要求所有业务方在bsub里显式指定项目账号。
3.2 资源约束参数:-R和-EXT如何影响排队决策
作业对资源的需求,直接决定了调度器把它放到哪台机器、允许它和什么作业共处。核心参数是-R,它有两种常见用法:
# 硬性资源约束:限定内存和CPU bsub -q batch -R "rusage[mem=8192] span[hosts=1]" ./longjob.sh # 按资源类型过滤:只在有GPU的机器上跑 bsub -q batch -R "type==gpu" ./train.pyrusage[mem=8192]的意思是为该作业预留8GB内存。调度器看到这个需求后,只会把作业放到能凑齐8GB空闲内存的机器上。这个预留值写太大,作业排队时间变长;写太小,作业运行时OS内存超限,会被Cgroup杀掉。所以要结合lsload和bhosts输出的实际机器空闲内存来定。
另外一个不太常被提到但极其实用的参数是-EXT,它可以给调度器传递外部资源需求,例如许可证数量:
bsub -q production -EXT "license_matlab=10" ./run_matlab.sh如果许可证不足,任务会一直排队,而不会启动后干等。从调度公平的角度看,这比作业起来后再等许可证要高效得多。
3.3 抢占相关选项:提交方不能直接指定“我要抢占”
有一个新手容易误解的点:bsub没有“抢占”参数。抢占行为完全由队列配置里的PREEMPTION字段决定,用户提交作业时只能决定进哪个队列,进而间接决定自己被抢占还是抢占别人。这样做是合理的,否则任何用户都可以给自己的作业标签上“必须立刻运行”,公平就无从谈起了。
用户真正能影响抢占体验的参数是当作业被抢占时的策略切换,也就是从“作业不可中断”变成“支持重跑”。比如提交时可加:
bsub -q batch -k ./checkpoint_job.sh-k表示作业在收到SIGUSR2时应该执行检查点机制,保存状态后自行退出。这样配合队列的PREEMPTION=REQUEUE,作业被抢占时能干净地进入重排队流程,而不是从零开始跑。生产队列抢占批量作业时,也不会因为强杀导致批量作业的数据损坏。
4. 抢占策略配置与生产环境踩坑记录
4.1 PREEMPTION的三种模式:SUSPEND、REQUEUE、KILL怎么选
LSF的PREEMPTION字段支持三种动作,选择的依据是作业类型和业务容忍度:
| 模式 | 动作 | 适用场景 | 风险 |
|---|---|---|---|
| SUSPEND | 暂停低优作业,保留内存和进程上下文 | 交互式、短作业 | 暂停太久占着内存不放 |
| REQUEUE | 向低优作业发信号,作业自行退出,重新排队 | 有检查点、可断点续算的作业 | 作业不支持信号时会数据丢失 |
| KILL | 直接杀掉低优作业,资源立即释放 | 完全不重要的试探性任务 | 强杀导致脏数据和任务失败 |
SUSPEND看上去最温柔,但有个隐藏问题:被挂起的作业进程虽然不占CPU,但内存仍然被占用。如果批量作业全都SUSPEND在那里,内存被占满,新任务根本调度不进来,等于资源被冻结了。所以用SUSPEND时必须设置RESUME_COND,让作业在低优队列资源宽裕时自动恢复。我给生产队列的配置里专门加了RESUME_COND=rusage[mem=3072],意思是当机器还有3GB以上空闲内存时,恢复被挂起的低优作业,避免长时间冻结。
REQUEUE是我在长周期批量计算里最推荐的方案,前提是业务方做好检查点。LSF会在发送信号后等待一段时间(由REQUEUE_EXIT_TIME控制),如果作业没按计划退出,它会升级处理。这里要提醒所有用户:REQUEUE不是万能的,它依赖作业对SIGUSR2信号的响应,不接受信号的任务会以失败告终。
KILL适合清洁任务,比如临时生成的脚本和一次性计算。但生产环境慎用,因为KILL掉的作业在LSF记录里是“任务失败”,不会自动重排,需要外部流程兜底。
4.2 抢占阈值:别让高优作业频繁打断低优作业
如果你让生产队列对任何batch作业都立即抢,集群会陷入“跑5分钟-抢-重排-再跑5分钟”的循环。这时候需要设置抢占阈值。常用参数是PREEMPTION条件字段和RUNLIMIT的配合:
Begin Queue QUEUE=production PREEMPTION=SUSPEND START_COND=rusage[mem=2048] && defined(info_cpu) SLOTS=128 End Queue此外,我通常会在lsb.params里打开抢占频率控制:
Begin Parameters PREEMPT_INTERVAL=30 PREEMPT_MIN_RUN_TIME=120 End ParametersPREEMPT_MIN_RUN_TIME=120的意思是,低优作业至少连续运行120秒后,才允许被抢占。这个参数极大减少了任务频繁重启的问题。很多长任务初始化阶段就要消耗几十秒加载数据,如果刚跑10秒就被抢,那整个集群的有效计算量基本为零。这个参数是我觉得最值得调的抢占参数,没有之一。
4.3 一次真实的抢占“踩坑”排查:抢占后作业卡死在BYEXIT
有一次用户反馈:批量作业被生产作业抢了之后,状态变成EXIT,但主机槽位一直被占用,新作业调度不进来。我查了bhosts发现该机器仍有槽位在跑,bjobs -l显示作业状态为BYEXIT,进程实际已经不存在。问题出在抢占信号结束后,作业没有正确回收。
排查链路是这样的:
- 先看
bhist -l <job_id>,确认作业事件序列:从RUN到SIGUSR2,再到EXIT。 - 再看
lsb.acct里该作业的最后一个事件:"job finished"还是"job exited"。 - 然后用
bjob -d查daemon日志,发现sbatchd在回收进程时,因为作业申请了超大共享内存段,清理超时,导致槽位释放失败。
最终定位是作业本身的问题,它申请了一片巨大的内存文件映射,信号处理完成后没有自动释放,内核回收卡在了page cache刷新上。解决方案有两层:一是让作业脚本在收到信号后主动清理共享内存再退出;二是在队列配置里给这类作业戴上RES_REQ限制,不允许申请无上限的共享内存段。这类问题做一次排查就能积累很多经验,以后再遇到基本15分钟内能定位。
5. fairshare调优:怎么让两个队列“吵架”时还能维持公平
5.1 fairshare计算原理:用户、项目、历史的三角关系
公平调度在LSF里叫fairshare,它并不是简单的“人人有份”,而是基于使用历史动态调整优先级。核心参数在lsb.params里:
Begin Parameters FAIRSHARE=YES SHARE_USERS=ALL SHARE_QUEUES=ALL FS_INTERVAL=30 FS_CUSTOM_POLICY=NO End Parametersfairshare计算的基本单位有三个层级:用户、项目、队列。调度器记录每个层级在最近一段窗口内的CPU消耗量,结合管理员设定的份额比例,算出一个“欠债”程度。用得多的实体,后续排队时的优先级权重会被压低;用得少的实体,权重升高。这就是所谓的“动态公平”,而不是简单的轮询队列。
举个例子,如果两个用户各分到50%的份额,用户A今天上午用掉了60%的资源,用户B只用40%,那么下午的调度中,B的作业排队时会比A的同类作业更快被调度。注意这是独立于队列优先级之外的另一个影响因子,两个用户提交到同一个普通优先级队列时,fairshare会发挥主要调节作用。
5.2 队列权重和fairshare的协同:优先级差多少才合理
队列优先级(PRIORITY)和fairshare权重(SHARE)是两套体系。优先级决定的是“先来后到”中的先后,份额决定的是不同用户/项目之间的“资源配额”。如果队列优先级差太大,低优队列里的fairshare调控基本失效,因为调度器在优先级排序阶段就把低优队列的任务挤到后面了。
我在生产环境给的常规参数是,三个队列的优先级差保持在2~4倍以内,同时给低优队列较大的fairshare份额,这样能保证它在整体排队中不被彻底遗忘:
Begin Parameters SHARES=user[user_a:20, user_b:20, user_c:15, user_d:15, others:30] End Parameters只有当低优队列的作业在队列内进行了fairshare调整后,仍然因为总资源不足而排不上,才说明集群是真的满了。这时候要做的是扩容或限流,而不是继续调高优先级。
5.3 调整fairshare参数后的验证方法
调完参数不能只看配置,必须观察一段时间的行为。我的验证方法是先用bqueues -l确认当前各个队列的调度顺序,再用bfairshare查看每个用户的公平份额状态:
bfairshare -u user_a这个命令会输出该用户的DEMAND、USED和SHARE,其中USED/SHARE的比值超过1.5就说明他正在“透支”资源,调度器会主动降低他后续作业的优先级。观察一周的数据后,再根据各业务的完成情况微调份额比例。
有一回我把某项目份额调到了30%,结果它还在继续大量提交作业,其他项目的作业排队时间暴涨。查了bfairshare才发现,那个项目用的是另一个未在SHARES里定义的用户名,它的份额被划归到others里,而others总共占30%,等于该项目事实上拿走了30%的份额。后来我把所有业务账号和项目账号的映射关系做了一次全面核对,确保配置里提到的每个名字都对应真实存在的实体。
6. 调度健康度检查:从日志里看懂LSF的实际决策
6.1 bjobs和bqueues的“信息差”:怎么判断作业是否健康排队
很多运维看bjobs -u all时只知道作业在排队(PEND),但搞不清为什么排队。其实LSF在bjobs -l里会把调度器拒排的原因写得很清楚,比如“no host selected”或“job is not eligible for execution”:
bjobs -l 123456输出里有一段Scheduling Reason,如果显示not eligible,多半是RUNLIMIT到了,或者作业的需求超出了任何主机的空闲资源;如果显示waiting for fairshare,那就是fairshare正在限制你,说明调度器认为你这个用户/项目已经“用多了”。
基于我的经验,发现PEND状态超过一小时,不要只看队列长度,先跑一遍bjobs -p。它会直接列出排队原因,比翻日志高效得多。
bjobs -pbjobs -p的输出中可以看到pending原因字段,例如NEW_JOB、RESOURCES、FAIRSHARE、EXCLUSIVE等。每种原因对应一种处理方案,处理好再谈调度调优。
6.2 事件日志与lsb.acct:掌握抢占和公平的真实轨迹
LSF的完整调度决策日志分散在几个文件里,关键是lsb.acct、lsb.events和lsb.msg。排查抢占问题最直接的是lsb.events:
ls -t $LSF_TOP/logging/*/lsb.events这个文件记录每一个作业从提交到结束的完整事件序列。搜到目标作业ID后,能看到PREEMPTED事件和SIGNAL事件,能精确到是哪台主机、哪个调度器进程发起的抢占。lsb.acct则记录作业的最终会计数据,包含CPU时间、内存峰值、结束码、退出的主机和原因。
我惯用的排查方式是双管齐下:先用bhist -l <job_id>看作业生命周期,再用grep <job_id> lsb.events找抢占事件。如果两者对不上,比如bhist显示作业被抢占,但lsb.events里没有对应记录,那通常说明日志轮转把数据截断了,或者是多集群环境里作业跨集群迁移导致的记录分裂。
6.3 监控指标:哪些数字值得每天看
调度健康度不用天天盯这些日志,太累。我在日常监控里只保留几个核心指标,阈值报警即可:
pending_time_avg:各队列平均排队时长,超过30分钟报警。preempt_count:每天抢占事件总数,超过100次说明抢占阈值设置太低。fairshare_util_ratio:used_share / target_share,超过2.0说明某个用户/项目严重透支。queue_slot_utilization:各队列槽位利用率,长期低于60%的高优队列要关注是否资源池过大。zombie_job_count:僵尸作业数量,这个值飙升时基本都在并发清理信号处理的环节出问题。
这些指标可以用bacct每天拉一次摘要,也可以自己写个小脚本定时执行bqueues和bfairshare,把输出灌进监控系统。最初我靠人工观察,后来干脆写了个20行的Shell脚本,每天早晨9点发一份调度健康邮件,里面有各队列的PEND/RUN数量和fairshare透支用户排名。这个习惯坚持下来后,很多调度恶化在萌芽阶段就被发现了,不用等用户投诉才知道集群有问题。
7. 最后再分享一个关于“文档化”的建议
调完这套抢占与公平体系后,最容易被忽略的是没人把规则写明白。业务方不知道批量作业会被抢占,也不知道生产队列有QMAX限制,他们只会在任务失败后不断提工单。我后来养成的习惯是每次调整队列参数后,立刻把变更记录在集群wiki里,包括修改的参数、原因、预期效果。同时给业务方出一份简短的提交规范,告诉他们:什么时候用哪个队列,什么样的作业建议加检查点,生产作业的许可证申请怎么写。这个动作看起来和“技术”无关,但实际对调度公平和集群稳定性的贡献,不亚于任何参数调优。毕竟调度器再聪明,也架不住不按规则出牌的人。