☰
Storm资源调度实战:从默认调度器到RAS自定义策略
2026/10/16 3:32:28 网站建设 项目流程

1. 先把Storm的资源调度基础盘明白

做实时计算这几年,我见过太多小伙伴对Storm的第一印象停留在“有Spout和Bolt就算完事”,集群一跑起来,数据来了,任务挂了,才开始手忙脚乱地翻日志。其实很多时候问题不在业务逻辑,而是资源调度根本没安排好。所谓资源调度,说白了就是决定“每个计算任务跑到哪台机器上、吃多少内存和CPU、跟谁打架不打架”的一套规则。Storm的调度器就是这个规则的执行者,它在提交拓扑(Topology)的时候运行一次,把拓扑里面的组件分配到集群的各个Worker节点上。

在聊调度策略之前,得先把Storm里的几层资源单位捋清楚,否则后续看任何调度器配置都像看天书。Storm集群的资源从大到小分三层:节点(Supervisor)、Worker进程、Executor线程、Task。一个Supervisor节点上可以跑多个Worker,每个Worker是一个独立的JVM进程,负责执行一部分Executor。每个Executor是一个线程,线程里装着一个或多个Task,Task才是真正我们写的Spout或Bolt实例。为了简化,大多数时候一个Executor只放一个Task,这样吞吐隔离最干净,排查问题也不会互相干扰。

默认情况下,Storm通过一个总体的资源逻辑来约束分配:每个Supervisor节点可以创建多少Worker,取决于配置的端口数量;每个Worker能放多少个Executor,取决于配置里的上限。典型的默认配置是每个Worker最多4个Executor,一个Supervisor节点总共可用的Worker数等于端口数,比如常见的4个端口就只能起4个Worker。这个模型足够应付实验环境,但生产环境一旦拓扑变多、数据量和计算复杂度上来,默认调度就会把集群搅成一锅粥。

1.1 资源单位到底谁说了算:Worker、Executor与Task的关系

很多人一开始以为在代码里设置的setBolt(...).setNumTasks(3)就是让Bolt跑3个线程,其实不完全对。Task是逻辑概念,Executor是物理执行线程;每个Executor内可以包含多个Task,即使Task数多于Executor数,也只是多个Task轮流复用线程。真正决定并行度的是Executor数量,而这个数量由setNumTasks和setNumExecutors共同决定,如果只设Task数不设Executor数,Storm默认会把Task均分到Executor里。

举个我踩过坑的例子:某个ETL拓扑,我给一个Bolt设置了setNumTasks(12),但忘了设置Executor数量,默认情况下Storm会为这个Bolt创建12个Executor,每个Executor一个Task,这当然没问题。但有一次我在代码里显式写了setNumExecutors(6),Task是12,结果发现两个Task共享一个线程,整个处理延迟反而比预期高。后来我才意识到,Task和Executor的映射关系直接影响数据分组(Grouping)的分发去向,比如Shuffle Grouping是按Executor个数而非Task个数去随机分发的,如果Task多Executor少,某些Task几乎收不到数据,白白占着内存。

从资源调度视角看,Worker是内存分配的基本单位,Executor是CPU调度的基本单位。Storm在调度时,先把拓扑所需的Executor装配成若干组,再把这些组放到Worker里,最后把Worker放到Supervisor上。调度器的每一次决策,都是在解决这个二维装箱问题:既要保证Worker内的Executor数量不超限,又要保证整个拓扑的Worker尽量均衡地分布在不同节点上。

1.2 默认调度器的分配逻辑与短板

默认调度器(DefaultScheduler)的逻辑很简单,就像按顺序发扑克牌:先处理所有拓扑,对每个拓扑依次获取它需要的Executor,然后从头到尾遍历Supervisor列表,把Executor轮询地放进Worker和节点,哪个节点当前分配得少就优先给哪个节点。这种逻辑的优势是实现简单、开销低,尤其适合只有一个拓扑或多个小拓扑的场景,能让集群快速跑起来。

但它有两个明显的短板。第一,它只关心Executor数量均衡,完全不管每个Executor实际吃的CPU和内存差异。如果一个Bolt是计算密集型的,比如做正则解析、加解密,另一个Bolt只是做简单的透传,默认调度器还是会给它们分配完全相同的资源。结果就是计算密集型的Executor成了集群的热点,另一头的Executor闲到发慌,整体吞吐被拖垮。第二,它不感知Worker容量上限。默认的Worker最多4个Executor,但如果某个Supervisor节点的内存明明很大,却因为端口数配置得少而被闲置,调度器也视而不见。我见过最夸张的一次,一个16核64G的节点只起了4个Worker,其余节点却起了8个Worker,任务延迟飙升,原因就是默认调度器只按端口数量轮询,没有按实际可用资源来决策。

所以,如果你的集群只有一台机器、两个测试拓扑,默认调度器完全够用;但如果生产环境有几十个节点、成百上千个Executor,还坚持用默认配置,那基本等同裸奔。接下来要讲的几种调度策略,都是在解决“如何不让资源空闲、不让人家打架”这件事上做文章。

2. 几种调度器怎么选:Even、Isolation、RAS,到底有什么区别

打开Storm源码,你能看到调度器的继承关系:Scheduler接口下面有DefaultScheduler、EvenScheduler、IsolationScheduler、ResourceAwareScheduler,还有一个Scheduler的抽象实现BaseScheduler。平时我们说的“调度策略”基本就是这几位的核心逻辑。它们不是互斥的,而是层层增强的关系。隔离调度器底层调用EvenScheduler来分配剩余资源,RAS在默认调度器之上加了资源和内存感知。

选择哪种调度器,得先理解每种策略的定位。默认调度器适合一切二切都随意的小集群;EvenScheduler适合要让拓扑尽量均匀分散的场合;IsolationScheduler适合大拓扑需要独占节点资源的场合;RAS则是面向生产环境的资源感知方案。下面一个个说。

2.1 EvenScheduler:尽力把每个节点都塞得差不多

EvenScheduler的核心思想非常简单粗暴:按Executor数量把拓扑尽可能均匀地分布到集群所有节点上。它和默认调度器的最大区别在于,默认调度器是先来后到逐台分配,EvenScheduler则是先把所有可用的节点和槽位列出来,然后计算每个节点已分配的Executor数,每次选择当前载荷最轻的节点进行分配。注意,是节点层面尽量均衡,而不是Worker层面。

这个策略在集群拓扑数量不多、各节点的硬件配置比较接近的时候表现很好。它能让网络流量、磁盘IO压力比较均衡地分摊到各个节点,避免“一台机器忙到冒烟,另一台机器吃了半天灰”的尴尬。但它同样不感知单个Executor的资源消耗强度,如果某个拓扑的Bolt计算量巨大,均衡分配只会让每个节点都放一个重Executor,最后所有节点的负载同时升高,谁也跑不快。

从实现上看,EvenScheduler其实就是把默认调度器中的节点选优逻辑替换成了“最小分配数优先”,代码量并不大。如果你只是想快速优化一下默认调度的随机性,把配置里的storm.scheduler改成org.apache.storm.scheduler.EvenScheduler,就能看到一些直观的改善。但请注意,这只优化了“数量均衡”,没有优化“资源均衡”。

2.2 IsolationScheduler:把资源“锁”给重活

生产环境里总会有那么一两个核心拓扑,比如用户的实时风控、交易流水统计,稍微抖动一下影响面就很大。这种拓扑不适合跟其他任务混跑,因为别的拓扑一旦出现内存泄漏或疯狂GC,就会拖累整个节点的Worker吞吐。Storm提供了隔离调度器IsolationScheduler,它可以指定某些拓扑独占整个节点上的全部Worker资源。

配置方式很直接:在storm.yaml里设置storm.scheduler为org.apache.storm.scheduler.IsolationScheduler,然后设置isolation.scheduler.machines,例如:

storm.scheduler: "org.apache.storm.scheduler.IsolationScheduler" isolation.scheduler.machines: "critical-topology": 2 "important-topology": 1

这段配置的意思是:名为critical-topology的拓扑独占2台机器,important-topology独占1台机器,其他拓扑在剩余机器上按照默认或EvenScheduler的逻辑运行。隔离调度的原理是,它会预先从所有Supervisor中挑出尚未被隔离的、端口数合适的节点,只允许被隔离的拓扑调度到这些节点上;如果一个被隔离的拓扑需要的Worker数超过指定机器数,那调度就失败,拓扑提交不上去,这其实是故意的,逼你在资源不足时提前暴露问题,而不是等到运行期才缓慢崩溃。

实际使用隔离调度器有一个非常值得注意的点:隔离不是“分配了节点就不放手”,而是“由该拓扑独占节点上的所有Worker端口”。如果被隔离的拓扑只用了一半的Worker数,剩下的一半也不会分配给其他拓扑,相当于资源被锁死。因此,隔离调度器适合确实需要物理级保障的拓扑,不适合把每个拓扑都丢进去,不然集群利用率会直线下降。

2.3 RAS资源感知调度:不看数量看需求

RAS(ResourceAwareScheduler)是Storm在0.10版本之后重点演进的一套调度器,它把“资源”真正建模成了两个维度:内存(Memory)和CPU。在RAS的世界里,每个Worker都有一个内存上限,每个Executor都可以声明自己需要多少内存和CPU,调度器在分配时不再是数个数,而是算总账,尽量让每个节点上的资源使用量不超标。

这里要理解几个核心概念:topology.worker.max.heap.size.mb决定每个Worker进程的最大堆内存,默认是768MB;topology.component.resources.onheap.memory.mb可以单独给某个组件设置堆内存需求;CPU的建模相对抽象,默认一个Executor算1个CPU单位,你可以通过topology.component.cpu.pcore.percent配置一个Executor占用单核的百分比,从而达到让调度器感知“这个Bolt比较吃CPU”的效果。

我自己的经验是,RAS配置不是越细越好,尤其初期,过度配置每个组件的内存和CPU,反而会让调度器陷入复杂的约束求解,拖慢拓扑提交速度。如果你刚开始迁移到RAS,我建议先做两件事:一是把topology.worker.max.heap.size.mb从默认值调整到与机器内存相匹配的水平,二是给那些明显吃CPU的Bolt设置topology.component.cpu.pcore.percent,比如从默认的100改成200,表示这个组件的每个Executor期望占用2个核心。其他组件保持默认,让调度器有足够的弹性空间。

下面这张表可以帮你快速对比几种调度器的核心差异:

调度器感知资源类型主要优化目标适用场景常见坑
DefaultScheduler无Executor数量轮询均衡单机测试、简单集群资源异构时负载不均
EvenScheduler无节点间Executor数量均衡同构节点、拓扑数量少不感知单Executor资源强度
IsolationScheduler节点数量核心拓扑独占机器强隔离需求资源利用率低
RAS内存与CPU总量不超限、按需分配生产多租户集群配置过细会增加求解开销

3. 实操:从配置文件到自定义调度器,一步步调优集群效率

纸上谈兵没有意义,下面我把自己在真实集群上调优Storm资源调度时用到的路径整理出来,从最简单的配置修改开始,一直延伸到需要写Java代码的自定义调度器。这里的操作基于我个人的实践总结,不同的Storm版本在类名和参数上可能有些许差异,但整体思路是通用的。

3.1 配置修改的基本姿势与验证方法

假设你现在集群跑的是Storm 1.x版本,想从默认调度器切换到RAS,第一步是修改每个Nimbus节点上的conf/storm.yaml:

storm.scheduler: "org.apache.storm.scheduler.ResourceAwareScheduler" topology.worker.max.heap.size.mb: 1024

然后重启Nimbus。注意,这里不能只改一个节点,Nimbus可能是主备部署的,如果只改主Nimbus,Failover之后调度器就变回默认了。我用过一次血泪教训,只改了主节点,结果某天主节点宕机,备份Nimbus接管后,整个集群调度策略一夜回到解放前,所有拓扑重新分配,线上任务全部抖了一遍。

改完配置之后怎么验证?不要光看日志,直接看UI。Storm UI的Scheduler页面会展示当前用的调度器类名。更直接的办法是提交一个测试拓扑,故意给某个Bolt设置很大的内存,比如topology.component.resources.onheap.memory.mb设为1024,然后观察这个拓扑被分配到的Worker节点有没有超过节点的实际总内存。如果调度器正常,它会倾向于把该组件放到内存充足的节点上;如果发现放到一个总共只有1G内存的节点上,那说明配置没生效,或者RAS版本对参数名的解析有差异。

RAS还有一个容易忽略的坑:它默认是“内存优先,CPU尽力而为”。也就是说,即使你给某个组件配置了CPU占用200%,调度器在节点CPU不足时也只会发出告警,而不会强制拒绝分配。这在很多场景下会导致拓扑运行一段时候后CPU超卖,所有Executor一起变慢。如果你需要严格限制CPU,需要同时设置topology.ras.oldscheduler或者更细粒度的CPU内存约束,具体要看版本支持。我个人的习惯是,不追求CPU强约束,而是用节点监控配合告警来兜底,因为CPU是时分复用的资源,短暂的超卖往往比内存不足带来的OOM更可控。

3.2 自定义调度器:最灵活的“因地制宜”方案

有些场景下,内置的调度器都不够贴合业务。比如你的集群里有GPU节点,某些拓扑需要把Executor调度到GPU节点上;再比如你有特殊的合规要求,某些数据必须在国内的节点上处理;又或者你的拓扑之间存在依赖关系,A拓扑的Bolt希望尽量和B拓扑的Spout放在同一台机器上,减少网络传输。这些需求内置调度器办不到,只能自己写。

自定义调度器的入口是IScheduler接口,核心方法是schedule(Topologies topologies, Cluster cluster)。Topologies包含当前所有拓扑的详细信息和资源需求;Cluster包含节点状态和已经分配的结果。你在这个方法里写清自己的分配规则,最后把分配结果通过cluster.assign写回去。

下面是一个极简的示例逻辑,目的是让名为gpu-bolt的组件只调度到名字里带gpu的Supervisor节点上:

public class GpuAwareScheduler implements IScheduler { public void schedule(Topologies topologies, Cluster cluster) { List<String> gpuNodes = new ArrayList<>(); for (SupervisorDetails supervisor : cluster.getSupervisors().values()) { if (supervisor.getHost().endsWith("-gpu")) { gpuNodes.add(supervisor.getId()); } } for (TopologyDetails topology : topologies.getTopologies()) { Map<String, List<ExecutorDetails>> executorsByComponent = topology.getExecutorToComponent(); for (Map.Entry<String, List<ExecutorDetails>> entry : executorsByComponent.entrySet()) { String componentName = entry.getKey(); if ("gpu-bolt".equals(componentName)) { for (ExecutorDetails executor : entry.getValue()) { // 从gpuNodes中挑选一个未满的Supervisor,调用cluster.assign } } } } } }

写自定义调度器时,最需要注意的是别剥离开了Storm内置的“防腐层”:cluster.assign的语义是把Executor分配给某个Supervisor的某个端口(即Worker),而一个Worker里可以放多个Executor。如果你只指定Supervisor而不指定端口,调度器可能把多个Executor分配到同一台机器的不同Worker上,如果你没有控制Worker的总数,很容易出现端口耗尽。另外,自定义调度器是在Nimbus进程内执行的,如果调度逻辑里有复杂循环、远程调用、数据库请求,会直接阻塞Nimbus的主线程,拖慢所有拓扑的提交与重新调度,这在生产上是大忌。

所以我的建议是:自定义调度器一定要保持轻量、无状态、可重入。如果确实需要读取外部数据,尽量在调度前把数据预热到内存里,而不是在schedule方法里实时请求。我在一个内部监控调度器项目上,最初在调度逻辑里做了实时查询Redis的操作,结果每次拓扑提交都要等好几秒,后来改成把Redis数据定期同步到本地缓存,问题就消失了。

3.3 基于RAS的常用参数调整速查

很多团队不会一步到位用自定义调度器,而是先把RAS的资源配置用好。下面是我验证过的一组常用参数组合,适合一个中等规模、节点内存不一的集群:

storm.scheduler: "org.apache.storm.scheduler.ResourceAwareScheduler" topology.worker.max.heap.size.mb: 1536 topology.component.resources.onheap.memory.mb: 256 topology.component.cpu.pcore.percent: 50 topology.ras.worker.slot.count: 4

第一行指定RAS;第二行把每个Worker进程的最大堆内存调到1.5G,配合节点可能更好的内存利用率;第三行表示如果没有特殊指定,每个Executor默认分配256MB堆内存;第四行表示每个Executor默认使用半个核心的CPU;第五行是每个节点逻辑上允许分配的Worker slot数量上限。这里topology.ras.worker.slot.count是个比较关键的参数,它和物理端口数不同,是RAS在逻辑上限制“每个节点最多能放下几个Worker”的约束。如果你觉得某些节点的网络带宽有限,想让拓扑的Worker尽量分散,可以把该值设小一点;如果机器内存很大,想让Worker紧凑一些,可以调大,但要注意堆内存叠加后超出物理内存的风险。

在实际调参过程中,我发现最有效的切入路径是先看Storm UI的Topology页面上每个Worker的占用率。刚开始往往能看到某些Worker的堆内存几乎打满,而另一些只有30%。这时候不要急着去改topology.component.resources.onheap.memory.mb,而是先找到占用最高的那个组件,问自己一个问题:这个组件的并行度是不是太小了?因为很多时候不是单个Executor吃太多内存,而是setNumTasks设得太少,导致数据全堆积在同一批Executor上。先加并行度,再调内存约束,最后再调整调度器配置,这样的顺序能少走很多弯路。

4. 真实集群里那些坑:资源碎片、超发、多租户打架

调度器选得再先进,具体到集群运行层面,还是会遇到一些“看起来是调度策略问题,其实是使用方式问题”的经典陷阱。这一章我整理了三个高频场景,基本都是我在维护实时集群过程中反复踩过的,相信不少同行也遇到过。

4.1 资源碎片和slot分配的尴尬

资源碎片这个问题,用默认调度器尤其明显。假设集群有三台节点,每台节点配置了4个Worker端口,提交第一个拓扑需要6个Worker,提交第二个拓扑需要6个Worker,一切很完美,第一轮分配刚好填满两台的8个Worker,剩下4个。如果你这时候提交第三个只需要2个Worker的拓扑,理论上集群还能接,但因为前两个拓扑的Worker分配方式不同,第三拓扑可能需要再开新的Worker,而有可能没有节点剩下足够的端口——这就是碎片。

RAS不直接解决碎片问题,它只保证每个Worker不超过内存上限。要真正减少碎片,得从拓扑本身入手:尽量让同一个拓扑的Worker数量与节点上的可用端口数匹配,比如每台节点4个端口,拓扑需要8个Worker,那就让它占满两台节点,而不是在四台上各占2个。这个目标可以通过自定义调度器里的“打包分配”策略实现,让调度器把属于同一个拓扑的Executor尽量填满整个节点再换下一个节点,而不是零零散散铺开。

碎片问题还有一个隐蔽来源:Worker重启。拓扑运行过程中如果某个Executor崩溃,nimbus会尝试在同一个Supervisor或另一个Supervisor上重新调度。如果原节点上资源已经被释放,但Slot没有完全清理,新Executor可能只能被分配到其他有零散Slot的节点,导致碎片越来越严重。我一般的做法是,定期在流量低峰期重启一下Nimbus,让所有拓扑重新调度一次,把碎片“压实”。不过这在严格7x24的团队里不一定可行,所以更推荐在拓扑版本更新时,主动将拓扑的topology.message.timeout.secs调大一点,给重新调度留出足够时间,减少调度过程中的二次失败。

4.2 超发与GC预期

很多大数据平台喜欢做超发,比如把节点内存规划为“Worker请求内存之和大于物理内存”。这在批量计算里也许可以接受,因为批任务有波峰波谷,不会全部满载;但Storm是常驻流计算,Worker进程一旦启动就一直在那里,内存占用长期稳定在较高水位。如果超发比例过高,被调度到同一节点上的多个Worker就有可能同时触发Full GC,节点整体卡顿,拓扑延迟飙升。

我在一个集群里见过一个典型的超发现象:节点物理内存64G,配置了8个Worker,每个Worker最大堆内存8G,看起来刚好64G,但JVM本身除了堆还有元空间、线程栈、直接内存,加上每个Worker里还要跑日志缓存,实际进程占用的RSS往往接近9G。八个Worker一放上去,还没开始处理数据,机器内存就已经90%以上了,结果一有流量突刺,直接OOM触发进程被杀,调度器又立刻重新调度,进入“分配-崩溃-再分配”的恶性循环。

要解决这个问题,第一步是把topology.worker.max.heap.size.mb往下调,留出JVM外部开销的余量;第二步是给节点上的Supervisor设置合适的worker.childopts,比如加上-XX:MaxDirectMemorySize和-XX:MaxMetaspaceSize,防止堆外内存无限增长。我个人的建议是,每个Worker的堆内存不要超过节点物理内存除以Worker数的80%,比如64G节点跑8个Worker,每个Worker堆内存控制在6G左右,留出的16G空间给系统缓存和JVM辅助开销。这个策略在实际运行中很稳,虽然单个Worker能处理的数据量稍微变小,但整体集群的稳定性显著提升。

GC的另一个被忽视点是:不同拓扑有不同的GC特性。有的拓扑内部有大量大对象,比如累积一批数据做聚合再输出,需要大堆;有的拓扑是纯转发,对象都是短命的,小堆就够。如果你把所有Worker都配置成统一大小,要么浪费内存,要么频繁GC。RAS支持按组件单独设置内存,但Worker级别的堆内存整体上限还是全局的。所以如果你的集群里同时有大内存拓扑和小内存拓扑,我更建议按拓扑分集群,或者用自定义调度器把大内存拓扑的Worker单独分配到内存更大的节点上。

4.3 多租户与隔离取舍

多租户环境是Storm资源调度最头疼的场景。多个团队共享一个集群,大家都想把任务跑得快一点,资源协调不当就会互相影响。IsolationScheduler虽然能强力隔离,但前面说了,它会让资源利用率直线下降,一个团队一个拓扑就要独占一整台机器,在机器数量有限时根本不够分。

另一种常见做法是给不同租户设置不同的topology.max.task.parallelism上限,再配合Nimbus上的用户权限来控制提交。但这只限制并发,不限制资源。两个拓扑可以在同一节点上各自跑得很欢,但一旦出现流量高峰期,磁盘、网络、CPU争抢在所难免。这时候RAS的资源约束能起到一定作用:你可以限制每个租户的拓扑占用的总内存不超过节点容量的某个百分比,虽然调度器不会做“抢占式回收”,但至少在新拓扑提交时能拒绝超限请求,而不是等到运行期才爆发冲突。

我个人的体验是,多租户集群里,调度器只是最后一道防线,真正的关键在业务侧的资源规划。每个租户提交拓扑时,应该填写一份“资源申请表”,写明预估峰值吞吐、每个Bolt的CPU和内存需求、允许的延迟上限。调度器根据这些信息做分配,避免一个租户用“最大信用额度”申请了资源但实际使用率很低,另一个租户却因为资源不够而排队。为了让资源配置不虚高,我在团队里推了一个“资源使用率周报”,定期统计每个拓扑的Executor实际CPU占用和内存占用,与配置值对比,凡是配置值超过实际使用率两倍的拓扑,都要求在下次发布时调低,否则新拓扑不优先调度。

5. 从资源调度看整集群效率:监控、容量预估与持续调优

说到最后,资源调度不是一个“配一次就一劳永逸”的事情,它本质上是集群效率的优化问题。调度策略选得好不好,最终要靠监控数据来说话。这一章我分享几个我对“集群效率”这件事本身的落地经验,包括看什么指标、怎么预估容量,以及日常调优的动作节奏。

5.1 集群效率要盯的四个核心指标

很多人盯Storm UI只看两个值:Complete latency和Capacity。这两个确实重要,但不够。要把调度问题与集群效率挂钩,我建议至少额外盯四个指标:

第一个是Cluster页面下的Slots Used和Slots Total之比。这个比值是集群资源利用率的“朴素近似”,但它有迷惑性,因为Slot总数是固定的,而每个Slot的实际负载差异很大。一个Slot占用的Executor即使只有一个,也会显示为“已使用”,利用率统计就会失真。所以我更习惯用每个节点的Worker Heap Used和Physical Memory Usage来综合判断。

第二个是Topology页面里每个Executor的Capacity,它表示该Executor在当前窗口内的忙碌比例。以我的经验,Capacity长期高于0.9,说明任务在排队,需要加并行度或拆分Bolt;长期低于0.2,说明资源浪费,可以合并Executor。但要注意Capacity的窗口粒度,默认是15分钟,如果流量有明显波峰波谷,最好把窗口调小到1分钟再看峰值情况。

第三个是nimbus日志中的调度耗时。虽然调度是异步触发的,但调度耗时过长会影响拓扑更新速度。正常情况一个小集群的调度耗时应该在几十毫秒到几百毫秒之间,如果经常看到秒级甚至十秒级,说明拓扑和Executor数量太大,调度器的遍历开销过高,或者自定义调度器里有了重逻辑。

第四个是Worker级别的GC时间和GC频率。通过jstat -gcutil可以拿到每个Worker的GC数据。如果一个Worker的FGC次数每分钟超过一次,且Full GC时间占比超过10%,哪怕Capacity不高,也要警惕:这不是调度的问题,而是组件本身的对象分配出了问题,调度器给再多资源也救不回来。遇到这种情况,先优化代码,再谈扩资源。

5.2 容量预估与压测的正确姿势

新集群上线或者新拓扑接入前,做一轮容量预估是非常值得的投入。我常用的方法并不复杂:先跑一个最小数据量的拓扑,把数据源头切到测试Topic,流量控制在实际预估峰值的20%,观察各个Executor的Capacity、内存曲线和节点系统负载。然后逐步把流量往上加到50%、80%、100%,每个档位稳定跑十分钟,记录各项指标。这样基本能得出“这个拓扑在每个节点能扛多少吞吐量”的参照值。

容量预估的关键在于“瓶颈定位”。有时候你会发现一个拓扑的流量加到某个值后,某个Bolt的Capacity瞬间涨到1.2,其他Bolt只有0.4,说明瓶颈在那个Bolt,增加整体Worker数没有用,需要单独给这个Bolt增加Executor数,或者把它的计算逻辑拆分到两个Bolt里。这种瓶颈如果不在压测时暴露,上了生产再发现,往往只能临时加机器。

调度器在容量预估里的作用,是为瓶颈Bolt预留“升级通道”。我在压测时通常会预计两倍的峰值吞吐,然后按这个目标配置拓扑资源。调度器在节点选择上会尽量保证这个拓扑的Worker不和其他拓扑挤在同一台机器上,这样即使后续流量超出预期,节点上也有一定的空闲CPU和内存可以兜底,而不是一超限就开始互相拖累。

5.3 持续调优的几个小习惯

写到这里,我不打算再罗列一堆配置参数了,反而是几个“软”习惯更能决定调度策略的成败。

第一,任何调度配置变更都要先在测试环境复现一遍。Storm的调度器跑在Nimbus里,而Nimbus的配置和UI展示之间有时候会出现缓存不一致,尤其在长时间运行的集群上,改完配置后直接线上变更,非常容易出错。我自己习惯先把新配置打包到测试集群,提交三五个拓扑跑一天,观察调度结果是否符合预期,再上生产。

第二,拓扑升级前先看旧的调度分布。每次发新版拓扑时,我习惯在UI上截一张当前的Worker分布图和Executor分布图,等新版跑起来再截一张,对比一下哪些Worker被重新调度了。如果发现某些Executor从原来物理上靠近Source节点的机器,跑到了距离较远的机器上,而你的拓扑里没有必须靠近的需求就没问题;但如果有,就得检查是不是自定义调度器的排序逻辑变了。

第三,别让调度器做的事情太多。这句话听起来玄,但很实在。有的团队在自定义调度器里塞进了负载均衡、故障预测、流数据亲和性等一堆需求,最后调度逻辑复杂到没人敢动,反而成了整个集群的“定时炸弹”。调度器不是万能的,它只是在每个调度时机点上做一个全局最优近似。合理的设计是,把确定性强的硬约束(内存不超限、CPU总量不超限、节点标签匹配)交给调度器,把软性优化(热点规避、亲和性)留到拓扑本身的并行度设计和网络拓扑规划里去解决。

我自己在维护一个几十个节点的实时集群时,调度器主体用的是RAS,但我在其之上只加了一个“节点标签匹配”逻辑,其余全部保持内置行为。这样既不会让调度器变成性能瓶颈,又能满足业务侧对数据本地性的要求。以后如果业务再复杂,我会优先考虑增加节点分组、分集群,而不是在一个集群里把所有需求都揉进调度器。

关于Storm资源调度,能聊的细节还有很多,比如资源动态更新、拓扑热替换时的调度时机、主备Nimbus同步等。但我觉得,对一个刚从“能跑”走向“跑得稳”的团队来说,先把基础调度模型吃透,把默认、Even、Isolation、RAS的适用边界搞清楚,再把监控和调参的闭环做起来,就已经能解决绝大多数效率问题了。千万别一上来就追求花哨的自定义调度策略——先把集群的各项指标摸清楚,比什么算法都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询