Spark SQL中怎么注册python以及使用python注册的UDF中数据流是怎么流转的

1 篇文章 0 订阅
1 篇文章 0 订阅

背景

本文基于Spark 3.5.1
无论是从 spark的官网Arrow Python UDFs,还是databricks的一些python udf,好像都没有说到在Spark SQL中怎么直接调用 python定义的UDF,但是其实在使用上,Spark SQL是可以直接使用 python定义的UDF的,
分享本文的目的就在于 使读者明确 怎么在Spark SQL中调用 python注册的UDF,这里的的SQL 可以不仅仅是在 python api 中调用,也可以是在 java或者scala api中调用的。

调用

我们直接到Spark中的类 SubquerySuite,可以看到如下的例子:

    import IntegratedUDFTestUtils._

    assume(shouldTestPythonUDFs)

    val pythonTestUDF = TestPythonUDF(name = "udf")
    registerTestUDF(pythonTestUDF, spark)

    // Case 1: Canonical example of the COUNT bug
    checkAnswer(
      sql("SELECT l.a FROM l WHERE (SELECT udf(count(*)) FROM r WHERE l.a = r.c) < l.a"),
      Row(1) :: Row(1) :: Row(3) :: Row(6) :: Nil)

其中 registerTestUDF如下:

def registerTestUDF(testUDF: TestUDF, session: SparkSession): Unit = testUDF match {
    case udf: TestPythonUDF => session.udf.registerPython(udf.name, udf.udf)
    case udf: TestScalarPandasUDF => session.udf.registerPython(udf.name, udf.udf)
    case udf: TestGroupedAggPandasUDF => session.udf.registerPython(udf.name, udf.udf)
    case udf: TestScalaUDF =>
      val registry = session.sessionState.functionRegistry
      registry.createOrReplaceTempFunction(udf.name, udf.builder, "scala_udf")
    case other => throw new RuntimeException(s"Unknown UDF class [${other.getClass}]")
  }

这里是在scala代码中的 Spark SQL调用了 python注册的UDF,而且从单元测试的结果来看,是没有什么异常的,最主要的是 TestPythonUDF,以下直接分析一下.

分析

怎么注册python udf

直接到TestPythonUDF代码块:

case class TestPythonUDF(name: String, returnType: Option[DataType] = None) extends TestUDF {
    private[IntegratedUDFTestUtils] lazy val udf = new UserDefinedPythonFunction(
      name = name,
      func = SimplePythonFunction(
        command = pythonFunc.toImmutableArraySeq,
        envVars = workerEnv.clone().asInstanceOf[java.util.Map[String, String]],
        pythonIncludes = List.empty[String].asJava,
        pythonExec = pythonExec,
        pythonVer = pythonVer,
        broadcastVars = List.empty[Broadcast[PythonBroadcast]].asJava,
        accumulator = null),
      dataType = StringType,
      pythonEvalType = PythonEvalType.SQL_BATCHED_UDF,
      udfDeterministic = true) {

      override def builder(e: Seq[Expression]): Expression = {
        assert(e.length == 1, "Defined UDF only has one column")
        val expr = e.head
        val rt = returnType.getOrElse {
          assert(expr.resolved, "column should be resolved to use the same type " +
              "as input. Try df(name) or df.col(name)")
          expr.dataType
        }
        val pythonUDF = new PythonUDFWithoutId(
          super.builder(Cast(expr, StringType) :: Nil).asInstanceOf[PythonUDF])
        Cast(pythonUDF, rt)
      }
    }

    def apply(exprs: Column*): Column = udf(exprs: _*)

    val prettyName: String = "Regular Python UDF"
  }

最主要的是UserDefinedPythonFunctionSimplePythonFunctioncommand,这个才是Python UDF的核心。,对应的pythonFunc如下:

private lazy val pythonFunc: Array[Byte] = if (shouldTestPythonUDFs) {
    var binaryPythonFunc: Array[Byte] = null
    withTempPath { path =>
      Process(
        Seq(
          pythonExec,
          "-c",
          "from pyspark.sql.types import StringType; " +
            "from pyspark.serializers import CloudPickleSerializer; " +
            s"f = open('$path', 'wb');" +
            "f.write(CloudPickleSerializer().dumps((" +
            "lambda x: None if x is None else str(x), StringType())))"),
        None,
        "PYTHONPATH" -> s"$pysparkPythonPath:$pythonPath").!!
      binaryPythonFunc = Files.readAllBytes(path.toPath)
    }
    assert(binaryPythonFunc != null)
    binaryPythonFunc
  } else {
    throw new RuntimeException(s"Python executable [$pythonExec] and/or pyspark are unavailable.")
  }

这块代码的逻辑是: 用CloudPickleSerializer 序列化到 文件中,并读取存储在文件中的二进制数组,该二进制数组组成了pythonFunc. 这个字节数组的comamnd 会在后续中会被反序列化为对应的方法,且被调用。

注意

其实在正常的使用中,比如在 udtf.py 中,会 使用 `py4j` 在python中调用java的方法来调用注册udf么,如下:
class UDTFRegistration:
  ...
 register_udtf = _create_udtf(
            cls=f.func,
            returnType=f.returnType,
            name=name,
            evalType=f.evalType,
            deterministic=f.deterministic,
        )
        self.sparkSession._jsparkSession.udtf().registerPython(name, register_udtf._judtf)
        return register_udtf

其中 udtf() 返回的是 UDTFRegistration;
_judtf 返回的是 UserDefinedPythonTableFunction ,而这里的command是 CloudPickleSerializer 反序列化后的字节数组

调用udf的数据流

就从 UDFRegistration.registerPython 注册这个方法入手,该方法会调用UserDefinedPythonFunction的builder方法生成PythonUDF,该 PythonUDF 是不可计算的,所以会经过Rule的转换:

PythonUDF 
  ||
  \/ 经过 Rule ExtractPythonUDFs
BatchEvalPython/ArrowEvalPython
  || 
  \/ 经过 Rule PythonEvals
ArrowEvalPythonExec/BatchEvalPythonExec

目前就拿ArrowEvalPythonExec 举例,ArrowEvalPythonExec 最终会调用EvalPythonEvaluatorFactory.compute方法:
这里会启动一个worker.py(python -m pyspark.daemo pyspark.worker),
worker.py中:

 if eval_type == PythonEvalType.NON_UDF:
            func, profiler, deserializer, serializer = read_command(pickleSer, infile)
        elif eval_type in (PythonEvalType.SQL_TABLE_UDF, PythonEvalType.SQL_ARROW_TABLE_UDF):
            func, profiler, deserializer, serializer = read_udtf(pickleSer, infile, eval_type)
        else:
            func, profiler, deserializer, serializer = read_udfs(pickleSer, infile, eval_type)

        init_time = time.time()

        def process():
            iterator = deserializer.load_stream(infile)
            out_iter = func(split_index, iterator)
            try:
                serializer.dump_stream(out_iter, outfile)
            finally:
                if hasattr(out_iter, "close"):
                    out_iter.close()

        if profiler:
            profiler.profile(process)
        else:
            process()

其中 func, profiler, deserializer, serializer = read_udfs(pickleSer, infile, eval_type) 以及out_iter = func(split_index, iterator)
就是用来反序列udf函数,以及用来处理数据的,
read_udfs 比较关键的代码为:

  arg_offsets, udf = read_single_udf(
            pickleSer, infile, eval_type, runner_conf, udf_index=0, profiler=profiler
        )

        def func(_, iterator):
            num_input_rows = 0

            def map_batch(batch):
                nonlocal num_input_rows

                udf_args = [batch[offset] for offset in arg_offsets]
                num_input_rows += len(udf_args[0])
                if len(udf_args) == 1:
                    return udf_args[0]
                else:
                    return tuple(udf_args)

            iterator = map(map_batch, iterator)
            result_iter = udf(iterator)

可以看到 这种运行 python UDF的方式是以socket的方式进行交互的,所以这种方式相对来说还是会比较慢的。

### 回答1: Spark 3.0 是一次重磅发布,经过近两年的开发,它在流处理、PythonSQL 方面都进行了重大更新。以下是对这些更新的全面解读: 1. 流处理:Spark 3.0 引入了结构化流处理 API 的重大更新,包括新的流式查询引擎和增强的流式数据源 API。这些更新使得 Spark 更加适合处理实时数据流,并提供了更好的容错机制和更高的性能。 2. PythonSpark 3.0 对 Python API 进行了重大更新,包括对 Pandas UDF 的支持和对 Python 3 的全面支持。这些更新使得 Python 用户能够更加方便地使用 Spark,并且能够更好地利用 Python 生态系统的工具和库。 3. SQLSpark 3.0 引入了许多 SQL 方面的更新,包括 ANSI SQL 支持、新的优化器和执行引擎、更好的分区管理和更好的数据源 API。这些更新使得 Spark 更加适合处理大规模数据,并提供了更好的性能和可扩展性。 总的来说,Spark 3.0 的更新使得它更加适合处理实时数据流和大规模数据,并提供了更好的性能和可扩展性。同时,它也更加方便 Python 用户使用,并且能够更好地利用 Python 生态系统的工具和库。 ### 回答2: 近日,Apache Spark 社区正式宣布发布了最新版 Spark 3.0。这是一次重磅的更新,涵盖了流处理、PythonSQL 三大方面的内容。下面就让我们来逐一解读这些更新吧。 1. 流处理:Spark 3.0 引入了一项名为 Structured Streaming 的重要功能。它能够以批处理的方式处理流数据,并且保证了完全幂等性(即能够在多次运行时保证相同的输出)。此外,这个版本还增加了更多的连接器,可以方便地从 Kafka、Flume、Twitter 和 HDFS 读取数据。 2. Python 支持:在 Spark 3.0 Python 支持得到了显著的提升。现在,Python 3 官方支持了 PySpark,而且这个版本同时也新增了 Python API 的许多改进。这里,值得一提的是,Python 开发者可以使用 Pandas 和 Pyarrow 来提高数据集和数据帧的操作速度。 3. SQLSpark 3.0 SQL 的更新主要体现在两个方面:一是 SQL 引擎升级至 Apache Arrow,二是 SQL 执行计划优化。这些更新使得 Spark 3.0 的 SQL 引擎能够更快地处理 SQL 查询,并且提高了查询的执行效率。 此外,Spark 3.0 还新增了 Pyspark 的 type hints 和注释支持,提供了更好的代码接口提示;改进了原有的分布式机器学习功能,加入了新的规范、API 和示例;提高了 Kerberos 和 Hadoop 文件系统(HDFS)的兼容性等。 总之,Spark 3.0 的发布,标志着 Apache Spark 在数据处理领域的核心地位得到了继续的巩固,并且为 Python 和流处理等开源生态提供了一种更加稳定、快速和可靠的解决方案。对于数据工程师和数据科学家们而言,这无疑是一个重要的里程碑。 ### 回答3: Apache Spark是一个快速通用的大数据处理引擎,Python是一种流行的编程语言,SQL是结构化查询语言的缩写,用于管理关系型数据库,这些都是当今最重要的技术学科。最近,Spark推出了Python3_Spark 3.0的重磅发布,这意味着Spark的核心技术已经经过了重大更新,让我们听听它是如何变得更加优秀。 Python3_Spark 3.0更新重大,首先是流式处理。在此版本,新引入的流处理模块提供了对无限数据流的完全支持,没有大小限制,可以直接应用于大多数Spark数据源和流数据源,可以轻松实现亚秒级响应,并且还包含新的UI各类展示函数,可以轻松监视流式应用程序。 其次是对Python的原生支持。Python在数据处理界面上极受欢迎,PySpark现在在Python3完全支持,包括与Python新功能的充分配合,如Python3的类型提示(typing),这意味着PySpark代码现在可以像使用Spark的Scala和Java API一样流畅地进行编写;大大提高了数据科学家和机器学习工程师的效率。 最后是SQL支持。Spark已经成为用户基础最广泛的SQL引擎之一之一。最新的Spark 3.0版本通过实现 ANSI SQL 标准来大幅度提高了 SQL 的兼容性和处理性能。Spark 3.0 将包括对 SQL 2016 的完整支持,包括 MATCH_RECOGNIZE和其他高级功能。此外,Spark 3.0 还支持更多的数据类型,并且还具备静态类型分析和优化,可以帮助用户快速有效地查询和处理大规模数据。 总之,Spark Python3_Spark 3.0的发布,在流、PythonSQL等方面提供了全面升级,使得它的核心技术更加完善和先进,有助于增强数据处理效率,实现更好的数据分析应用。对于正在使用Spark的用户来说,这让他们的生活更加容易。 对于Spark未来的发展,它的不断升级和创新发展势头十分强劲,我们期待它的更多惊喜发布。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值