数据倾斜这个话题做分布式计算的人迟早都要正面撞上。我最早被它坑是在跑一个日志ETL任务的时候明明前一天跑20分钟某天突然卡了两个小时都结束不了集群上其他任务全被它拖死。打开监控一看几百个Executor里有一个跑到100% CPU其他全在空转等它。那种感觉就是你明明用的是并行计算结果却硬生生跑成了单线程。后来这类问题见得多了自己也在生产环境里排查、修复过不少慢慢摸清了数据倾斜的套路。这篇就用实际经验把数据倾斜这件事讲透包括它为什么会产生、在Spark和Hive里怎么定位、有哪些经过验证的解决手段以及怎么在架构层面提前规避。不管你是刚接触分布式计算的新手还是已经被长尾任务折磨过的老手这篇文章都能给你一些可以直接拿去用的思路。1. 什么样的现象才算数据倾斜先别急着把锅甩给数据量大很多刚接触分布式计算的人容易把数据倾斜和数据量大混为一谈。数据量大是整体规模的问题而数据倾斜是分布不均的问题。举个例子你处理1亿条数据如果每个任务都分到差不多的量哪怕总耗时很长那也是正常的。但如果1亿条数据里有一条特殊的Key占了5000万条那么无论集群有多大处理这个Key的任务永远只能有一个在某些聚合场景下其他任务再快也得等它。判断是不是数据倾斜最直观的信号是看任务的耗时分布。正常情况下一个Stage里的所有任务耗时应该大致接近呈一个比较集中的分布。倾斜的时候你会看到绝大多数任务几十秒就结束了唯独一两个任务卡在那里跑一两个小时。这种“长尾效应”就是最典型的倾斜信号。再往下看还可以观察任务处理的数据量。在Spark的Web UI里进入某个Stage后能看到每个Task处理的记录数和Shuffle Read大小。如果某个Task读取了上GB的数据而其他Task只有几十MB那基本可以确定倾斜就出在它身上。还有一个容易被忽略的信号是频繁的GC和磁盘溢写。当某个Task需要处理远超预期的数据量时它的内存很快就会不够用表现为日志里反复出现Full GC以及数据不断溢写到磁盘。这个现象和倾斜经常是一起出现的而且往往意味着问题已经很严重了。所以我的第一个建议是遇到任务卡顿先不要急着调大Executor内存或增加并行度先看看各个Task之间的数据量和耗时分布。倾斜问题不解决加多少资源都只是把问题往后拖。2. 数据倾斜的根源在Shuffle机制为什么某些Key会卡住整个任务要彻底理解数据倾斜就得从分布式计算的Shuffle机制说起。无论是MapReduce、Spark还是Flink在做分组、聚合、Join这类操作时都需要把相同Key的数据汇聚到同一个节点上处理。这个“汇聚”的过程就是Shuffle。Shuffle的基本逻辑是上游的每个任务处理完自己的数据后会根据Key的哈希值决定这些数据该发给下游的哪个任务。下游任务再去拉取属于自己的那部分数据。换句话说下游每个任务负责处理的是“落入自己这个分区的所有Key”。这里的问题在于哈希分区的目标是让数据均匀分布但它有个前提——Key本身的分布是相对均匀的。如果数据里某个Key的数量极其庞大那不管哈希函数怎么设计它都会完整地落到同一个分区里该分区的下游任务就得独自扛下所有压力。举一个生活化的例子假设你开了一个快递中转站有100个分拣员每个分拣员负责一个片区。正常情况下包裹按收件地址均匀分到各个片区大家都能按时完工。但如果有一天某个商场的促销活动产生了海量包裹收件地址全都是这个商场那这100个分拣员里只有负责这个片区的那个兄弟会累死其他人都在喝茶等下班。在真实的业务场景里容易产生倾斜的Key通常都有比较明显的特点极热门的实体比如电商里的某个爆款商品、短视频里的某个头部创作者。空值或默认值比如某个字段大量为NULL或者某个默认值如“unknown”“0”等在数据里占据了很大比例。时间或地区的高度集中比如某一天的日志数据里某个时段的流量占比异常高或者某个城市的业务量远超其他城市。业务规则导致的Key重复比如订单表里的“平台单”共用同一个Key导致单Key数据量膨胀。这些情况在真实数据里非常常见。所以问题的关键不是抱怨数据为什么这么偏而是要想办法在计算过程中把这些“不均匀”消化掉。这里顺便说一句数据倾斜的严重程度和计算引擎的架构也有关系。在MapReduce模型里Map端会先做一次本地合并Combiner能在一定程度上减少倾斜的影响。Spark的Shuffle机制更灵活但Spark SQL中部分算子依然需要在Reduce端做全量聚合所以倾斜依然是绕不开的坎。Flink虽然采用流式处理但涉及到KeyBy的窗口计算时同样会面临单Key成为瓶颈的问题。可以说只要你做的是分布式计算数据倾斜就是跨引擎的“通病”。3. 生产环境如何精准定位数据倾斜从日志到Web UI的一整套排查方法数据倾斜的解决方案千千万但前提是你能准确判断“到底是不是倾斜、倾斜发生在哪个Stage、绑架任务的到底是哪个Key”。这一步做不好后面所有的优化方案都是瞎猜。3.1 先看执行计划确定倾斜发生在哪个Stage拿到一个跑得慢的SQL或DataFrame任务我通常第一步是看物理执行计划。在Spark里用df.explain(true)在Hive里用EXPLAIN都能看到任务被拆分成了哪些Stage每个Stage里做了什么操作。有一个经验倾斜通常发生在需要Shuffle的环节也就是Exchange节点Spark里叫ExchangeHive里叫Reduce。常见的触发点就三个——group by、distinct、join。你在执行计划里找到这些算子所在的Stage重点排查对应的任务耗时。3.2 用Spark Web UI看任务维度的数据分布如果你用的是SparkWeb UI是最直观的定位工具。点进一个慢的Stage关注三列Duration单个Task的耗时倾斜时会有明显的断崖式差异。Shuffle Read Size / Records单个Task拉取的数据量倾斜时某个Task会异常突出。GC Time倾斜Task的GC时间通常也会异常高。我记得有一次排查一个慢任务看到某个Task的Shuffle Read Size是6GB而其他Task平均只有200MB差距接近30倍。这种情况下不用看第二眼直接确定是数据倾斜。3.3 找出那个“作恶”的Key确认了倾斜下一个问题是到底哪个Key的量这么大这里有一个很实用的方法就是在倾斜的Stage之前做一次分级统计。举个例子在Spark里可以这样快速定位Top Key// 对疑似倾斜字段做分组统计取Top 10 df.groupBy(city) .count() .orderBy(desc(count)) .limit(10) .show()如果你用的是Hive SQL意思一样SELECT city, COUNT(*) AS cnt FROM dwd_order_detail GROUP BY city ORDER BY cnt DESC LIMIT 10;这一步能让你清楚地看到每个Key的量级差异。我遇到过的情况里Top1的Key比Top2高出两三个数量级是常有的事。确定了具体的Key后面的解决手段就能有的放矢。还有一个小技巧如果倾斜的Key全是NULL或者默认值可以在查询里把这些值单独标记出来看一下占比。很多时候你会发现一个业务上的“脏数据”就占了全表的80%这种处理起来反而简单——直接过滤就行。3.4 日志层面的辅助判断除了Web UI日志也能提供不少线索。当某个Executor频繁出现Full GC、spill到磁盘的记录或者报内存溢出OOM错误时通常意味着这个节点接收了远超预期的数据。把这些日志和Web UI里Task维度的数据量分布放在一起看基本就能把情况定死。4. 数据倾斜的经典解法从SQL改写到自定义分区器的六种实战方案定位问题只是第一关真正要花心思的是选择什么样的方案去解决问题。这里要说明一个基本原则不同的倾斜场景最优解是完全不同的。不要一上来就搞最高级的花活先看看能不能用最简单的方式解决。4.1 方案一先过滤无效数据如果你定位出的倾斜Key是NULL、空字符串或者业务上已经不使用的默认值最简单有效的方式就是直接把这类数据过滤掉。比如日志表里大量设备ID为“unknown”的记录在做聚合之前可以先筛掉SELECT device_type, COUNT(*) FROM dwd_app_log WHERE device_id IS NOT NULL AND device_id ! unknown GROUP BY device_type;这种方法成本极低效果立竿见影。但要注意过滤之前先确认这些数据对业务指标有没有影响。如果这些“脏数据”本身也是业务关心的对象那就不能简单删除得想办法单独处理。4.2 方案二Map端的预聚合对于group by和distinct这类聚合操作一个非常实用且无侵入性的方法是开启Map端预聚合。在Spark里这个能力是默认开启的对应的参数是spark.sql.aggregate.mapPartitionsOutputToAnotherTable不是这个Map端聚合在Spark SQL中默认开启参数是spark.sql.aggregate.mapGroupByKey不对这里准确说Spark DataFrame的groupBy在Map端pre-aggregate默认是开启的用户一般不需要管。准确地说Spark SQL在做group by时Map端会先做一次Partial Aggregation然后把部分聚合结果发到Reduce端做最终聚合。Pred聚合能大幅减少Shuffle的数据量。你可以通过执行计划确认如果计划里有Partial和Final两层的聚合节点就说明Map端预聚合已经在生效了。Map端预聚合对大多数常规聚合任务都有明显效果但它对“单Key数据量极大”的情况依然无能为力。因为预聚合只是把一个Key的多条数据提前算成一条但最终那个极热Key还是得全部落到同一个Reduce Task里做最终聚合。这种情况下就需要更主动的方案——加盐拆Key。4.3 方案三加盐拆Key用随机前缀打散热点加盐Salting是最经典、也最通用的倾斜解决方案。核心思路很朴素既然某个Key太大一个Task处理不动那就给它拆成多个Key分散到多个Task里并行处理最后再把拆开的结果合并。具体做法分两步第一步在聚合之前给热点Key加上一个随机前缀比如0到N之间的随机数让原本只能进同一个Task的数据发散到N个Task里。第二步按加了前缀的新Key做一次聚合然后再把前缀去掉做二次聚合得到最终结果。用Spark Scala代码写一个示例import org.apache.spark.sql.functions._ val salted df .withColumn(salt, when(col(city).equalTo(上海), (rand() * 100).cast(int) // 对热点Key加0~99的随机前缀 ).otherwise(0) ) .withColumn(salted_key, concat(col(city), lit(_), col(salt))) val result salted .groupBy(salted_key) .agg(sum(amount).as(partial_sum)) .groupBy(salted_key) .agg( ... ) // 这个写法不够严谨用下面这个上面的写法在第二层聚合时还带着盐这是不对的。更严谨的做法是加盐只用于第一层打散第二层聚合时要还原出真正的Keyimport org.apache.spark.sql.functions._ val salted df .withColumn(salt, when(col(city).equalTo(上海), (rand() * 100).cast(int)) .otherwise(0) ) .withColumn(salted_key, concat(col(city), lit(_), col(salt))) // 第一层按加盐后的Key做局部聚合 val partial salted .groupBy(salted_key) .agg(sum(amount).as(partial_amount)) // 第二层还原真实Key做全局聚合 val result partial .withColumn(real_key, split(col(salted_key), _).getItem(0)) .groupBy(real_key) .agg(sum(partial_amount).as(total_amount))这段逻辑有几个关键细节盐的粒度N怎么定我一般取并行度的2到3倍。如果该Stage的并行度是200那盐值范围就取0~399或者0~599。盐值太小打散效果不明显盐值太大会让第一层的Task数量暴涨反而增加调度开销。只对热点Key加盐如果对所有Key都加盐一是会让非热点Key白白多出一轮聚合二是聚合结果还要按真实Key再合并一次性能反而变差。Salt和Key之间要选一个不会冲突的分隔符如果Key本身就是字符串且可能包含下划线用下划线做分隔符可能拆错。我比较常用的是|或者\u0001这类更冷门的字符。在Hive SQL里做加盐处理逻辑类似SELECT real_key, SUM(partial_amount) AS total_amount FROM ( SELECT SPLIT(salted_key, \\|)[0] AS real_key, SUM(amount) AS partial_amount FROM ( SELECT CONCAT(IF(city 上海, CONCAT(上海|, FLOOR(RAND() * 100)), city)) AS salted_key, amount FROM dwd_order_detail ) t1 GROUP BY salted_key ) t2 GROUP BY real_key;加盐方案确实能解决单Key过大的问题但它也有代价多了一轮Shuffle和聚合整体处理时间会增加。所以它更适合“热点特别明显但确实业务上必须统计”的场景。我自己的判断标准是热点Key占比超过20%且总量在千万级以上加盐的收益才能明显体现出来。4.4 方案四热点Key分拆Join如果说group by加盐是很好理解的那么Join场景的数据倾斜就要麻烦一些。因为Join不只是聚合还涉及两个表之间的关联处理起来更讲究。Join倾斜最典型的情况是大表A的某个Key在小表B里命中好几条记录或者反过来小表里某个Key对应了大表的巨量数据导致Shuffle时分配给某个Task的匹配量远超其他Task。处理Join倾斜有几种常见的思路思路a广播小表如果Join的其中一方数据量很小比如几MB到几十MB的量级直接把它广播到所有Executor完全跳过Shuffle阶段在Map端完成匹配。Spark中可以用Hint来强制广播val result df_large.join(broadcast(df_small), Seq(city), left)在Hive中对应的是/* MAPJOIN(b) */SELECT /* MAPJOIN(b) */ a.id, b.name FROM table_large a LEFT JOIN table_small b ON a.city b.city;广播的方式简单粗暴直接把Shuffle环节删掉了也就没有数据倾斜的生存空间。它的限制是小表能不能被放进广播阈值默认的spark.sql.autoBroadcastJoinThreshold是10MB可以根据实际情况调整但不要调得太大否则Driver端打包广播变量的内存压力会很大。思路b热点Key拆出来单独算如果两个表都不小没法广播那就要把热点Key和非热点Key分开处理。逻辑上是这样把大表A里的热点Key分别拎出来加盐拆散。把表B里和热点Key对应的数据也拎出来做数据膨胀。膨胀的含义是按盐值的范围把每条数据复制多份分别和不同的盐前缀组合这样热点数据才能和大表A里加盐后的数据一一匹配上。热点部分用加盐后的Key去Join。非热点部分正常Join。最后把两个结果Union起来。用代码表示大概是这个意思val hotKeys Seq(上海) val saltRange 100 // 大表热点数据加盐 val hotA df.filter(col(city).isin(hotKeys:_*)) .withColumn(salt, (rand() * saltRange).cast(int)) .withColumn(salted_key, concat(col(city), lit(|), col(salt))) // 维表热点数据膨胀 val hotB df_dim.filter(col(city).isin(hotKeys:_*)) .crossJoin(seq(0 until saltRange).toDF(salt)) .withColumn(salted_key, concat(col(city), lit(|), col(salt))) // 非热点正常Join val normalA df.filter(not(col(city).isin(hotKeys:_*))) val normalResult normalA.join(df_dim, Seq(city), left) // 热点Join Union val hotResult hotA.join(hotB.drop(city), Seq(salted_key), left) .select(id, name) val result normalResult.union(hotResult)这套方案可以精确解决Join场景下的热点问题但实现复杂度明显更高而且对小表的膨胀会带来额外内存开销。实际生产中我只在“热点Key确实无法避免、广播又不现实”的情况下才用这招。4.5 方案五调整并行度参数有些场景下数据倾斜的程度并不算很严重单纯是并行度设置不合理导致单任务压力偏大。这时候调参数是最省事的办法。Spark里最常调的是spark.sql.shuffle.partitionsSpark SQL中Shuffle的默认分区数默认是200。如果你的数据量大或者倾斜键本身不是特别极端可以适当上调到400、600甚至更高让数据分散到更多任务上。spark.default.parallelismRDD算子的默认并行度建议设置为Executor核数的2到3倍。这背后的原理是分区数变多了每个Task处理的数据量自然就下降了。但请记住这个方案不能根治倾斜——如果某个Key的数据量本身就大到离谱无论分区数怎么调它该进哪个分区还是进哪个分区最后瓶颈还是卡在同一点。4.6 方案六自定义分区器如果需要更精细的控制可以自己写分区器。MapReduce时代很流行这个做法。思路是先统计各Key的数据分布然后在分区时单独把热点Key分到独立的Reducer上其余Key均匀分到其他Reducer。在Spark的RDD层面可以自定义Partitionerclass SkewAwarePartitioner( skewedKeys: Set[String], numPartitions: Int ) extends Partitioner { override def numPartitions: Int numPartitions override def getPartition(key: Any): Int { val k key.asInstanceOf[String] if (skewedKeys.contains(k)) { // 给热点Key分配独立分区 math.abs(k.hashCode) % numPartitions } else { math.abs(k.hashCode) % numPartitions } } }这个写法的关键是你需要提前知道哪些Key是热点并且要保证热点Key的分区数足够。另一种思路是在创建RDD时手写一个抽样逻辑统计每个Key的估算大小然后按“数据量均衡”而不是“Key数量均分”的原则来分配分区。这个方向实现成本比较高一般只有在自研引擎或极特殊场景下才会用到。4.7 方案对比速查表说了这么多方案整理一个对比表方便你根据实际场景快速选择场景推荐方案实现成本风险与代价倾斜Key为NULL或脏数据直接过滤极低可能影响业务统计口径group by聚合倾斜加盐拆Key两阶段聚合中等多一轮Shuffle整体耗时增加distinct count倾斜加盐后分桶计数再汇总中等注意去重语义别把数据算重小表join大表倾斜广播小表极低小表过大时Driver内存压力大大表join大表、热点Key少热点Key分拆Join高实现复杂维表膨胀占用内存整体分布略不均匀调大shuffle分区数极低只能缓解不能根治极端倾斜、Key总数少自定义分区器高需要提前统计Key分布5. 更多思路如何从根源上让数据倾斜少发生解决问题的最高境界是让它压根不发生。虽然数据倾斜在分布式计算里不可能完全消灭但通过架构和模型层面的设计确实能大幅降低它的发生频率。5.1 数据预处理在数仓建模时就拆好很多倾斜问题其实在数据源头就已经埋下了。比如你在明细表里把“城市”直接作为分析维度而业务天然集中在几个头部城市那么查询时按城市聚合就必然倾斜。如果你在做数仓建模时就把热点维度单独拆分出来或者在ETL阶段对全量数据做一层轻度聚合比如小时级别的预聚合下游分析时的压力就会小很多。这种“预聚合”思路本质上是把计算压力从查询时转移到写入时属于牺牲一定的数据时效性来换取查询的稳定。对于报表和BI类的场景这是非常常见的做法。5.2 利用引擎的自适应能力我用Spark 3.x比较多。Spark 3.0开始引入的Dynamic Partition Pruning和Adaptive Query ExecutionAQE对数据倾斜有内置的优化能力。AQE里和倾斜相关的特性是spark.sql.adaptive.skewJoin.enabled默认是开启的。它能自动检测Shuffle后数据量不均匀的分区然后把这些倾斜分区拆成多个小分区同时把对应的小表侧数据膨胀复制实现在引擎层面自动处理一部分Join倾斜。这个功能对很多场景挺有效的但它的处理上限也有限——如果热点Key的数据量大到极端AQE的拆分粒度还是不够用。用好AQE的前提是打开开关并合理配置spark.sql.adaptive.enabledtrue spark.sql.adaptive.skewJoin.enabledtrue spark.sql.adaptive.skewJoin.skewedPartitionFactor5 spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes256MBskewedPartitionFactor表示某个分区的中位数和最大值的比值达到多少倍才算倾斜skewedPartitionThresholdInBytes是倾斜分区的数据量下限。这两个参数需要根据你的数据分布情况调整如果阈值设得太低会把太多正常分区误判为倾斜反而影响性能。5.3 设计层面规避不要在大明细上反复做重聚合还有一个我后来才悟出来的经验很多倾斜不是数据天然倾斜而是因为计算逻辑设计得不好。比如在明细表上直接做count(distinct user_id)这种操作Este聚合本身就是分布式计算里比较重的一类算子。如果你把“用户数”这类指标提前在DWS层算好下游的应用层只需要去读结果那倾斜自然就不存在了。另外在生成业务Key的时候尽量保证它的设计是有业务含义的、分布是合理的。比如订单表里如果用“订单状态 日期”做Key某个状态的订单量可能天然就非常大。如果改用“订单唯一ID”每个Key只有一行分布就会特别均匀。我见过不少“伪倾斜”的case其实是字段设计问题而不是计算框架的问题。5.4 建立倾斜监控和告警机制预防做得再好上线后依然有可能出现新的倾斜。所以在平台层面有一套监控告警是很有必要的。可以考虑在每个Stage结束时自动采集任务耗时和Shuffle数据量的分布计算最大耗时/中位数耗时的比值。如果这个比值超过一定阈值比如5倍就自动给值班人员推送告警并把疑似倾斜的Stage和涉及的表、SQL记录下来。我在某公司的数据平台上就做过类似的事情给任务的日志增加了倾斜异常检测规则跑批任务一旦出现长尾会立刻通知到人。效果很明显很多倾斜问题在生产报表发现之前就已经被定位和修复了。6. 真实案例复盘从几百行Executor日志里找到的那个“罪魁祸首”讲一个我印象比较深的真实案例复盘一遍完整的排查和解决过程那段时间几乎每天都在和数据倾斜打交道。当时负责一个跨团队的订单分析任务。任务本身不复杂就是把订单明细表和一个商家维表做关联计算出每个商家的经营指标。之前一直运行正常某天开始突然从一个半小时暴涨到七个多小时。最诡异的是整个集群的资源并没有明显变化。我第一步就是打开Spark Web UI定位到最慢的那个Stage。点进去看到这个Stage的任务数有500多个平均耗时在30秒左右但有一个Task跑了将近5个小时它的Shuffle Read Size是3.8GB而其他任务平均只有几十MB。差距大到令人发指。接着我拉了订单明细表里商家ID的分布统计用GROUP BY merchant_id ORDER BY COUNT(*) DESC LIMIT 10查了一下。结果让我没绷住排名第一的商家ID数据量是4000多万条第二名只有2万多条第一名的数据量比其他所有商家加起来还要多几十倍。后来去和业务方确认发现这个商家ID实际上是一个“测试店铺”承担了线上压测和导流任务所以产生了异常多的订单。本来这种数据在数仓里就应该被标记和剔除的。确认是测试数据造成的倾斜后我用了最简单的方式处理在ETL层直接加了过滤条件把测试商家的数据排除掉。改动上线后整个任务稳定在了1小时以内。数据倾斜解决得异常顺利。这个案例给我最大的启发是数据倾斜的根因多数时候不是计算框架的问题而是数据本身出了问题。遇到倾斜先查数据分布再上解决方案顺序一定不能颠倒。如果当时我不查数据分布一上来就加盐、拆Key、调并行度不仅解决不了问题还会把任务搞得更复杂。7. 常见问题速查表关于数据倾斜日常被问得最多的问题大概集中在下面这些方面。我整理成一张速查表方便大家定位时对照参考。问题可能原因排查方向优先尝试手段某个Stage有几个Task耗时特别长单Key数据量过大查看Shuffle Read Records、GC耗时定位热Key过滤或加盐任务频繁Full GC且溢写磁盘单个Executor内存扛不住查看Executor内存和溢写日志加盐拆分热点Key或降低单个Task数据量count(distinct) 计算极慢distinct聚合在reduce端做全量去重查看执行计划该算子的数据量用加盐两层group by替代join时小表广播失败小表超过了广播阈值查看广播变量大小调大autoBroadcastJoinThreshold或改用分拆方案数据量不大但任务很慢分区数不合理或谓词下推失效查看执行计划是否有不必要的Shuffle检查分区键和过滤条件调整并行度加盐后任务反而更慢了所有Key都被加了盐多了一轮Shuffle确认是不是只对热点Key加盐抽样统计key分布缩小加盐范围还有一个容易被忽略的坑加盐方案在处理count distinct时要格外小心。比如count(distinct user_id)如果你给Key加盐后直接分组再计数最后把各分组的数量相加结果是对的。但如果你在两层聚合之间用了approx_count_distinct或者中间步骤没有正确保留用户ID的原始语义就容易算重或者算漏。我自己在这个问题上就吃过亏后来处理这类指标时习惯加一道“分组内先去重、再合并结果”的校验逻辑。8. 写在最后的一点个人体会数据倾斜这个问题看起来是一个“技术问题”但实际排查时往往需要业务视角和数据敏感度。一个测试商家ID、一个默认值、一个全局统一的“other”分类背后都是业务规则在设计时留下的隐患。你问技术方案之前最好先问问数据到底是怎么来的。我现在的习惯是接手任何一个新的数据处理任务第一件事就是抽一遍数据看分布尤其是group by和join的字段。花十分钟做这个检查往往能帮你省下后面几小时的排障时间。人在遇到性能问题的时候总是本能地想去调参数、改架构但数据倾斜的问题最根本的解法永远在数据本身。另外前面讲的那些方案在实际落地的过程中大概率还会遇到各种变体。比如加盐时的分隔符和数据冲突、广播阈值和Driver内存的权衡、AQE开起来之后对非倾斜任务的影响等等。这些细节没法在一篇文章里穷尽但大方向掌握住了即使遇到新变体排查思路也是通用的先定位再分析最后下手。
阅读完成 · 觉得有帮助?