首页 / 资讯中心 / 文章详情

Storm资源调度实战:从默认调度器到RAS自定义策略

Storm资源调度实战:从默认调度器到RAS自定义策略 ★ FEATURED ARTICLE
1. 先把Storm的资源调度基础盘明白做实时计算这几年我见过太多小伙伴对Storm的第一印象停留在“有Spout和Bolt就算完事”集群一跑起来数据来了任务挂了才开始手忙脚乱地翻日志。其实很多时候问题不在业务逻辑而是资源调度根本没安排好。所谓资源调度说白了就是决定“每个计算任务跑到哪台机器上、吃多少内存和CPU、跟谁打架不打架”的一套规则。Storm的调度器就是这个规则的执行者它在提交拓扑Topology的时候运行一次把拓扑里面的组件分配到集群的各个Worker节点上。在聊调度策略之前得先把Storm里的几层资源单位捋清楚否则后续看任何调度器配置都像看天书。Storm集群的资源从大到小分三层节点Supervisor、Worker进程、Executor线程、Task。一个Supervisor节点上可以跑多个Worker每个Worker是一个独立的JVM进程负责执行一部分Executor。每个Executor是一个线程线程里装着一个或多个TaskTask才是真正我们写的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资源感知调度不看数量看需求RASResourceAwareScheduler是Storm在0.10版本之后重点演进的一套调度器它把“资源”真正建模成了两个维度内存Memory和CPU。在RAS的世界里每个Worker都有一个内存上限每个Executor都可以声明自己需要多少内存和CPU调度器在分配时不再是数个数而是算总账尽量让每个节点上的资源使用量不超标。这里要理解几个核心概念topology.worker.max.heap.size.mb决定每个Worker进程的最大堆内存默认是768MBtopology.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.yamlstorm.scheduler: org.apache.storm.scheduler.ResourceAwareScheduler topology.worker.max.heap.size.mb: 1024然后重启Nimbus。注意这里不能只改一个节点Nimbus可能是主备部署的如果只改主NimbusFailover之后调度器就变回默认了。我用过一次血泪教训只改了主节点结果某天主节点宕机备份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) { ListString gpuNodes new ArrayList(); for (SupervisorDetails supervisor : cluster.getSupervisors().values()) { if (supervisor.getHost().endsWith(-gpu)) { gpuNodes.add(supervisor.getId()); } } for (TopologyDetails topology : topologies.getTopologies()) { MapString, ListExecutorDetails executorsByComponent topology.getExecutorToComponent(); for (Map.EntryString, ListExecutorDetails 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的适用边界搞清楚再把监控和调参的闭环做起来就已经能解决绝大多数效率问题了。千万别一上来就追求花哨的自定义调度策略——先把集群的各项指标摸清楚比什么算法都管用。
阅读完成 · 觉得有帮助?
咨询建站