You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spark中用户自定义聚合函数能否与Dataset.observe结合使用?

Spark observe方法结合自定义Typed UDAF(Aggregator)出现序列化异常的解决方法

问题重现

Spark的observe方法是实用的作业监控手段,但要实现原生不支持的复杂聚合(比如用sketch对列采样)时,使用自定义Aggregator(Typed UDAF)会触发序列化异常。

已尝试两种调用方式:

方式一:

session.udf().register("myAgg", functions.udaf(new MyAggregator(), Encoders.STRING()));
Column aggCol = functions.expr("myAgg(inputCol)");

方式二:

Column aggCol = functions.udaf(new MyAggregator(), Encoders.STRING()).apply(ds.col("inputCol"));

执行ds = ds.observe("foo", aggCol);后抛出异常:

org.apache.spark.SparkException: Job aborted due to stage failure: task 0.0 in stage 4.0 (TID 2) had a not serializable result: org.apache.spark.sql.catalyst.expressions.GeneratedClass$SpecificUnsafeProjection
Serialization stack:
    - object not serializable (class: org.apache.spark.sql.catalyst.expressions.GeneratedClass$SpecificUnsafeProjection, value: <function1>)
    - field (class: org.apache.spark.sql.execution.aggregate.ScalaAggregator, name: inputProjection, type: class org.apache.spark.sql.catalyst.expressions.UnsafeProjection)
    - object (class org.apache.spark.sql.execution.aggregate.ScalaAggregator, myAgg(input[5, string, true]))
    - element of array (index: 0)
    - array (class [Lorg.apache.spark.sql.catalyst.expressions.aggregate.TypedImperativeAggregate;, size 1)
    - field (class: org.apache.spark.sql.execution.AggregatingAccumulator, name: typedImperatives, type: class [Lorg.apache.spark.sql.catalyst.expressions.aggregate.TypedImperativeAggregate;)
    - object (class org.apache.spark.sql.execution.AggregatingAccumulator, AggregatingAccumulator(id: 111, name: Some(Collected metrics), value: [empty row]))
    - writeExternal data
    - externalizable object (class org.apache.spark.scheduler.DirectTaskResult, org.apache.spark.scheduler.DirectTaskResult@7c17b6a8)

异常原因

observe机制依赖AggregatingAccumulator收集聚合结果,而Typed UDAF(基于Aggregator实现)对应的ScalaAggregator内部会生成UnsafeProjection实例处理输入数据。这个运行时生成的投影类未实现Serializable接口,当任务结束后AggregatingAccumulator需要序列化回Driver时,就会触发序列化失败。

解决方案

方案1:改用传统UDAF(基于UserDefinedAggregateFunction实现)

传统UDAF的实现模式不会引入不可序列化的UnsafeProjection对象,能兼容observe方法。示例实现如下:

import org.apache.spark.sql.expressions.UserDefinedAggregateFunction;
import org.apache.spark.sql.types.*;
import org.apache.spark.sql.Row;

public class MySketchUDAF extends UserDefinedAggregateFunction {
    // 输入列的schema,这里定义为字符串类型
    @Override
    public StructType inputSchema() {
        return new StructType().add("inputCol", DataTypes.StringType);
    }

    // 聚合缓冲区的schema,根据你的sketch逻辑定义结构
    @Override
    public StructType bufferSchema() {
        // 示例:用字符串存储sketch的中间状态(实际根据你的sketch实现调整)
        return new StructType().add("sketchState", DataTypes.StringType);
    }

    // 输出结果的数据类型
    @Override
    public DataType dataType() {
        return DataTypes.StringType;
    }

    // 是否为确定性聚合(相同输入返回相同结果)
    @Override
    public boolean deterministic() {
        return true;
    }

    // 初始化缓冲区
    @Override
    public void initialize(MutableAggregationBuffer buffer) {
        // 根据你的sketch逻辑初始化,比如空字符串
        buffer.update(0, "");
    }

    // 单条输入数据更新缓冲区
    @Override
    public void update(MutableAggregationBuffer buffer, Row input) {
        String currentState = buffer.getString(0);
        String newInput = input.getString(0);
        // 替换为你的sketch更新逻辑,比如将新输入加入sketch
        String newState = currentState + "," + newInput;
        buffer.update(0, newState);
    }

    // 合并两个缓冲区
    @Override
    public void merge(MutableAggregationBuffer buffer1, Row buffer2) {
        String state1 = buffer1.getString(0);
        String state2 = buffer2.getString(0);
        // 替换为你的sketch合并逻辑
        buffer1.update(0, state1 + state2);
    }

    // 根据缓冲区计算最终结果
    @Override
    public Object evaluate(Row buffer) {
        String finalState = buffer.getString(0);
        // 替换为你的sketch最终结果生成逻辑,比如返回采样结果
        return finalState.replaceFirst("^,", "");
    }
}

调用方式:

// 注册UDAF
spark.udf().register("mySketchAgg", new MySketchUDAF());
// 使用observe执行聚合监控
Dataset<Row> ds = ...; // 你的数据集
ds = ds.observe("sketch_metrics", expr("mySketchAgg(inputCol)"));

方案2:调整Typed UDAF实现(仅限特殊场景)

如果必须使用Aggregator,需要确保MyAggregator的所有成员变量都是可序列化的,并且避免依赖Spark内部生成的不可序列化对象。但这种方式复杂度较高,且不一定能完全规避ScalaAggregator内部的序列化问题,因此优先推荐方案1。

内容的提问来源于stack exchange,提问作者Alex Field

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 03:55:42