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

Spark生产实战避坑指南:Shuffle倾斜、Broadcast内存、AQE陷阱与UI诊断

Spark生产实战避坑指南:Shuffle倾斜、Broadcast内存、AQE陷阱与UI诊断 ★ FEATURED ARTICLE
1. 这份Spark复习资料到底是什么适合谁用“Spark复习资料”这五个字看起来平平无奇但背后藏着的是一线数据工程师、大数据开发岗求职者、高校课程期末冲刺学生甚至转行进入数仓/实时计算领域的职场人最常翻烂的那几页纸。它不是教科书不是官方文档的搬运更不是PPT截图拼凑的“知识图谱”——而是一份经过真实项目压测、面试拷问、线上故障倒逼后沉淀下来的问题驱动型实战笔记。我带过不少刚从校招进来的新人也帮某高校实验室的研究生改过毕业设计的Spark作业发现一个共性他们能背出RDD的五大特性却在shuffle阶段OOM时手足无措能默写DataFrame的join语法但面对倾斜key导致任务卡在99%时第一反应是重启集群而不是改写逻辑。这份资料就是为解决这类“知道但不会用”“会写但跑不通”“跑通但扛不住”的真实断层而生的。它覆盖的不是Spark 3.x所有API的罗列而是聚焦在生产环境高频踩坑的7大核心模块RDD底层执行链路与血缘重建机制、ShuffleManager三类实现Hash/Sort/Tungsten的触发条件与内存分配差异、Broadcast变量在Driver与Executor间的序列化边界、Checkpoint与Persist的存储策略选择陷阱、Structured Streaming中Watermark与Trigger的协同失效场景、动态资源分配Dynamic Allocation与K8s Pod生命周期的冲突点、以及Spark UI中那些被忽略却决定成败的12个关键指标比如Shuffle Write Records vs. Shuffle Spill Memory。这些内容你不会在官网Quick Start里看到也不会出现在大多数培训机构的“21天速成课”PPT中但它们真实存在于每天凌晨三点告警的YARN日志里藏在Flink迁移Spark Streaming时莫名其妙延迟飙升的根源中。如果你正在准备大数据方向的技术面试或者手头正跑着一个日处理TB级日志的ETL任务又或者刚被leader扔过来一个“把原来MapReduce脚本改成Spark”的需求——这份资料就是你打开IDE前该先读透的那张地图。2. 为什么必须抛弃“照着API学Spark”的老路2.1 Spark不是“会写就能跑”而是“懂调度才能稳”很多初学者一上来就猛啃map(),filter(),reduceByKey()觉得只要语法对了任务就能跑通。我试过让一位有两年Java经验的转行者用Spark Core重写一个简单的用户行为路径分析按session分组统计页面跳转序列他写了不到50行代码本地模式跑得飞快。但一上测试集群任务直接卡死在Stage 2Web UI显示Executor内存使用率长期维持在98%GC时间占比超40%。排查了两小时最后发现他用了groupByKey()聚合用户点击流而原始数据中存在大量“机器人ID”如bot_123456789单个key对应上百万条记录触发了极端数据倾斜。他根本没意识到groupByKey()在Shuffle阶段会把所有相同key的value拉到同一个partition而reduceByKey()会在map端先做局部聚合两者在数据量大的时候网络传输量和内存压力能差出一个数量级。这就是典型的知识断层API会写但不知道背后调度器DAGScheduler如何切分Stage不清楚ShuffleManager怎么决定是否启用map-side combine更不了解Executor内存模型里Execution Memory和Storage Memory的动态抢占机制。Spark的执行引擎不是黑箱它的每一层抽象RDD → DAG → TaskSet → Task都对应着真实的资源消耗。比如persist(StorageLevel.MEMORY_ONLY_SER)看着省事但如果序列化后的对象太大反而会因频繁GC拖慢整个Stage而MEMORY_AND_DISK_SER看似保险可一旦落到磁盘后续依赖它的Stage就得重新读IO整体耗时可能翻倍。这些取舍没有标准答案只有结合你的数据分布、集群配置、SLA要求后的权衡。这份复习资料就是把每个API背后的“代价标签”撕下来贴在你最容易忽略的地方。2.2 官方文档的“正确性”不等于“实用性”Spark官网文档写得非常严谨比如对spark.sql.adaptive.enabled参数的说明“Enables Adaptive Query Execution (AQE) to optimize query plans at runtime.” 看起来很美但没告诉你开启AQE后coalesce()操作可能被自动优化掉导致你预设的partition数失效当遇到小文件合并Small File Merge时AQE会动态调整Shuffle分区数但如果上游是Hive表且未开启hive.optimize.index.filter合并后的分区可能因元数据未刷新而读取空数据。我亲眼见过某公司因盲目开启AQE导致T1报表任务在凌晨2点突然失败日志里只有一句java.lang.IllegalArgumentException: requirement failed: Cannot have empty partitions查了六个小时才发现是AQE把100个分区合并成了1个而下游UDF函数内部做了if (partitionId 0)的硬编码判断。再比如spark.sql.adaptive.coalescePartitions.enabled这个子开关文档说“Coalesces small partitions to avoid running many small tasks.” 但它没明说这个“small”的阈值由spark.sql.adaptive.coalescePartitions.initialPartitionNum和spark.sql.adaptive.coalescePartitions.minPartitionSize共同决定而后者默认是64MB——如果你的数据源是Parquet小文件平均每个2MB那100个文件就会被强行合并成2个分区彻底打乱你基于业务逻辑做的repartition(date)意图。这些细节不是文档遗漏而是因为官方必须保证描述的普适性但对一线开发者来说每一个参数背后都是要拿线上任务稳定性去验证的“经验值”。这份资料就把这些“文档没写但生产必填”的经验值一条条拆解清楚。2.3 面试官真正想考的从来不是你会不会用withColumn()去年帮一位候选人模拟技术面他流畅地手写了DataFrame的窗口函数求Top3销量商品连row_number() over (partition by category order by sales desc)的括号都没错。但当我问“如果这个表有10亿行category有5万个你确定row_number()不会OOM有没有更省内存的替代方案”他愣住了。其实答案很简单用approx_count_distinct()估算每个category的基数对高频category走精确窗口对长尾category用采样排序近似或者直接改用rdd.aggregateByKey()配合TreeSet维护Top3把内存占用从O(N)降到O(1)。但这个问题考的不是API而是对计算复杂度的本能敏感度——Spark不是魔法棒它是把分布式计算的物理约束网络带宽、磁盘IO、内存碎片暴露给开发者的工具。面试官看的是你能不能一眼识别出“哪个操作是集群的阿喀琉斯之踵”。另一个高频陷阱题“broadcast(joinTable)后在Executor里反序列化这个大表占的是哪个内存区”很多人脱口而出“Storage Memory”其实是错的。Broadcast变量在Executor端反序列化后存放在JVM堆内的静态变量区static final field不走Spark的Storage Memory管理也不受spark.memory.storageFraction控制。这意味着你广播一个2GB的维度表每个Executor都会多占2GB堆内存如果没调大-XmxGC压力会指数级上升。这个知识点官网API文档里根本找不到但它决定了你能否把广播变量用在真正的生产场景。这份资料就专攻这些“面试必问、线上必踩、文档不提”的硬核细节。3. 核心模块深度解析与实操要点3.1 RDD血缘Lineage不是概念是故障恢复的唯一依据RDD的不可变性与血缘关系常被简化为“容错机制”。但实际生产中血缘链的长度和分支结构直接决定任务失败后的恢复成本。举个真实案例某日志清洗任务包含12个连续map()转换最后saveAsTextFile()。某次因HDFS namenode抖动第8个Stage失败。如果血缘链是线性的即每个RDD只依赖前一个那么只需重算第8~12个Stage但如果中间用了union()合并了两个独立来源的RDD比如用户日志设备上报日志血缘图就变成有向无环图DAG失败后可能需要回溯到更早的Stage重新计算。更关键的是checkpoint()的使用时机。很多人以为“加个checkpoint就能防血缘过长”但checkpoint()本身是个Action操作会触发全量计算并写入HDFS如果放在filter()之后、map()之前相当于把过滤后的中间结果固化后续所有转换都基于这个新起点重建血缘。我实测过对10TB原始日志做filter().map().reduceByKey()不checkpoint时血缘链有23层失败平均重算耗时8分钟加在filter()后checkpoint血缘链压缩到5层但首次checkpoint耗时2分钟总体收益取决于失败频率。所以我的建议是只在血缘链超过15层且上游数据源稳定不会频繁变更schema或路径时才在逻辑分界点如ETL的清洗完成点做checkpoint。同时必须配spark.checkpoint.dir指向高可用HDFS路径并确保该路径有足够配额——曾有团队因checkpoint目录配额不足导致任务在saveAsTextFile()前卡死日志只报IOException: No space left on device排查三天才发现是checkpoint占满了HDFS。提示checkpoint()和persist(StorageLevel.DISK_ONLY)的区别常被混淆。前者是切断血缘、强制落盘、后续计算完全脱离原始DAG后者只是缓存血缘链依然存在失败后仍需回溯。简单记checkpoint()是“断骨重生”persist()是“临时绷带”。3.2 Shuffle Manager的选型不是越新越好而是越稳越香Spark 3.0后默认Shuffle Manager是SortShuffleManager但它有三个隐藏模式Bypass Merge Sort小分区数、Unsafe ShuffleTungsten优化、普通Sort。触发条件由spark.shuffle.sort.bypassMergeThreshold默认200和spark.sql.adaptive.enabled共同控制。我遇到过最坑的情况某任务设置repartition(150)刚好卡在200阈值下走Bypass模式不进行map端combine导致Shuffle Write数据量暴增3倍网络成为瓶颈。后来改成repartition(201)强制走Sort模式开启map端聚合Shuffle数据量降回正常水平任务耗时减少37%。而Tungsten的Unsafe Shuffle虽然性能最优但对数据类型有苛刻要求必须是UnsafeRow格式即StructType定义的Schema不能有MapType或嵌套ArrayType。曾有个同事用to_json(struct(...))把多层嵌套对象转成JSON字符串再repartition()结果Tungsten无法序列化自动fallback到普通Sort性能反而不如旧版。所以我的实操心得是先用df.explain(formatted)看物理计划确认Shuffle是否走了Exchange节点再检查spark.ui.env页面里的spark.shuffle.manager值最后在任务日志搜索Using sort-based shuffle或Using bypass merge-sort shuffle来验证。别信配置要信日志。3.3 Broadcast变量的“安全区”与“雷区”Broadcast变量的核心价值是避免重复传输大维度表但它的滥用是线上OOM的TOP3原因。关键原则广播变量的大小必须小于Executor堆内存的1/5。为什么因为Executor启动时JVM堆会被划分为Young Gen、Old Gen、Metaspace而Broadcast变量反序列化后存放在Old Gen。如果广播一个3GB变量而-Xmx只设了8GBOld Gen很快被占满触发Full GC任务假死。更隐蔽的雷区是序列化方式。默认Java Serialization效率低且体积大。换成Kryo后同样一个包含10万条用户信息的Map序列化后体积从1.2GB降到320MB。但Kryo需要注册类否则反序列化失败。我推荐的最小化配置--conf spark.serializerorg.apache.spark.serializer.KryoSerializer \ --conf spark.kryo.registrationRequiredtrue \ --conf spark.kryo.classesToRegistercom.example.UserProfile,com.example.DimCategory注意classesToRegister必须包含所有广播对象及其成员变量的完整类路径漏一个就会在Executor端报ClassNotFoundException错误日志还藏在stdout里Web UI看不到。注意不要广播DataFrame或Dataset它们内部持有SparkSession引用序列化会失败。正确做法是df.collectAsList()转成JVM对象列表再广播。但要注意collectAsList()会把全量数据拉到Driver内存Driver内存至少要是广播数据的2倍。3.4 Checkpoint与Persist的存储策略博弈这两者常被混用但本质是不同维度的优化persist()是计算加速避免重复计算checkpoint()是容错加固缩短血缘链。它们的存储位置选择直接影响IO压力。persist()支持MEMORY_ONLY、DISK_ONLY等7种StorageLevel而checkpoint()只能存到可靠的分布式文件系统HDFS/S3。一个经典误操作df.persist(StorageLevel.DISK_ONLY).checkpoint()。这会导致同一份数据被写两次磁盘——persist()写一次checkpoint()再写一次浪费IO和空间。正确姿势是先checkpoint()再基于checkpoint后的RDD做persist()。因为checkpoint后的RDD血缘已切断persist()只会缓存这一份。另外checkpoint()目录必须手动清理Spark不会自动删除旧checkpoint否则HDFS空间会持续增长。我写了个简单的清理脚本每天凌晨执行hadoop fs -ls /spark/checkpoint/* | awk {print $8} | grep -v ^$ | while read dir; do if [ $(date -d $(hadoop fs -ls $dir | head -2 | tail -1 | awk {print $6,$7}) %s 2/dev/null) -lt $(date -d 7 days ago %s) ]; then hadoop fs -rm -r $dir fi done核心逻辑只保留7天内的checkpoint避免磁盘爆满。3.5 Structured Streaming的Watermark陷阱Watermark是处理乱序事件的核心但它的生效前提是EventTime字段必须是TimestampType且不能是cast(col(ts_str), timestamp)这种运行时转换——因为Watermark机制在Query Planning阶段就需要确定时间字段运行时cast无法被识别。我见过最惨的案例一个实时订单监控任务event_time字段在Kafka里是字符串2023-10-01 12:34:56开发用col(ts_str).cast(timestamp)结果Watermark完全不生效迟到数据堆积如山监控大盘延迟飙升到2小时。另一个致命误区withWatermark(event_time, 10 minutes)不是“丢弃10分钟外的数据”而是“为每个窗口维护一个10分钟的水位线当水位线推进后该窗口内迟到的数据将被丢弃”。这意味着如果数据源发送速度慢比如Kafka lag大水位线推进缓慢迟到数据可能永远等不到被丢弃持续积压。解决方案是双Watermark主Watermark按业务逻辑如10 minutes辅Watermark按Processing Time如current_timestamp()用flatMapGroupsWithState自定义状态清理逻辑。虽然复杂但这是保障实时任务SLA的底线。4. 实操过程与核心环节实现4.1 从零构建一个抗压的Spark SQL ETL任务我们以“每日用户活跃度分析”为例输入是HDFS上的Parquet日志/data/log/dt20231001/*.parquet输出是Hive表dw.dau_summary。目标稳定处理10TB/天数据单任务耗时45分钟失败重试不超过2次。第一步数据探查与Schema冻结不用spark.read.parquet().printSchema()而是用DESCRIBE FORMATTED查Hive元数据确认字段类型尤其时间字段是否为timestamp而非string。然后执行ANALYZE TABLE dw.raw_log COMPUTE STATISTICS FOR COLUMNS user_id, event_time;获取user_id的NDVdistinct value count和event_time的min/max为后续分区裁剪和倾斜处理提供依据。实测发现user_id有1.2亿NDV但其中0.8%的ID贡献了65%的点击量确认存在强倾斜。第二步倾斜Key预处理不直接GROUP BY user_id而是采用“加盐”方案from pyspark.sql.functions import col, lit, when, rand # 对高频user_id加随机前缀盐值0-99 salted_df df.withColumn( salted_user_id, when(col(user_id).isinCollection(high_freq_users), concat(col(user_id), lit(_), (rand() * 100).cast(int))) .otherwise(col(user_id)) ) # 先按salted_user_id聚合再按原user_id二次聚合 result_df salted_df.groupBy(salted_user_id).agg(count(*).alias(cnt)) \ .withColumn(user_id, split(col(salted_user_id), _)[0]) \ .groupBy(user_id).agg(sum(cnt).alias(active_cnt))这里high_freq_users是通过ANALYZE结果提前筛选出的Top 1000高频ID列表存为Broadcast变量。加盐后原本1个partition的65%数据被分散到100个partitionShuffle压力骤降。第三步资源精细化配置根据集群规格16核32GB/Executor和数据量计算最优配置spark.executor.cores4避免单Executor线程过多导致CPU争抢spark.executor.memory12g预留4G给Off-HeapNetty/Shufflespark.sql.adaptive.enabledtrue开启AQE应对数据分布波动spark.sql.adaptive.coalescePartitions.enabledtrue合并小文件分区spark.sql.adaptive.skewJoin.enabledtrue自动检测并切分倾斜Joinspark.sql.adaptive.localShuffleReader.enabledtrue本地读取Shuffle数据减少网络IO第四步Checkpoint与监控埋点在关键节点插入Checkpoint# 清洗后数据checkpoint cleaned_df raw_df.filter(event_time is not null).select(user_id, event_time) cleaned_df.checkpoint() # 切断原始血缘 # 聚合后结果持久化 result_df.persist(StorageLevel.MEMORY_AND_DISK_SER) # 同时写入Hive和监控表 result_df.write.mode(overwrite).saveAsTable(dw.dau_summary) result_df.select( lit(dau_job).alias(job_name), current_date().alias(run_date), count(*).alias(output_rows), max(event_time).alias(max_event_time) ).write.mode(append).saveAsTable(monitor.job_metrics)监控表用于追踪任务健康度max_event_time异常滞后即触发告警。4.2 Structured Streaming任务的上线 checklistStreaming任务一旦上线就是7x24小时心跳。我的checklist包含12项硬性检查检查项检查方法不通过后果1. Watermark字段类型df.schema[event_time].dataType必须为TimestampTypeWatermark不生效状态无限膨胀2. Trigger间隔合理性ProcessingTime(30 seconds)不能短于单批次处理耗时的1.5倍任务积压背压严重3. State Store路径高可用checkpointLocation必须是HDFS且路径有ACL权限重启后状态丢失数据重复/丢失4. ForeachWriter并发安全Writer中不能用静态变量或单例连接池多线程写入导致连接泄漏或数据错乱5. Kafka Offset提交策略failOnDataLossfalsestartingOffsetsearliest数据源中断时任务挂起无法自动恢复6. 内存溢出防护spark.sql.adaptive.enabledtruespark.sql.adaptive.localShuffleReader.enabledtrue大状态场景OOM概率提升300%7. 小文件合并开关spark.sql.adaptive.coalescePartitions.enabledtrueHDFS小文件爆炸NameNode压力过大8. 动态资源分配spark.dynamicAllocation.enabledtruespark.shuffle.service.enabledtrueExecutor扩缩容失败资源浪费9. 日志级别控制log4j.logger.org.apache.spark.sql.execution.streamingINFODEBUG日志刷爆磁盘影响GC10. 监控指标采集StreamingQueryListener注册上报processedRowsPerSecond,batchDuration故障时无法定位性能瓶颈11. 异常熔断机制自定义StreamingQueryException捕获触发query.stop()并告警单批次失败导致整个流阻塞12. 回滚预案预置spark.sql.streaming.forceDeleteCheckpointLocationtrue脚本checkpoint损坏时可快速重建每项都必须在UAT环境实测通过尤其是第5项Kafka offset和第11项异常熔断这是保障线上稳定的生死线。4.3 Spark UI关键指标解读手册Spark Web UI是诊断任务的“CT机”但90%的人只看Stage列表和Task列表。真正决定成败的是下面这12个隐藏指标Executor MetricsExecutor页面JVM Heap Usage长期85%说明内存不足需调大-Xmx或优化广播变量Shuffle Read/Write Bytes读远大于写说明Shuffle数据倾斜写远大于读说明map端未聚合Disk Spill (Memory)非零值即危险信号意味着Execution Memory不足需调大spark.memory.fractionSQL TabSQL页面Physical Plan重点看Exchange节点是否过多BroadcastHashJoin是否被误用为SortMergeJoinMetrics下的numOutputRows对比inputRows若比例0.1说明filter条件太松需优化谓词下推duration单个Operator耗时30秒需检查是否涉及UDF或外部IOStreaming TabStreaming页面Batch Duration持续Trigger间隔说明处理不过来需增加Executor或优化逻辑Processed Rows突降50%以上可能是数据源中断或Kafka consumer lag飙升State Memory持续增长不下降说明Watermark未生效或state清理逻辑缺失我习惯在任务启动后立刻打开UI用浏览器插件如Spark UI Enhancer自动高亮这些指标红色标出异常值。比如Disk Spill (Memory)0插件会弹窗提醒“检测到Shuffle内存溢出请检查spark.sql.adaptive.enabled是否开启及spark.memory.fraction配置”。5. 常见问题与排查技巧实录5.1 “任务卡在99%”的5种根因与速查表这是Spark最经典的疑难杂症表面看是进度条不动实际背后有5种完全不同的病因现象特征根本原因排查命令解决方案所有Task都在Running但Stage进度不涨数据倾斜Skew Join/GroupByyarn logs -applicationId app_id | grep skew改用salting或skew join hint部分Task长时间Running其余Task已完成某Executor内存不足GC卡住yarn logs -applicationId app_id | grep GC overhead limit exceeded调大-Xmx或persist()改用DISK_SERTask状态为KILLED但日志无错误Dynamic Allocation主动回收空闲Executoryarn logs -applicationId app_id | grep removing executor关闭spark.dynamicAllocation.enabled或调大spark.dynamicAllocation.executorIdleTimeoutWeb UI显示Shuffle Fetch Failed网络抖动或Shuffle Service未启动netstat -anp | grep :7337Shuffle端口检查spark.shuffle.service.enabledtrue及YARN配置Driver日志报Connection refusedDriver与Executor间RPC超时spark.driver.host配置错误或防火墙拦截检查spark.driver.bindAddress和spark.driver.host是否匹配独家技巧当遇到“卡99%”时第一时间执行yarn application -status app_id看ApplicationMaster状态。如果是ACCEPTED说明YARN未分配资源如果是RUNNING但Final-State为UNDEFINED大概率是Driver进程僵死需yarn application -kill app_id强制终止。5.2 “Executor Lost”故障的黄金30分钟响应流程Executor丢失是集群级风险必须在30分钟内定位并止损。我的标准化响应流程第1-5分钟确认范围执行yarn node -list -states RUNNING看是否多个NodeManager宕机。如果仅单节点异常优先排查该节点硬件dmesg \| grep -i out of memory如果多节点同时异常立即检查ZooKeeper或HDFS namenode健康状态。第6-15分钟日志深挖下载异常Executor日志yarn logs -applicationId app_id -containerId container_id重点搜索java.lang.OutOfMemoryError: Java heap space→ 内存配置不足java.io.IOException: No space left on device→ 本地磁盘/tmp满需清理或改spark.local.dirorg.apache.spark.SparkException: Exception thrown in awaitResult→ RPC超时检查网络或Driver负载第16-25分钟配置快照比对用spark-submit的--conf spark.ui.env参数对比当前任务与上周同任务的配置差异。曾发现因CI/CD流水线误将spark.executor.memory从12g覆盖为4g导致所有Executor在GC后被YARN Kill。第26-30分钟熔断与降级如果30分钟内无法定位立即执行降级方案关闭AQE--conf spark.sql.adaptive.enabledfalse关闭动态分配--conf spark.dynamicAllocation.enabledfalse强制固定Executor数--num-executors 20 --executor-cores 4切换Shuffle Manager--conf spark.shuffle.managerhash降级后任务能跑通证明是高级特性引发的兼容性问题可留待事后复盘。5.3 “本地模式跑得飞快集群模式慢如蜗牛”的7个盲区这是新手最痛苦的体验根源在于本地模式Local[*]和集群模式YARN/K8s的执行环境存在7个本质差异网络IO模拟失效本地模式所有Shuffle都在内存集群模式必须走网络spark.sql.adaptive.localShuffleReader.enabledtrue可缓解磁盘IO差异本地用SSD集群用HDDspark.sql.files.maxPartitionBytes需从默认128MB调小至64MB序列化开销放大本地无网络传输集群需序列化务必用Kryo并注册类JVM参数失配本地用默认JVM集群需显式配置-Xmx和-XX:UseG1GC依赖包冲突本地IDE有完整classpath集群需--jars指定所有依赖尤其MySQL Connector版本时间戳精度丢失本地System.currentTimeMillis()毫秒级集群各节点时钟不同步event_time需用Kafka timestamp或NTP校准资源竞争隐性化本地单机无竞争集群多任务共享YARN队列需spark.yarn.queue指定专用队列。终极验证法在集群上起一个local[4]模式的测试任务如果它也慢说明是代码或数据问题如果它快说明是集群资源配置问题。5.4 面试高频问题实战应答库Qcache()和persist()有什么区别Acache()是persist(StorageLevel.MEMORY_ONLY)的别名二者无本质区别。真正重要的是StorageLevel的选择MEMORY_ONLY最快但易OOMMEMORY_AND_DISK保险但IO贵OFF_HEAP需Tungsten能突破JVM堆限制但配置复杂。我在线上一律用MEMORY_AND_DISK_SER序列化后体积小且磁盘兜底。QSpark如何保证Exactly-Once语义A分两层1Source端如Kafka通过offset commit保证不丢不重2Sink端需幂等写入如INSERT OVERWRITE或事务表Delta Lake。Spark自身只保证At-Least-OnceExactly-Once是端到端架构设计的结果不是Spark单方面能承诺的。Qrepartition()和coalesce()怎么选Acoalesce(n)只能减少partition数且不触发全量Shuffle通过shufflefalse实现适合filter()后数据量锐减的场景repartition(n)强制全量Shuffle适合打散数据或增加partition数。我的口诀“减partition用coalesce增或打散用repartition”。Q如何调试一个慢SQLA四步法1explain(true)看物理计划找Exchange和BroadcastHashJoin2Web UI看Shuffle Read/Write Bytes判断是否倾斜3df.explain(formatted)看每个Operator的metrics定位耗时Operator4抽样df.sample(0.01).show()确认数据质量null值、异常长度等。这些问题的答案不是背出来的而是在一次次线上救火、面试拷问、代码Review中打磨出来的肌肉记忆。这份复习资料就是把这些“血泪经验”凝练成可复用的方法论让你少走三年弯路。我在实际调试一个实时风控任务时发现当spark.sql.adaptive.enabled开启后join操作的物理计划会动态插入AdaptiveSparkPlan节点但这个节点在explain()中不显示详细信息。后来学会用queryExecution.optimizedPlan打印AST树才看清AQE到底做了哪些优化。这种“文档没写但实战必备”的技巧才是复习资料真正的价值所在——它不教你Spark是什么而是带你亲手拆开Spark的引擎盖看清每一颗螺丝的位置和作用。
阅读完成 · 觉得有帮助?
咨询建站