一、 架构设计篇:做减法
在写代码之前,先问自己:这个逻辑必须在 UDF 里做吗?
- 过滤先行 :在调用 UDF 之前,先用 SQL 过滤掉
NULL、空字符串或明显异常的超长数据。 - 并行度治理 :如果数据总量不大但 Task 极多(如你的 9 万个 Task),在 UDF 执行前调用
.coalesce(适当分区数)。减少 Task 切换和广播变量重复初始化的开销。 - 拆分"大数组" :如果单行数据包含巨大数组(如
applicants过长),考虑先在 SQL 层explode炸开,处理完后再聚合,避免单行数据卡死单个 Executor。
二、 初始化篇:利用 initBroadCast
initBroadCast 是"一次性"的准备工作,要在这里完成所有昂贵的操作。
-
广播变量 (Broadcast):用于加载几百 MB 以内的只读映射表。它能避免几万个 Task 重复下载数据。
-
工具类单例化 :类似
AddressParseUtils这种工具类,在initBroadCast中初始化一次,作为类成员变量供call复用。 -
预编译正则 :禁止 在循环内使用
str.replaceAll("regex", "")。必须在类成员位置预编译:private static final Pattern MY_PATTERN = Pattern.compile("...");
三、 执行篇:内存与 CPU 的"极致压榨"
call() 方法每秒可能被调用数万次,这里的代码必须"滴水不漏"。
- 对象复用 (Zero-Allocation) :
- 不要 在循环内
new Entity()、new ArrayList()或new Map()。 - 做法 :在类成员定义一个
reuseEntity,每次循环调用set方法重置它的值。
- 不要 在循环内
- 告别语法糖 :
- 禁用 Java 8 Stream API (
.stream().filter().findFirst())。 - 做法 :回归最原始的
for循环。流操作产生的中间对象是海量 GC 的罪魁祸首。
- 禁用 Java 8 Stream API (
- 字符串调优 :
- 字符串是不可变的,连续的
replace()或toUpperCase()会产生大量垃圾对象。 - 大量拼接时使用
StringBuilder且手动setLength(0)复用。
- 字符串是不可变的,连续的
四、 性能诊断篇:通过 Spark UI 看穿本质
当任务变慢时,盯着这几个指标:
- GC Time:如果 GC 时间超过 Task Duration 的 10%,说明你的 UDF 产生了太多临时对象,或者广播变量太大。
- Duration (Max vs Median):如果 Max 远大于 Median,说明存在数据倾斜。检查是否某些行的数据量(如数组长度)异常大。
- Input Size / Records :查看每个 Task 处理的数据分布,如果每个 Task 只有几 KB,说明分区太多,需要
coalesce。
五、 避坑口诀(开发 Checklist)
一禁: 禁用循环内
new对象。二禁: 禁用循环内
Stream流。三禁: 禁用循环内正则编译。
一复: 复用成员变量与工具类。
二复: 复用静态常量与映射 Map。