sparkforensics-server 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. package/index.js +8 -4
  2. package/package.json +1 -1
  3. package/public/assets/AutoscalingChurn-9Zo6IMGS.js +1 -0
  4. package/public/assets/CacheUtilization-MBDfMsKC.js +1 -0
  5. package/public/assets/CachingOpportunity-Cfw3wAFa.js +1 -0
  6. package/public/assets/ColdStart-BtS12b6_.js +1 -0
  7. package/public/assets/ConfigAudit-DNPV6EQg.js +1 -0
  8. package/public/assets/CoreUsageArea-BObEeenE.js +1 -0
  9. package/public/assets/Dashboard-CC_BXrNy.js +2 -0
  10. package/public/assets/DisclosureContext-Ds0hcZFn.js +1 -0
  11. package/public/assets/DuplicatePlanSubtree-vN3zqRsi.js +1 -0
  12. package/public/assets/ExecutorUtilization-CZObSLIj.js +1 -0
  13. package/public/assets/GcPressure-ByJlLOua.js +1 -0
  14. package/public/assets/IncompleteRun-MzqV4W8y.js +1 -0
  15. package/public/assets/JobFailures-LbMvAwAb.js +1 -0
  16. package/public/assets/MemoryUtilization-DH4jYvJK.js +1 -0
  17. package/public/assets/OverBroadcast-euk6HKg4.js +1 -0
  18. package/public/assets/PartitionHint-DThaMMRT.js +1 -0
  19. package/public/assets/PartitionSizing-yIN-iZzx.js +1 -0
  20. package/public/assets/PlanExplorer-Dd9WPK4S.js +1 -0
  21. package/public/assets/PlanGraphRoute-NV5mCXRm.js +1 -0
  22. package/public/assets/RetryWaste-CXH4D-xH.js +1 -0
  23. package/public/assets/ShuffleIO-CW7U68ph.js +1 -0
  24. package/public/assets/Skew-PL79TppX.js +1 -0
  25. package/public/assets/SlowHost-GBO73hCi.js +1 -0
  26. package/public/assets/SmallFiles-B5cUjEav.js +1 -0
  27. package/public/assets/SpeculationWaste-mxi3jgb0.js +1 -0
  28. package/public/assets/Spill-CH1bNpxL.js +1 -0
  29. package/public/assets/StageFailed-YvxC11ii.js +1 -0
  30. package/public/assets/StageFindingGroup-B1A0nf-e.js +1 -0
  31. package/public/assets/StageHeader-BMNmUy-s.js +1 -0
  32. package/public/assets/StageShape-DO3mQ6NS.js +1 -0
  33. package/public/assets/StageSlowness-SwRxfAEx.js +1 -0
  34. package/public/assets/Straggler-DHLTiBY-.js +1 -0
  35. package/public/assets/TaskFailures-Bj0QxiOc.js +1 -0
  36. package/public/assets/{TinyTask-Be-pvTjJ.js → TinyTask-COUQQ5bQ.js} +1 -1
  37. package/public/assets/UnderBroadcast-RAUbQ6vw.js +1 -0
  38. package/public/assets/{finding-anchor-vVODA_VC.js → finding-anchor-Dl8mIHiU.js} +1 -1
  39. package/public/assets/impact-sort-Eihua9dY.js +1 -0
  40. package/public/assets/index-BcqTLKFp.css +2 -0
  41. package/public/assets/index-DaC8A2FS.js +13 -0
  42. package/public/assets/{parser-worker-jAP0kWvS.js → parser-worker-CfDvPkwF.js} +9 -9
  43. package/public/assets/react-vendor-BAKt3vny.js +9 -0
  44. package/public/assets/select-DkjFHlXR.js +1 -0
  45. package/public/assets/usePagedRows-Bo-aSztx.js +1 -0
  46. package/public/assets/useRecentFiles-Bb4-ukVz.js +11 -0
  47. package/public/assets/useSortMode-lDedD5a3.js +1 -0
  48. package/public/docs/404.html +4 -3
  49. package/public/docs/assets/{app.C5E-wy6e.js → app.yjczoibf.js} +1 -1
  50. package/public/docs/assets/chunks/@localSearchIndexroot.r9imX8Aq.js +1 -0
  51. package/public/docs/assets/chunks/{VPLocalSearchBox.DtyygpbL.js → VPLocalSearchBox.DOBbPfix.js} +1 -1
  52. package/public/docs/assets/chunks/{theme.DQS9-w_Z.js → theme.DFcAl8Ws.js} +2 -2
  53. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.D0Kkk_r5.js +1 -0
  54. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.D0Kkk_r5.lean.js +1 -0
  55. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.caA46z-u.js +1 -0
  56. package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.CsCytCho.lean.js → contributor-guide_architecture_detector-contract.md.caA46z-u.lean.js} +1 -1
  57. package/public/docs/assets/contributor-guide_architecture_drill-down.md.CdtlKHl-.js +1 -0
  58. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.BNp14LtL.js +1 -0
  59. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.BNp14LtL.lean.js +1 -0
  60. package/public/docs/assets/contributor-guide_architecture_index.md.B9yZieYL.js +1 -0
  61. package/public/docs/assets/{contributor-guide_architecture_index.md.3TO9ic6w.lean.js → contributor-guide_architecture_index.md.B9yZieYL.lean.js} +1 -1
  62. package/public/docs/assets/contributor-guide_architecture_overview.md.DBGo-bwn.js +1 -0
  63. package/public/docs/assets/contributor-guide_architecture_overview.md.DBGo-bwn.lean.js +1 -0
  64. package/public/docs/assets/contributor-guide_architecture_state-and-history.md.w4lwybdh.js +1 -0
  65. package/public/docs/assets/{contributor-guide_architecture_state-and-history.md.B8Wc_ZTg.lean.js → contributor-guide_architecture_state-and-history.md.w4lwybdh.lean.js} +1 -1
  66. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.CcvqxnZI.js +1 -0
  67. package/public/docs/assets/{contributor-guide_architecture_widget-rendering.md.DYwFeyjV.lean.js → contributor-guide_architecture_widget-rendering.md.CcvqxnZI.lean.js} +1 -1
  68. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.CKeFttKj.js +6 -0
  69. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.CKeFttKj.lean.js +1 -0
  70. package/public/docs/assets/contributor-guide_contributing.md.CQMvGwFt.js +1 -0
  71. package/public/docs/assets/{contributor-guide_contributing.md.B2E9ni79.lean.js → contributor-guide_contributing.md.CQMvGwFt.lean.js} +1 -1
  72. package/public/docs/assets/contributor-guide_development-setup.md.D9bu4j_c.js +15 -0
  73. package/public/docs/assets/contributor-guide_development-setup.md.D9bu4j_c.lean.js +1 -0
  74. package/public/docs/assets/contributor-guide_testing.md.By6XGR9a.js +1 -0
  75. package/public/docs/assets/{contributor-guide_testing.md.Cnn6gz1C.lean.js → contributor-guide_testing.md.By6XGR9a.lean.js} +1 -1
  76. package/public/docs/assets/{style.DSixAiZE.css → style.tmDhD3c8.css} +1 -1
  77. package/public/docs/assets/tuning-reference_anti-patterns.md.D08Uaod7.js +1 -0
  78. package/public/docs/assets/tuning-reference_anti-patterns.md.D08Uaod7.lean.js +1 -0
  79. package/public/docs/assets/tuning-reference_aqe.md.C1Ae6A8u.js +1 -0
  80. package/public/docs/assets/tuning-reference_aqe.md.C1Ae6A8u.lean.js +1 -0
  81. package/public/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.6wjKuJNE.js +1 -0
  82. package/public/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.6wjKuJNE.lean.js +1 -0
  83. package/public/docs/assets/tuning-reference_bottleneck-cold-start.md.BVQbsuHP.js +7 -0
  84. package/public/docs/assets/tuning-reference_bottleneck-cold-start.md.BVQbsuHP.lean.js +1 -0
  85. package/public/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.DtaYnaiF.js +1 -0
  86. package/public/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.DtaYnaiF.lean.js +1 -0
  87. package/public/docs/assets/{tuning-reference_bottleneck-failures.md.CAsFf3RR.js → tuning-reference_bottleneck-failures.md.Z1Tdmdd3.js} +2 -2
  88. package/public/docs/assets/tuning-reference_bottleneck-failures.md.Z1Tdmdd3.lean.js +1 -0
  89. package/public/docs/assets/tuning-reference_bottleneck-gc.md.BCnXLf1n.js +6 -0
  90. package/public/docs/assets/tuning-reference_bottleneck-gc.md.BCnXLf1n.lean.js +1 -0
  91. package/public/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.CcvQj2g9.js +8 -0
  92. package/public/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.CcvQj2g9.lean.js +1 -0
  93. package/public/docs/assets/tuning-reference_bottleneck-memory-utilization.md.B-C0-8h-.js +7 -0
  94. package/public/docs/assets/tuning-reference_bottleneck-memory-utilization.md.B-C0-8h-.lean.js +1 -0
  95. package/public/docs/assets/tuning-reference_bottleneck-retry-waste.md.DCnhWiqj.js +1 -0
  96. package/public/docs/assets/tuning-reference_bottleneck-retry-waste.md.DCnhWiqj.lean.js +1 -0
  97. package/public/docs/assets/{tuning-reference_bottleneck-shuffle.md.DDMQVOW6.js → tuning-reference_bottleneck-shuffle.md.B5OMJCd3.js} +2 -2
  98. package/public/docs/assets/tuning-reference_bottleneck-shuffle.md.B5OMJCd3.lean.js +1 -0
  99. package/public/docs/assets/{tuning-reference_bottleneck-skew.md.B0MzhWcX.js → tuning-reference_bottleneck-skew.md.Cvo6NQm_.js} +1 -1
  100. package/public/docs/assets/tuning-reference_bottleneck-skew.md.Cvo6NQm_.lean.js +1 -0
  101. package/public/docs/assets/{tuning-reference_bottleneck-slow-host.md.BlIo6UDW.js → tuning-reference_bottleneck-slow-host.md.ja5qr3wq.js} +1 -1
  102. package/public/docs/assets/tuning-reference_bottleneck-slow-host.md.ja5qr3wq.lean.js +1 -0
  103. package/public/docs/assets/tuning-reference_bottleneck-small-files.md.Crjtb0i2.js +5 -0
  104. package/public/docs/assets/tuning-reference_bottleneck-small-files.md.Crjtb0i2.lean.js +1 -0
  105. package/public/docs/assets/tuning-reference_bottleneck-spill.md.Bl8KtSRg.js +6 -0
  106. package/public/docs/assets/tuning-reference_bottleneck-spill.md.Bl8KtSRg.lean.js +1 -0
  107. package/public/docs/assets/tuning-reference_bottleneck-straggler.md.B0VN8reT.js +7 -0
  108. package/public/docs/assets/tuning-reference_bottleneck-straggler.md.B0VN8reT.lean.js +1 -0
  109. package/public/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.DQCftGhu.js +8 -0
  110. package/public/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.DQCftGhu.lean.js +1 -0
  111. package/public/docs/assets/{tuning-reference_bottleneck-utilization.md.DqC_iGkw.js → tuning-reference_bottleneck-utilization.md.BXDl30f9.js} +1 -1
  112. package/public/docs/assets/tuning-reference_bottleneck-utilization.md.BXDl30f9.lean.js +1 -0
  113. package/public/docs/assets/tuning-reference_caching.md.CcaiMn_7.js +1 -0
  114. package/public/docs/assets/tuning-reference_caching.md.CcaiMn_7.lean.js +1 -0
  115. package/public/docs/assets/tuning-reference_cluster-config.md.DbWG1yZq.js +1 -0
  116. package/public/docs/assets/tuning-reference_cluster-config.md.DbWG1yZq.lean.js +1 -0
  117. package/public/docs/assets/tuning-reference_config.md.YUqSpwFB.js +1 -0
  118. package/public/docs/assets/tuning-reference_config.md.YUqSpwFB.lean.js +1 -0
  119. package/public/docs/assets/tuning-reference_data-formats.md.C5DX5xL4.js +1 -0
  120. package/public/docs/assets/tuning-reference_data-formats.md.C5DX5xL4.lean.js +1 -0
  121. package/public/docs/assets/tuning-reference_index.md.ZVIpQ6Dk.js +1 -0
  122. package/public/docs/assets/tuning-reference_index.md.ZVIpQ6Dk.lean.js +1 -0
  123. package/public/docs/assets/tuning-reference_intro.md.BSSaDlqx.js +1 -0
  124. package/public/docs/assets/tuning-reference_intro.md.BSSaDlqx.lean.js +1 -0
  125. package/public/docs/assets/tuning-reference_joins.md.BVhNCDnE.js +1 -0
  126. package/public/docs/assets/tuning-reference_joins.md.BVhNCDnE.lean.js +1 -0
  127. package/public/docs/assets/tuning-reference_memory-model.md.CfjFdgbl.js +1 -0
  128. package/public/docs/assets/tuning-reference_memory-model.md.CfjFdgbl.lean.js +1 -0
  129. package/public/docs/assets/tuning-reference_metrics.md.C64VNEL4.js +1 -0
  130. package/public/docs/assets/tuning-reference_metrics.md.C64VNEL4.lean.js +1 -0
  131. package/public/docs/assets/tuning-reference_partitioning.md.CtLK0xsS.js +1 -0
  132. package/public/docs/assets/tuning-reference_partitioning.md.CtLK0xsS.lean.js +1 -0
  133. package/public/docs/assets/tuning-reference_pyspark.md.D-92XgF8.js +6 -0
  134. package/public/docs/assets/tuning-reference_pyspark.md.D-92XgF8.lean.js +1 -0
  135. package/public/docs/assets/tuning-reference_shuffle.md.Iq2v1oHJ.js +1 -0
  136. package/public/docs/assets/tuning-reference_shuffle.md.Iq2v1oHJ.lean.js +1 -0
  137. package/public/docs/assets/tuning-reference_spark-architecture.md._qwnE_Sc.js +1 -0
  138. package/public/docs/assets/tuning-reference_spark-architecture.md._qwnE_Sc.lean.js +1 -0
  139. package/public/docs/assets/tuning-reference_table-formats.md.BSYEK-eN.js +1 -0
  140. package/public/docs/assets/tuning-reference_table-formats.md.BSYEK-eN.lean.js +1 -0
  141. package/public/docs/assets/user-guide_alternative-log-retrieval.md.z-8npxNk.js +7 -0
  142. package/public/docs/assets/user-guide_alternative-log-retrieval.md.z-8npxNk.lean.js +1 -0
  143. package/public/docs/assets/user-guide_getting-started.md.BJW5oVhe.js +24 -0
  144. package/public/docs/assets/user-guide_getting-started.md.BJW5oVhe.lean.js +1 -0
  145. package/public/docs/assets/user-guide_mcp-tools.md.DcLiOa9t.js +148 -0
  146. package/public/docs/assets/user-guide_mcp-tools.md.DcLiOa9t.lean.js +1 -0
  147. package/public/docs/assets/user-guide_run-comparison.md.qin0a4IX.js +1 -0
  148. package/public/docs/assets/user-guide_run-comparison.md.qin0a4IX.lean.js +1 -0
  149. package/public/docs/assets/user-guide_understanding-findings.md.DzReyCXb.js +1 -0
  150. package/public/docs/assets/{user-guide_understanding-findings.md.YBBLOwWi.lean.js → user-guide_understanding-findings.md.DzReyCXb.lean.js} +1 -1
  151. package/public/docs/contributor-guide/architecture/board-widgets.html +7 -6
  152. package/public/docs/contributor-guide/architecture/detector-contract.html +7 -6
  153. package/public/docs/contributor-guide/architecture/drill-down.html +7 -6
  154. package/public/docs/contributor-guide/architecture/impact-estimation.html +7 -6
  155. package/public/docs/contributor-guide/architecture/index.html +7 -6
  156. package/public/docs/contributor-guide/architecture/overview.html +7 -6
  157. package/public/docs/contributor-guide/architecture/state-and-history.html +7 -6
  158. package/public/docs/contributor-guide/architecture/widget-rendering.html +7 -6
  159. package/public/docs/contributor-guide/architecture/worker-protocol.html +10 -9
  160. package/public/docs/contributor-guide/contributing.html +7 -6
  161. package/public/docs/contributor-guide/development-setup.html +12 -11
  162. package/public/docs/contributor-guide/testing.html +7 -6
  163. package/public/docs/hashmap.json +1 -1
  164. package/public/docs/index.html +5 -4
  165. package/public/docs/tuning-reference/anti-patterns.html +8 -7
  166. package/public/docs/tuning-reference/aqe.html +8 -7
  167. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +8 -7
  168. package/public/docs/tuning-reference/bottleneck-cold-start.html +8 -7
  169. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +8 -7
  170. package/public/docs/tuning-reference/bottleneck-failures.html +8 -7
  171. package/public/docs/tuning-reference/bottleneck-gc.html +8 -7
  172. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +8 -7
  173. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +8 -7
  174. package/public/docs/tuning-reference/bottleneck-retry-waste.html +8 -7
  175. package/public/docs/tuning-reference/bottleneck-shuffle.html +8 -7
  176. package/public/docs/tuning-reference/bottleneck-skew.html +8 -7
  177. package/public/docs/tuning-reference/bottleneck-slow-host.html +8 -7
  178. package/public/docs/tuning-reference/bottleneck-small-files.html +8 -7
  179. package/public/docs/tuning-reference/bottleneck-spill.html +8 -7
  180. package/public/docs/tuning-reference/bottleneck-straggler.html +8 -7
  181. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +8 -7
  182. package/public/docs/tuning-reference/bottleneck-utilization.html +8 -7
  183. package/public/docs/tuning-reference/caching.html +8 -7
  184. package/public/docs/tuning-reference/cluster-config.html +8 -7
  185. package/public/docs/tuning-reference/config.html +8 -7
  186. package/public/docs/tuning-reference/data-formats.html +8 -7
  187. package/public/docs/tuning-reference/index.html +8 -7
  188. package/public/docs/tuning-reference/intro.html +8 -7
  189. package/public/docs/tuning-reference/joins.html +8 -7
  190. package/public/docs/tuning-reference/memory-model.html +8 -7
  191. package/public/docs/tuning-reference/metrics.html +8 -7
  192. package/public/docs/tuning-reference/partitioning.html +8 -7
  193. package/public/docs/tuning-reference/pyspark.html +8 -7
  194. package/public/docs/tuning-reference/shuffle.html +8 -7
  195. package/public/docs/tuning-reference/spark-architecture.html +8 -7
  196. package/public/docs/tuning-reference/table-formats.html +8 -7
  197. package/public/docs/user-guide/alternative-log-retrieval.html +14 -7
  198. package/public/docs/user-guide/getting-started.html +29 -7
  199. package/public/docs/user-guide/mcp-tools.html +51 -27
  200. package/public/docs/user-guide/run-comparison.html +7 -6
  201. package/public/docs/user-guide/understanding-findings.html +7 -6
  202. package/public/export-template.html +111 -0
  203. package/public/index.html +4 -3
  204. package/vendor-core/allocation.js +106 -0
  205. package/vendor-core/analyzer.js +168 -60
  206. package/vendor-core/check-coverage.js +88 -0
  207. package/vendor-core/cli/budgets.js +54 -27
  208. package/vendor-core/cli/collect-run.js +84 -32
  209. package/vendor-core/cli/regression-budgets.js +83 -0
  210. package/vendor-core/cli/threshold-config.js +28 -0
  211. package/vendor-core/comparison-verdict.js +177 -0
  212. package/vendor-core/core-source-hash.txt +1 -0
  213. package/vendor-core/core-usage-locality.js +56 -2
  214. package/vendor-core/detector-docs.js +58 -0
  215. package/vendor-core/detectors.js +1094 -500
  216. package/vendor-core/docs-config.js +0 -36
  217. package/vendor-core/docs-content/chapters/nav-index.json +31 -0
  218. package/vendor-core/docs-content/detection/cache.md +3 -2
  219. package/vendor-core/docs-content/detection/cfg.md +9 -8
  220. package/vendor-core/docs-content/detection/chrn.md +1 -2
  221. package/vendor-core/docs-content/detection/cold.md +4 -2
  222. package/vendor-core/docs-content/detection/cstor.md +9 -0
  223. package/vendor-core/docs-content/detection/fail.md +3 -2
  224. package/vendor-core/docs-content/detection/gc.md +3 -2
  225. package/vendor-core/docs-content/detection/host.md +2 -1
  226. package/vendor-core/docs-content/detection/local.md +1 -1
  227. package/vendor-core/docs-content/detection/mem.md +5 -2
  228. package/vendor-core/docs-content/detection/plan.md +2 -1
  229. package/vendor-core/docs-content/detection/sfail.md +2 -1
  230. package/vendor-core/docs-content/detection/shape.md +5 -4
  231. package/vendor-core/docs-content/detection/skew.md +3 -1
  232. package/vendor-core/docs-content/detection/slow.md +2 -2
  233. package/vendor-core/docs-content/detection/spec.md +2 -3
  234. package/vendor-core/docs-content/detection/spill.md +1 -1
  235. package/vendor-core/docs-site-config.js +3 -0
  236. package/vendor-core/effective-conf.js +107 -0
  237. package/vendor-core/efficiency-model.js +8 -6
  238. package/vendor-core/event-handlers.js +321 -44
  239. package/vendor-core/event-schemas.js +23 -0
  240. package/vendor-core/evidence-report.js +432 -115
  241. package/vendor-core/export-data.js +79 -6
  242. package/vendor-core/finding-action-label.js +9 -88
  243. package/vendor-core/finding-filter-predicate.js +9 -0
  244. package/vendor-core/finding-generic-recommendation.js +26 -105
  245. package/vendor-core/finding-names.js +28 -45
  246. package/vendor-core/finding-presentation.js +368 -0
  247. package/vendor-core/finding-tag-help.js +110 -0
  248. package/vendor-core/finding-types.js +373 -0
  249. package/vendor-core/findings-of-type.js +11 -0
  250. package/vendor-core/format-utils.js +96 -30
  251. package/vendor-core/html-export.js +51 -0
  252. package/vendor-core/impact-band.js +21 -8
  253. package/vendor-core/impact-estimator.js +25 -520
  254. package/vendor-core/impact-format.js +115 -0
  255. package/vendor-core/impact-model.js +197 -0
  256. package/vendor-core/ingest.js +6 -2
  257. package/vendor-core/intervals.js +13 -0
  258. package/vendor-core/list-runs.js +7 -5
  259. package/vendor-core/load-vendored.js +70 -5
  260. package/vendor-core/mcp-server-factory.js +14 -10
  261. package/vendor-core/mcp-tools.js +105 -45
  262. package/vendor-core/model-assembler.js +35 -1
  263. package/vendor-core/occupancy.js +1 -1
  264. package/vendor-core/parser-worker.js +2 -2
  265. package/vendor-core/plan-graph-model.js +3 -2
  266. package/vendor-core/plan-node-detail.js +1 -1
  267. package/vendor-core/proxy.js +3 -1
  268. package/vendor-core/python-stage.js +25 -0
  269. package/vendor-core/recommendation-rollup.js +70 -3
  270. package/vendor-core/redact.js +96 -37
  271. package/vendor-core/remediation.js +20 -0
  272. package/vendor-core/run-comparison.js +73 -29
  273. package/vendor-core/run-interpretation.js +291 -0
  274. package/vendor-core/run-metrics.js +198 -0
  275. package/vendor-core/run-outcome.js +74 -0
  276. package/vendor-core/run-payload.js +17 -0
  277. package/vendor-core/run-shape.js +40 -0
  278. package/vendor-core/run-totals.js +24 -0
  279. package/vendor-core/run-verdict.js +352 -0
  280. package/vendor-core/scaling-sim.js +4 -5
  281. package/vendor-core/scorecard-estimates.js +63 -0
  282. package/vendor-core/session-snapshot.js +7 -0
  283. package/vendor-core/shs-schemas.js +2 -2
  284. package/vendor-core/spark-memory.js +17 -0
  285. package/vendor-core/sql-stages.js +11 -0
  286. package/vendor-core/stage-plan-nodes.js +18 -0
  287. package/vendor-core/stage-quantiles.js +6 -0
  288. package/vendor-core/threshold-overrides.js +160 -0
  289. package/vendor-core/threshold-summary.js +11 -33
  290. package/vendor-core/types.js +54 -42
  291. package/vendor-core/wall-clock.js +1 -12
  292. package/vendor-core/wasted-core-hours.js +12 -9
  293. package/vendor-core/write-targets.js +312 -0
  294. package/public/assets/AutoscalingChurn-lxZHWIwq.js +0 -1
  295. package/public/assets/CacheUtilization-DuAJ-Lve.js +0 -1
  296. package/public/assets/CachingOpportunity-DC1t7o2_.js +0 -1
  297. package/public/assets/ColdStart-BOYXeiUZ.js +0 -1
  298. package/public/assets/ConfigAudit-MCPrJomC.js +0 -1
  299. package/public/assets/CoreUsageArea-ChNuqGA0.js +0 -1
  300. package/public/assets/Dashboard-VuyUGvZx.js +0 -1
  301. package/public/assets/DuplicatePlanSubtree-JZut9xwq.js +0 -1
  302. package/public/assets/DurationHistogram-_7C1w0l4.js +0 -1
  303. package/public/assets/EmptyState-D_ulfIIi.js +0 -1
  304. package/public/assets/ExecutorUtilization-B08E76jT.js +0 -1
  305. package/public/assets/GcPressure-BZMi2fTx.js +0 -1
  306. package/public/assets/ImpactEstimate-36WxuPdV.js +0 -1
  307. package/public/assets/IncompleteRun-t8JU7HRt.js +0 -1
  308. package/public/assets/JobFailures-CguB1bec.js +0 -1
  309. package/public/assets/MemoryUtilization-DDk8lz9-.js +0 -1
  310. package/public/assets/OverBroadcast-BHXLt6s7.js +0 -1
  311. package/public/assets/PartitionSizing-COj3uwV-.js +0 -1
  312. package/public/assets/PlanExplorer-CnCdEY_i.js +0 -1
  313. package/public/assets/PlanGraphRoute-BesZOW1n.js +0 -1
  314. package/public/assets/RetryWaste-CcU4ctAn.js +0 -1
  315. package/public/assets/RowPagination-D2aIcfGv.js +0 -1
  316. package/public/assets/ShuffleIO-NPHkWbg1.js +0 -1
  317. package/public/assets/Skew-CzztP-C4.js +0 -1
  318. package/public/assets/SlowHost-Dh2KHypN.js +0 -1
  319. package/public/assets/SmallFiles-CgYSuZQw.js +0 -1
  320. package/public/assets/SpeculationWaste-C8J5z1WS.js +0 -1
  321. package/public/assets/Spill-DwTQPZ_L.js +0 -1
  322. package/public/assets/StageFailed-aFeIEh3p.js +0 -1
  323. package/public/assets/StageFindingGroup-DQmAE3uk.js +0 -1
  324. package/public/assets/StageHeader-D6GBbcZp.js +0 -1
  325. package/public/assets/StagePill-CSKyM8Kd.js +0 -1
  326. package/public/assets/StageShape-J6U4cUcM.js +0 -1
  327. package/public/assets/StageSlowness-C5F2v_ZO.js +0 -1
  328. package/public/assets/Straggler-DcP3dFGA.js +0 -1
  329. package/public/assets/TaskFailures-C-vIEheo.js +0 -1
  330. package/public/assets/UnderBroadcast-DfjYGhoK.js +0 -1
  331. package/public/assets/WidgetLeadSummary-BdUUi7L5.js +0 -1
  332. package/public/assets/downsample-WmNM1qc9.js +0 -1
  333. package/public/assets/impact-sort-DxaUheWV.js +0 -1
  334. package/public/assets/inbox-LtbS_kBS.js +0 -1
  335. package/public/assets/index-27MbqTFr.js +0 -11
  336. package/public/assets/index-CWjjbtyo.css +0 -2
  337. package/public/assets/react-vendor-D2TLlSzI.js +0 -9
  338. package/public/assets/select-CC2Lyrmi.js +0 -1
  339. package/public/assets/toggle-group-lmgO21t9.js +0 -1
  340. package/public/assets/usePagedRows-jNUWgVTD.js +0 -1
  341. package/public/assets/useRecentFiles-qH5Ap4xm.js +0 -12
  342. package/public/assets/useSortMode-CvPY99UV.js +0 -1
  343. package/public/docs/assets/chunks/@localSearchIndexroot.CJBoRv1b.js +0 -1
  344. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.9ToZXcTk.js +0 -1
  345. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.9ToZXcTk.lean.js +0 -1
  346. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.CsCytCho.js +0 -1
  347. package/public/docs/assets/contributor-guide_architecture_drill-down.md.Bawx6NgP.js +0 -1
  348. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.sf9DU2bA.js +0 -1
  349. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.sf9DU2bA.lean.js +0 -1
  350. package/public/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.js +0 -1
  351. package/public/docs/assets/contributor-guide_architecture_overview.md.rhfxoGsI.js +0 -1
  352. package/public/docs/assets/contributor-guide_architecture_overview.md.rhfxoGsI.lean.js +0 -1
  353. package/public/docs/assets/contributor-guide_architecture_state-and-history.md.B8Wc_ZTg.js +0 -1
  354. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.DYwFeyjV.js +0 -1
  355. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.CBJkwH24.js +0 -6
  356. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.CBJkwH24.lean.js +0 -1
  357. package/public/docs/assets/contributor-guide_contributing.md.B2E9ni79.js +0 -1
  358. package/public/docs/assets/contributor-guide_development-setup.md.s85j2l8b.js +0 -15
  359. package/public/docs/assets/contributor-guide_development-setup.md.s85j2l8b.lean.js +0 -1
  360. package/public/docs/assets/contributor-guide_testing.md.Cnn6gz1C.js +0 -1
  361. package/public/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.js +0 -1
  362. package/public/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.lean.js +0 -1
  363. package/public/docs/assets/tuning-reference_aqe.md.BIsCtLzm.js +0 -1
  364. package/public/docs/assets/tuning-reference_aqe.md.BIsCtLzm.lean.js +0 -1
  365. package/public/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.js +0 -1
  366. package/public/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.lean.js +0 -1
  367. package/public/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.js +0 -7
  368. package/public/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.lean.js +0 -1
  369. package/public/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.js +0 -1
  370. package/public/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.lean.js +0 -1
  371. package/public/docs/assets/tuning-reference_bottleneck-failures.md.CAsFf3RR.lean.js +0 -1
  372. package/public/docs/assets/tuning-reference_bottleneck-gc.md.B4T3kJYV.js +0 -6
  373. package/public/docs/assets/tuning-reference_bottleneck-gc.md.B4T3kJYV.lean.js +0 -1
  374. package/public/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.js +0 -8
  375. package/public/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.lean.js +0 -1
  376. package/public/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.js +0 -7
  377. package/public/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.lean.js +0 -1
  378. package/public/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.js +0 -1
  379. package/public/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.lean.js +0 -1
  380. package/public/docs/assets/tuning-reference_bottleneck-shuffle.md.DDMQVOW6.lean.js +0 -1
  381. package/public/docs/assets/tuning-reference_bottleneck-skew.md.B0MzhWcX.lean.js +0 -1
  382. package/public/docs/assets/tuning-reference_bottleneck-slow-host.md.BlIo6UDW.lean.js +0 -1
  383. package/public/docs/assets/tuning-reference_bottleneck-small-files.md.DHGMTmPs.js +0 -5
  384. package/public/docs/assets/tuning-reference_bottleneck-small-files.md.DHGMTmPs.lean.js +0 -1
  385. package/public/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.js +0 -6
  386. package/public/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.lean.js +0 -1
  387. package/public/docs/assets/tuning-reference_bottleneck-straggler.md.DIbXdAcQ.js +0 -7
  388. package/public/docs/assets/tuning-reference_bottleneck-straggler.md.DIbXdAcQ.lean.js +0 -1
  389. package/public/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.CB05v3zh.js +0 -8
  390. package/public/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.CB05v3zh.lean.js +0 -1
  391. package/public/docs/assets/tuning-reference_bottleneck-utilization.md.DqC_iGkw.lean.js +0 -1
  392. package/public/docs/assets/tuning-reference_caching.md.B7aQ8asB.js +0 -1
  393. package/public/docs/assets/tuning-reference_caching.md.B7aQ8asB.lean.js +0 -1
  394. package/public/docs/assets/tuning-reference_cluster-config.md.CD6vzYtD.js +0 -1
  395. package/public/docs/assets/tuning-reference_cluster-config.md.CD6vzYtD.lean.js +0 -1
  396. package/public/docs/assets/tuning-reference_config.md.UvveiWG3.js +0 -1
  397. package/public/docs/assets/tuning-reference_config.md.UvveiWG3.lean.js +0 -1
  398. package/public/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.js +0 -1
  399. package/public/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.lean.js +0 -1
  400. package/public/docs/assets/tuning-reference_index.md.BQ_NooMV.js +0 -1
  401. package/public/docs/assets/tuning-reference_index.md.BQ_NooMV.lean.js +0 -1
  402. package/public/docs/assets/tuning-reference_intro.md.CobD-lGB.js +0 -1
  403. package/public/docs/assets/tuning-reference_intro.md.CobD-lGB.lean.js +0 -1
  404. package/public/docs/assets/tuning-reference_joins.md.BtKs_CuW.js +0 -1
  405. package/public/docs/assets/tuning-reference_joins.md.BtKs_CuW.lean.js +0 -1
  406. package/public/docs/assets/tuning-reference_memory-model.md.CWhvgevP.js +0 -1
  407. package/public/docs/assets/tuning-reference_memory-model.md.CWhvgevP.lean.js +0 -1
  408. package/public/docs/assets/tuning-reference_metrics.md.mLOh7Apj.js +0 -1
  409. package/public/docs/assets/tuning-reference_metrics.md.mLOh7Apj.lean.js +0 -1
  410. package/public/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.js +0 -1
  411. package/public/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.lean.js +0 -1
  412. package/public/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.js +0 -6
  413. package/public/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.lean.js +0 -1
  414. package/public/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.js +0 -1
  415. package/public/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.lean.js +0 -1
  416. package/public/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.js +0 -1
  417. package/public/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.lean.js +0 -1
  418. package/public/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.js +0 -1
  419. package/public/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.lean.js +0 -1
  420. package/public/docs/assets/user-guide_alternative-log-retrieval.md.ZHPSbBkF.js +0 -1
  421. package/public/docs/assets/user-guide_alternative-log-retrieval.md.ZHPSbBkF.lean.js +0 -1
  422. package/public/docs/assets/user-guide_getting-started.md.Cpe0U-eO.js +0 -3
  423. package/public/docs/assets/user-guide_getting-started.md.Cpe0U-eO.lean.js +0 -1
  424. package/public/docs/assets/user-guide_mcp-tools.md.DrphooJv.js +0 -125
  425. package/public/docs/assets/user-guide_mcp-tools.md.DrphooJv.lean.js +0 -1
  426. package/public/docs/assets/user-guide_run-comparison.md.CQc1aoU8.js +0 -1
  427. package/public/docs/assets/user-guide_run-comparison.md.CQc1aoU8.lean.js +0 -1
  428. package/public/docs/assets/user-guide_understanding-findings.md.YBBLOwWi.js +0 -1
  429. /package/public/docs/assets/{contributor-guide_architecture_drill-down.md.Bawx6NgP.lean.js → contributor-guide_architecture_drill-down.md.CdtlKHl-.lean.js} +0 -0
@@ -0,0 +1 @@
1
+ import{_ as o,o as t,c as a,a5 as r}from"./chunks/framework.DSg0KOwT.js";const s="/docs/assets/container-memory.DIO0AnIm.svg",n="/docs/assets/container-memory.dark.CP-5zuCl.svg",m=JSON.parse('{"title":"Cluster Tuning","titleTemplate":":title | Spark Tuning Reference","description":"Cluster-level sizing: executor core/memory formulas, container memory budgeting, dynamic allocation, and locality-wait tuning.","frontmatter":{"title":"Cluster Tuning","titleTemplate":":title | Spark Tuning Reference","description":"Cluster-level sizing: executor core/memory formulas, container memory budgeting, dynamic allocation, and locality-wait tuning."},"headers":[],"relativePath":"tuning-reference/cluster-config.md","filePath":"tuning-reference/cluster-config.md"}'),c={name:"tuning-reference/cluster-config.md"};function i(f,e,l,d,u,h){return t(),a("div",null,[...e[0]||(e[0]=[r('<h1 id="cluster-config" tabindex="-1">Cluster Tuning <a class="header-anchor" href="#cluster-config" aria-label="Permalink to &quot;Cluster Tuning {#cluster-config}&quot;">​</a></h1><h2 id="sizing-executors-and-containers" tabindex="-1">Sizing executors and containers <a class="header-anchor" href="#sizing-executors-and-containers" aria-label="Permalink to &quot;Sizing executors and containers&quot;">​</a></h2><p>Cluster tuning is the set of decisions that map an application&#39;s cores, memory, and executor count onto the underlying cluster (YARN or Kubernetes) plus a handful of related knobs (data locality, dynamic allocation, per-task CPU reservation) that all interact with that sizing decision.</p><p>The starting point is executor sizing. Working through a concrete example (a 10-node cluster with 16 cores and 64GB RAM per node), the derivation runs: assign 5 cores per executor; reserve about 1 core per node for Hadoop/YARN/OS daemons, leaving 15 usable cores per node (150 total); dividing by 5 cores per executor gives 30 executors, minus 1 reserved for the YARN ApplicationMaster (29); with 3 executors per node, memory per executor is 64GB / 3 ≈ 21GB, and after subtracting about 7% for YARN memory overhead that comes out to roughly 18GB usable, a recommended shape of 29 executors × 5 cores × 18GB<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. Cloudera&#39;s own worked example, on a 6-node cluster with the same 16-core / 64GB-per-node hardware, lands on the same shape: <code>--num-executors 17 --executor-cores 5 --executor-memory 19G</code>, rather than one &quot;fat&quot; executor per node (<code>--num-executors 6 --executor-cores 15 --executor-memory 63G</code>)<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. Both sources treat the resulting task count (executor-cores × num-executors) as the single most important tuning lever, since Spark cannot compensate for too little parallelism on its own<sup class="footnote-ref"><a href="#fn2" id="fnref2:1">[2:1]</a></sup>. <code>spark.executor.cores</code> is the config that controls concurrent tasks per executor; it defaults to 1 in YARN mode, or to all available cores on the worker in standalone mode<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>, and <code>spark.executor.memory</code> sets the JVM heap size per executor<sup class="footnote-ref"><a href="#fn2" id="fnref2:2">[2:2]</a></sup>. Independent of the sizing formula above, the Spark tuning guide recommends targeting 2–3 tasks per CPU core across the cluster<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>.</p><p>On top of executor memory sits container memory. Spark computes the total container/pod allocation as <code>executorMemoryMiB + memoryOverheadMiB + memoryOffHeapMiB + pysparkMemToUseMiB</code>. On YARN this is what gets allocated for the container, on Kubernetes it becomes the pod memory limit<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>. <code>spark.executor.memoryOverhead</code> itself defaults to <code>max(0.1 * executorMemory, 384MB)</code><sup class="footnote-ref"><a href="#fn5" id="fnref5:1">[5:1]</a></sup><sup class="footnote-ref"><a href="#fn3" id="fnref3:1">[3:1]</a></sup>.</p><img class="light-only" src="'+s+'" alt="Container memory sums executor heap, memory overhead, off-heap size, and pyspark memory into the requested container size; the resource manager grants a container of that size and OOMKills the executor when its actual runtime footprint spills past that limit."><img class="dark-only" src="'+n+'" alt="Container memory sums executor heap, memory overhead, off-heap size, and pyspark memory into the requested container size; the resource manager grants a container of that size and OOMKills the executor when its actual runtime footprint spills past that limit."><blockquote><p><strong>PySpark:</strong> <code>spark.executor.pyspark.memory</code> is only added as its own container-memory term when it&#39;s set explicitly; otherwise PySpark&#39;s memory use is folded into the general overhead budget rather than tracked separately.<sup class="footnote-ref"><a href="#fn5" id="fnref5:2">[5:2]</a></sup></p></blockquote><p>On Kubernetes specifically, <code>spark.kubernetes.executor.request.cores</code> and <code>spark.kubernetes.executor.limit.cores</code> take priority over <code>spark.executor.cores</code> for the pod&#39;s CPU request/limit sent to the Kubernetes scheduler<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup>, while <code>spark.executor.cores</code> remains the config Spark itself uses to size the number of concurrent task slots<sup class="footnote-ref"><a href="#fn3" id="fnref3:2">[3:2]</a></sup>.</p><p>Two more knobs round out the picture. <code>spark.locality.wait</code> (default <code>3s</code>) controls how long a task waits for a data-local placement before Spark gives up and schedules it less locally, stepping through the same wait across process-local → node-local → rack-local → any; each level can be overridden independently via <code>spark.locality.wait.process</code>, <code>.node</code>, and <code>.rack</code>, all of which default to the base wait when left unset<sup class="footnote-ref"><a href="#fn3" id="fnref3:3">[3:3]</a></sup>. Dynamic allocation lets Spark add and remove executors as work changes, but it requires one of several supporting mechanisms: an external shuffle service, shuffle tracking (<code>spark.dynamicAllocation.shuffleTracking.enabled</code>, default <code>true</code> since Spark 3.0), shuffle-block decommission, or the experimental sort-IO plugin<sup class="footnote-ref"><a href="#fn3" id="fnref3:4">[3:4]</a></sup>. Removing an executor can otherwise destroy shuffle state it&#39;s holding. Finally, <code>spark.task.cpus</code> (default <code>1</code>) sets how many cores each task reserves; the number of concurrent task slots per executor is derived jointly from it and <code>spark.executor.cores</code>, roughly <code>spark.executor.cores / spark.task.cpus</code><sup class="footnote-ref"><a href="#fn3" id="fnref3:5">[3:5]</a></sup>.</p><h2 id="spotting-a-bad-sizing-decision" tabindex="-1">Spotting a bad sizing decision <a class="header-anchor" href="#spotting-a-bad-sizing-decision" aria-label="Permalink to &quot;Spotting a bad sizing decision&quot;">​</a></h2><p>Getting that sizing wrong shows up in a handful of specific symptoms. The clearest sign of an under-sized executor layout is HDFS throughput dropping under load: &quot;HDFS client has trouble with tons of concurrent threads. It was observed that HDFS achieves full write throughput with ~5 tasks per executor&quot;<sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[1:1]</a></sup>. The &quot;fat executor&quot; case (one executor per node, using all 16 cores) shows the failure mode directly: &quot;with all 16 cores per executor... HDFS throughput will hurt and it&#39;ll result in excessive garbage [collection]&quot;<sup class="footnote-ref"><a href="#fn1" id="fnref1:2">[1:2]</a></sup>.</p><p>On the memory side, an executor or pod that dies with no Spark-level error is a strong signal that off-heap memory was never added into the container budget. Enabling <code>spark.memory.offHeap.enabled=true</code> with <code>spark.memory.offHeap.size=1g</code> on top of an 8G executor with default overhead (819MB) means real usage is 8192+819+1024 = 10,035MB, while the container was only granted 8192+819 = 9,011MB. The executor gets killed by YARN, or OOMKilled by the Kubernetes kubelet, with no warning from Spark itself<sup class="footnote-ref"><a href="#fn5" id="fnref5:3">[5:3]</a></sup>.</p><p>With dynamic allocation on, unexplained shuffle recomputation is a detectable symptom of executors being reclaimed mid-shuffle: &quot;In the event of stragglers... dynamic allocation may remove an executor before the shuffle completes, in which case the shuffle files written by that executor must be recomputed unnecessarily&quot;<sup class="footnote-ref"><a href="#fn6" id="fnref6:1">[6:1]</a></sup>. Jobs that abort with a serialized-result-size error are hitting the <code>spark.driver.maxResultSize</code> guardrail rather than a driver heap exhaustion: &quot;Jobs will be aborted if the total size [of serialized action results] is above this limit&quot;<sup class="footnote-ref"><a href="#fn3" id="fnref3:6">[3:6]</a></sup>. And on the resource-waste side, executors that are provisioned but never assigned work are a sign that dynamic allocation is targeting full parallelism against a workload made of many small tasks<sup class="footnote-ref"><a href="#fn3" id="fnref3:7">[3:7]</a></sup>.</p><h2 id="what-a-bad-sizing-decision-costs" tabindex="-1">What a bad sizing decision costs <a class="header-anchor" href="#what-a-bad-sizing-decision-costs" aria-label="Permalink to &quot;What a bad sizing decision costs&quot;">​</a></h2><p>Each symptom above carries a specific price tag. Task count (<code>executor-cores × num-executors</code>) is treated by both cited sources as the single most important tuning lever, since Spark cannot compensate for too little parallelism on its own<sup class="footnote-ref"><a href="#fn2" id="fnref2:3">[2:3]</a></sup>. Going the other way, cramming too many cores into one executor degrades HDFS throughput and drives up garbage collection<sup class="footnote-ref"><a href="#fn1" id="fnref1:3">[1:3]</a></sup>.</p><p>Container-memory misconfiguration matters because the failure is silent from Spark&#39;s point of view: off-heap memory that isn&#39;t accounted for in <code>spark.executor.memoryOverhead</code> causes the executor to actually use more memory than the container/pod was granted, so it gets killed externally (by YARN or the kubelet) with no Spark-level diagnostic to point at the real cause<sup class="footnote-ref"><a href="#fn5" id="fnref5:4">[5:4]</a></sup>.</p><p>Dynamic allocation&#39;s interaction with shuffle state matters because, before dynamic allocation existed, an executor exiting alongside its application meant all its state could be safely discarded; with dynamic allocation, the application keeps running after an executor is explicitly removed, so any later need for that executor&#39;s state forces a recompute<sup class="footnote-ref"><a href="#fn6" id="fnref6:2">[6:2]</a></sup>. That is exactly why Spark needs &quot;a mechanism to decommission an executor gracefully by preserving its state before removing it&quot;<sup class="footnote-ref"><a href="#fn6" id="fnref6:3">[6:3]</a></sup>. Without shuffle tracking, an external shuffle service, or shuffle-block decommission enabled, dynamic allocation either can&#39;t be turned on at all, or, if it&#39;s active regardless, an executor holding unpreserved shuffle output that gets removed forces exactly that unnecessary recompute<sup class="footnote-ref"><a href="#fn3" id="fnref3:8">[3:8]</a></sup><sup class="footnote-ref"><a href="#fn6" id="fnref6:4">[6:4]</a></sup>.</p><p>On the driver side, <code>spark.driver.maxResultSize</code> and <code>spark.driver.memory</code> are separate budgets that still interact: whether a high <code>maxResultSize</code> actually causes an out-of-memory error &quot;depends on spark.driver.memory and memory overhead of objects in JVM&quot;<sup class="footnote-ref"><a href="#fn3" id="fnref3:9">[3:9]</a></sup>. Raising one without considering the other doesn&#39;t fully protect the driver.</p><p>Finally, over-provisioning executors against a small-task workload wastes cluster resources: &quot;with small tasks this setting can waste a lot of resources due to executor allocation overhead, as some executor might not even do any work&quot;<sup class="footnote-ref"><a href="#fn3" id="fnref3:10">[3:10]</a></sup>.</p><h2 id="sizing-the-cluster-correctly" tabindex="-1">Sizing the cluster correctly <a class="header-anchor" href="#sizing-the-cluster-correctly" aria-label="Permalink to &quot;Sizing the cluster correctly&quot;">​</a></h2><p>Avoiding those costs starts from a formula, not one fat executor per node. Apply the balanced-executor formula instead; the worked examples above give shapes like 29 × 5 × 18GB for a 10-node/16-core/64GB cluster, or 17 × 5 × 19G for a 6-node cluster with the same per-node hardware<sup class="footnote-ref"><a href="#fn1" id="fnref1:4">[1:4]</a></sup><sup class="footnote-ref"><a href="#fn2" id="fnref2:4">[2:4]</a></sup>. Keep <code>--executor-cores</code> at or below roughly 5 so HDFS client concurrency stays in the range where it sustains full write throughput<sup class="footnote-ref"><a href="#fn1" id="fnref1:5">[1:5]</a></sup>.</p><p>When enabling off-heap memory, account for <code>spark.memory.offHeap.size</code> in the container/pod memory budget explicitly; <code>spark.executor.memoryOverhead</code>&#39;s default of <code>max(0.1 * executorMemory, 384MB)</code> does not include it<sup class="footnote-ref"><a href="#fn5" id="fnref5:5">[5:5]</a></sup>. If you&#39;re still referencing the older <code>spark.yarn.executor.memoryOverhead</code> name, note it was removed in Spark 3.0<sup class="footnote-ref"><a href="#fn5" id="fnref5:6">[5:6]</a></sup>.</p><p>On Kubernetes, remember that <code>spark.kubernetes.executor.request.cores</code> and <code>.limit.cores</code> govern the pod&#39;s CPU request/limit and take priority over <code>spark.executor.cores</code> for that purpose<sup class="footnote-ref"><a href="#fn6" id="fnref6:5">[6:5]</a></sup>, while <code>spark.executor.cores</code> separately governs Spark&#39;s own task-slot count<sup class="footnote-ref"><a href="#fn3" id="fnref3:11">[3:11]</a></sup>. The two need to be reasoned about together rather than assumed to be redundant.</p><p>Tune <code>spark.locality.wait</code> (and its per-level overrides) upward when tasks are long-running and locality is poor, since the default is tuned for typical workloads<sup class="footnote-ref"><a href="#fn3" id="fnref3:12">[3:12]</a></sup>; set <code>spark.locality.wait.node</code> to <code>0</code> to skip straight to rack locality when node locality isn&#39;t achievable<sup class="footnote-ref"><a href="#fn3" id="fnref3:13">[3:13]</a></sup>.</p><p>Turn on shuffle tracking (<code>spark.dynamicAllocation.shuffleTracking.enabled</code>, default <code>true</code> since Spark 3.0) or the external shuffle service so dynamic allocation can reclaim executors without losing shuffle output or forcing recomputation<sup class="footnote-ref"><a href="#fn3" id="fnref3:14">[3:14]</a></sup><sup class="footnote-ref"><a href="#fn6" id="fnref6:6">[6:6]</a></sup>. The external shuffle service additionally lets persisted RDD blocks survive executor removal when <code>spark.shuffle.service.fetch.rdd.enabled</code> is set, and executors holding cached blocks are, by default, never removed at all, tunable via <code>spark.dynamicAllocation.cachedExecutorIdleTimeout</code><sup class="footnote-ref"><a href="#fn6" id="fnref6:7">[6:7]</a></sup>.</p><p>Set <code>spark.driver.maxResultSize</code> as a guardrail on serialized action-result size, and size <code>spark.driver.memory</code> with it in mind rather than in isolation<sup class="footnote-ref"><a href="#fn3" id="fnref3:15">[3:15]</a></sup>. Use <code>spark.dynamicAllocation.executorAllocationRatio</code> (default <code>1.0</code>, added in 2.4.0) to scale down over-allocation when tasks are small; for example, a value of <code>0.5</code> halves the target executor count dynamic allocation would otherwise compute<sup class="footnote-ref"><a href="#fn3" id="fnref3:16">[3:16]</a></sup>. Raise <code>spark.task.cpus</code> above its default of <code>1</code> when a task needs more than one core; doing so proportionally reduces concurrent task slots per executor (<code>spark.executor.cores / spark.task.cpus</code>)<sup class="footnote-ref"><a href="#fn3" id="fnref3:17">[3:17]</a></sup>.</p><h2 id="bottleneck-autoscaling-churn" tabindex="-1">Autoscaling churn <a class="header-anchor" href="#bottleneck-autoscaling-churn" aria-label="Permalink to &quot;Autoscaling churn {#bottleneck-autoscaling-churn}&quot;">​</a></h2><p><span class="tag">CHRN</span> <span class="tag">EXPERIMENTAL</span></p><p>Dynamic allocation adds executors once tasks back up and frees them once they go idle<sup class="footnote-ref"><a href="#fn6" id="fnref6:8">[6:8]</a></sup>. Churn is a narrower failure mode inside that same mechanism: executors that get stood up and torn down again before they&#39;ve done much useful work, cycling through provisioning and JVM startup cost repeatedly instead of running tasks.</p><h3 id="how-it-s-detected" tabindex="-1">How it&#39;s detected <a class="header-anchor" href="#how-it-s-detected" aria-label="Permalink to &quot;How it&#39;s detected&quot;">​</a></h3><p>An executor counts as short-lived when its lifetime, from its <code>ExecutorAdded</code> event to either its <code>ExecutorRemoved</code> event or the end of the run if it was never removed, is under 2 minutes. This is evaluated once a run has added at least 5 executors, a floor that keeps a two- or three-executor job from registering on ordinary scale-down.</p><table tabindex="0"><thead><tr><th>Signal</th><th>Warning</th><th>Critical</th></tr></thead><tbody><tr><td>Share of added executors that are short-lived (&lt; 2 min lifetime)</td><td>&gt; 30%</td><td>&gt; 60%</td></tr></tbody></table><p>The 2-minute lifetime cutoff and the 30%/60% split are unvalidated heuristics rather than figures published in Spark&#39;s own documentation or benchmarks, so treat a finding here as a prompt to look at the executor timeline rather than a calibrated verdict.</p><h3 id="why-it-matters" tabindex="-1">Why it matters <a class="header-anchor" href="#why-it-matters" aria-label="Permalink to &quot;Why it matters&quot;">​</a></h3><p>Every short-lived executor pays the same provisioning and JVM-startup cost as a long-lived one, but returns little task work in exchange. A run that keeps flapping between scaling up and down spends more of its wall-clock time paying that repeated overhead than one that settles into a stable executor count.</p><h3 id="how-to-fix-it" tabindex="-1">How to fix it <a class="header-anchor" href="#how-to-fix-it" aria-label="Permalink to &quot;How to fix it&quot;">​</a></h3><ul><li>Raise <code>spark.dynamicAllocation.executorIdleTimeout</code> (default 60s<sup class="footnote-ref"><a href="#fn3" id="fnref3:18">[3:18]</a></sup>) so an executor survives a brief lull instead of being released the moment it goes idle, only to be requested again shortly after.</li><li>Widen the gap between <code>spark.dynamicAllocation.minExecutors</code> and <code>.maxExecutors</code><sup class="footnote-ref"><a href="#fn3" id="fnref3:19">[3:19]</a></sup>: bounds set too close together force Spark to repeatedly add and remove executors to track small fluctuations in the task backlog instead of settling into a stable range.</li></ul><h2 id="sources" tabindex="-1">Sources <a class="header-anchor" href="#sources" aria-label="Permalink to &quot;Sources&quot;">​</a></h2><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p><a href="https://raw.githubusercontent.com/spoddutur/spark-notes/master/distribution_of_executors_cores_and_memory_for_spark_application.md" target="_blank" rel="noreferrer">Distribution of Executors, Cores and Memory for a Spark Application</a> <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a> <a href="#fnref1:2" class="footnote-backref">↩︎</a> <a href="#fnref1:3" class="footnote-backref">↩︎</a> <a href="#fnref1:4" class="footnote-backref">↩︎</a> <a href="#fnref1:5" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p><a href="https://blog.cloudera.com/how-to-tune-your-apache-spark-jobs-part-2/" target="_blank" rel="noreferrer">How to Tune Your Apache Spark Jobs (Part 2)</a> <a href="#fnref2" class="footnote-backref">↩︎</a> <a href="#fnref2:1" class="footnote-backref">↩︎</a> <a href="#fnref2:2" class="footnote-backref">↩︎</a> <a href="#fnref2:3" class="footnote-backref">↩︎</a> <a href="#fnref2:4" class="footnote-backref">↩︎</a></p></li><li id="fn3" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/configuration.html" target="_blank" rel="noreferrer">Configuration — Spark</a> <a href="#fnref3" class="footnote-backref">↩︎</a> <a href="#fnref3:1" class="footnote-backref">↩︎</a> <a href="#fnref3:2" class="footnote-backref">↩︎</a> <a href="#fnref3:3" class="footnote-backref">↩︎</a> <a href="#fnref3:4" class="footnote-backref">↩︎</a> <a href="#fnref3:5" class="footnote-backref">↩︎</a> <a href="#fnref3:6" class="footnote-backref">↩︎</a> <a href="#fnref3:7" class="footnote-backref">↩︎</a> <a href="#fnref3:8" class="footnote-backref">↩︎</a> <a href="#fnref3:9" class="footnote-backref">↩︎</a> <a href="#fnref3:10" class="footnote-backref">↩︎</a> <a href="#fnref3:11" class="footnote-backref">↩︎</a> <a href="#fnref3:12" class="footnote-backref">↩︎</a> <a href="#fnref3:13" class="footnote-backref">↩︎</a> <a href="#fnref3:14" class="footnote-backref">↩︎</a> <a href="#fnref3:15" class="footnote-backref">↩︎</a> <a href="#fnref3:16" class="footnote-backref">↩︎</a> <a href="#fnref3:17" class="footnote-backref">↩︎</a> <a href="#fnref3:18" class="footnote-backref">↩︎</a> <a href="#fnref3:19" class="footnote-backref">↩︎</a></p></li><li id="fn4" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/tuning.html" target="_blank" rel="noreferrer">Spark Tuning Guide</a> <a href="#fnref4" class="footnote-backref">↩︎</a></p></li><li id="fn5" class="footnote-item"><p><a href="https://luminousmen.com/post/dive-into-spark-memory" target="_blank" rel="noreferrer">Dive into Spark Memory</a> <a href="#fnref5" class="footnote-backref">↩︎</a> <a href="#fnref5:1" class="footnote-backref">↩︎</a> <a href="#fnref5:2" class="footnote-backref">↩︎</a> <a href="#fnref5:3" class="footnote-backref">↩︎</a> <a href="#fnref5:4" class="footnote-backref">↩︎</a> <a href="#fnref5:5" class="footnote-backref">↩︎</a> <a href="#fnref5:6" class="footnote-backref">↩︎</a></p></li><li id="fn6" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/job-scheduling.html" target="_blank" rel="noreferrer">Job Scheduling — Dynamic Resource Allocation</a> <a href="#fnref6" class="footnote-backref">↩︎</a> <a href="#fnref6:1" class="footnote-backref">↩︎</a> <a href="#fnref6:2" class="footnote-backref">↩︎</a> <a href="#fnref6:3" class="footnote-backref">↩︎</a> <a href="#fnref6:4" class="footnote-backref">↩︎</a> <a href="#fnref6:5" class="footnote-backref">↩︎</a> <a href="#fnref6:6" class="footnote-backref">↩︎</a> <a href="#fnref6:7" class="footnote-backref">↩︎</a> <a href="#fnref6:8" class="footnote-backref">↩︎</a></p></li></ol></section>',41)])])}const g=o(c,[["render",i]]);export{m as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as o,o as t,c as a,a5 as r}from"./chunks/framework.DSg0KOwT.js";const s="/docs/assets/container-memory.DIO0AnIm.svg",n="/docs/assets/container-memory.dark.CP-5zuCl.svg",m=JSON.parse('{"title":"Cluster Tuning","titleTemplate":":title | Spark Tuning Reference","description":"Cluster-level sizing: executor core/memory formulas, container memory budgeting, dynamic allocation, and locality-wait tuning.","frontmatter":{"title":"Cluster Tuning","titleTemplate":":title | Spark Tuning Reference","description":"Cluster-level sizing: executor core/memory formulas, container memory budgeting, dynamic allocation, and locality-wait tuning."},"headers":[],"relativePath":"tuning-reference/cluster-config.md","filePath":"tuning-reference/cluster-config.md"}'),c={name:"tuning-reference/cluster-config.md"};function i(f,e,l,d,u,h){return t(),a("div",null,[...e[0]||(e[0]=[r("",41)])])}const g=o(c,[["render",i]]);export{m as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as a,o,c as t,a5 as r}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"Spark Config Quick-Reference","titleTemplate":":title | Spark Tuning Reference","description":"Cross-reference of the Spark/PySpark config properties and JVM GC flags discussed elsewhere in the guide, with defaults and version notes.","frontmatter":{"title":"Spark Config Quick-Reference","titleTemplate":":title | Spark Tuning Reference","description":"Cross-reference of the Spark/PySpark config properties and JVM GC flags discussed elsewhere in the guide, with defaults and version notes."},"headers":[],"relativePath":"tuning-reference/config.md","filePath":"tuning-reference/config.md"}'),s={name:"tuning-reference/config.md"};function n(f,e,c,i,d,l){return o(),t("div",null,[...e[0]||(e[0]=[r('<h1 id="config" tabindex="-1">Spark Config Quick-Reference <a class="header-anchor" href="#config" aria-label="Permalink to &quot;Spark Config Quick-Reference {#config}&quot;">​</a></h1><p>A cross-reference of the Spark and PySpark configuration properties discussed elsewhere in this guide, plus the JVM garbage-collection flags relevant to executor tuning. Defaults and version notes below are traced to Spark&#39;s own configuration docs, its SQLConf source, and the cited literature. Where a source doesn&#39;t give a hard number (a range, a ceiling, a recommended workload profile), the table leaves it out rather than guess.</p><h2 id="spark-properties" tabindex="-1">Spark properties <a class="header-anchor" href="#spark-properties" aria-label="Permalink to &quot;Spark properties&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Key</th><th>Default</th><th>Recommended</th><th>Notes</th></tr></thead><tbody><tr><td><code>spark.sql.shuffle.partitions</code></td><td>200 (unchanged since Spark 1.1.0)<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup><sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup><sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup></td><td></td><td>AQE doesn&#39;t override this config value; see notes below for the effective runtime count.</td></tr><tr><td><code>spark.sql.adaptive.enabled</code></td><td><code>true</code><sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup></td><td><code>true</code></td><td></td></tr><tr><td><code>spark.sql.adaptive.coalescePartitions.enabled</code></td><td><code>true</code> (since 3.0.0)<sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[1:1]</a></sup></td><td><code>true</code></td><td>Requires AQE; merges contiguous small post-shuffle partitions instead of one task per configured partition<sup class="footnote-ref"><a href="#fn1" id="fnref1:2">[1:2]</a></sup>.</td></tr><tr><td><code>spark.sql.adaptive.coalescePartitions.initialPartitionNum</code></td><td>unset → falls back to <code>spark.sql.shuffle.partitions</code> (200)<sup class="footnote-ref"><a href="#fn1" id="fnref1:3">[1:3]</a></sup></td><td></td><td>Sets the partition count entering the coalescing step.</td></tr><tr><td><code>spark.sql.adaptive.coalescePartitions.parallelismFirst</code></td><td><code>true</code> (since 3.2.0)<sup class="footnote-ref"><a href="#fn1" id="fnref1:4">[1:4]</a></sup><sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup></td><td><code>false</code> on busy clusters<sup class="footnote-ref"><a href="#fn1" id="fnref1:5">[1:5]</a></sup></td><td>When <code>true</code>, ignores <code>advisoryPartitionSizeInBytes</code> and derives a target from the cluster&#39;s default parallelism, respecting only the <code>minPartitionSize</code> floor.</td></tr><tr><td><code>spark.sql.adaptive.advisoryPartitionSizeInBytes</code></td><td>64MB (since 3.0.0)<sup class="footnote-ref"><a href="#fn1" id="fnref1:6">[1:6]</a></sup><sup class="footnote-ref"><a href="#fn4" id="fnref4:1">[4:1]</a></sup><sup class="footnote-ref"><a href="#fn5" id="fnref5:1">[5:1]</a></sup></td><td></td><td>Ignored during coalescing when <code>parallelismFirst=true</code> (the default).</td></tr><tr><td><code>spark.sql.adaptive.coalescePartitions.minPartitionSize</code></td><td>1MB (since 3.2.0)<sup class="footnote-ref"><a href="#fn5" id="fnref5:2">[5:2]</a></sup></td><td></td><td>Floor enforced when the target size is ignored: the default <code>parallelismFirst=true</code> case<sup class="footnote-ref"><a href="#fn1" id="fnref1:7">[1:7]</a></sup>.</td></tr><tr><td><code>spark.memory.fraction</code></td><td>0.6<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup></td><td></td><td>See notes below; no hard 0–1 ceiling documented in the cited sources.</td></tr><tr><td><code>spark.memory.storageFraction</code></td><td>0.5<sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup></td><td></td><td>A fraction <em>of</em> the region sized by <code>spark.memory.fraction</code>, not an independent pool; see notes below.</td></tr><tr><td><code>spark.sql.execution.arrow.pyspark.enabled</code></td><td><code>false</code><sup class="footnote-ref"><a href="#fn8" id="fnref8">[8]</a></sup></td><td></td><td>See PySpark callout below.</td></tr><tr><td><code>spark.sql.execution.arrow.pyspark.fallback.enabled</code></td><td><code>true</code> (inherited from the deprecated <code>arrow.fallback.enabled</code>)<sup class="footnote-ref"><a href="#fn8" id="fnref8:1">[8:1]</a></sup><sup class="footnote-ref"><a href="#fn5" id="fnref5:3">[5:3]</a></sup><sup class="footnote-ref"><a href="#fn4" id="fnref4:2">[4:2]</a></sup></td><td></td><td>Falls back to the non-Arrow path automatically if a conversion error occurs, before computation runs<sup class="footnote-ref"><a href="#fn8" id="fnref8:2">[8:2]</a></sup>.</td></tr><tr><td><code>spark.dynamicAllocation.shuffleTracking.enabled</code></td><td><code>true</code> (since 3.0.0)<sup class="footnote-ref"><a href="#fn4" id="fnref4:3">[4:3]</a></sup></td><td></td><td>Lets dynamic allocation track shuffle files per executor without an external shuffle service.</td></tr><tr><td><code>spark.dynamicAllocation.shuffleTracking.timeout</code></td><td><code>infinity</code><sup class="footnote-ref"><a href="#fn4" id="fnref4:4">[4:4]</a></sup></td><td></td><td>Executors holding shuffle data wait for it to be garbage collected before release, by default; set a finite value if GC isn&#39;t keeping up.</td></tr><tr><td><code>spark.shuffle.service.enabled</code></td><td></td><td></td><td>External shuffle service; see <code>#config-shuffle-service</code> below.</td></tr><tr><td><code>spark.dynamicAllocation.minExecutors</code> / <code>maxExecutors</code></td><td><code>0</code> / <code>infinity</code><sup class="footnote-ref"><a href="#fn4" id="fnref4:5">[4:5]</a></sup></td><td></td><td>Autoscale bounds; see <code>#config-autoscale-bounds</code> below.</td></tr><tr><td><code>spark.serializer</code></td><td><code>org.apache.spark.serializer.JavaSerializer</code><sup class="footnote-ref"><a href="#fn4" id="fnref4:6">[4:6]</a></sup></td><td><code>KryoSerializer</code></td><td>See <code>#config-serializer</code> below.</td></tr><tr><td><code>spark.executor.memoryOverhead</code></td><td>greater of 10% of executor memory or 384MB<sup class="footnote-ref"><a href="#fn7" id="fnref7:1">[7:1]</a></sup></td><td></td><td>Off-heap overhead; see <code>#config-memory-overhead</code> below.</td></tr></tbody></table><h3 id="shuffle-partitions-and-aqe-coalescing" tabindex="-1">Shuffle partitions and AQE coalescing <a class="header-anchor" href="#shuffle-partitions-and-aqe-coalescing" aria-label="Permalink to &quot;Shuffle partitions and AQE coalescing&quot;">​</a></h3><p>The 200 default for <code>spark.sql.shuffle.partitions</code> hasn&#39;t moved since 1.1.0, and AQE (on by default since Spark 3.0) doesn&#39;t change that config value<sup class="footnote-ref"><a href="#fn1" id="fnref1:8">[1:8]</a></sup><sup class="footnote-ref"><a href="#fn2" id="fnref2:1">[2:1]</a></sup><sup class="footnote-ref"><a href="#fn3" id="fnref3:1">[3:1]</a></sup>. What it changes is the effective number of partitions used at runtime. With <code>coalescePartitions.enabled</code> also on by default, Spark merges small contiguous post-shuffle partitions instead of running one task per configured partition<sup class="footnote-ref"><a href="#fn1" id="fnref1:9">[1:9]</a></sup>. Because <code>parallelismFirst</code> defaults to <code>true</code> since 3.2.0, that merge target usually isn&#39;t the 64MB <code>advisoryPartitionSizeInBytes</code> value; it&#39;s derived from the cluster&#39;s default parallelism, with <code>minPartitionSize</code> (1MB) as the only enforced floor<sup class="footnote-ref"><a href="#fn1" id="fnref1:10">[1:10]</a></sup><sup class="footnote-ref"><a href="#fn5" id="fnref5:4">[5:4]</a></sup>. Databricks&#39; own writeup on AQE shows the reduce-task count actually shrinking based on measured data volume<sup class="footnote-ref"><a href="#fn9" id="fnref9">[9]</a></sup>, and Learning Spark accordingly calls the static 200 default &quot;too high for smaller or streaming workloads&quot;<sup class="footnote-ref"><a href="#fn3" id="fnref3:2">[3:2]</a></sup>. A related, distinct knob (<code>spark.sql.adaptive.coalescePartitions.minPartitionNum</code>) sets a minimum parallelism floor for data that&#39;s slow to compute despite being small, per High Performance Spark<sup class="footnote-ref"><a href="#fn10" id="fnref10">[10]</a></sup>.</p><h3 id="spark-memory-fraction-and-spark-memory-storagefraction" tabindex="-1"><code>spark.memory.fraction</code> and <code>spark.memory.storageFraction</code> <a class="header-anchor" href="#spark-memory-fraction-and-spark-memory-storagefraction" aria-label="Permalink to &quot;`spark.memory.fraction` and `spark.memory.storageFraction`&quot;">​</a></h3><p><code>spark.memory.fraction</code> sizes the unified execution/storage region (M) as a fraction of (JVM heap − 300MiB); the tuning guide frames it as a knob to fit M &quot;comfortably within the JVM&#39;s old or tenured generation&quot; rather than stating an enforced numeric range<sup class="footnote-ref"><a href="#fn6" id="fnref6:1">[6:1]</a></sup>. The cited sources document no failure threshold tied to a specific value like 0.9. What they do document is the risk of pushing the fraction high: the complement, <code>1 − spark.memory.fraction</code>, is untracked &quot;User Memory&quot; for UDFs, Python/Arrow glue, and native buffers, and starving it (which is what raising the fraction toward 0.9 does) leads to &quot;GC pressure or random OOMs,&quot; with no warning from Spark<sup class="footnote-ref"><a href="#fn7" id="fnref7:2">[7:2]</a></sup>.</p><p><code>spark.memory.storageFraction</code> isn&#39;t independent of that: it&#39;s a fraction <em>of</em> M, the same region <code>spark.memory.fraction</code> sizes<sup class="footnote-ref"><a href="#fn6" id="fnref6:2">[6:2]</a></sup>. Inside that shared pool, execution can evict storage down to the threshold set by <code>storageFraction</code>, but storage can never evict execution<sup class="footnote-ref"><a href="#fn11" id="fnref11">[11]</a></sup><sup class="footnote-ref"><a href="#fn6" id="fnref6:3">[6:3]</a></sup>. With the defaults (0.6 and 0.5), that works out to execution and storage each getting 30% of usable heap; raising <code>memory.fraction</code> scales both pools at once, while <code>storageFraction</code> only re-splits the pool that&#39;s already been carved out<sup class="footnote-ref"><a href="#fn7" id="fnref7:3">[7:3]</a></sup>.</p><blockquote><p><strong>PySpark:</strong> <code>spark.sql.execution.arrow.pyspark.enabled</code> is off by default and governs Arrow use for <code>DataFrame.toPandas()</code> and <code>SparkSession.createDataFrame()</code> from a Pandas DataFrame or NumPy array<sup class="footnote-ref"><a href="#fn8" id="fnref8:3">[8:3]</a></sup>. Its documented risk is a type-coverage gap, not silent data corruption: <code>ArrayType</code> of <code>TimestampType</code> is explicitly unsupported<sup class="footnote-ref"><a href="#fn4" id="fnref4:7">[4:7]</a></sup><sup class="footnote-ref"><a href="#fn5" id="fnref5:5">[5:5]</a></sup>, and more generally an unsupported column type raises an error rather than converting wrongly<sup class="footnote-ref"><a href="#fn8" id="fnref8:4">[8:4]</a></sup>. <code>spark.sql.execution.arrow.pyspark.fallback.enabled</code> defaults to <code>true</code> and automatically falls back to the non-Arrow path if a conversion error occurs, before any computation runs<sup class="footnote-ref"><a href="#fn8" id="fnref8:5">[8:5]</a></sup><sup class="footnote-ref"><a href="#fn5" id="fnref5:6">[5:6]</a></sup><sup class="footnote-ref"><a href="#fn4" id="fnref4:8">[4:8]</a></sup>. So the failure mode is an exception plus fallback, not a wrong result. Separately, and not specific to this Arrow config, PySpark&#39;s own type coercion has its own hazards: numeric values passed for <code>ByteType</code>/<code>ShortType</code>/<code>IntegerType</code> must fall within fixed ranges or get rejected or converted unexpectedly<sup class="footnote-ref"><a href="#fn2" id="fnref2:2">[2:2]</a></sup>.</p></blockquote><h3 id="config-shuffle-service" tabindex="-1">Shuffle service <a class="header-anchor" href="#config-shuffle-service" aria-label="Permalink to &quot;Shuffle service {#config-shuffle-service}&quot;">​</a></h3><p>During a shuffle, an executor writes its map output to local disk and then serves fetch requests for that data itself<sup class="footnote-ref"><a href="#fn12" id="fnref12">[12]</a></sup>. Dynamic allocation can reclaim an idle executor before a later stage has fetched all the shuffle blocks it holds (especially with stragglers, tasks that run much longer than their peers), and once that executor is gone, any stage that still needs its shuffle output gets a <code>FetchFailed</code> and has to recompute it<sup class="footnote-ref"><a href="#fn12" id="fnref12:1">[12:1]</a></sup>. The external shuffle service fixes this by moving shuffle-block serving out of the executor process: it&#39;s a long-running process on each node, independent of any particular application&#39;s executors, and once <code>spark.shuffle.service.enabled</code> is <code>true</code>, executors fetch shuffle blocks from it instead of from each other, so an executor&#39;s shuffle output keeps being served after that executor is reclaimed<sup class="footnote-ref"><a href="#fn12" id="fnref12:2">[12:2]</a></sup><sup class="footnote-ref"><a href="#fn4" id="fnref4:9">[4:9]</a></sup>. Setting up dynamic allocation requires enabling this service on every worker node in addition to <code>spark.dynamicAllocation.enabled</code><sup class="footnote-ref"><a href="#fn2" id="fnref2:3">[2:3]</a></sup>. As of Spark 3.0, shuffle-file tracking (<code>spark.dynamicAllocation.shuffleTracking.enabled</code>) is an alternative to the external shuffle service for the same safe-removal problem<sup class="footnote-ref"><a href="#fn4" id="fnref4:10">[4:10]</a></sup>.</p><p><strong>Limitations / false-positive risk:</strong> the service can be intentionally left off when dynamic allocation relies on shuffle-file tracking instead, so a disabled setting is not automatically a misconfiguration.</p><h3 id="config-autoscale-bounds" tabindex="-1">Autoscale bounds <a class="header-anchor" href="#config-autoscale-bounds" aria-label="Permalink to &quot;Autoscale bounds {#config-autoscale-bounds}&quot;">​</a></h3><p>By default, <code>spark.dynamicAllocation.minExecutors</code> is <code>0</code> and <code>spark.dynamicAllocation.maxExecutors</code> is <code>infinity</code><sup class="footnote-ref"><a href="#fn4" id="fnref4:11">[4:11]</a></sup>. Leaving <code>maxExecutors</code> unset therefore leaves dynamic allocation unbounded on the high end as far as Spark itself is concerned; in practice the ceiling ends up being whatever the cluster or scheduler enforces outside Spark<sup class="footnote-ref"><a href="#fn4" id="fnref4:12">[4:12]</a></sup>. <code>spark.dynamicAllocation.initialExecutors</code> defaults to <code>minExecutors</code>, unless <code>--num-executors</code> (or <code>spark.executor.instances</code>) sets a larger starting value<sup class="footnote-ref"><a href="#fn4" id="fnref4:13">[4:13]</a></sup>. Whatever executor count <code>executorAllocationRatio</code> computes to maximize parallelism, that target is still clamped by the <code>minExecutors</code>/<code>maxExecutors</code> floor and ceiling<sup class="footnote-ref"><a href="#fn4" id="fnref4:14">[4:14]</a></sup>.</p><p><strong>Limitations / false-positive risk:</strong> an unbounded <code>maxExecutors</code> is often deliberate on clusters where the scheduler enforces the real ceiling, so an unset high end is not always wrong.</p><h3 id="config-serializer" tabindex="-1">Serializer <a class="header-anchor" href="#config-serializer" aria-label="Permalink to &quot;Serializer {#config-serializer}&quot;">​</a></h3><p>The default <code>spark.serializer</code> is <code>org.apache.spark.serializer.JavaSerializer</code>, which works with any <code>Serializable</code> Java object but is &quot;quite slow&quot;; Spark&#39;s configuration reference recommends switching to <code>KryoSerializer</code> &quot;when speed is necessary&quot;<sup class="footnote-ref"><a href="#fn4" id="fnref4:15">[4:15]</a></sup>. The tuning guide is more specific: Kryo is &quot;significantly faster and more compact than Java serialization (often as much as 10x),&quot; though it doesn&#39;t support all <code>Serializable</code> types and needs its classes registered in advance for best performance<sup class="footnote-ref"><a href="#fn6" id="fnref6:4">[6:4]</a></sup>. That registration requirement is the stated reason Kryo isn&#39;t the default: &quot;The only reason Kryo is not the default is because of the custom registration requirement,&quot; and Spark recommends trying it for any network-intensive application<sup class="footnote-ref"><a href="#fn6" id="fnref6:5">[6:5]</a></sup>. Since Spark 2.0.0, Spark internally uses Kryo regardless of this setting when shuffling RDDs of simple types, arrays of simple types, or strings, and auto-registers common Scala classes via Twitter chill&#39;s <code>AllScalaRegistrar</code><sup class="footnote-ref"><a href="#fn6" id="fnref6:6">[6:6]</a></sup>. A practitioner summary puts Java serialization at &quot;2-10x slower and larger on the wire&quot; on every shuffle, with the caveat that classes left unregistered silently fall back to Java serialization unless registered via <code>spark.kryo.classesToRegister</code> or <code>spark.kryo.registrator</code><sup class="footnote-ref"><a href="#fn13" id="fnref13">[13]</a></sup>. Persisting RDDs in serialized form is another case where Kryo can be more space-efficient than Java serialization<sup class="footnote-ref"><a href="#fn10" id="fnref10:1">[10:1]</a></sup>.</p><p><strong>Limitations / false-positive risk:</strong> <code>JavaSerializer</code> may be kept on purpose when an application depends on <code>Serializable</code> types Kryo cannot handle, so a non-Kryo setting is not necessarily a mistake.</p><h3 id="config-memory-overhead" tabindex="-1">Memory overhead <a class="header-anchor" href="#config-memory-overhead" aria-label="Permalink to &quot;Memory overhead {#config-memory-overhead}&quot;">​</a></h3><p><code>spark.executor.memoryOverhead</code> covers everything an executor needs outside the JVM heap sized by <code>spark.executor.memory</code>: thread stacks, JIT buffers, metaspace, JNI, native libraries, and, if <code>spark.executor.pyspark.memory</code> isn&#39;t set separately, the memory used by PySpark&#39;s per-task Python worker processes<sup class="footnote-ref"><a href="#fn7" id="fnref7:4">[7:4]</a></sup>. Left unset, Spark defaults it to the greater of 10% of executor memory or 384MB<sup class="footnote-ref"><a href="#fn7" id="fnref7:5">[7:5]</a></sup>; the configuration reference formalizes that floor as <code>spark.executor.minMemoryOverhead</code> (default <code>384m</code>, since Spark 4.0) and the percentage as <code>spark.executor.memoryOverheadFactor</code> (default 0.10, or 0.40 for non-JVM jobs on Kubernetes, since those need more non-JVM heap space)<sup class="footnote-ref"><a href="#fn4" id="fnref4:16">[4:16]</a></sup>. The resource manager (YARN&#39;s NodeManager, or the Kubernetes kubelet) sizes the container/pod to the sum of <code>spark.executor.memoryOverhead</code>, <code>spark.executor.memory</code>, <code>spark.memory.offHeap.size</code>, and <code>spark.executor.pyspark.memory</code><sup class="footnote-ref"><a href="#fn4" id="fnref4:17">[4:17]</a></sup>. If off-heap memory or PySpark worker processes consume memory that isn&#39;t reflected in a correspondingly larger <code>memoryOverhead</code>, that extra usage still shows up in the process&#39;s actual RSS, which the resource manager does track. Once real usage exceeds the container&#39;s allocated size, the executor is killed by YARN or OOMKilled by the kubelet, with no Spark-level error, just an abrupt process death<sup class="footnote-ref"><a href="#fn7" id="fnref7:6">[7:6]</a></sup>.</p><p><strong>Limitations / false-positive risk:</strong> the default overhead is adequate for many JVM-only jobs, so a value left at the default only signals trouble on off-heap-heavy or PySpark workloads that need more non-heap room.</p><h2 id="jvm-gc-flags" tabindex="-1">JVM GC flags <a class="header-anchor" href="#jvm-gc-flags" aria-label="Permalink to &quot;JVM GC flags&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Flag</th><th>Effect</th></tr></thead><tbody><tr><td><code>-XX:+UseG1GC</code></td><td>Default collector since Spark 4.0.0, which defaults to JDK 17<sup class="footnote-ref"><a href="#fn6" id="fnref6:7">[6:7]</a></sup>.</td></tr><tr><td><code>-XX:G1HeapRegionSize</code></td><td>May need raising alongside large executor heaps<sup class="footnote-ref"><a href="#fn6" id="fnref6:8">[6:8]</a></sup>.</td></tr><tr><td><code>-XX:InitiatingHeapOccupancyPercent</code></td><td>Tuned (with <code>-XX:ConcGCThreads</code> and RSet-update settings) to fix a documented ~100-second G1 full-GC pause on an 88GB executor heap<sup class="footnote-ref"><a href="#fn14" id="fnref14">[14]</a></sup>.</td></tr><tr><td><code>-XX:ConcGCThreads</code></td><td>See <code>-XX:InitiatingHeapOccupancyPercent</code> above<sup class="footnote-ref"><a href="#fn14" id="fnref14:1">[14:1]</a></sup>.</td></tr><tr><td><code>-XX:+UseZGC</code></td><td>Concurrent, low-latency collector; sub-millisecond pause target independent of heap size, from a few hundred megabytes up to 16TB<sup class="footnote-ref"><a href="#fn15" id="fnref15">[15]</a></sup>.</td></tr></tbody></table><h3 id="g1-vs-zgc" tabindex="-1">G1 vs. ZGC <a class="header-anchor" href="#g1-vs-zgc" aria-label="Permalink to &quot;G1 vs. ZGC&quot;">​</a></h3><p>G1 was designed as a CMS replacement aiming at both throughput and low latency: it partitions the heap into equal-sized regions and copies out only the live objects from collected regions rather than compacting the whole heap<sup class="footnote-ref"><a href="#fn14" id="fnref14:2">[14:2]</a></sup>. Even so, a documented 88GB-heap Spark benchmark hit &quot;unacceptable full GC,&quot; with one job pausing nearly 100 seconds under default G1 settings. Only after tuning <code>InitiatingHeapOccupancyPercent</code>, <code>ConcGCThreads</code>, and RSet-update parameters did G1 beat Parallel/CMS GC on both throughput and latency<sup class="footnote-ref"><a href="#fn14" id="fnref14:3">[14:3]</a></sup>. ZGC, per the OpenJDK project page, &quot;performs all expensive work concurrently, without stopping the execution of application threads for more than a millisecond,&quot; with pause times that stay flat as heap size scales from a few hundred megabytes up to 16TB<sup class="footnote-ref"><a href="#fn15" id="fnref15:1">[15:1]</a></sup>.</p><h2 id="sources" tabindex="-1">Sources <a class="header-anchor" href="#sources" aria-label="Permalink to &quot;Sources&quot;">​</a></h2><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html" target="_blank" rel="noreferrer">Performance Tuning — Spark SQL, DataFrames and Datasets Guide</a> <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a> <a href="#fnref1:2" class="footnote-backref">↩︎</a> <a href="#fnref1:3" class="footnote-backref">↩︎</a> <a href="#fnref1:4" class="footnote-backref">↩︎</a> <a href="#fnref1:5" class="footnote-backref">↩︎</a> <a href="#fnref1:6" class="footnote-backref">↩︎</a> <a href="#fnref1:7" class="footnote-backref">↩︎</a> <a href="#fnref1:8" class="footnote-backref">↩︎</a> <a href="#fnref1:9" class="footnote-backref">↩︎</a> <a href="#fnref1:10" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p><em>Spark: The Definitive Guide</em>, Chambers &amp; Zaharia, ch. 4, ch. 10 <a href="#fnref2" class="footnote-backref">↩︎</a> <a href="#fnref2:1" class="footnote-backref">↩︎</a> <a href="#fnref2:2" class="footnote-backref">↩︎</a> <a href="#fnref2:3" class="footnote-backref">↩︎</a></p></li><li id="fn3" class="footnote-item"><p><em>Learning Spark, 2nd Edition</em>, Damji, Wenig, Das &amp; Lee, ch. 7 <a href="#fnref3" class="footnote-backref">↩︎</a> <a href="#fnref3:1" class="footnote-backref">↩︎</a> <a href="#fnref3:2" class="footnote-backref">↩︎</a></p></li><li id="fn4" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/configuration.html" target="_blank" rel="noreferrer">Configuration — Spark</a> <a href="#fnref4" class="footnote-backref">↩︎</a> <a href="#fnref4:1" class="footnote-backref">↩︎</a> <a href="#fnref4:2" class="footnote-backref">↩︎</a> <a href="#fnref4:3" class="footnote-backref">↩︎</a> <a href="#fnref4:4" class="footnote-backref">↩︎</a> <a href="#fnref4:5" class="footnote-backref">↩︎</a> <a href="#fnref4:6" class="footnote-backref">↩︎</a> <a href="#fnref4:7" class="footnote-backref">↩︎</a> <a href="#fnref4:8" class="footnote-backref">↩︎</a> <a href="#fnref4:9" class="footnote-backref">↩︎</a> <a href="#fnref4:10" class="footnote-backref">↩︎</a> <a href="#fnref4:11" class="footnote-backref">↩︎</a> <a href="#fnref4:12" class="footnote-backref">↩︎</a> <a href="#fnref4:13" class="footnote-backref">↩︎</a> <a href="#fnref4:14" class="footnote-backref">↩︎</a> <a href="#fnref4:15" class="footnote-backref">↩︎</a> <a href="#fnref4:16" class="footnote-backref">↩︎</a> <a href="#fnref4:17" class="footnote-backref">↩︎</a></p></li><li id="fn5" class="footnote-item"><p><a href="https://raw.githubusercontent.com/apache/spark/v3.5.0/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala" target="_blank" rel="noreferrer">SQLConf.scala (Spark 3.5.0)</a> <a href="#fnref5" class="footnote-backref">↩︎</a> <a href="#fnref5:1" class="footnote-backref">↩︎</a> <a href="#fnref5:2" class="footnote-backref">↩︎</a> <a href="#fnref5:3" class="footnote-backref">↩︎</a> <a href="#fnref5:4" class="footnote-backref">↩︎</a> <a href="#fnref5:5" class="footnote-backref">↩︎</a> <a href="#fnref5:6" class="footnote-backref">↩︎</a></p></li><li id="fn6" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/tuning.html" target="_blank" rel="noreferrer">Tuning Spark</a> <a href="#fnref6" class="footnote-backref">↩︎</a> <a href="#fnref6:1" class="footnote-backref">↩︎</a> <a href="#fnref6:2" class="footnote-backref">↩︎</a> <a href="#fnref6:3" class="footnote-backref">↩︎</a> <a href="#fnref6:4" class="footnote-backref">↩︎</a> <a href="#fnref6:5" class="footnote-backref">↩︎</a> <a href="#fnref6:6" class="footnote-backref">↩︎</a> <a href="#fnref6:7" class="footnote-backref">↩︎</a> <a href="#fnref6:8" class="footnote-backref">↩︎</a></p></li><li id="fn7" class="footnote-item"><p><a href="https://luminousmen.com/post/dive-into-spark-memory" target="_blank" rel="noreferrer">Dive Into Spark Memory Management</a> <a href="#fnref7" class="footnote-backref">↩︎</a> <a href="#fnref7:1" class="footnote-backref">↩︎</a> <a href="#fnref7:2" class="footnote-backref">↩︎</a> <a href="#fnref7:3" class="footnote-backref">↩︎</a> <a href="#fnref7:4" class="footnote-backref">↩︎</a> <a href="#fnref7:5" class="footnote-backref">↩︎</a> <a href="#fnref7:6" class="footnote-backref">↩︎</a></p></li><li id="fn8" class="footnote-item"><p><a href="https://spark.apache.org/docs/3.5.8/api/python/user_guide/sql/arrow_pandas.html" target="_blank" rel="noreferrer">Apache Arrow in PySpark</a> <a href="#fnref8" class="footnote-backref">↩︎</a> <a href="#fnref8:1" class="footnote-backref">↩︎</a> <a href="#fnref8:2" class="footnote-backref">↩︎</a> <a href="#fnref8:3" class="footnote-backref">↩︎</a> <a href="#fnref8:4" class="footnote-backref">↩︎</a> <a href="#fnref8:5" class="footnote-backref">↩︎</a></p></li><li id="fn9" class="footnote-item"><p><a href="https://www.databricks.com/blog/2020/05/29/adaptive-query-execution-speeding-up-spark-sql-at-runtime.html" target="_blank" rel="noreferrer">Adaptive Query Execution: Speeding Up Spark SQL at Runtime</a> <a href="#fnref9" class="footnote-backref">↩︎</a></p></li><li id="fn10" class="footnote-item"><p><em>High Performance Spark, 2nd Edition</em>, Karau, Polak &amp; Warren, ch. 5 <a href="#fnref10" class="footnote-backref">↩︎</a> <a href="#fnref10:1" class="footnote-backref">↩︎</a></p></li><li id="fn11" class="footnote-item"><p><a href="https://raw.githubusercontent.com/spoddutur/spark-notes/master/task_memory_management_in_spark.md" target="_blank" rel="noreferrer">task_memory_management_in_spark.md</a> <a href="#fnref11" class="footnote-backref">↩︎</a></p></li><li id="fn12" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/job-scheduling.html" target="_blank" rel="noreferrer">Job Scheduling — Spark</a> <a href="#fnref12" class="footnote-backref">↩︎</a> <a href="#fnref12:1" class="footnote-backref">↩︎</a> <a href="#fnref12:2" class="footnote-backref">↩︎</a></p></li><li id="fn13" class="footnote-item"><p><a href="https://luminousmen.com/post/the-apache-spark-optimization-checklist" target="_blank" rel="noreferrer">The Apache Spark Optimization Checklist</a> <a href="#fnref13" class="footnote-backref">↩︎</a></p></li><li id="fn14" class="footnote-item"><p><a href="https://www.databricks.com/blog/2015/05/28/tuning-java-garbage-collection-for-spark-applications.html" target="_blank" rel="noreferrer">Tuning Java Garbage Collection for Spark Applications</a> <a href="#fnref14" class="footnote-backref">↩︎</a> <a href="#fnref14:1" class="footnote-backref">↩︎</a> <a href="#fnref14:2" class="footnote-backref">↩︎</a> <a href="#fnref14:3" class="footnote-backref">↩︎</a></p></li><li id="fn15" class="footnote-item"><p><a href="https://wiki.openjdk.org/display/zgc" target="_blank" rel="noreferrer">ZGC — The Z Garbage Collector</a> <a href="#fnref15" class="footnote-backref">↩︎</a> <a href="#fnref15:1" class="footnote-backref">↩︎</a></p></li></ol></section>',29)])])}const h=a(s,[["render",n]]);export{u as __pageData,h as default};
@@ -0,0 +1 @@
1
+ import{_ as a,o,c as t,a5 as r}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"Spark Config Quick-Reference","titleTemplate":":title | Spark Tuning Reference","description":"Cross-reference of the Spark/PySpark config properties and JVM GC flags discussed elsewhere in the guide, with defaults and version notes.","frontmatter":{"title":"Spark Config Quick-Reference","titleTemplate":":title | Spark Tuning Reference","description":"Cross-reference of the Spark/PySpark config properties and JVM GC flags discussed elsewhere in the guide, with defaults and version notes."},"headers":[],"relativePath":"tuning-reference/config.md","filePath":"tuning-reference/config.md"}'),s={name:"tuning-reference/config.md"};function n(f,e,c,i,d,l){return o(),t("div",null,[...e[0]||(e[0]=[r("",29)])])}const h=a(s,[["render",n]]);export{u as __pageData,h as default};
@@ -0,0 +1 @@
1
+ import{_ as a,o as t,c as o,a5 as r}from"./chunks/framework.DSg0KOwT.js";const s="/docs/assets/columnar-layout.PghGeOEA.svg",n="/docs/assets/columnar-layout.dark.BVNlz0ff.svg",m=JSON.parse('{"title":"Data Formats","titleTemplate":":title | Spark Tuning Reference","description":"Columnar file format tradeoffs: Parquet vs. ORC internals, splittability, predicate pushdown, and compression codec choice.","frontmatter":{"title":"Data Formats","titleTemplate":":title | Spark Tuning Reference","description":"Columnar file format tradeoffs: Parquet vs. ORC internals, splittability, predicate pushdown, and compression codec choice."},"headers":[],"relativePath":"tuning-reference/data-formats.md","filePath":"tuning-reference/data-formats.md"}'),f={name:"tuning-reference/data-formats.md"};function i(l,e,c,d,p,h){return t(),o("div",null,[...e[0]||(e[0]=[r('<h1 id="data-formats" tabindex="-1">Data Formats <a class="header-anchor" href="#data-formats" aria-label="Permalink to &quot;Data Formats {#data-formats}&quot;">​</a></h1><h2 id="how-parquet-and-orc-lay-out-data" tabindex="-1">How Parquet and ORC lay out data <a class="header-anchor" href="#how-parquet-and-orc-lay-out-data" aria-label="Permalink to &quot;How Parquet and ORC lay out data&quot;">​</a></h2><p>Parquet and ORC are both self-describing, columnar file formats for storing Spark&#39;s structured data<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. They&#39;re closer than they are different: <em>Spark: The Definitive Guide</em> frames the choice as &quot;for the most part, they&#39;re quite similar; the fundamental difference is that Parquet is further optimized for use with Spark, whereas ORC is further optimized for Hive&quot;<sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[1:1]</a></sup>, and notes that ORC &quot;has no options for reading in data because Spark understands the file format quite well&quot;<sup class="footnote-ref"><a href="#fn1" id="fnref1:2">[1:2]</a></sup>: Spark treats it as a format it understands natively rather than one with extra read-side knobs.</p><p>Both formats group rows into chunks (row groups in Parquet, stripes in ORC), each holding every column&#39;s data for that slice, with a footer or file-level metadata section recording where each chunk lives on disk. Parquet&#39;s <code>FileMetaData</code> footer records the offset and size of every row group and column chunk, and &quot;readers are expected to first read the file metadata to find all the column chunks they are interested in. The column chunks should then be read sequentially&quot;<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. Because compression is applied per column chunk rather than as one continuous stream over the whole file, a reader can seek straight to a row group&#39;s footer-recorded offset and decode just that chunk, which is exactly why Parquet files compressed with Snappy stay splittable even though the Snappy stream format itself provides no split points: it has &quot;no entropy encoder backend nor framing layer -- the latter is assumed to be handled by other parts of the system&quot;<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.</p><img class="light-only" src="'+s+'" alt="A columnar file&#39;s footer holds row-group offsets and per-column min/max statistics that let a reader skip row groups whose stats exclude the predicate."><img class="dark-only" src="'+n+'" alt="A columnar file&#39;s footer holds row-group offsets and per-column min/max statistics that let a reader skip row groups whose stats exclude the predicate."><p>Both formats also carry the statistics that later drive predicate pushdown. Parquet records min/max values per column chunk, plus an optional page-level <code>ColumnIndex</code> that lets a reader binary-search ordered columns for matching pages<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>, and optional Bloom filters for columns whose cardinality is too high for a dictionary to be practical<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>. ORC&#39;s <code>ColumnStatistics</code> protobuf records row count and, for most primitive types, min/max (plus sum for numeric types); from Hive 1.1.0 it also records a <code>hasNull</code> flag used specifically by ORC&#39;s predicate pushdown to answer <code>IS NULL</code> queries<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup>. ORC layers this at two granularities: a <code>RowIndexEntry</code> per row group (10,000 rows by default), kept at the front of each stripe so it&#39;s read only when pushdown or seeking is actually needed<sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup>, and file-level <code>StripeStatistics</code> that let whole stripes be skipped by predicate pushdown<sup class="footnote-ref"><a href="#fn8" id="fnref8">[8]</a></sup>. ORC also supports Bloom filters (Hive 1.2.0+), but only evaluates them against row groups that already passed the min/max row-index check first: a second-stage filter, not an independent one<sup class="footnote-ref"><a href="#fn7" id="fnref7:1">[7:1]</a></sup>.</p><p>On compression: Parquet defaults to Snappy, while ORC&#39;s default has been Zstd since Spark 2.3.0<sup class="footnote-ref"><a href="#fn9" id="fnref9">[9]</a></sup>. Zstandard&#39;s own manual describes it as &quot;a fast lossless compression algorithm, targeting real-time compression scenarios at zlib-level and better compression ratios,&quot; offering regular levels 1–22 plus negative levels that trade ratio for speed<sup class="footnote-ref"><a href="#fn10" id="fnref10">[10]</a></sup>. Spark exposes the level directly through <code>spark.io.compression.zstd.level</code> (default <code>1</code>)<sup class="footnote-ref"><a href="#fn9" id="fnref9:1">[9:1]</a></sup>, along with <code>spark.io.compression.zstd.workers</code> for parallel compression threads (default <code>0</code>, since 4.0.0) and <code>spark.io.compression.zstd.bufferSize</code> (default 32k)<sup class="footnote-ref"><a href="#fn9" id="fnref9:2">[9:2]</a></sup>.</p><p>Parquet has one more structural knob: <code>parquet.writer.version</code>. Version 2 changes the on-disk page-header encoding, and the spec is explicit that this is a forward-<em>incompatible</em> change: &quot;a reader that only understands <code>DataPageHeader</code> cannot parse <code>DataPageHeaderV2</code> pages&quot;<sup class="footnote-ref"><a href="#fn11" id="fnref11">[11]</a></sup>. The spec also flags that the file&#39;s own version marker &quot;has historically been used inconsistently: writers populate 1 or 2 without a consistent relationship to the features actually used&quot;<sup class="footnote-ref"><a href="#fn11" id="fnref11:1">[11:1]</a></sup>, so the marker itself isn&#39;t a reliable way to tell which page format a file actually contains.</p><h2 id="what-shows-up-at-read-time" tabindex="-1">What shows up at read time <a class="header-anchor" href="#what-shows-up-at-read-time" aria-label="Permalink to &quot;What shows up at read time&quot;">​</a></h2><p>A file&#39;s layout shapes the job the moment Spark reads it. Spark&#39;s file readers derive partition counts from file metadata rather than from a fixed rule: &quot;for structured formats like Parquet, ORC, or Avro, Spark actually reads the metadata (footers, row groups, that kind of thing) and tries to slice the file in a way that makes sense. Often you&#39;ll see one partition per row group, though Spark may merge or split depending on file sizes and configs&quot;<sup class="footnote-ref"><a href="#fn12" id="fnref12">[12]</a></sup>. A concrete case: a single 30 GB Parquet file with 300 row groups gets split into exactly 300 partitions<sup class="footnote-ref"><a href="#fn12" id="fnref12:1">[12:1]</a></sup>. So a surprising partition count for a given file is usually explained by its row-group layout, separately from <code>spark.sql.files.maxPartitionBytes</code> (128 MB by default) and its <code>maxPartitionNum</code>/<code>minPartitionNum</code> companions, which govern splitting for file-based sources generally<sup class="footnote-ref"><a href="#fn13" id="fnref13">[13]</a></sup>.</p><p>The <a href="./bottleneck-small-files.html">small-files problem</a> shows up as metadata overhead rather than raw I/O cost: &quot;when you&#39;re writing lots of small files, there&#39;s a significant metadata overhead that you incur managing all of those files. Spark especially does not do well with small files&quot;<sup class="footnote-ref"><a href="#fn1" id="fnref1:3">[1:3]</a></sup>. At the other extreme, oversized or misaligned row groups show up as lost locality: the Parquet spec&#39;s rationale for matching block size to row-group size is that &quot;since an entire row group might need to be read, we want it to completely fit on one HDFS block&quot;<sup class="footnote-ref"><a href="#fn14" id="fnref14">[14]</a></sup>. A row group straddling a block boundary can&#39;t get that benefit.</p><p>Raw (non-Parquet) gzip or zip source files reveal themselves as a single-executor bottleneck at read time, visible as one task doing dramatically more work than the rest of its stage: Spark &quot;needs to download the whole file on one executor, unpack it on just one core, and then redistribute the partitions to the cluster nodes&quot;<sup class="footnote-ref"><a href="#fn15" id="fnref15">[15]</a></sup>.</p><p>Version and schema mismatches surface as read failures rather than silent corruption. A file written with <code>parquet.writer.version=2</code> can&#39;t be parsed by an older Spark version, or a non-Spark engine such as Hive or Impala, if that reader only implements the original <code>DataPageHeader</code><sup class="footnote-ref"><a href="#fn11" id="fnref11:2">[11:2]</a></sup>. <em>Spark: The Definitive Guide</em> frames this as a general risk: &quot;you can still encounter problems if you&#39;re working with incompatible Parquet files. Be careful when you write out Parquet files with different versions of Spark (especially older ones) because this can cause significant headache&quot;<sup class="footnote-ref"><a href="#fn1" id="fnref1:4">[1:4]</a></sup>.</p><h2 id="what-the-layout-buys-you" tabindex="-1">What the layout buys you <a class="header-anchor" href="#what-the-layout-buys-you" aria-label="Permalink to &quot;What the layout buys you&quot;">​</a></h2><p>Those read-time symptoms trace back to specific tradeoffs in the layout. Splittability is what decides whether a file can be processed in parallel at all. A non-splittable raw compressed source file forces Spark onto a single core for the whole download-and-unpack step before it can redistribute anything: &quot;as you can imagine, this becomes a huge bottleneck in your distributed processing&quot;<sup class="footnote-ref"><a href="#fn15" id="fnref15:1">[15:1]</a></sup>. Parquet sidesteps this at the container level: because its footer records row-group and column-chunk offsets independently of whichever codec compressed the bytes inside them, a task can seek and decode a row group on its own, so even a non-splittable codec like Snappy doesn&#39;t cost the file its parallelism<sup class="footnote-ref"><a href="#fn2" id="fnref2:1">[2:1]</a></sup><sup class="footnote-ref"><a href="#fn3" id="fnref3:1">[3:1]</a></sup>.</p><p>Row-group and block-size alignment governs the same kind of locality at a coarser grain. The spec recommends large row groups, 512 MB–1 GB, &quot;because larger row groups allow for larger column chunks which makes it possible to do larger sequential IO,&quot; at the cost of more write-side buffering, paired with a block size sized the same way; its own worked example is &quot;1GB row groups, 1GB HDFS block size, 1 HDFS block per HDFS file&quot;<sup class="footnote-ref"><a href="#fn14" id="fnref14:1">[14:1]</a></sup>.</p><p>The statistics both formats carry are what predicate pushdown runs against. Spark plans queries directly off &quot;statistics that Spark reads directly from the underlying data source, like the counts and min/max values in the metadata of Parquet files&quot;<sup class="footnote-ref"><a href="#fn16" id="fnref16">[16]</a></sup>, gated by <code>spark.sql.parquet.filterPushdown</code> (default <code>true</code> since Spark 1.2.0) and, for ORC, <code>spark.sql.orc.filterPushdown</code> (default <code>true</code> since 1.4.0)<sup class="footnote-ref"><a href="#fn17" id="fnref17">[17]</a></sup><sup class="footnote-ref"><a href="#fn18" id="fnref18">[18]</a></sup>. A more aggressive option, <code>spark.sql.parquet.aggregatePushdown</code> (default <code>false</code>, since 3.3.0), pushes <code>MIN</code>, <code>MAX</code>, and <code>COUNT</code> down to Parquet&#39;s footer statistics directly, and throws if the needed statistic is missing from a file&#39;s footer<sup class="footnote-ref"><a href="#fn17" id="fnref17:1">[17:1]</a></sup>. Skipping a row group or stripe this way means its bytes are never read off disk, which beats any amount of post-read filtering.</p><p>Small files cost more in metadata management than their data volume would suggest, and Spark &quot;especially does not do well&quot; with them<sup class="footnote-ref"><a href="#fn1" id="fnref1:5">[1:5]</a></sup>. On the scheduling side, Spark&#39;s tuning guide notes it &quot;can efficiently support tasks as short as 200 ms&quot; because it reuses one executor JVM across many tasks and has low task-launch cost, while separately recommending &quot;2-3 tasks per CPU core in your cluster&quot;<sup class="footnote-ref"><a href="#fn19" id="fnref19">[19]</a></sup>, which implies <a href="./bottleneck-tiny-tasks.html">scheduling overhead</a> only becomes proportionally significant once tasks (and the files behind them) shrink below roughly that 200 ms floor.</p><p>Compression choice trades CPU against I/O and storage. Once I/O stops being the bottleneck, paying a codec&#39;s decompression cost is a net loss: &quot;uncompressed files are clearly outperforming compressed files. This is because uncompressed files are I/O bound, and compressed files are CPU bound, but I/O is good enough here&quot;<sup class="footnote-ref"><a href="#fn15" id="fnref15:2">[15:2]</a></sup>. Bzip2 illustrates the opposite failure mode: it&#39;s splittable, but compresses so aggressively that &quot;you get very few partitions and therefore they can be poorly distributed&quot;<sup class="footnote-ref"><a href="#fn15" id="fnref15:3">[15:3]</a></sup>.</p><p>Writer-version and schema changes matter because they fail at read time, for whoever reads the data next (not at write time, for whoever wrote it). A <code>parquet.writer.version=2</code> file is simply unreadable by any engine that only understands the original page header<sup class="footnote-ref"><a href="#fn11" id="fnref11:3">[11:3]</a></sup>. Schema drift has its own correctness angle: for <a href="./table-formats.html">Delta Lake</a> tables, adding a column via <code>mergeSchema</code> causes existing rows, when read back, to have that new column&#39;s value read as <code>NULL</code><sup class="footnote-ref"><a href="#fn20" id="fnref20">[20]</a></sup>, a defined outcome, but one that changes what a downstream query sees for rows written before the schema changed.</p><h2 id="choosing-and-tuning-the-format" tabindex="-1">Choosing and tuning the format <a class="header-anchor" href="#choosing-and-tuning-the-format" aria-label="Permalink to &quot;Choosing and tuning the format&quot;">​</a></h2><p>A few defaults cover most cases despite those tradeoffs. Default to Parquet. Reach for ORC specifically when the same data also has to serve Hive consumers or existing Hive ORC tables, where ORC is the better-optimized target<sup class="footnote-ref"><a href="#fn1" id="fnref1:6">[1:6]</a></sup>.</p><p>Size row groups and the underlying filesystem block together, not independently. The Parquet spec&#39;s own recommended setup is 1 GB row groups paired with a 1 GB HDFS block size, one block per file<sup class="footnote-ref"><a href="#fn14" id="fnref14:2">[14:2]</a></sup>. Leave <code>spark.sql.files.maxPartitionBytes</code> (128 MB default) and its <code>maxPartitionNum</code>/<code>minPartitionNum</code> companions for the general file-splitting case, but expect row-group boundaries (not these settings) to be what actually decides partition count for row-group-oriented formats<sup class="footnote-ref"><a href="#fn13" id="fnref13:1">[13:1]</a></sup><sup class="footnote-ref"><a href="#fn12" id="fnref12:2">[12:2]</a></sup>.</p><p>Leave predicate pushdown on: <code>spark.sql.parquet.filterPushdown</code> and <code>spark.sql.orc.filterPushdown</code> both default to <code>true</code> already<sup class="footnote-ref"><a href="#fn17" id="fnref17:2">[17:2]</a></sup><sup class="footnote-ref"><a href="#fn18" id="fnref18:1">[18:1]</a></sup>, so the main action is not disabling them, plus considering <code>spark.sql.parquet.aggregatePushdown</code> when a workload is dominated by <code>MIN</code>/<code>MAX</code>/<code>COUNT</code> over Parquet sources with complete footer statistics<sup class="footnote-ref"><a href="#fn17" id="fnref17:3">[17:3]</a></sup>.</p><p>Cap output file size directly instead of letting the small-files problem accumulate: <code>maxRecordsPerFile</code>, introduced in Spark 2.2, targets an optimum file size by capping the number of records written per file, e.g. <code>df.write.option(&quot;maxRecordsPerFile&quot;, 5000)</code><sup class="footnote-ref"><a href="#fn1" id="fnref1:7">[1:7]</a></sup>.</p><p>Pick a compression codec based on the actual bottleneck. Parquet&#39;s default, Snappy, favors speed; ORC has already defaulted to Zstd since Spark 2.3.0<sup class="footnote-ref"><a href="#fn9" id="fnref9:3">[9:3]</a></sup>, and at least one optimization checklist recommends overriding Parquet&#39;s default to Zstd as well<sup class="footnote-ref"><a href="#fn21" id="fnref21">[21]</a></sup>. Zstd&#39;s level is tunable through <code>spark.io.compression.zstd.level</code>: higher levels buy better compression &quot;at the expense of more CPU and memory&quot;<sup class="footnote-ref"><a href="#fn9" id="fnref9:4">[9:4]</a></sup>. Avoid feeding Spark large raw <code>.gz</code>/<code>.zip</code> source files directly (unpack them before loading), since the non-splittability lives in the raw container format rather than in gzip itself; the same <em>Definitive Guide</em> that warns about raw gzip files elsewhere still recommends Parquet with gzip compression once gzip is wrapped inside Parquet&#39;s row-group container<sup class="footnote-ref"><a href="#fn1" id="fnref1:8">[1:8]</a></sup>.</p><p>Treat <code>spark.sql.parquet.mergeSchema</code> (default <code>false</code>) as opt-in rather than default-on: enabling it &quot;merges schemas collected from all data files,&quot; instead of trusting a single summary file or a random file&#39;s schema<sup class="footnote-ref"><a href="#fn22" id="fnref22">[22]</a></sup>, useful for evolving schemas, but it means every file in the dataset gets scanned for its schema. For Delta tables specifically, remember that <code>mergeSchema</code>-added columns read back as <code>NULL</code> on pre-existing rows<sup class="footnote-ref"><a href="#fn20" id="fnref20:1">[20:1]</a></sup> before relying on it for a backfill.</p><p>Don&#39;t flip <code>parquet.writer.version</code> to <code>2</code> without confirming every downstream reader of that data (an older Spark version, Hive, Impala, or anything else touching the files) actually supports <code>DataPageHeaderV2</code> first; it&#39;s a forward-incompatible page-format change, not an additive one<sup class="footnote-ref"><a href="#fn11" id="fnref11:4">[11:4]</a></sup><sup class="footnote-ref"><a href="#fn1" id="fnref1:9">[1:9]</a></sup>.</p><h2 id="sources" tabindex="-1">Sources <a class="header-anchor" href="#sources" aria-label="Permalink to &quot;Sources&quot;">​</a></h2><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p><em>Spark: The Definitive Guide</em>, Chambers &amp; Zaharia, ch. 9 <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a> <a href="#fnref1:2" class="footnote-backref">↩︎</a> <a href="#fnref1:3" class="footnote-backref">↩︎</a> <a href="#fnref1:4" class="footnote-backref">↩︎</a> <a href="#fnref1:5" class="footnote-backref">↩︎</a> <a href="#fnref1:6" class="footnote-backref">↩︎</a> <a href="#fnref1:7" class="footnote-backref">↩︎</a> <a href="#fnref1:8" class="footnote-backref">↩︎</a> <a href="#fnref1:9" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p><a href="https://parquet.apache.org/_print/docs/file-format/" target="_blank" rel="noreferrer">Parquet File Format: File Metadata and Row Groups</a> <a href="#fnref2" class="footnote-backref">↩︎</a> <a href="#fnref2:1" class="footnote-backref">↩︎</a></p></li><li id="fn3" class="footnote-item"><p><a href="https://github.com/google/snappy/blob/main/format_description.txt" target="_blank" rel="noreferrer">Snappy Compressed Format Description</a> <a href="#fnref3" class="footnote-backref">↩︎</a> <a href="#fnref3:1" class="footnote-backref">↩︎</a></p></li><li id="fn4" class="footnote-item"><p><a href="https://parquet.apache.org/_print/docs/file-format/" target="_blank" rel="noreferrer">Parquet File Format: Column Index</a> <a href="#fnref4" class="footnote-backref">↩︎</a></p></li><li id="fn5" class="footnote-item"><p><a href="https://parquet.apache.org/docs/file-format/bloomfilter/" target="_blank" rel="noreferrer">Parquet File Format: Bloom Filter</a> <a href="#fnref5" class="footnote-backref">↩︎</a></p></li><li id="fn6" class="footnote-item"><p><a href="https://orc.apache.org/specification/ORCv1/" target="_blank" rel="noreferrer">ORC Specification v1: Column Statistics</a> <a href="#fnref6" class="footnote-backref">↩︎</a></p></li><li id="fn7" class="footnote-item"><p><a href="https://orc.apache.org/specification/ORCv1/" target="_blank" rel="noreferrer">ORC Specification v1: Row Index and Bloom Filters</a> <a href="#fnref7" class="footnote-backref">↩︎</a> <a href="#fnref7:1" class="footnote-backref">↩︎</a></p></li><li id="fn8" class="footnote-item"><p><a href="https://orc.apache.org/specification/ORCv1/" target="_blank" rel="noreferrer">ORC Specification v1: Stripe Statistics</a> <a href="#fnref8" class="footnote-backref">↩︎</a></p></li><li id="fn9" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/configuration.html" target="_blank" rel="noreferrer">Configuration: Spark</a> <a href="#fnref9" class="footnote-backref">↩︎</a> <a href="#fnref9:1" class="footnote-backref">↩︎</a> <a href="#fnref9:2" class="footnote-backref">↩︎</a> <a href="#fnref9:3" class="footnote-backref">↩︎</a> <a href="#fnref9:4" class="footnote-backref">↩︎</a></p></li><li id="fn10" class="footnote-item"><p><a href="https://facebook.github.io/zstd/zstd_manual.html" target="_blank" rel="noreferrer">Zstandard Manual</a> <a href="#fnref10" class="footnote-backref">↩︎</a></p></li><li id="fn11" class="footnote-item"><p><a href="https://parquet.apache.org/_print/docs/file-format/" target="_blank" rel="noreferrer">Parquet File Format: Data Pages (V1/V2)</a> <a href="#fnref11" class="footnote-backref">↩︎</a> <a href="#fnref11:1" class="footnote-backref">↩︎</a> <a href="#fnref11:2" class="footnote-backref">↩︎</a> <a href="#fnref11:3" class="footnote-backref">↩︎</a> <a href="#fnref11:4" class="footnote-backref">↩︎</a></p></li><li id="fn12" class="footnote-item"><p><a href="https://luminousmen.com/post/spark-partitions" target="_blank" rel="noreferrer">How Spark Determines Partitions for a File</a> <a href="#fnref12" class="footnote-backref">↩︎</a> <a href="#fnref12:1" class="footnote-backref">↩︎</a> <a href="#fnref12:2" class="footnote-backref">↩︎</a></p></li><li id="fn13" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html" target="_blank" rel="noreferrer">Performance Tuning: Spark SQL, DataFrames and Datasets Guide</a> <a href="#fnref13" class="footnote-backref">↩︎</a> <a href="#fnref13:1" class="footnote-backref">↩︎</a></p></li><li id="fn14" class="footnote-item"><p><a href="https://parquet.apache.org/_print/docs/file-format/" target="_blank" rel="noreferrer">Parquet File Format: Row Group Size</a> <a href="#fnref14" class="footnote-backref">↩︎</a> <a href="#fnref14:1" class="footnote-backref">↩︎</a> <a href="#fnref14:2" class="footnote-backref">↩︎</a></p></li><li id="fn15" class="footnote-item"><p><a href="https://luminousmen.com/post/spark-tips-dont-collect-data-on-driver" target="_blank" rel="noreferrer">Spark Tips: Don&#39;t Collect Data on Driver</a> <a href="#fnref15" class="footnote-backref">↩︎</a> <a href="#fnref15:1" class="footnote-backref">↩︎</a> <a href="#fnref15:2" class="footnote-backref">↩︎</a> <a href="#fnref15:3" class="footnote-backref">↩︎</a></p></li><li id="fn16" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html" target="_blank" rel="noreferrer">Performance Tuning: Spark SQL, DataFrames and Datasets Guide</a> <a href="#fnref16" class="footnote-backref">↩︎</a></p></li><li id="fn17" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/configuration.html" target="_blank" rel="noreferrer">Configuration: Spark</a> <a href="#fnref17" class="footnote-backref">↩︎</a> <a href="#fnref17:1" class="footnote-backref">↩︎</a> <a href="#fnref17:2" class="footnote-backref">↩︎</a> <a href="#fnref17:3" class="footnote-backref">↩︎</a></p></li><li id="fn18" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/configuration.html" target="_blank" rel="noreferrer">Configuration: Spark</a> <a href="#fnref18" class="footnote-backref">↩︎</a> <a href="#fnref18:1" class="footnote-backref">↩︎</a></p></li><li id="fn19" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/tuning.html" target="_blank" rel="noreferrer">Spark Tuning Guide: Level of Parallelism</a> <a href="#fnref19" class="footnote-backref">↩︎</a></p></li><li id="fn20" class="footnote-item"><p><em>Learning Spark, 2nd Edition</em>, Damji, Wenig, Das &amp; Lee, ch. 9 <a href="#fnref20" class="footnote-backref">↩︎</a> <a href="#fnref20:1" class="footnote-backref">↩︎</a></p></li><li id="fn21" class="footnote-item"><p><a href="https://luminousmen.com/post/the-apache-spark-optimization-checklist" target="_blank" rel="noreferrer">The Apache Spark Optimization Checklist</a> <a href="#fnref21" class="footnote-backref">↩︎</a></p></li><li id="fn22" class="footnote-item"><p><a href="https://raw.githubusercontent.com/apache/spark/v3.5.0/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala" target="_blank" rel="noreferrer">SQLConf.scala</a> <a href="#fnref22" class="footnote-backref">↩︎</a></p></li></ol></section>',32)])])}const g=a(f,[["render",i]]);export{m as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as a,o as t,c as o,a5 as r}from"./chunks/framework.DSg0KOwT.js";const s="/docs/assets/columnar-layout.PghGeOEA.svg",n="/docs/assets/columnar-layout.dark.BVNlz0ff.svg",m=JSON.parse('{"title":"Data Formats","titleTemplate":":title | Spark Tuning Reference","description":"Columnar file format tradeoffs: Parquet vs. ORC internals, splittability, predicate pushdown, and compression codec choice.","frontmatter":{"title":"Data Formats","titleTemplate":":title | Spark Tuning Reference","description":"Columnar file format tradeoffs: Parquet vs. ORC internals, splittability, predicate pushdown, and compression codec choice."},"headers":[],"relativePath":"tuning-reference/data-formats.md","filePath":"tuning-reference/data-formats.md"}'),f={name:"tuning-reference/data-formats.md"};function i(l,e,c,d,p,h){return t(),o("div",null,[...e[0]||(e[0]=[r("",32)])])}const g=a(f,[["render",i]]);export{m as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as n,C as t,o as a,c as o,E as r}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"Spark Tuning Reference","titleTemplate":false,"description":"Spark Tuning Reference: move from a job symptom to the Spark mechanics and settings that can change its outcome.","frontmatter":{"layout":"page","title":"Spark Tuning Reference","titleTemplate":false,"description":"Spark Tuning Reference: move from a job symptom to the Spark mechanics and settings that can change its outcome.","sidebar":false},"headers":[],"relativePath":"tuning-reference/index.md","filePath":"tuning-reference/index.md"}'),i={name:"tuning-reference/index.md"};function c(s,m,p,d,f,l){const e=t("TuningLanding");return a(),o("div",null,[r(e)])}const _=n(i,[["render",c]]);export{u as __pageData,_ as default};
@@ -0,0 +1 @@
1
+ import{_ as n,C as t,o as a,c as o,E as r}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"Spark Tuning Reference","titleTemplate":false,"description":"Spark Tuning Reference: move from a job symptom to the Spark mechanics and settings that can change its outcome.","frontmatter":{"layout":"page","title":"Spark Tuning Reference","titleTemplate":false,"description":"Spark Tuning Reference: move from a job symptom to the Spark mechanics and settings that can change its outcome.","sidebar":false},"headers":[],"relativePath":"tuning-reference/index.md","filePath":"tuning-reference/index.md"}'),i={name:"tuning-reference/index.md"};function c(s,m,p,d,f,l){const e=t("TuningLanding");return a(),o("div",null,[r(e)])}const _=n(i,[["render",c]]);export{u as __pageData,_ as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as n,a5 as s}from"./chunks/framework.DSg0KOwT.js";const g=JSON.parse('{"title":"Introduction","titleTemplate":":title | Spark Tuning Reference","description":"Guide overview: what the reference covers, the severity-dot legend, and the tag system linking into the Bottleneck Reference.","frontmatter":{"title":"Introduction","titleTemplate":":title | Spark Tuning Reference","description":"Guide overview: what the reference covers, the severity-dot legend, and the tag system linking into the Bottleneck Reference."},"headers":[],"relativePath":"tuning-reference/intro.md","filePath":"tuning-reference/intro.md"}'),r={name:"tuning-reference/intro.md"};function o(i,e,l,c,p,h){return a(),n("div",null,[...e[0]||(e[0]=[s('<h1 id="intro" tabindex="-1">Introduction <a class="header-anchor" href="#intro" aria-label="Permalink to &quot;Introduction {#intro}&quot;">​</a></h1><p>This guide is a hands-on reference for optimizing Spark and PySpark jobs. It covers <a href="./spark-architecture.html">Spark internals</a>, <a href="./memory-model.html">memory management</a>, <a href="./joins.html">joins</a>, <a href="./shuffle.html">shuffle</a>, <a href="./data-formats.html">data formats</a>, <a href="./pyspark.html">PySpark-specific patterns</a>, <a href="./aqe.html">Adaptive Query Execution</a> (AQE), and <a href="./cluster-config.html">cluster tuning</a>, followed by a bottleneck diagnostic reference.</p><p>Read it top to bottom for a full grounding in Spark performance, or jump straight to the <strong>Bottleneck Reference</strong> section below when you already know which symptom you&#39;re chasing.</p><h2 id="severity-dots" tabindex="-1">Severity dots <a class="header-anchor" href="#severity-dots" aria-label="Permalink to &quot;Severity dots&quot;">​</a></h2><p>Each finding is marked with a severity dot:</p><ul><li><span class="severity-dot info"></span> <strong>Info</strong>: worth knowing, not yet a problem</li><li><span class="severity-dot warning"></span> <strong>Warning</strong>: likely hurting job performance</li><li><span class="severity-dot critical"></span> <strong>Critical</strong>: actively bottlenecking the job</li></ul><p>Each bottleneck section below documents the exact thresholds behind these dots.</p><h2 id="tag-system" tabindex="-1">Tag system <a class="header-anchor" href="#tag-system" aria-label="Permalink to &quot;Tag system&quot;">​</a></h2><p>Every bottleneck has a short tag used throughout this reference:</p><p><span class="tag">SKEW</span> <span class="tag">SHFL</span> <span class="tag">SPILL</span><span class="tag">GC</span> <span class="tag">COLD</span> <span class="tag">UTIL</span><span class="tag">HOST</span> <span class="tag">FAIL</span> <span class="tag">STRAG</span><span class="tag">RETRY</span> <span class="tag">TINY</span> <span class="tag">FAIL-RATE</span></p><p>See the <a href="./bottleneck-skew.html">Bottleneck Reference</a> for what each one means, how it&#39;s detected, and how to fix it.</p>',11)])])}const f=t(r,[["render",o]]);export{g as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as n,a5 as s}from"./chunks/framework.DSg0KOwT.js";const g=JSON.parse('{"title":"Introduction","titleTemplate":":title | Spark Tuning Reference","description":"Guide overview: what the reference covers, the severity-dot legend, and the tag system linking into the Bottleneck Reference.","frontmatter":{"title":"Introduction","titleTemplate":":title | Spark Tuning Reference","description":"Guide overview: what the reference covers, the severity-dot legend, and the tag system linking into the Bottleneck Reference."},"headers":[],"relativePath":"tuning-reference/intro.md","filePath":"tuning-reference/intro.md"}'),r={name:"tuning-reference/intro.md"};function o(i,e,l,c,p,h){return a(),n("div",null,[...e[0]||(e[0]=[s("",11)])])}const f=t(r,[["render",o]]);export{g as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a5 as s}from"./chunks/framework.DSg0KOwT.js";const r="/docs/assets/join-strategy.C_FvrCEo.svg",n="/docs/assets/join-strategy.dark.ChMLnNII.svg",b=JSON.parse(`{"title":"Join Optimization","titleTemplate":":title | Spark Tuning Reference","description":"Physical join strategy selection: broadcast vs. shuffle joins, join hints, bucketing, and AQE's runtime join-strategy switching.","frontmatter":{"title":"Join Optimization","titleTemplate":":title | Spark Tuning Reference","description":"Physical join strategy selection: broadcast vs. shuffle joins, join hints, bucketing, and AQE's runtime join-strategy switching."},"headers":[],"relativePath":"tuning-reference/joins.md","filePath":"tuning-reference/joins.md"}`),i={name:"tuning-reference/joins.md"};function f(c,e,l,h,d,p){return a(),o("div",null,[...e[0]||(e[0]=[s('<h1 id="joins" tabindex="-1">Join Optimization <a class="header-anchor" href="#joins" aria-label="Permalink to &quot;Join Optimization {#joins}&quot;">​</a></h1><h2 id="join-strategy-selection" tabindex="-1">Join strategy selection <a class="header-anchor" href="#join-strategy-selection" aria-label="Permalink to &quot;Join strategy selection&quot;">​</a></h2><p>Spark SQL&#39;s Catalyst optimizer picks from five physical join operators: broadcast hash join, broadcast nested loop join, shuffle hash join, shuffle sort-merge join (SMJ), and shuffle-and-replicate nested loop (cartesian) join<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. Broadcast hash join avoids a shuffle entirely by sending one small side to every executor; it requires an equi-join condition and supports every join type except full outer<sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[1:1]</a></sup>. Broadcast nested loop join relaxes the equi-join requirement (it supports non-equi conditions and every join type) at the cost of scanning one side repeatedly, so it&#39;s normally a fallback rather than a first choice<sup class="footnote-ref"><a href="#fn1" id="fnref1:2">[1:2]</a></sup>. Unlike hand-tuned RDD joins, where the partitioner is chosen explicitly, Spark SQL&#39;s optimizer can also push down or reorder other operators automatically to make the eventual join cheaper<sup class="footnote-ref"><a href="#fn1" id="fnref1:3">[1:3]</a></sup>.</p><p>At the communication level, Spark&#39;s choice is binary: an all-to-all <a href="./shuffle.html">shuffle</a> join, or a broadcast join that replicates the small side once so every node can then join locally with no further network traffic<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. Once a join has been routed to a shuffle-based strategy (broadcast wasn&#39;t used), Spark defaults to preferring sort-merge join over shuffle hash join, a preference controlled by <code>spark.sql.join.preferSortMergeJoin</code><sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. Since Spark 3.2, <a href="./aqe.html">Adaptive Query Execution</a> (AQE), on by default, re-optimizes the physical plan mid-execution using runtime statistics gathered after the shuffle actually runs, rather than relying only on pre-execution estimates<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>.</p><img class="light-only" src="'+r+'" alt="Decision tree for choosing a physical join operator: the equi-join test splits off the nested-loop strategies, then the broadcast-threshold and preferSortMergeJoin checks select broadcast hash, sort-merge, or shuffle hash join, with AQE able to promote sort-merge to broadcast at runtime."><img class="dark-only" src="'+n+'" alt="Decision tree for choosing a physical join operator: the equi-join test splits off the nested-loop strategies, then the broadcast-threshold and preferSortMergeJoin checks select broadcast hash, sort-merge, or shuffle hash join, with AQE able to promote sort-merge to broadcast at runtime."><h2 id="reading-it-in-the-plan" tabindex="-1">Reading it in the plan <a class="header-anchor" href="#reading-it-in-the-plan" aria-label="Permalink to &quot;Reading it in the plan&quot;">​</a></h2><p>That choice is legible before a job even runs. The deciding signal is the relation&#39;s size relative to <code>spark.sql.autoBroadcastJoinThreshold</code>, which defaults to <code>10485760</code> bytes (10 MB), unchanged since it was introduced in Spark 1.1.0 and still the documented default in Spark 3.5<sup class="footnote-ref"><a href="#fn4" id="fnref4:1">[4:1]</a></sup><sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>. That comparison is driven by table/plan size statistics rather than a fresh scan of the data: Spark consults catalog statistics (collected via <code>ANALYZE TABLE</code>, inspectable through <code>DESCRIBE EXTENDED</code>) and the cost estimates shown in <code>EXPLAIN COST</code><sup class="footnote-ref"><a href="#fn4" id="fnref4:2">[4:2]</a></sup>. When those statistics are missing, <code>spark.sql.statistics.fallBackToHdfs</code> (default <code>false</code>) controls whether Spark falls back to on-disk file size to judge broadcast eligibility, and for partitioned tables without statistics Spark instead uses the <code>spark.sql.defaultSizeInBytes</code> placeholder<sup class="footnote-ref"><a href="#fn5" id="fnref5:1">[5:1]</a></sup>.</p><blockquote><p><strong>PySpark:</strong> don&#39;t guess whether a join will broadcast. Call <code>df.explain(mode=&quot;cost&quot;)</code> to see the same size estimates Catalyst used to pick the strategy.</p></blockquote><p>Because AQE re-optimizes at runtime, the physical join operator visible in a plan can change after execution starts. Two runtime-driven overrides are worth checking for in the Spark UI or an <code>EXPLAIN</code> plan: <code>spark.sql.adaptive.maxShuffledHashJoinLocalMapThreshold</code> (default <code>0</code>, disabled) can make AQE prefer shuffled hash join over sort-merge &quot;regardless of the value of <code>spark.sql.join.preferSortMergeJoin</code>&quot; whenever every post-shuffle partition stays under that threshold and above <code>spark.sql.adaptive.advisoryPartitionSizeInBytes</code><sup class="footnote-ref"><a href="#fn4" id="fnref4:3">[4:3]</a></sup>; separately, AQE can convert an already-planned sort-merge join into a broadcast hash join mid-execution when the runtime statistics of either join side turn out smaller than the adaptive broadcast threshold<sup class="footnote-ref"><a href="#fn4" id="fnref4:4">[4:4]</a></sup>.</p><p>Bucketing status is also visible directly in the physical plan: a shuffle-free bucketed join only appears once both sides are bucketed to the same count, at which point the <code>Exchange</code> nodes disappear entirely<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup>. This is demonstrated with 4 buckets on each side<sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup> and with 16 buckets on each side, where the plan shows <code>SelectedBucketsCount: 16 out of 16</code> on both branches of the <code>SortMergeJoin</code><sup class="footnote-ref"><a href="#fn8" id="fnref8">[8]</a></sup>. A separate real-world account of eliminating a bucketed shuffle frames the same signal just as plainly: the giveaway is that &quot;the right branch is missing an Exchange (i.e. shuffle)&quot;<sup class="footnote-ref"><a href="#fn6" id="fnref6:1">[6:1]</a></sup>. Removing the shuffle this way does not necessarily remove the sort phase: in the 16-bucket demo, a <code>Sort</code> operator still appears on each branch of the <code>SortMergeJoin</code> even with the <code>Exchange</code> gone<sup class="footnote-ref"><a href="#fn8" id="fnref8:1">[8:1]</a></sup>.</p><p>Whether Catalyst is reordering joins (rather than just individual operators) is also a config check: <code>spark.sql.cbo.enabled</code> and the separate <code>spark.sql.cbo.joinReorder.enabled</code> flag both default to <code>false</code>, so multi-way join reordering is off unless both are explicitly enabled<sup class="footnote-ref"><a href="#fn5" id="fnref5:2">[5:2]</a></sup>. That&#39;s distinct from Catalyst&#39;s regular rule-based optimizations, which run regardless of those flags. For example, a filter written after a join in the DataFrame API was observed moved before the join (and pushed into the JDBC source) automatically, visible in the physical plan<sup class="footnote-ref"><a href="#fn9" id="fnref9">[9]</a></sup>.</p><h2 id="costs-the-plan-doesn-t-show" tabindex="-1">Costs the plan doesn&#39;t show <a class="header-anchor" href="#costs-the-plan-doesn-t-show" aria-label="Permalink to &quot;Costs the plan doesn&#39;t show&quot;">​</a></h2><p>Picking a strategy is one thing; paying for it is another. Broadcasting isn&#39;t free on the driver side. Building a broadcast join replicates the small-side DataFrame to every worker, but that replication is preceded by collecting the DataFrame back to the driver first: an <a href="./bottleneck-broadcast-sizing.html">oversized broadcast</a>, whether chosen automatically or forced via a hint or <code>broadcast()</code> call, &quot;can crash your driver node (because that collect is expensive)&quot;<sup class="footnote-ref"><a href="#fn2" id="fnref2:1">[2:1]</a></sup>. The shuffle-and-replicate nested loop (cartesian-style) strategy carries a related but distinct risk: because every partition is joined against every other partition, it has a high chance of data explosion<sup class="footnote-ref"><a href="#fn1" id="fnref1:4">[1:4]</a></sup>.</p><p>Bucketing&#39;s shuffle-free path also has a real tradeoff once bucket counts don&#39;t match exactly. Since Spark 3.1, <code>spark.sql.bucketing.coalesceBucketsInJoin.enabled</code> lets Spark coalesce the side with more buckets down to the smaller count, but only within a ratio bounded by <code>spark.sql.bucketing.coalesceBucketsInJoin.maxBucketRatio</code> (default <code>4</code>). Enabling it can still remove the shuffle, but it does so by cutting parallelism on the finer-grained side, and Spark&#39;s own docs note it &quot;could possibly cause OOM for shuffled hash join&quot; as a result<sup class="footnote-ref"><a href="#fn5" id="fnref5:3">[5:3]</a></sup>. Outside that ratio, or with coalescing disabled, mismatched bucket counts get no free win at all: the join simply falls back to a normal shuffle.</p><h2 id="forcing-a-strategy" tabindex="-1">Forcing a strategy <a class="header-anchor" href="#forcing-a-strategy" aria-label="Permalink to &quot;Forcing a strategy&quot;">​</a></h2><p>When the automatic choice isn&#39;t the right one, Spark still lets you override it directly. Join hints force a specific strategy per relation, and Spark honors them even against its own size-based defaults:</p><ul><li><strong>BROADCAST</strong> (also accepted as <code>BROADCASTJOIN</code>/<code>MAPJOIN</code>) forces a broadcast join with the hinted relation as the build side (broadcast hash if there&#39;s an equi-join key, broadcast nested loop otherwise), and this is honored &quot;even if the size of table &#39;t1&#39; suggested by the statistics is above the configuration <code>spark.sql.autoBroadcastJoinThreshold</code>&quot;<sup class="footnote-ref"><a href="#fn4" id="fnref4:5">[4:5]</a></sup>.</li><li><strong>MERGE</strong> forces a shuffle sort-merge join; it needs an equi-join on sortable keys and high cardinality to pay off, and is less prone to OOM than the hash-based options since it never builds an in-memory hash table<sup class="footnote-ref"><a href="#fn1" id="fnref1:5">[1:5]</a></sup>.</li><li><strong>SHUFFLE_HASH</strong> forces a shuffle hash join, supporting all join types on an equi-join key but, like MERGE, wanting high cardinality; it builds a per-partition hash table after the shuffle<sup class="footnote-ref"><a href="#fn1" id="fnref1:6">[1:6]</a></sup>.</li><li><strong>SHUFFLE_REPLICATE_NL</strong> forces the shuffle-and-replicate nested loop join, supporting inner and cartesian joins with equi or non-equi conditions<sup class="footnote-ref"><a href="#fn1" id="fnref1:7">[1:7]</a></sup>.</li></ul><p>When both sides of a join carry conflicting hints, Spark resolves them by a fixed priority: BROADCAST over MERGE over SHUFFLE_HASH over SHUFFLE_REPLICATE_NL. If both sides carry the <em>same</em> BROADCAST or SHUFFLE_HASH hint, Spark still picks a build side based on join type and relation sizes<sup class="footnote-ref"><a href="#fn4" id="fnref4:6">[4:6]</a></sup>. As with any hint, none of this is guaranteed: Spark won&#39;t honor a hinted strategy that structurally can&#39;t support the actual join type being performed<sup class="footnote-ref"><a href="#fn4" id="fnref4:7">[4:7]</a></sup>. The threshold itself can also be turned off outright: setting <code>spark.sql.autoBroadcastJoinThreshold</code> to <code>-1</code> forces every join to fall back to shuffle sort-merge instead of ever considering broadcast<sup class="footnote-ref"><a href="#fn3" id="fnref3:1">[3:1]</a></sup>.</p><p>For repeated joins on the same key, bucketing both tables to the <em>same</em> bucket count removes the shuffle for free. To also remove the sort phase (not just the shuffle), the bucketed tables additionally need to be written pre-sorted on the join key, using <code>sortBy</code> alongside <code>bucketBy</code>; done that way, the merge phase has nothing left to do, since &quot;the joined output is sorted ... because we saved the tables sorted in ascending order ... there&#39;s no need to sort during the <code>SortMergeJoin</code>,&quot; and the Spark UI shows the query going straight to <code>WholeStageCodegen</code> with no <code>Exchange</code> at all<sup class="footnote-ref"><a href="#fn3" id="fnref3:2">[3:2]</a></sup>.</p><blockquote><p><strong>PySpark:</strong> <code>df.write.bucketBy(n, &quot;key&quot;).saveAsTable(...)</code> drops the shuffle; add <code>.sortBy(&quot;key&quot;)</code> on both tables to drop the sort phase too.</p></blockquote><p>Multi-way join reordering is a deliberate opt-in: enable <code>spark.sql.cbo.enabled</code> for cost-based statistics, then <code>spark.sql.cbo.joinReorder.enabled</code> for the reordering rule itself. <code>spark.sql.cbo.joinReorder.dp.threshold</code> caps the dynamic-programming enumeration at 12 joined nodes by default, and <code>spark.sql.cbo.joinReorder.dp.star.filter</code> applies star-join filter heuristics on top of it<sup class="footnote-ref"><a href="#fn5" id="fnref5:4">[5:4]</a></sup>. A lighter-weight alternative that skips full CBO is <code>spark.sql.cbo.starSchemaDetection</code> (default <code>false</code>), which enables join reordering based on star-schema detection alone<sup class="footnote-ref"><a href="#fn5" id="fnref5:5">[5:5]</a></sup>.</p><p>For <a href="./bottleneck-skew.html">skewed join keys</a>, Spark offers two built-in alternatives to hand-rolled salting: AQE&#39;s skew-join handling (<code>spark.sql.adaptive.skewJoin.enabled</code>), which detects oversized shuffle partitions at runtime and splits them automatically, replicating if needed<sup class="footnote-ref"><a href="#fn10" id="fnref10">[10]</a></sup><sup class="footnote-ref"><a href="#fn5" id="fnref5:6">[5:6]</a></sup>, and Databricks&#39; declarative <code>SKEW</code> hint, which builds a skew-aware plan without any manual salting<sup class="footnote-ref"><a href="#fn11" id="fnref11">[11]</a></sup>. Manual salting remains the fallback where neither is available: add a random salt column to the join key on both sides so a hot key spreads across many partitions (exploding the dimension side into one row per salt value and assigning a random salt on the fact side), then join on the composite <code>(key, salt)</code> pair<sup class="footnote-ref"><a href="#fn12" id="fnref12">[12]</a></sup>.</p><h2 id="sources" tabindex="-1">Sources <a class="header-anchor" href="#sources" aria-label="Permalink to &quot;Sources&quot;">​</a></h2><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p><em>High Performance Spark, 2nd Edition</em>, Karau, Polak &amp; Warren, ch. 6 <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a> <a href="#fnref1:2" class="footnote-backref">↩︎</a> <a href="#fnref1:3" class="footnote-backref">↩︎</a> <a href="#fnref1:4" class="footnote-backref">↩︎</a> <a href="#fnref1:5" class="footnote-backref">↩︎</a> <a href="#fnref1:6" class="footnote-backref">↩︎</a> <a href="#fnref1:7" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p><em>Spark: The Definitive Guide</em>, Chambers &amp; Zaharia, ch. 8 <a href="#fnref2" class="footnote-backref">↩︎</a> <a href="#fnref2:1" class="footnote-backref">↩︎</a></p></li><li id="fn3" class="footnote-item"><p><em>Learning Spark, 2nd Edition</em>, Damji, Wenig, Das, Lee, ch. 7 <a href="#fnref3" class="footnote-backref">↩︎</a> <a href="#fnref3:1" class="footnote-backref">↩︎</a> <a href="#fnref3:2" class="footnote-backref">↩︎</a></p></li><li id="fn4" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/sql-performance-tuning.html" target="_blank" rel="noreferrer">Performance Tuning — Spark SQL, DataFrames and Datasets Guide</a> <a href="#fnref4" class="footnote-backref">↩︎</a> <a href="#fnref4:1" class="footnote-backref">↩︎</a> <a href="#fnref4:2" class="footnote-backref">↩︎</a> <a href="#fnref4:3" class="footnote-backref">↩︎</a> <a href="#fnref4:4" class="footnote-backref">↩︎</a> <a href="#fnref4:5" class="footnote-backref">↩︎</a> <a href="#fnref4:6" class="footnote-backref">↩︎</a> <a href="#fnref4:7" class="footnote-backref">↩︎</a></p></li><li id="fn5" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/configuration.html" target="_blank" rel="noreferrer">Configuration — Spark</a> <a href="#fnref5" class="footnote-backref">↩︎</a> <a href="#fnref5:1" class="footnote-backref">↩︎</a> <a href="#fnref5:2" class="footnote-backref">↩︎</a> <a href="#fnref5:3" class="footnote-backref">↩︎</a> <a href="#fnref5:4" class="footnote-backref">↩︎</a> <a href="#fnref5:5" class="footnote-backref">↩︎</a> <a href="#fnref5:6" class="footnote-backref">↩︎</a></p></li><li id="fn6" class="footnote-item"><p><a href="https://www.taboola.com/engineering/bucket-the-shuffle-out-of-here/" target="_blank" rel="noreferrer">Bucket the Shuffle Out of Here</a> <a href="#fnref6" class="footnote-backref">↩︎</a> <a href="#fnref6:1" class="footnote-backref">↩︎</a></p></li><li id="fn7" class="footnote-item"><p><a href="https://books.japila.pl/spark-sql-internals/bucketing/" target="_blank" rel="noreferrer">Bucketing — The Internals of Spark SQL</a> <a href="#fnref7" class="footnote-backref">↩︎</a></p></li><li id="fn8" class="footnote-item"><p><a href="https://luminousmen.com/post/the-5-minute-guide-to-using-bucketing-in-pyspark" target="_blank" rel="noreferrer">The 5-Minute Guide to Using Bucketing in PySpark</a> <a href="#fnref8" class="footnote-backref">↩︎</a> <a href="#fnref8:1" class="footnote-backref">↩︎</a></p></li><li id="fn9" class="footnote-item"><p><a href="https://luminousmen.com/post/spark-tips-dataframe-api" target="_blank" rel="noreferrer">Spark Tips: DataFrame API</a> <a href="#fnref9" class="footnote-backref">↩︎</a></p></li><li id="fn10" class="footnote-item"><p><a href="https://issues.apache.org/jira/browse/SPARK-29544" target="_blank" rel="noreferrer">SPARK-29544 — Optimize Skewed Join at Runtime</a> <a href="#fnref10" class="footnote-backref">↩︎</a></p></li><li id="fn11" class="footnote-item"><p><a href="https://docs.databricks.com/aws/en/archive/legacy/skew-join" target="_blank" rel="noreferrer">Skew Join Hint</a> <a href="#fnref11" class="footnote-backref">↩︎</a></p></li><li id="fn12" class="footnote-item"><p><a href="https://luminousmen.com/post/spark-tips-partition-tuning" target="_blank" rel="noreferrer">Spark Tips: Partition Tuning</a> <a href="#fnref12" class="footnote-backref">↩︎</a></p></li></ol></section>',26)])])}const g=t(i,[["render",f]]);export{b as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a5 as s}from"./chunks/framework.DSg0KOwT.js";const r="/docs/assets/join-strategy.C_FvrCEo.svg",n="/docs/assets/join-strategy.dark.ChMLnNII.svg",b=JSON.parse(`{"title":"Join Optimization","titleTemplate":":title | Spark Tuning Reference","description":"Physical join strategy selection: broadcast vs. shuffle joins, join hints, bucketing, and AQE's runtime join-strategy switching.","frontmatter":{"title":"Join Optimization","titleTemplate":":title | Spark Tuning Reference","description":"Physical join strategy selection: broadcast vs. shuffle joins, join hints, bucketing, and AQE's runtime join-strategy switching."},"headers":[],"relativePath":"tuning-reference/joins.md","filePath":"tuning-reference/joins.md"}`),i={name:"tuning-reference/joins.md"};function f(c,e,l,h,d,p){return a(),o("div",null,[...e[0]||(e[0]=[s("",26)])])}const g=t(i,[["render",f]]);export{b as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as o,o as a,c as t,a5 as r}from"./chunks/framework.DSg0KOwT.js";const s="/docs/assets/memory-regions.XHvO7jHG.svg",n="/docs/assets/memory-regions.dark.D4TP9_08.svg",i="/docs/assets/memory-borrowing.BqQRJg0u.svg",f="/docs/assets/memory-borrowing.dark.Yhh20O9C.svg",y=JSON.parse('{"title":"Memory Management","titleTemplate":":title | Spark Tuning Reference","description":"Unified memory management: on-heap/off-heap execution vs. storage regions, spill triggers, and executor memory overhead sizing.","frontmatter":{"title":"Memory Management","titleTemplate":":title | Spark Tuning Reference","description":"Unified memory management: on-heap/off-heap execution vs. storage regions, spill triggers, and executor memory overhead sizing."},"headers":[],"relativePath":"tuning-reference/memory-model.md","filePath":"tuning-reference/memory-model.md"}'),c={name:"tuning-reference/memory-model.md"};function l(h,e,d,p,u,m){return a(),t("div",null,[...e[0]||(e[0]=[r('<h1 id="memory-model" tabindex="-1">Memory Management <a class="header-anchor" href="#memory-model" aria-label="Permalink to &quot;Memory Management {#memory-model}&quot;">​</a></h1><h2 id="the-unified-memory-model" tabindex="-1">The unified memory model <a class="header-anchor" href="#the-unified-memory-model" aria-label="Permalink to &quot;The unified memory model&quot;">​</a></h2><p>Since Spark 1.6, the <code>UnifiedMemoryManager</code> has replaced the earlier static split between execution and storage memory with a single shared region, referred to as M<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup><sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. M is carved out of the JVM heap after first setting aside 300 MiB of reserved system memory: <code>M = (JVM heap − 300 MiB) × spark.memory.fraction</code>, with <code>spark.memory.fraction</code> defaulting to 0.6<sup class="footnote-ref"><a href="#fn2" id="fnref2:1">[2:1]</a></sup><sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. Within M, <code>spark.memory.storageFraction</code> (default 0.5) marks off a subregion reserved for cached blocks that are immune to eviction; with defaults, this works out to roughly a 30%/30% on-heap split between execution and storage out of total executor memory<sup class="footnote-ref"><a href="#fn3" id="fnref3:1">[3:1]</a></sup>.</p><img class="light-only" src="'+s+'" alt="The executor memory layout showing reserved memory, the unified region split into execution and storage, and user memory inside the JVM heap, with the off-heap pool and memory overhead outside it."><img class="dark-only" src="'+n+'" alt="The executor memory layout showing reserved memory, the unified region split into execution and storage, and user memory inside the JVM heap, with the off-heap pool and memory overhead outside it."><p>Execution and storage borrow from each other dynamically rather than sitting behind a hard wall: when execution memory is unused, storage can acquire all of it, and vice versa<sup class="footnote-ref"><a href="#fn2" id="fnref2:2">[2:2]</a></sup><sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[1:1]</a></sup>. The two are not equal peers, though. Execution always has priority, taking memory immediately and evicting cached storage blocks if necessary<sup class="footnote-ref"><a href="#fn3" id="fnref3:2">[3:2]</a></sup>.</p><p>The reserved 300 MB is a flat constant (<code>RESERVED_SYSTEM_MEMORY_BYTES</code> in the Spark source), not a formula, and it&#39;s hardcoded: there is no supported production configuration to change it. The only override is the internal <code>spark.testing.reservedMemory</code> property, which exists for Spark&#39;s own test suite rather than production tuning<sup class="footnote-ref"><a href="#fn3" id="fnref3:3">[3:3]</a></sup>. <code>spark.memory.fraction</code> is applied against heap minus that reserved 300 MB, not against total heap. The tuning guide is explicit that it &quot;expresses the size of M as a fraction of the (JVM heap space − 300MiB)&quot;<sup class="footnote-ref"><a href="#fn2" id="fnref2:3">[2:3]</a></sup>.</p><p>Off-heap memory extends the same model outside the JVM heap. When <code>spark.memory.offHeap.enabled=true</code>, Spark allocates raw off-heap buffers via <code>sun.misc.Unsafe</code> (routed through <code>jdk.internal.misc.Unsafe</code> on JDK 17+, which is why some builds need <code>--add-opens</code> flags), and that off-heap region is split into its own execution and storage pools, following the same borrowing rules as on-heap memory: execution has priority, storage gets evicted first<sup class="footnote-ref"><a href="#fn3" id="fnref3:4">[3:4]</a></sup>. Enabling it requires <code>spark.memory.offHeap.size</code> to be set to a positive value; leaving it enabled with the default size of <code>0</code> is a documented-invalid combination, not a supported way to run with off-heap &quot;on but empty&quot;<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>. The setting also &quot;has no impact on heap memory usage&quot;. Turning on off-heap memory does not shrink the JVM <code>-Xmx</code> for you, so the on-heap size has to be reduced manually to keep total footprint constant<sup class="footnote-ref"><a href="#fn4" id="fnref4:1">[4:1]</a></sup>. This off-heap execution memory underlies Project Tungsten: its compact binary row encoding, its explicit-memory-managed hash map for aggregations, and its cache-aware sort/join algorithms are all built to run against off-heap, GC-invisible memory<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup><sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup>.</p><p>Executor memory overhead is a separate pool layered on top of M. <code>spark.executor.memoryOverhead</code> defaults to <code>executorMemory × spark.executor.memoryOverheadFactor</code>, floored at <code>spark.executor.minMemoryOverhead</code> (384 MiB)<sup class="footnote-ref"><a href="#fn4" id="fnref4:2">[4:2]</a></sup>. <code>spark.executor.memoryOverheadFactor</code> itself defaults to 0.10 for ordinary JVM executors, but Spark bumps that default to 0.40 specifically for Kubernetes non-JVM jobs, since those workloads tend to need more non-JVM heap space<sup class="footnote-ref"><a href="#fn4" id="fnref4:3">[4:3]</a></sup>. The legacy, YARN-only <code>spark.yarn.executor.memoryOverhead</code> property was removed in Spark 3.0 in favor of the cluster-manager-agnostic <code>spark.executor.memoryOverhead</code><sup class="footnote-ref"><a href="#fn3" id="fnref3:5">[3:5]</a></sup>.</p><blockquote><p><strong>PySpark:</strong> <code>spark.python.worker.memory</code> (default <code>512m</code>) is a soft spill threshold for a Python worker&#39;s own aggregation buffering, not a JVM-enforced cap. A PySpark worker runs as a separate OS process outside the JVM heap, so the JVM cannot police it directly<sup class="footnote-ref"><a href="#fn3" id="fnref3:6">[3:6]</a></sup>. The only setting that actually tries to bound PySpark memory per executor is <code>spark.executor.pyspark.memory</code>, and even that depends on Python&#39;s <code>resource</code> module, which isn&#39;t supported on Windows and doesn&#39;t actually limit anything on macOS<sup class="footnote-ref"><a href="#fn4" id="fnref4:4">[4:4]</a></sup>. Left unset, PySpark memory usage is folded into <code>spark.executor.memoryOverhead</code> by default<sup class="footnote-ref"><a href="#fn4" id="fnref4:5">[4:5]</a></sup>.</p></blockquote><h2 id="watching-eviction-and-spill" tabindex="-1">Watching eviction and spill <a class="header-anchor" href="#watching-eviction-and-spill" aria-label="Permalink to &quot;Watching eviction and spill&quot;">​</a></h2><p>That borrowing leaves a trace once eviction actually happens. Storage eviction under memory pressure follows an LRU (least recently used) policy<sup class="footnote-ref"><a href="#fn1" id="fnref1:2">[1:2]</a></sup><sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup>. The granularity is the block: the <code>BlockManager</code> tracks cached data as per-partition blocks (one block per RDD/DataFrame partition) and LRU operates at that level rather than evicting an entire RDD or cached table in one shot<sup class="footnote-ref"><a href="#fn8" id="fnref8">[8]</a></sup><sup class="footnote-ref"><a href="#fn9" id="fnref9">[9]</a></sup>. In practice this tends to evict the oldest partitions first, those materialized in the earliest job or stage, though lazy evaluation makes it hard to predict exactly which partitions will go ahead of time<sup class="footnote-ref"><a href="#fn9" id="fnref9:1">[9:1]</a></sup>.</p><p>What happens to an evicted block is visible in its downstream effect and depends on its <code>StorageLevel</code>. For <code>MEMORY_ONLY</code>, the block is simply dropped and recomputed from the RDD&#39;s lineage the next time it&#39;s needed. For a disk-backed level like <code>MEMORY_AND_DISK</code>, the evicted block is written to disk and read back from there instead of being recomputed<sup class="footnote-ref"><a href="#fn9" id="fnref9:2">[9:2]</a></sup><sup class="footnote-ref"><a href="#fn7" id="fnref7:1">[7:1]</a></sup>. Blocks can also be evicted well before you ever try to reuse them: <code>.cache()</code> does not guarantee a block stays resident, especially on busy clusters or long pipelines<sup class="footnote-ref"><a href="#fn7" id="fnref7:2">[7:2]</a></sup>.</p><p>On the execution side, each task gets its own <code>TaskMemoryManager</code>, which enforces a soft per-task cap: with <code>n</code> tasks running concurrently, each task is allowed to allocate somewhere between <code>1/(2n)</code> and <code>1/n</code> of total execution memory, and the first task to arrive on an idle executor typically grabs more than its later-arriving neighbors<sup class="footnote-ref"><a href="#fn3" id="fnref3:7">[3:7]</a></sup>.</p><p>When a sort or hash-aggregation operator keeps requesting execution-memory pages and can&#39;t get more, Spark doesn&#39;t fail immediately. It blocks the requesting task, spills that task&#39;s in-memory data structure to disk, or, in the worst case, throws an <code>OutOfMemoryError</code><sup class="footnote-ref"><a href="#fn3" id="fnref3:8">[3:8]</a></sup>. The spill path runs through Spark&#39;s map/shuffle I/O machinery: shuffle partitions created by wide transformations like <code>groupBy()</code> or <code>join()</code> <a href="./bottleneck-spill.html">spill</a> to the executors&#39; local disks at the location set by <code>spark.local.directory</code><sup class="footnote-ref"><a href="#fn6" id="fnref6:1">[6:1]</a></sup>. SQL physical operators apply the same idea with their own row-count guardrails: <code>sortMergeJoinExec</code>&#39;s in-memory buffer and the cartesian-product operator&#39;s buffer both spill once they cross a configured row threshold, which by default is set to the value of <code>spark.shuffle.spill.numElementsForceSpillThreshold</code><sup class="footnote-ref"><a href="#fn10" id="fnref10">[10]</a></sup>.</p><p>Whatever the trigger, the resulting spilled bytes are exposed on the task metrics as <code>memoryBytesSpilled</code>, visible in the Spark UI and event log. This is the concrete signal to watch for execution memory pressure<sup class="footnote-ref"><a href="#fn11" id="fnref11">[11]</a></sup>.</p><p>Memory-overhead misconfiguration shows up differently: it manifests as the container or pod being OOMKilled with a vague container-memory error rather than a Spark-level exception, since the enforcement happens at the YARN NodeManager or Kubernetes kubelet/cgroup layer, not inside the JVM<sup class="footnote-ref"><a href="#fn3" id="fnref3:9">[3:9]</a></sup>. Under the old default overhead factor of 0.10, non-JVM Kubernetes jobs commonly failed with &quot;Memory Overhead Exceeded&quot; errors, which is exactly why Spark bumped the Kubernetes non-JVM default to 0.40<sup class="footnote-ref"><a href="#fn4" id="fnref4:6">[4:6]</a></sup>.</p><h2 id="what-the-borrowing-costs-you" tabindex="-1">What the borrowing costs you <a class="header-anchor" href="#what-the-borrowing-costs-you" aria-label="Permalink to &quot;What the borrowing costs you&quot;">​</a></h2><p>None of this is free for whoever&#39;s counting on cached data staying put. Because execution always wins the tug-of-war over storage, <a href="./caching.html">caching</a> is never a guarantee; it&#39;s a best effort. As luminousmen-memory-management puts it: &quot;If Execution needs memory, it takes it. If Storage is using that space (cached RDDs, broadcasts), Spark starts evicting blocks. If Execution is idle, Storage can grow into that space, until Execution comes back.&quot;<sup class="footnote-ref"><a href="#fn3" id="fnref3:10">[3:10]</a></sup> That growth-then-shrink-back behavior is dynamic borrowing, not storage evicting execution; eviction is strictly one-directional. As spark-notes-task-memory-management summarizes the agreement between the two regions: &quot;keep acquiring execution memory and evict storage as you need more execution memory&quot;<sup class="footnote-ref"><a href="#fn1" id="fnref1:3">[1:3]</a></sup>, never the reverse. The same asymmetric rule carries over when off-heap memory is enabled: execution still has priority and storage still gets evicted first<sup class="footnote-ref"><a href="#fn3" id="fnref3:11">[3:11]</a></sup>. Practically, this means a cached DataFrame can silently lose blocks under memory pressure, forcing an expensive lineage recompute (for <code>MEMORY_ONLY</code>) or an extra disk round-trip (for <code>MEMORY_AND_DISK</code>) the next time it&#39;s touched.</p><img class="light-only" src="'+i+'" alt="How storage memory borrows idle execution space while execution reclaims its own space by evicting storage in one direction."><img class="dark-only" src="'+f+'" alt="How storage memory borrows idle execution space while execution reclaims its own space by evicting storage in one direction."><p>The <code>TaskMemoryManager</code>&#39;s soft per-task cap explains why spill behavior is workload-shape-dependent rather than a fixed threshold: with more tasks packed onto an executor, each one&#39;s guaranteed share of execution memory shrinks toward <code>1/(2n)</code>, making spills more likely under high task concurrency even when total execution memory hasn&#39;t changed<sup class="footnote-ref"><a href="#fn3" id="fnref3:12">[3:12]</a></sup>.</p><p>Off-heap memory&#39;s payoff is specifically about <a href="./bottleneck-gc.html">garbage collection</a>: because off-heap buffers sit outside the JVM heap, they are invisible to the garbage collector, so fewer and smaller live objects need to be tracked, scanned, and copied, which reduces both the frequency and duration of GC pauses<sup class="footnote-ref"><a href="#fn3" id="fnref3:13">[3:13]</a></sup>. Project Tungsten&#39;s off-heap hash map for aggregations was benchmarked at over 1 million operations per second in a single thread, with &quot;almost no performance degradation as memory utilization increases,&quot; unlike the JVM default <code>java.util.HashMap</code>, which eventually thrashes on GC<sup class="footnote-ref"><a href="#fn5" id="fnref5:1">[5:1]</a></sup><sup class="footnote-ref"><a href="#fn6" id="fnref6:2">[6:2]</a></sup>. The tradeoff is that off-heap memory removes the GC safety net along with the GC overhead: there&#39;s no garbage collector cleaning up if something goes wrong<sup class="footnote-ref"><a href="#fn3" id="fnref3:14">[3:14]</a></sup>.</p><p>Memory overhead matters because off-heap memory and (if unset) PySpark memory are not automatically folded into the overhead calculation the way heap memory is. Enabling <code>spark.memory.offHeap.size</code> without also raising <code>spark.executor.memoryOverhead</code> (or explicitly setting <code>spark.executor.pyspark.memory</code>) can push the executor&#39;s real footprint past what the cluster manager granted, and the process gets killed with a vague container-memory error instead of a Spark-level exception<sup class="footnote-ref"><a href="#fn3" id="fnref3:15">[3:15]</a></sup>. Worked example: with <code>--executor-memory=8G</code>, the default 10% overhead gives <code>max(0.1 × 8192 MB, 384 MB) = 819 MB</code>, so the total memory requested from the cluster manager is <code>8192 + 819 = 9011 MB</code><sup class="footnote-ref"><a href="#fn3" id="fnref3:16">[3:16]</a></sup>, useful to keep in mind when <a href="./cluster-config.html">sizing containers or pods</a> against a cluster&#39;s available capacity.</p><h2 id="tuning-the-memory-pools" tabindex="-1">Tuning the memory pools <a class="header-anchor" href="#tuning-the-memory-pools" aria-label="Permalink to &quot;Tuning the memory pools&quot;">​</a></h2><p>Most of this is adjustable, within limits. Tune <code>spark.memory.fraction</code> and <code>spark.memory.storageFraction</code> only if your workload&#39;s execution/storage balance genuinely needs to shift away from the ~30/30 default. But don&#39;t try to reclaim the 300 MB reserved region; it&#39;s a fixed, non-tunable constant in production<sup class="footnote-ref"><a href="#fn3" id="fnref3:17">[3:17]</a></sup>.</p><p>If losing cached partitions to eviction is costly, prefer a disk-backed <code>StorageLevel</code> such as <code>MEMORY_AND_DISK</code> over <code>MEMORY_ONLY</code>: an evicted block gets written to disk and read back rather than triggering a full lineage recompute<sup class="footnote-ref"><a href="#fn9" id="fnref9:3">[9:3]</a></sup><sup class="footnote-ref"><a href="#fn7" id="fnref7:3">[7:3]</a></sup>. Keep in mind <code>.cache()</code> alone is not a residency guarantee even with this choice<sup class="footnote-ref"><a href="#fn7" id="fnref7:4">[7:4]</a></sup>.</p><p>Watch <code>memoryBytesSpilled</code> in the Spark UI or history server to catch execution-memory pressure early<sup class="footnote-ref"><a href="#fn11" id="fnref11:1">[11:1]</a></sup>. If sort or hash-aggregation spills are heavy, consider giving the executor more memory or reducing the number of concurrently running tasks per executor, since the <code>TaskMemoryManager</code>&#39;s per-task guarantee shrinks as task concurrency <code>n</code> grows<sup class="footnote-ref"><a href="#fn3" id="fnref3:18">[3:18]</a></sup>. For SQL joins and cartesian products, <code>spark.shuffle.spill.numElementsForceSpillThreshold</code> governs when <code>sortMergeJoinExec</code> and the cartesian-product operator spill their buffers<sup class="footnote-ref"><a href="#fn10" id="fnref10:1">[10:1]</a></sup>.</p><p>Set <code>spark.executor.memoryOverhead</code> explicitly rather than relying on the default, especially on Kubernetes for non-JVM workloads (where the default factor is already bumped to 0.40) or whenever off-heap memory or heavy PySpark usage is in play: those aren&#39;t automatically folded into the overhead calculation, so an unadjusted overhead can lead to an OOMKilled container instead of a clean Spark-level error<sup class="footnote-ref"><a href="#fn3" id="fnref3:19">[3:19]</a></sup><sup class="footnote-ref"><a href="#fn4" id="fnref4:7">[4:7]</a></sup>. Use <code>spark.executor.memoryOverhead</code>, not the removed <code>spark.yarn.executor.memoryOverhead</code><sup class="footnote-ref"><a href="#fn3" id="fnref3:20">[3:20]</a></sup>. For capacity planning, remember the total requested from the cluster manager is executor memory plus overhead, e.g., <code>8192 + 819 = 9011 MB</code> for an 8 GB executor at the default 10% factor<sup class="footnote-ref"><a href="#fn3" id="fnref3:21">[3:21]</a></sup>.</p><p>When enabling off-heap memory, always pair <code>spark.memory.offHeap.enabled=true</code> with an explicit positive <code>spark.memory.offHeap.size</code> (never leave it enabled at the default size of <code>0</code><sup class="footnote-ref"><a href="#fn4" id="fnref4:8">[4:8]</a></sup>) and manually reduce the JVM <code>-Xmx</code> to compensate, since enabling off-heap memory does not shrink the heap for you<sup class="footnote-ref"><a href="#fn4" id="fnref4:9">[4:9]</a></sup>.</p><blockquote><p><strong>PySpark:</strong> if you need PySpark&#39;s own memory bounded rather than folded silently into the overhead, set <code>spark.executor.pyspark.memory</code> explicitly, though its enforcement relies on Python&#39;s <code>resource</code> module and won&#39;t work on Windows and won&#39;t actually limit anything on macOS<sup class="footnote-ref"><a href="#fn4" id="fnref4:10">[4:10]</a></sup>.</p></blockquote><h2 id="bottleneck-cache-utilization" tabindex="-1">Cache utilization <a class="header-anchor" href="#bottleneck-cache-utilization" aria-label="Permalink to &quot;Cache utilization {#bottleneck-cache-utilization}&quot;">​</a></h2><p><span class="tag">CSTOR</span></p><p>Spark&#39;s event log carries no block-access or hit-rate events, so cache utilization is read from periodic per-RDD storage snapshots instead: how much of a persisted RDD is actually resident where it was asked to be.</p><h3 id="how-it-s-detected" tabindex="-1">How it&#39;s detected <a class="header-anchor" href="#how-it-s-detected" aria-label="Permalink to &quot;How it&#39;s detected&quot;">​</a></h3><p>A persisted RDD, one whose storage level requests memory and/or disk with at least one partition actually cached, is evaluated on two independent measures and can register on both at once:</p><table tabindex="0"><thead><tr><th>Signal</th><th>Info</th><th>Warning</th></tr></thead><tbody><tr><td>Cached ratio (cached partitions ÷ total partitions)</td><td>&lt; 90%</td><td>&lt; 50%</td></tr><tr><td>Disk ratio (disk bytes ÷ (memory bytes + disk bytes)), <code>MEMORY_AND_DISK*</code> levels only</td><td>&gt; 15%</td><td>&gt; 40%</td></tr></tbody></table><p>Both ratios come from a point-in-time storage snapshot taken at stage-submission events, not a runtime block-access count, so confirm against the Spark UI&#39;s Storage tab before acting. Confidence scales with the RDD&#39;s partition count: below 10 partitions the ratio is noisy enough to call low confidence, at 50 or more it&#39;s high.</p><h3 id="why-it-matters" tabindex="-1">Why it matters <a class="header-anchor" href="#why-it-matters" aria-label="Permalink to &quot;Why it matters&quot;">​</a></h3><p>A partially cached RDD still pays the recomputation cost for its evicted share on every later read, undercutting the reason it was cached in the first place. Disk spillover under a <code>MEMORY_AND_DISK*</code> level keeps that data around instead of forcing a recompute, but a disk read is still far slower than serving it out of memory.</p><h3 id="how-to-fix-it" tabindex="-1">How to fix it <a class="header-anchor" href="#how-to-fix-it" aria-label="Permalink to &quot;How to fix it&quot;">​</a></h3><ul><li>Increase executor memory, or reduce the size of the dataset being cached, so more of it fits in the storage region without eviction.</li><li>If the RDD is only cached for the occasional narrow scan, weigh whether persisting it at all is worth the partial-cache overhead, versus recomputing the slice you actually need.</li></ul><h2 id="sources" tabindex="-1">Sources <a class="header-anchor" href="#sources" aria-label="Permalink to &quot;Sources&quot;">​</a></h2><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p><a href="https://raw.githubusercontent.com/spoddutur/spark-notes/master/task_memory_management_in_spark.md" target="_blank" rel="noreferrer">Task Memory Management in Spark</a> <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a> <a href="#fnref1:2" class="footnote-backref">↩︎</a> <a href="#fnref1:3" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/tuning.html" target="_blank" rel="noreferrer">Tuning Spark</a> <a href="#fnref2" class="footnote-backref">↩︎</a> <a href="#fnref2:1" class="footnote-backref">↩︎</a> <a href="#fnref2:2" class="footnote-backref">↩︎</a> <a href="#fnref2:3" class="footnote-backref">↩︎</a></p></li><li id="fn3" class="footnote-item"><p><a href="https://luminousmen.com/post/dive-into-spark-memory" target="_blank" rel="noreferrer">Dive into Spark Memory</a> <a href="#fnref3" class="footnote-backref">↩︎</a> <a href="#fnref3:1" class="footnote-backref">↩︎</a> <a href="#fnref3:2" class="footnote-backref">↩︎</a> <a href="#fnref3:3" class="footnote-backref">↩︎</a> <a href="#fnref3:4" class="footnote-backref">↩︎</a> <a href="#fnref3:5" class="footnote-backref">↩︎</a> <a href="#fnref3:6" class="footnote-backref">↩︎</a> <a href="#fnref3:7" class="footnote-backref">↩︎</a> <a href="#fnref3:8" class="footnote-backref">↩︎</a> <a href="#fnref3:9" class="footnote-backref">↩︎</a> <a href="#fnref3:10" class="footnote-backref">↩︎</a> <a href="#fnref3:11" class="footnote-backref">↩︎</a> <a href="#fnref3:12" class="footnote-backref">↩︎</a> <a href="#fnref3:13" class="footnote-backref">↩︎</a> <a href="#fnref3:14" class="footnote-backref">↩︎</a> <a href="#fnref3:15" class="footnote-backref">↩︎</a> <a href="#fnref3:16" class="footnote-backref">↩︎</a> <a href="#fnref3:17" class="footnote-backref">↩︎</a> <a href="#fnref3:18" class="footnote-backref">↩︎</a> <a href="#fnref3:19" class="footnote-backref">↩︎</a> <a href="#fnref3:20" class="footnote-backref">↩︎</a> <a href="#fnref3:21" class="footnote-backref">↩︎</a></p></li><li id="fn4" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/configuration.html" target="_blank" rel="noreferrer">Configuration — Spark</a> <a href="#fnref4" class="footnote-backref">↩︎</a> <a href="#fnref4:1" class="footnote-backref">↩︎</a> <a href="#fnref4:2" class="footnote-backref">↩︎</a> <a href="#fnref4:3" class="footnote-backref">↩︎</a> <a href="#fnref4:4" class="footnote-backref">↩︎</a> <a href="#fnref4:5" class="footnote-backref">↩︎</a> <a href="#fnref4:6" class="footnote-backref">↩︎</a> <a href="#fnref4:7" class="footnote-backref">↩︎</a> <a href="#fnref4:8" class="footnote-backref">↩︎</a> <a href="#fnref4:9" class="footnote-backref">↩︎</a> <a href="#fnref4:10" class="footnote-backref">↩︎</a></p></li><li id="fn5" class="footnote-item"><p><a href="https://www.databricks.com/blog/2015/04/28/project-tungsten-bringing-spark-closer-to-bare-metal.html" target="_blank" rel="noreferrer">Project Tungsten: Bringing Spark Closer to Bare Metal</a> <a href="#fnref5" class="footnote-backref">↩︎</a> <a href="#fnref5:1" class="footnote-backref">↩︎</a></p></li><li id="fn6" class="footnote-item"><p><em>Learning Spark, 2nd Edition</em>, Damji, Wenig, Das, Lee, ch. 6–7 <a href="#fnref6" class="footnote-backref">↩︎</a> <a href="#fnref6:1" class="footnote-backref">↩︎</a> <a href="#fnref6:2" class="footnote-backref">↩︎</a></p></li><li id="fn7" class="footnote-item"><p><a href="https://luminousmen.com/post/explaining-the-mechanics-of-spark-caching" target="_blank" rel="noreferrer">Explaining the Mechanics of Spark Caching</a> <a href="#fnref7" class="footnote-backref">↩︎</a> <a href="#fnref7:1" class="footnote-backref">↩︎</a> <a href="#fnref7:2" class="footnote-backref">↩︎</a> <a href="#fnref7:3" class="footnote-backref">↩︎</a> <a href="#fnref7:4" class="footnote-backref">↩︎</a></p></li><li id="fn8" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/rdd-programming-guide.html" target="_blank" rel="noreferrer">RDD Programming Guide</a> <a href="#fnref8" class="footnote-backref">↩︎</a></p></li><li id="fn9" class="footnote-item"><p><em>High Performance Spark, 2nd Edition</em>, Karau, Polak &amp; Warren, ch. 7 <a href="#fnref9" class="footnote-backref">↩︎</a> <a href="#fnref9:1" class="footnote-backref">↩︎</a> <a href="#fnref9:2" class="footnote-backref">↩︎</a> <a href="#fnref9:3" class="footnote-backref">↩︎</a></p></li><li id="fn10" class="footnote-item"><p><a href="https://raw.githubusercontent.com/apache/spark/v3.5.0/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala" target="_blank" rel="noreferrer">SQLConf.scala</a> <a href="#fnref10" class="footnote-backref">↩︎</a> <a href="#fnref10:1" class="footnote-backref">↩︎</a></p></li><li id="fn11" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/monitoring.html#spark-history-server" target="_blank" rel="noreferrer">Monitoring and Instrumentation</a> <a href="#fnref11" class="footnote-backref">↩︎</a> <a href="#fnref11:1" class="footnote-backref">↩︎</a></p></li></ol></section>',45)])])}const k=o(c,[["render",l]]);export{y as __pageData,k as default};
@@ -0,0 +1 @@
1
+ import{_ as o,o as a,c as t,a5 as r}from"./chunks/framework.DSg0KOwT.js";const s="/docs/assets/memory-regions.XHvO7jHG.svg",n="/docs/assets/memory-regions.dark.D4TP9_08.svg",i="/docs/assets/memory-borrowing.BqQRJg0u.svg",f="/docs/assets/memory-borrowing.dark.Yhh20O9C.svg",y=JSON.parse('{"title":"Memory Management","titleTemplate":":title | Spark Tuning Reference","description":"Unified memory management: on-heap/off-heap execution vs. storage regions, spill triggers, and executor memory overhead sizing.","frontmatter":{"title":"Memory Management","titleTemplate":":title | Spark Tuning Reference","description":"Unified memory management: on-heap/off-heap execution vs. storage regions, spill triggers, and executor memory overhead sizing."},"headers":[],"relativePath":"tuning-reference/memory-model.md","filePath":"tuning-reference/memory-model.md"}'),c={name:"tuning-reference/memory-model.md"};function l(h,e,d,p,u,m){return a(),t("div",null,[...e[0]||(e[0]=[r("",45)])])}const k=o(c,[["render",l]]);export{y as __pageData,k as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as r,a5 as s}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"Metrics Glossary","titleTemplate":":title | Spark Tuning Reference","description":"Glossary of every metric surfaced elsewhere on the site: what each one measures, its event-log source field, and what a problematic value looks like.","frontmatter":{"title":"Metrics Glossary","titleTemplate":":title | Spark Tuning Reference","description":"Glossary of every metric surfaced elsewhere on the site: what each one measures, its event-log source field, and what a problematic value looks like."},"headers":[],"relativePath":"tuning-reference/metrics.md","filePath":"tuning-reference/metrics.md"}'),o={name:"tuning-reference/metrics.md"};function i(c,e,n,l,d,f){return a(),r("div",null,[...e[0]||(e[0]=[s('<h1 id="metrics" tabindex="-1">Metrics Glossary <a class="header-anchor" href="#metrics" aria-label="Permalink to &quot;Metrics Glossary {#metrics}&quot;">​</a></h1><p>Reference for every metric surfaced elsewhere in this guide: what it measures, where Spark records it in the event log, and what a problematic value looks like.</p><h2 id="metric-task-duration" tabindex="-1">Task duration (P50/P95/max) <a class="header-anchor" href="#metric-task-duration" aria-label="Permalink to &quot;Task duration (P50/P95/max) {#metric-task-duration}&quot;">​</a></h2><p>Per-task wall-clock duration, aggregated across a stage&#39;s tasks into percentiles (P50, P95, max) to surface skew and stragglers. It is computed from <code>SparkListenerTaskEnd</code> events rather than read off a single field: each task end carries its own duration, and the percentiles are derived by aggregating those values across the stage.</p><p>A problematic value looks like a small fraction of tasks running far past the rest: a stage is classified as a straggler when more than 5% of tasks run at least 4x the median duration (with at least 10 tasks in the stage), or when speculative tasks were fired.</p><h2 id="metric-shuffle-read-bytes" tabindex="-1">Shuffle read bytes <a class="header-anchor" href="#metric-shuffle-read-bytes" aria-label="Permalink to &quot;Shuffle read bytes {#metric-shuffle-read-bytes}&quot;">​</a></h2><p>The volume of shuffle data a task reads from remote executors, recorded in <code>taskMetrics.shuffleReadMetrics.remoteBytesRead</code>.</p><h2 id="metric-shuffle-write-bytes" tabindex="-1">Shuffle write bytes <a class="header-anchor" href="#metric-shuffle-write-bytes" aria-label="Permalink to &quot;Shuffle write bytes {#metric-shuffle-write-bytes}&quot;">​</a></h2><p>The size of the shuffle output a task writes, recorded in <code>taskMetrics.shuffleWriteMetrics.bytesWritten</code>. Spark&#39;s own metric description calls it simply the &quot;Number of bytes written in shuffle operations,&quot; without stating compression state on its own<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>.</p><p>The sort-based shuffle writer fills in the mechanics: incoming records are serialized as soon as they reach the shuffle writer and buffered in serialized form while sorting<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. When the spill compression codec supports concatenating compressed data, the final merge step concatenates the already-compressed spill partitions directly into the output file, using <code>transferTo</code> rather than decompressing and recompressing<sup class="footnote-ref"><a href="#fn2" id="fnref2:1">[2:1]</a></sup>. So <code>bytesWritten</code> counts compressed, post-serialization bytes, and in this common fast-merge path the final written output is built directly from data that had already been spilled to disk during sorting, rather than written fresh at merge time.</p><h2 id="metric-memory-bytes-spilled" tabindex="-1">Memory bytes spilled <a class="header-anchor" href="#metric-memory-bytes-spilled" aria-label="Permalink to &quot;Memory bytes spilled {#metric-memory-bytes-spilled}&quot;">​</a></h2><p>Bytes a task spilled from in-memory structures, recorded in <code>taskMetrics.memoryBytesSpilled</code>. Any value above zero is a spill warning, with the skew-vs-volume classification (based on what fraction of a stage&#39;s tasks show zero spill) as the actionable signal.</p><h2 id="metric-disk-bytes-spilled" tabindex="-1">Disk bytes spilled <a class="header-anchor" href="#metric-disk-bytes-spilled" aria-label="Permalink to &quot;Disk bytes spilled {#metric-disk-bytes-spilled}&quot;">​</a></h2><p>The on-disk counterpart to memory spill: bytes a task spilled to disk, recorded in <code>taskMetrics.diskBytesSpilled</code>. It feeds the same spill classification as memory bytes spilled.</p><h2 id="metric-jvm-gc-time" tabindex="-1">JVM GC time <a class="header-anchor" href="#metric-jvm-gc-time" aria-label="Permalink to &quot;JVM GC time {#metric-jvm-gc-time}&quot;">​</a></h2><p>Elapsed time the JVM spent in garbage collection while a task executed, recorded in <code>taskMetrics.jvmGCTime</code> and expressed in milliseconds<sup class="footnote-ref"><a href="#fn1" id="fnref1:1">[1:1]</a></sup>. The value is cumulative across the task&#39;s full execution window: the sum of every GC pause that occurred during that task&#39;s run, not just the most recent one<sup class="footnote-ref"><a href="#fn1" id="fnref1:2">[1:2]</a></sup>. This matches how it&#39;s serialized: as a single scalar long, consistent with an accumulator rather than a per-GC-event log entry<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.</p><h2 id="metric-gcpct" tabindex="-1">gcPct <a class="header-anchor" href="#metric-gcpct" aria-label="Permalink to &quot;gcPct {#metric-gcpct}&quot;">​</a></h2><p>A synthetic ratio, <code>jvmGCTime / executorRunTime</code>, not a raw Spark field. It drives GC-bottleneck classification: above 10% is a warning, above 20% is critical.</p><h2 id="metric-executor-run-time" tabindex="-1">Executor run time <a class="header-anchor" href="#metric-executor-run-time" aria-label="Permalink to &quot;Executor run time {#metric-executor-run-time}&quot;">​</a></h2><p>Elapsed time the executor spent running a task, recorded in <code>taskMetrics.executorRunTime</code> and expressed in milliseconds<sup class="footnote-ref"><a href="#fn1" id="fnref1:3">[1:3]</a></sup>. <code>TaskMetrics</code> (and therefore <code>executorRunTime</code>) is serialized as an optional part of the <code>SparkListenerTaskEnd</code> payload, not guaranteed on every task end<sup class="footnote-ref"><a href="#fn3" id="fnref3:1">[3:1]</a></sup>. In practice it is available for failed tasks that got far enough to actually run, such as <code>ExceptionFailure</code> or <code>TaskKilled</code>. The event-log deserialization code even falls back to reading accumulator updates out of the embedded <code>TaskMetrics</code> for old, Spark-1.x-era logs, which only makes sense if the metrics object is normally populated for that failure type<sup class="footnote-ref"><a href="#fn3" id="fnref3:2">[3:2]</a></sup>. It can be absent for reasons like <code>Resubmitted</code>, where the task attempt never completed on that executor<sup class="footnote-ref"><a href="#fn3" id="fnref3:3">[3:3]</a></sup>.</p><h2 id="metric-fetch-wait-time-ratio" tabindex="-1">Fetch wait time ratio <a class="header-anchor" href="#metric-fetch-wait-time-ratio" aria-label="Permalink to &quot;Fetch wait time ratio {#metric-fetch-wait-time-ratio}&quot;">​</a></h2><p>A synthetic ratio, <code>fetchWaitTime / taskDuration</code>, not a raw Spark field: the time a task spent blocked waiting on remote shuffle blocks, relative to its total duration.</p><h2 id="metric-input-bytes" tabindex="-1">Input bytes <a class="header-anchor" href="#metric-input-bytes" aria-label="Permalink to &quot;Input bytes {#metric-input-bytes}&quot;">​</a></h2><p>Bytes a task read as input, recorded in <code>taskMetrics.inputMetrics.bytesRead</code>.</p><h2 id="metric-output-bytes" tabindex="-1">Output bytes <a class="header-anchor" href="#metric-output-bytes" aria-label="Permalink to &quot;Output bytes {#metric-output-bytes}&quot;">​</a></h2><p>Bytes a task wrote as output, recorded in <code>taskMetrics.outputMetrics.bytesWritten</code>.</p><h2 id="metric-io-ratio" tabindex="-1">I/O ratio <a class="header-anchor" href="#metric-io-ratio" aria-label="Permalink to &quot;I/O ratio {#metric-io-ratio}&quot;">​</a></h2><p>A synthetic ratio, <code>outputBytes / inputBytes</code>, not a raw Spark field.</p><h2 id="metric-peak-execution-memory" tabindex="-1">Peak execution memory <a class="header-anchor" href="#metric-peak-execution-memory" aria-label="Permalink to &quot;Peak execution memory {#metric-peak-execution-memory}&quot;">​</a></h2><p>Peak memory recorded in <code>taskMetrics.peakExecutionMemory</code>. This is a task-level accumulator, distinct from the separate executor-level <code>peakMemoryMetrics.OnHeapExecutionMemory</code> and <code>.OffHeapExecutionMemory</code> gauges, which report the on-heap and off-heap execution pools as two separate numbers at the executor level rather than as a single per-task figure<sup class="footnote-ref"><a href="#fn1" id="fnref1:4">[1:4]</a></sup>.</p><h2 id="metric-failed-tasks" tabindex="-1">Failed tasks / failure rate <a class="header-anchor" href="#metric-failed-tasks" aria-label="Permalink to &quot;Failed tasks / failure rate {#metric-failed-tasks}&quot;">​</a></h2><p>Tasks whose <code>SparkListenerTaskEnd</code> reason is not <code>Success</code>. The <code>Reason</code> field holds the formatted class name of whichever <code>TaskEndReason</code> was assigned to that task end<sup class="footnote-ref"><a href="#fn3" id="fnref3:4">[3:4]</a></sup>, and the canonical set of reasons includes <code>FetchFailed</code>, <code>ExceptionFailure</code>, <code>TaskResultLost</code>, <code>TaskKilled</code>, <code>TaskCommitDenied</code>, <code>ExecutorLostFailure</code>, and <code>UnknownReason</code><sup class="footnote-ref"><a href="#fn3" id="fnref3:5">[3:5]</a></sup>. Because <code>TaskMetrics</code> is only an optional part of the task-end payload, it can be absent for some of these reasons: for example <code>Resubmitted</code>, where the task attempt never actually completed on that executor<sup class="footnote-ref"><a href="#fn3" id="fnref3:6">[3:6]</a></sup>.</p><h2 id="metric-speculative-tasks" tabindex="-1">Speculative tasks / straggler count <a class="header-anchor" href="#metric-speculative-tasks" aria-label="Permalink to &quot;Speculative tasks / straggler count {#metric-speculative-tasks}&quot;">​</a></h2><p>Tasks launched as speculative retries of a slow-running task, recorded via <code>SparkListenerTaskStart</code> where <code>speculative = true</code>. Any speculative task firing (or more than 5% of a stage&#39;s tasks running at least 4x the median duration, with at least 10 tasks in the stage) is a straggler signal.</p><h2 id="metric-executor-count" tabindex="-1">Executor count (added/removed/concurrent) <a class="header-anchor" href="#metric-executor-count" aria-label="Permalink to &quot;Executor count (added/removed/concurrent) {#metric-executor-count}&quot;">​</a></h2><p>The number of executors added, removed, or concurrently running, recorded via <code>SparkListenerExecutorAdded</code> / <code>SparkListenerExecutorRemoved</code> events.</p><h2 id="metric-stage-duration" tabindex="-1">Stage wall-clock duration <a class="header-anchor" href="#metric-stage-duration" aria-label="Permalink to &quot;Stage wall-clock duration {#metric-stage-duration}&quot;">​</a></h2><p>A stage&#39;s total elapsed time, recorded from <code>SparkListenerStageCompleted</code>: <code>completionTime − submissionTime</code>.</p><h2 id="metric-first-stage-submitted-at" tabindex="-1">firstStageSubmittedAt <a class="header-anchor" href="#metric-first-stage-submitted-at" aria-label="Permalink to &quot;firstStageSubmittedAt {#metric-first-stage-submitted-at}&quot;">​</a></h2><p>A synthetic field: the timestamp of the first <code>SparkListenerStageSubmitted</code> event in the event log. The gap between this timestamp and the application&#39;s start time drives cold-start classification: more than 30 seconds is a warning.</p><h2 id="sources" tabindex="-1">Sources <a class="header-anchor" href="#sources" aria-label="Permalink to &quot;Sources&quot;">​</a></h2><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p><a href="https://spark.apache.org/docs/latest/monitoring.html#spark-history-server" target="_blank" rel="noreferrer">Monitoring and Instrumentation</a> <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref1:1" class="footnote-backref">↩︎</a> <a href="#fnref1:2" class="footnote-backref">↩︎</a> <a href="#fnref1:3" class="footnote-backref">↩︎</a> <a href="#fnref1:4" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p><a href="https://raw.githubusercontent.com/apache/spark/v3.5.0/core/src/main/scala/org/apache/spark/shuffle/sort/SortShuffleManager.scala" target="_blank" rel="noreferrer">SortShuffleManager.scala</a> <a href="#fnref2" class="footnote-backref">↩︎</a> <a href="#fnref2:1" class="footnote-backref">↩︎</a></p></li><li id="fn3" class="footnote-item"><p><a href="https://raw.githubusercontent.com/apache/spark/v3.5.0/core/src/main/scala/org/apache/spark/util/JsonProtocol.scala" target="_blank" rel="noreferrer">JsonProtocol.scala</a> <a href="#fnref3" class="footnote-backref">↩︎</a> <a href="#fnref3:1" class="footnote-backref">↩︎</a> <a href="#fnref3:2" class="footnote-backref">↩︎</a> <a href="#fnref3:3" class="footnote-backref">↩︎</a> <a href="#fnref3:4" class="footnote-backref">↩︎</a> <a href="#fnref3:5" class="footnote-backref">↩︎</a> <a href="#fnref3:6" class="footnote-backref">↩︎</a></p></li></ol></section>',43)])])}const m=t(o,[["render",i]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as r,a5 as s}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"Metrics Glossary","titleTemplate":":title | Spark Tuning Reference","description":"Glossary of every metric surfaced elsewhere on the site: what each one measures, its event-log source field, and what a problematic value looks like.","frontmatter":{"title":"Metrics Glossary","titleTemplate":":title | Spark Tuning Reference","description":"Glossary of every metric surfaced elsewhere on the site: what each one measures, its event-log source field, and what a problematic value looks like."},"headers":[],"relativePath":"tuning-reference/metrics.md","filePath":"tuning-reference/metrics.md"}'),o={name:"tuning-reference/metrics.md"};function i(c,e,n,l,d,f){return a(),r("div",null,[...e[0]||(e[0]=[s("",43)])])}const m=t(o,[["render",i]]);export{u as __pageData,m as default};