1. 别急着调参先把瓶颈层次分清1.1 任务变慢首先要回答“慢在哪一层”做分布式计算框架优化这几年我见过一个非常普遍的现象任务一慢大家第一反应就是调并行度、加内存、改超时时间。结果参数改了一圈任务还是那个鸟样甚至更慢了。分布式计算框架优化这件事难点从来不在“调参数”本身而在于你怎么定位问题。一个跑得慢的分布式任务可能慢在四个完全不同的地方调度层、计算层、数据读写层、网络传输层。不同层次的瓶颈对应的优化手段是完全不同甚至互相矛盾的。比如你盲目提高并行度如果是数据倾斜导致的慢任务数量越多长尾反而越明显如果是小文件过多导致的慢并行度提上去之后文件碎片化只会更严重。所以我现在的习惯是先回答三个问题再动手。第一任务慢是整个作业都慢还是只有某个阶段慢如果整个作业都慢多半是资源总量不够或者调度策略有问题如果只有特定阶段慢那个阶段内部一定有异常。第二慢的阶段是CPU密集、内存密集还是网络和磁盘密集这三个方向的优化动作完全不一样。CPU密集要考虑简化计算逻辑、压缩数据量内存密集要查对象大小和GC情况网络磁盘密集要重点看Shuffle读写量和数据本地性。第三任务运行过程中有多少执行单元是真正在干活的很多分布式任务出现性能问题的时候你会发现大量执行单元处于空闲等待状态只有少数几个在拼命跑。这时候你再怎么加资源都没有意义因为问题不在资源总量而在数据分布。1.2 用一条流水线理解分布式作业的性能结构我习惯把分布式计算框架比作一条多工位流水线数据是流水线上的物料每个执行任务是一个工位框架的调度器就是工厂的排产系统。物料从上游流向下游经过每个工位的处理最终产出结果。流水线有一个很直白的规律整条线的产出速度不由最快的工位决定而由最慢的工位决定。分布式计算框架里的“最慢工位”通常表现为两类。一类是的确有个别工位处理的数据量特别大这就是数据倾斜另一类是工位本身不缺活干但物料传输太慢工位一直在空等数据这类问题常见于网络拥塞、本地性差、序列化开销高的场景。在动手之前建议先打开框架自带的执行历史页面把每个阶段的耗时分布图看一遍。不管是什么框架只要是类MapReduce模型都会有类似的阶段耗时视图。你要找的是那些“尾巴”特别长的阶段大部分任务几秒钟就跑完了但某个阶段里有几个任务跑了十几分钟甚至更长。这种长尾形态是最重要的线索。看到长尾先别急着调参继续往下定位大概率能发现问题本质。2. 调度层优化并行度、动态分配与数据本地性2.1 并行度不是越大越好它有一个合理区间并行度是分布式计算框架里最常见的调整项但也是被误解最多的一个参数。很多人认为并行度开得越大任务跑得越快实际上并行度只是决定了任务被切成多少份来并行处理真正决定处理速度的是你有多少可用的CPU核心和内存。最合理的并行度区间大致是集群可用CPU核心数的2到3倍。为什么不是1倍因为分布式任务的执行阶段之间有小幅波动有的任务快有的任务慢留出一些余量可以让执行单元忙起来之后仍然有新任务可领。超过3倍之后收益就开始不明显了反而会引入额外的调度开销、序列化开销和上下文切换开销。反过来如果并行度小于核心数那么有大量CPU资源会被浪费。举个例子一个集群有32个执行单元每个执行单元2个核心那么可用核心数大约是64个并行度建议设定在128到192之间。我之前排查过一个任务并行度被人从200改成了2000结果单阶段创建了两千个小分区每个分区只装了几百条数据。执行完一看绝大多数时间都花在任务的调度和元数据管理上真正计算的时间反而没多少。把并行度调回256之后时间立刻降了一半还不止。并行度还有一个隐性作用它会影响Shuffle时生成的文件数量。并行度越高每个上游任务产生的输出分区越多下游要拉取的数据块就越碎。文件一多NameNode层面压力大下游任务拉数据的次数也多。所以调整并行度的时候一定要同时关注Shuffle读写量和文件数量别只看任务跑完的时间。2.2 动态资源分配什么时候开什么时候关动态资源分配是一个很实用的调度层优化手段。简单说就是框架根据当前任务积压情况动态地向资源管理器申请或释放执行单元。任务多的时候就多申请几个执行单元任务少了就自动回收避免资源闲着浪费、忙时又不能用的尴尬局面。什么场景适合开动态分配最典型的是混合负载的场景不同团队提交不同类型的作业作业到达时间不均匀有的凌晨跑批有的白天跑交互式查询。这种场景下固定分配资源要么浪费要么不够用动态分配能让集群整体利用率上去不少。什么场景不适合开如果你的任务是分钟级甚至秒级的短任务那动态分配的收益就很小。因为每个任务的执行时间本身就不长动态申请资源也要花时间申请过程中任务可能已经跑完了大半这反而拖慢了整体进度。还有一种情况是任务本身对稳定性要求极高比如核心链路的准实时计算这种任务更推荐固定资源池保证资源随时可用而不是和别的作业抢资源。如果决定开启动态分配有几个参数值得注意。一个是执行单元空闲回收的超时时间默认值通常偏保守适合长任务如果你跑的是半小时以内的短任务建议把回收时间调短一些让资源能更快流转给其他作业。另一个是同时运行任务的并行度上限建议保留一个合理上限防止极端情况下资源被某个任务无限制地吞掉影响其他核心作业。2.3 数据本地性调度器最容易被忽视的一条规则调度器在给任务分配执行单元的时候会尽量让计算发生在数据所在的节点上。这样任务可以直接读取本机磁盘上的数据块不用走网络。数据本地性从好到差依次有四个级别进程内本地、节点内本地、机架内本地和跨机架读取。级别每降一级网络传输的开销就上一个台阶。在实践里最容易出问题的不是本地性本身而是本地性“退化”。什么是退化本来任务应该在本节点执行但由于本节点的执行单元不够、节点负载过高、或者节点出了故障调度器等了一段时间之后发现本地执行不了就会退而求其次把任务调度到别的节点上去读远程数据。任务虽然跑起来了但代价是大量网络IO。如何判断本地性是否退化看执行历史里的本地性级别统计如果任务大量跑在“任意节点”级别而很少跑在“节点内”级别那就要注意了。常见的修复方向有几个检查节点是否频繁宕机或重启、避免执行单元数量被其他任务挤占、尽量让任务队列长度短一些。还有一个非常容易被忽略的原因数据文件放在外部存储上每次读取都要走网络这时候本地性级别天然就不高优化方向应该是把热数据预热到本地缓存或者调整存储挂载方式。3. 数据倾斜与Shuffle重分布默认分区方案往往就是瓶颈3.1 从长尾任务到倾斜判定三步完成数据倾斜是分布式计算框架里最经典也最让人头疼的问题。它的本质是数据在分区之间分布不均匀某个分区处理的数据量远远大于其他分区。判定数据倾斜我一般分三步。第一步看耗时分布。打开执行历史如果某个阶段的大部分任务都在一分钟内跑完但有少量任务跑了十分钟以上而且这类任务的耗时是正常任务的三五倍起步基本可以锁定倾斜嫌疑。第二步看每个任务读取的记录数。不用精确到条数看大体的量级就行。正常情况下各个任务读取的数据量应该在差不多的量级如果个别任务读取的数据量是其他任务的几十倍那倾斜几乎是可以确定的。第三步看GC和内存表现。倾斜任务通常会因为处理数据量过大出现频繁的垃圾回收甚至直接内存溢出。GC日志里面如果看到长暂停频繁出现而其他任务没有这个现象那基本可以实锤了。数据倾斜最常见的高发场景有两个。一个是宽表关联关联键集中在少数几个热门值上比如一个订单系统里某个大客户贡献了全站一半以上的订单量按客户ID关联时这个客户对应的分区就会被打爆。另一个是分组聚合比如按城市维度做统计一线城市的数据量可能是四五线城市的几十倍导致聚合任务严重不均。3.2 加盐和广播两种最常用的处理手段处理倾斜没有万能方案要根据操作类型选择手段。我做得最多的两组组合是聚合场景用“加盐两阶段聚合”关联场景用“小表广播”。加盐的思路其实很朴素既然某个键的数据太多那就把这个键拆散。第一阶段在键上附加一个随机后缀把原键拆成多个子键这样原本一个分区承受的压力就被分散到多个分区上第一阶段在子键层面上做局部聚合然后第二阶段去掉后缀再对原始键做一次最终聚合。这里有个细节加盐的核心是随机后缀的基数基数太小起不到拆分效果基数太大又会在第二阶段聚合时引入额外开销。比较稳妥的做法是结合集群核心数来设一般取核心数的1到2倍不要让第一阶段产生太多临时分区。还有一个限制条件需要特别留意如果业务要求精确结果而且计算里有去重、排序这类不能被两阶段拆解的操作那加盐方案就不合适了只能退而求其次把热点键单独抽出来处理其余键走正常流程。广播适用于关联场景逻辑也很直白如果一张表很小比如几MB到几十MB那就把它广播到每个执行单元上去每个任务都在本地内存里维护这张表的查找结构关联的时候直接查内存完全不需要走Shuffle。这个方案对分布式框架优化的收益非常大因为它直接从机制上消灭了关联操作里的网络传输。但广播也有边界条件。首先小表大小必须可控一般控制在几十MB以内太大的表广播一次就要占满执行单元内存。其次要确认小表的内容相对稳定如果关联键经常变化、小表频繁更新广播的成本就会升高。我在实际项目中见过有人把一张几百MB的维表强行广播结果每个执行单元多吃了两百多MB内存整个集群的GC时间直线上升反而比Shuffle Join还慢。3.3 Shuffle相关配置排序、压缩和缓冲区的取舍Shuffle是分布式计算框架中成本最高的环节之一因为它伴随大量磁盘读写和网络传输。围绕Shuffle的优化本质上就是在四个开销之间做平衡CPU压缩开销、磁盘IO开销、网络传输开销和内存缓冲开销。先说分区数。Shuffle的分区数决定了下游读取数据时的并行度也决定了生成文件的数量。一个常见的错误是Shuffle分区数设置得过大产生大量几KB甚至几百字节的小文件。这些小文件的元数据开销比数据本身还大。比较稳妥的做法是让Shuffle后每个分区的数据量控制在50MB到200MB之间。如果一群小文件铺面而来优先考虑重分区合并或者调小分区数。再说压缩。Shuffle数据写入磁盘和网络传输时默认会做压缩。压缩能减少磁盘占用和网络传输量但代价是CPU开销。如果你的集群CPU很紧张而磁盘和网络相对宽裕可以考虑关闭压缩或者换更轻量的压缩算法反过来如果磁盘带宽或网络带宽是瓶颈那就开更高压缩比的算法。这个选择没有绝对标准完全取决于你的集群到底缺什么资源。我见过一个任务开启高压缩比后网络传输量降了70%但CPU使用率涨了快一倍最终任务时间几乎没变。所以在压缩这件事上一定要实测之后再决定。还有缓冲区。Shuffle读写过程中数据会经过内存缓冲区。缓冲区太小数据就要频繁刷盘磁盘IO次数激增缓冲区太大会挤占执行单元堆内存诱发GC压力。比较合理的做法是给Shuffle缓冲区一个适中的默认值然后重点观察GC耗时和磁盘IO等待时间。看到GC耗时占比过高就调小缓冲区或增加执行单元内存看到磁盘等待时间居高不下就适当调大缓冲区。4. 序列化、缓存与算子链从传输和存储里抠时间4.1 序列化器的选择直接影响CPU和网络占用分布式计算里数据经常要在任务之间传输、在磁盘和内存之间读写。每一次传输和落盘都要经过序列化和反序列化。序列化器的好坏直接决定了两件事一是传输数据体积的大小二是CPU消耗的多少。Java内置的序列化机制虽然用起来方便但效果很差。它序列化出来的对象体积往往是紧凑二进制的3到10倍而且序列化过程本身CPU开销也很大。对分布式计算框架来说默认的Java序列化几乎算得上性能毒药。换成高性能序列化框架之后效果通常立竿见影。具体做法一般就是注册类、启用相关配置、把序列化器指定为紧凑二进制类型。有几点要注意第一类注册后如果类的字段结构发生变化旧数据可能反序列化失败所以升级类的时候要谨慎第二不要对无意义的类型启用序列化保持对接类的规模稳定第三泛型类型需要注册为对应的具体类型否则框架会回退到性能很差的Java默认实现。有人会问序列化器优化的收益能有多大我做一个实际的项目时曾把一个每天处理上亿条数据的统计任务迁移到高性能序列化结果Shuffle数据量从800GB降到了190GB任务整体时间缩短了将近30%。这个提升完全不需要动业务逻辑只是换了一下序列化方式。4.2 缓存级别不是越强越好关键看数据复用度很多框架都支持对中间计算结果做缓存把数据驻留在内存中后续阶段直接复用。缓存是双刃剑用得好少计算一次甚至少读写一轮磁盘用不好白占内存不说还会频繁触发GC把其他正常计算拖下水。我先说什么时候该缓存。最典型的是迭代式计算任务比如多次迭代直到收敛的算法中间结果在每一轮迭代里都会被反复读取这种情况下缓存收益极大。另一种场景是同一份数据集被多个下游任务复用比如一份基础统计数据被三个报表任务共同使用那这份数据缓存一次三个任务都能受益。什么时候不该缓存数据只被计算一次、后续再也不会被使用的场景缓存纯属浪费。很多新手犯的毛病是不分青红皂白把所有中间结果都标记成缓存结果缓存堆积执行单元内存被塞满框架频繁把缓存溢写到磁盘任务到最后不比原来快反而多了大量磁盘IO和GC停顿。缓存级别的选择也很有讲究。纯内存缓存访问最快但有一定丢失风险执行单元崩溃后缓存数据需要重新计算内存加磁盘备用的方式稳妥一些但读取时可能涉及到磁盘IO。我对缓存级别的建议是默认优先纯内存追求稳定性再考虑磁盘备份版本如果有多个执行单元要共享大体积缓存直接不缓存或者让它按需重复计算因为大体积缓存会占满执行单元大部分内存空间拖垮整体并发能力。4.3 算子链与执行计划裁剪尽量少制造宽依赖分布式计算框架在执行任务前会把用户写的一系列操作解析成一个计算图。这个计算图里有一条非常关键的判断标准相邻算子之间是窄依赖还是宽依赖。窄依赖指的是每个父分区只对应一个子分区这种依赖可以在同一个执行单元内部直接串联不需要移动数据。宽依赖指的是一个父分区被多个子分区引用或者子分区需要从多个父分区拉数据这种依赖会引入Shuffle也就是重新分区和网络传输。宽依赖的代价比窄依赖高出几个数量级。所以优化计算图的核心思路就是尽可能减少宽依赖同时让窄依赖算子尽量合并执行。举一个非常典型的例子。统计某张用户表里最近七天内活跃用户的总量有人会把所有需要的数据全部读出来然后做一次全量过滤再进行严格关联操作最后聚合。但更合理的顺序是先做过滤再做关联最后聚合。过滤这一步能极大减少后面节点的数据量。虽然这个顺序看起来就是代码里几个操作的前后挪动但实际执行计划的差别非常大。过滤提前Shuffle的数据量可能减少一个数量级任务耗时也会成倍下降。另一个常见优化是把小数据集的多种处理合并到一起减少中间结果的落盘次数。比如对同一个维度表做多次转换这些操作如果在同一个执行阶段内完成就能避免多轮读写。暗含的一个原则是对数据流的每一步操作都要问一句“这步操作是否让数据量变大了还是变小了”。尽量让数据量大的计算往后排让过滤、裁剪、字段精简这些缩小数据量的操作尽可能前置。5. 用监控和复盘代替直觉一次长尾任务的完整定位记录5.1 必看的监控指标清单调优如果没有数据支撑就和闭着眼睛开车差不多。做分布式计算框架优化至少要看这些指标我把它们整理成了我日常工作里必备的检查清单指标维度具体指标正常表现异常信号作业总体总运行时长、等待调度时长等待时长占比不超过总时长10%长时间处于等待资源状态阶段耗时每个阶段的耗时分布各任务耗时差距在3倍以内长尾明显个别任务远超中位数计算资源执行单元数量、CPU使用率CPU使用率整体平稳大量执行单元空闲或长时间满载内存GCGC次数、GC时长、堆内存占用GC时间占比低于5%GC时间占比高老年代频繁回收ShuffleShuffle写数据量、读数据量、文件数数据量符合预期且波动不大某个阶段读写量异常高小文件数量激增数据分布输入数据量、分区大小分布各分区数据量大致均匀少量分区数据量远超均值这套指标配合框架自带的执行历史页面来看效率非常高。执行历史页面能看到每个阶段的耗时、每条任务读取的数据量、GC时间、Shuffle大小等信息基本覆盖了上表的大部分指标。我拿到一个慢任务第一步就是打开页面截图然后按这张表的顺序逐项排查。5.2 一次长尾调优的完整排查链路有一次我负责的一套离线统计作业里有一条报表任务最近跑得越来越慢从之前的大概18分钟涨到了43分钟。我打开执行历史页面第一眼看到的是阶段列表里某个关联阶段占了总耗时的近八成。再看这个阶段的详细列表一共101个并行任务其中99个任务都在两分钟内跑完了剩下两个任务一个跑了29分钟一个跑了32分钟。这个长尾形态非常典型不是整个阶段都慢而是极少数任务拖住了整个阶段。我随后查看了这两个任务各自处理的数据量其中一个任务读取的记录数接近全阶段均值的35倍。到这里数据倾斜的判定基本成立。至于倾斜的键是哪一种我看了一眼关联条件这个阶段是按订单归属机构的编号做的关联报表里某个大机构的数据量明显远高于其他小机构。修复方案很简单两步走。第一步针对这个大机构对应的订单数据和订单明细之间不加盐直接关联第二步把第二步之后的维表数据量很小适合做广播。同时把关联阶段原本的1000个分区降到200个减少小文件数量。改动之后重新跑任务总耗时从43分钟降到了15分钟关联阶段从30多分钟缩短到不足6分钟。在这个例子里我没有改任何业务逻辑没有增加一台机器全靠定位和选区得当。这件事给我一个很深的印象分布式框架优化里很多问题本质是数据分布问题不是资源问题。5.3 三个常见的负优化案例负优化就是我说的“越调越慢”相信不少人经历过。举三个我实际遇到过的典型案例。第一个是把并行度盲目调大。有人觉得任务慢一定是并行度不够于是一步到位调成原来的三倍。结果任务被切成了几万个微小的分区每个分区的数据量只有几百条调度开销和序列化开销反而成了大头最终任务时间翻倍。负优化的原因很简单并行度不是资源只是一个切分维度维度太细了反而增加管理成本。第二个是缓存所有中间结果。有人给计算链路里每一个Dataset都加了缓存标记结果是执行单元内存被巨量缓存占满GC连续不断任务还没跑完一半就频繁触发老年代回收整个过程比不缓存还慢。负优化的原因是把缓存当成免费午餐没有考虑缓存本身也是有成本和生命周期的。第三个是统一开启压缩和高压缩比。压缩确实能减少数据传输量但如果集群CPU已经吃紧开高压缩比只会让计算速度更慢。压缩、序列化、缓存这些配置项都存在明显的资源置换关系最忌讳的就是“一把梭”地全局开启。6. 工程化收尾把优化变成可回归、可验证的过程6.1 建立基准任务集一次只改一个变量我做分布式计算框架优化到了一定阶段后最大的体会是优化本身并不难难的是让优化结果可复现、可对比、可回归。建议你在自己的环境里固化一组“基准任务集”。这组任务要能代表线上大部分作业的特征有计算密集型的有Shuffle密集型的有数据倾斜倾向的。每次调整配置之前先用这组基准任务跑一遍记录每个任务的耗时、资源消耗、Shuffle数据量。改完配置之后再跑一遍直接对比变化。这里有一个非常重要的实验纪律一次只改一个变量。并行度、序列化器、缓存级别、分区数这些参数之间常常有相互影响如果同时改两个以上出问题了都不知道是谁的问题调优变成了瞎猜。先改一个记录结果再改下一个调优才是可控的。另外要强调一点参数调优的结果并不具备普适性。同一个并行度设置在一个集群上很好用在另一个集群上可能就很差。因为执行单元的核心数、内存大小、任务特征都不一样。所以不要照抄别人的配置一定要基于自己的基准任务集得出自己的参数组合。6.2 把“数据健康”纳入日常监控最后再说一个容易被忽视的工程化习惯不仅监控任务的运行状态还要监控输入数据本身的健康度。数据健康度主要看三个维度分区大小是否均匀、文件数量是否合理、字段取值分布是否发生了剧烈变化。很多倾斜问题的根因都是上游数据分布发生了变化。比如某个关联键的取值从过去占比5%突然涨到30%你昨天还跑得好好的任务今天就慢如蜗牛。这种变化靠参数调整是没法根治的必须回溯到数据链路里找到数据分布突变的原因。所以我现在会为关键任务配套一个数据分布检查在任务启动前先对最耗时的关联键做一次取样统计。如果发现取值分布有明显长尾就提前触发倾斜优化逻辑。这个做法看起来多花了一点时间但能避免大量任务跑到一半才发现倾斜、然后再白跑一遍的情况。做分布式计算框架优化这么多年我最大的体会是通用配置永远只存在于文档里不存在于你的集群里。每次手痒想调参之前先回答三个问题慢在哪一层数据分布怎么样这次改动能否被下一次任务验证这三句话帮我少走了很多弯路也分享给你。