Una función con valores de tabla definida por el usuario (UDTF) recibe cero o más valores escalares como entrada y devuelve varias filas como salida. Cada fila devuelta puede tener una o más columnas. Esto la diferencia de una función escalar, que asigna la entrada a un único valor de salida.
Flink distingue varios tipos de funciones:
|
Tipo de función |
Comportamiento |
|
Función escalar |
Asigna valores escalares a un nuevo valor escalar |
|
Función con valores de tabla (UDTF) |
Asigna valores escalares a nuevas filas |
|
Función de agregación |
Asigna valores escalares de varias filas a un nuevo valor escalar |
|
Función de agregación de tabla |
Asigna valores escalares de varias filas a nuevas filas |
Utilice una UDTF cuando su lógica deba generar varias filas de salida a partir de una sola fila de entrada. Por ejemplo, al dividir una cadena delimitada en filas independientes.
Para obtener contexto sobre las funciones definidas por el usuario en Flink, consulte Funciones definidas por el usuario.
Requisitos previos
Antes de empezar, asegúrese de contar con lo siguiente:
Apache Maven instalado en su computadora local
IntelliJ IDEA instalado en su computadora local
Un entorno de trabajo de Flink SQL configurado en Realtime Compute for Apache Flink
Desarrollar una UDTF
Realtime Compute for Apache Flink proporciona un proyecto de ejemplo (ASI_UDX_Demo) con entornos de desarrollo preconfigurados e implementaciones de ejemplo para funciones escalares, funciones de agregación (UDAF) y UDTF definidas por el usuario. El acceso a este repositorio externo de GitHub puede ser lento o no estar disponible de manera ocasional.
-
Descargue y descomprima el proyecto de ejemplo ASI_UDX_Demo en su computadora local. Tras la descompresión, se crea la carpeta
ASI_UDX-maincon los siguientes archivos:pom.xml: archivo de configuración del proyecto de Maven que describe las coordenadas, dependencias y ajustes de compilación del proyecto.\ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.java: código Java de ejemplo de la UDTF.
En IntelliJ IDEA, haga clic en File > Open y abra la carpeta descomprimida
ASI_UDX-main.-
Abra
\ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.javae implemente la lógica de su UDTF. Una UDTF extiendeTableFunction<T>, dondeTes el tipo de salida. El métodoeval()debe devolvervoid. En lugar de usar una instrucciónreturn, llame acollect()para emitir las filas de salida. Este comportamiento difiere de las funciones escalares, que utilizanreturn. El siguiente ejemplo divide una cadena en dos columnas mediante una barra vertical (|) como delimitador:package ASI_UDTF; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.table.functions.TableFunction; public class ASI_UDTF extends TableFunction<Tuple2<String, String>> { public void eval(String str) { String[] split = str.split("\\|"); String name = split[0]; String place = split[1]; // emit one row with two columns collect(Tuple2.of(name, place)); } }Declaración de tipos de salida Flink admite dos enfoques para declarar el esquema de salida de una UDTF:
-
Tipo Row con
@FunctionHinty@DataTypeHint(recomendado): declara de forma explícita los nombres y tipos de los campos. De esta manera, el esquema de salida queda claro tanto para Flink como para quienes lean el código.@FunctionHint(output = @DataTypeHint("ROW<word STRING, length INT>")) public static class SplitFunction extends TableFunction<Row> { public void eval(String str) { for (String s : str.split(" ")) { // use collect(...) to emit a row collect(Row.of(s, s.length())); } } } -
Tipo Tuple (el tipo se infiere a partir de los genéricos): resulta más sencillo de escribir, pero los nombres de las columnas no son explícitos.
TableFunction<Tuple2<String, Integer>>
NotaLos tipos de datos y el mecanismo de inferencia de tipos compatibles con
TableFunctionvarían entre las versiones principales de Flink. Los ejemplos anteriores siguen la documentación de Flink 1.15 (Tipos de datos e Inferencia de tipos). Revise la documentación de Flink correspondiente a su versión de Ververica Runtime (VVR). Para conocer su versión de Flink, consulte Storage management and operations. -
-
Abra
pom.xmlen la carpetaASI_UDX-mainy configure sus dependencias. El proyecto de ejemplo incluye las siguientes dependencias de Maven para Flink 1.11:<dependencies> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-java_2.12</artifactId> <version>1.11.0</version> <!--<scope>provided</scope>--> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table</artifactId> <version>1.11.0</version> <type>pom</type> <!--<scope>provided</scope>--> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-core</artifactId> <version>1.11.0</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table-common</artifactId> <version>1.11.0</version> </dependency> </dependencies>Si su UDTF no requiere dependencias adicionales a las listadas, pase al siguiente paso. De lo contrario, agregue las entradas de dependencia obligatorias en
pom.xml. -
En el directorio que contiene
pom.xml, ejecute el siguiente comando para compilar el proyecto:mvn package -Dcheckstyle.skipSi la compilación es exitosa, se genera
ASI_UDX-1.0-SNAPSHOT.jaren el directorio\ASI_UDX-main\target\. Con esto concluye el desarrollo de la UDTF.
Funciones de tabla asíncronas (AsyncTableFunction)
Una función de tabla asíncrona (AsyncTableFunction) accede a servicios externos de forma asíncrona en Flink SQL o en la Table API. Devuelve cero, una o varias filas por cada llamada y constituye un tipo de función con valores de tabla definida por el usuario (UDTF). Algunos escenarios típicos incluyen consultar varias etiquetas por identificador de producto o devolver múltiples registros coincidentes para una condición de búsqueda. Esta función puede invocarse mediante LATERAL TABLE en SQL o joinLateral en la Table API para formar una operación de correlación asíncrona. También puede proporcionarla un conector de tabla de dimensiones como implementación de consulta de una unión de búsqueda asíncrona.
Parámetros de configuración
A continuación se muestran los parámetros que controlan el comportamiento en tiempo de ejecución de las funciones de tabla asíncronas.
|
Parámetro |
Escenario |
Propósito |
|
|
Async Correlate |
Controla el número de solicitudes simultáneas de la función de tabla asíncrona. |
|
|
Async Correlate |
Establece el tiempo total de espera de una llamada, incluidas las reintentos habilitados. |
|
|
Async Correlate |
Define la estrategia de reintento de las llamadas a la función. |
|
|
Async Lookup Join |
Controla la capacidad del búfer de las búsquedas asíncronas. |
|
|
Async Lookup Join |
Establece el tiempo de espera de las búsquedas asíncronas en tablas de dimensiones. Un hint |
Ejemplo de desarrollo
Al implementar una AsyncTableFunction, declare un método eval público y no estático. El primer parámetro es un objeto CompletableFuture<Collection<T>> que se usa para enviar los resultados. Los parámetros restantes son los argumentos de negocio que pasa la llamada SQL.
La función regresa de inmediato después de enviar la solicitud asíncrona. Envía los resultados en la devolución de llamada de finalización mediante result.complete(rows) o notifica un error con result.completeExceptionally(error). Las múltiples filas de una sola llamada se envían como una única colección. Si no hay resultados, envíe una colección vacía.
package com.example.udf;
import org.apache.flink.table.functions.AsyncTableFunction;
import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.annotation.FunctionHint;
import org.apache.flink.types.Row;
import org.apache.flink.table.functions.FunctionContext;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
/**
* Queries product tags through an asynchronous HTTP interface.
*
* <p>Interface contract: 200 returns UTF-8 text with one tag per line; 204 and 404 indicate no tags.
* This example suits read-only queries that return short text. Other status codes are treated as invocation failures and handed over to the engine.
*/
@FunctionHint(output = @DataTypeHint("ROW<tag STRING>"))
public class AsyncProductTags extends AsyncTableFunction<Row> {
private transient HttpClient client;
private transient URI serviceBaseUri;
@Override
public void open(FunctionContext context) {
String baseUrl = context.getJobParameter("product.service.base-url", null);
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("Configure product.service.base-url.");
}
serviceBaseUri = URI.create(baseUrl);
if (!("https".equalsIgnoreCase(serviceBaseUri.getScheme())
|| "http".equalsIgnoreCase(serviceBaseUri.getScheme()))
|| serviceBaseUri.getHost() == null
|| serviceBaseUri.getRawQuery() != null
|| serviceBaseUri.getRawFragment() != null
|| !baseUrl.endsWith("/")) {
throw new IllegalArgumentException("The service URL must be an HTTP(S) base URL that ends with a forward slash (/).");
}
client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
}
@Override
public boolean isDeterministic() {
// Product information may change. The same ID may return different results at different times.
return false;
}
public void eval(CompletableFuture<Collection<Row>> result, Long productId) {
if (productId == null) {
result.complete(Collections.emptyList());
return;
}
try {
HttpRequest request =
HttpRequest.newBuilder(
serviceBaseUri.resolve("products/" + productId + "/tags"))
.timeout(Duration.ofSeconds(5))
.header("Accept", "text/plain")
.GET()
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
.thenApply(
response -> {
if (response.statusCode() == 200) {
return response.body().lines()
.map(String::strip)
.filter(tag -> !tag.isEmpty())
.map(tag -> Row.of(tag))
.collect(Collectors.toList());
}
if (response.statusCode() == 204 || response.statusCode() == 404) {
return Collections.<Row>emptyList();
}
throw new CompletionException(
new IOException(
"Failed to query the product. HTTP status code: " + response.statusCode()));
})
.whenComplete(
(rows, error) -> {
if (error != null) {
result.completeExceptionally(error);
} else {
result.complete(rows);
}
});
} catch (RuntimeException e) {
result.completeExceptionally(e);
}
}
}
HttpClient.sendAsync ejecuta las solicitudes de red de forma asíncrona, por lo que la función no necesita crear su propio pool de hilos. La devolución de llamada solo analiza texto corto y envía los resultados. Si utiliza otro cliente, prefiera también su interfaz asíncrona nativa. Para conocer el comportamiento del cliente del JDK, consulte API de HttpClient.
Las etiquetas de los productos pueden cambiar con el tiempo, por lo que el ejemplo establece isDeterministic() en false. Si su función siempre devuelve el mismo resultado para una misma entrada, mantenga el comportamiento predeterminado.
Manejo personalizado de tiempos de espera
De forma predeterminada, si una llamada a eval no completa el objeto CompletableFuture antes de que expire el tiempo de espera y se agoten los reintentos configurados, el framework lanza una excepción java.util.concurrent.TimeoutException y marca el trabajo como fallido. Puede declarar un método timeout(...) que coincida con el método eval en una subclase de AsyncTableFunction para devolver una fila alternativa o implementar un manejo personalizado del tiempo de espera.
Los tiempos de espera son más probables al llamar a un clúster remoto de LLM, y por lo general no deben provocar el fallo directo del trabajo. Al declarar un método timeout, se devuelve un resultado alternativo cuando ocurre el tiempo de espera y el trabajo continúa.
El método timeout debe cumplir todas las condiciones siguientes:
Declaración del método: debe ser un método de instancia
public, no puede serstaticy debe llamarsetimeout.**Firma alineada con
eval**: la lista de parámetros debe ser estrictamente idéntica a la del métodoevalcorrespondiente. El primer parámetro es un objetoCompletableFuturecuyo tipo genérico debe coincidir exactamente con el deeval. Los tipos y el orden de los parámetros restantes también deben ser idénticos. El métodotimeoutadmite sobrecarga: puede declarar un métodotimeoutindependiente para cada sobrecarga deevalque requiera lógica alternativa.-
Finalización síncrona (obligatorio): esta devolución de llamada se ejecuta en el hilo de buzón del operador y debe completar el future antes de que el método termine. Inmediatamente después de que el método regrese, el framework comprueba
future.isDone(). Si el future no se completó, el framework lo fuerza con una excepciónIllegalStateException. Por lo tanto:No inicie otra llamada asíncrona dentro del cuerpo del método, como un reintento adicional o una segunda consulta, esperando que su devolución de llamada complete el future. Cuando esa devolución de llamada se active, el framework ya habrá descartado el registro.
No inicie un nuevo hilo para completar el future de forma asíncrona por la misma razón.
El cuerpo del método debe hacer una sola cosa: proporcionar un resultado alternativo ligero, como una fila constante, una fila NULL o una colección vacía. También puede llamar a
completeExceptionally(...)con una excepción de negocio personalizada.
Lanzar excepciones es seguro: lanzar una excepción de forma síncrona dentro del cuerpo del método es válido. El framework pasa la excepción al objeto
ResultFuturedescendente, lo cual equivale a llamar explícitamente afuture.completeExceptionally(thrown).
Además de estas restricciones, el framework garantiza un comportamiento estable en los siguientes escenarios:
**No se declaró ningún método
timeout**: se utiliza la excepciónTimeoutExceptionpredeterminada del framework y no ocurre ningún fallo durante la generación de código.Firma incompatible: si el método
timeouttiene visibilidad válida, pero su lista de parámetros no coincide con los tipos de argumento reales en el sitio de llamada, el framework falla durante la planificación (generación de código) con una excepciónValidationException. El mensaje de error contiene el nombre de clase completo de la función y muestra tanto la firma esperada como la real.Colección vacía como alternativa: cuando se llama a
future.complete(Collections.emptyList()), la fila se descarta en una unión de búsqueda INNER. En una unión de búsqueda LEFT OUTER, los campos de la tabla derecha se llenan con NULL.
El siguiente ejemplo agrega una devolución de llamada timeout a una función de tabla asíncrona. Muestra cómo completar el future de forma síncrona y devolver una fila alternativa:
import org.apache.flink.table.functions.AsyncTableFunction;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
public static class BackgroundFunctionWithTimeout extends AsyncTableFunction<Long> {
public void eval(CompletableFuture<Collection<Long>> future, Integer waitMax) {
// ...same logic as the preceding development example: dispatch the asynchronous task,
// and then complete `future` in the callback thread.
}
// The parameter list must align with eval(): the same CompletableFuture generic type,
// and the same types and order for the remaining parameters (Integer waitMax).
// The method body must complete `future` synchronously. Do not start another asynchronous call
// or a new thread to complete it asynchronously. When this callback is triggered,
// the operator has already given up on this record.
public void timeout(CompletableFuture<Collection<Long>> future, Integer waitMax) {
future.complete(Collections.singletonList(-1L));
// You can also use a custom business exception instead of the default TimeoutException:
// future.completeExceptionally(new RuntimeException(waitMax));
}
}
Registrar una UDTF
Para obtener instrucciones sobre cómo registrar una UDTF, consulte Manage user-defined functions (UDFs).
Usar una UDTF
Después de registrar la UDTF, utilícela en un trabajo de Flink SQL.
-
Desarrolle un trabajo de Flink SQL. Para obtener más información, consulte Job development map. El siguiente código SQL divide el campo
messagedeASI_UDTF_Sourceen dos columnas mediante el delimitador|. Luego inserta los resultados enASI_UDTF_Sink. La sintaxisLATERAL TABLErealiza un cross join de cada fila de entrada con las filas que emite la UDTF.CREATE TEMPORARY TABLE ASI_UDTF_Source ( `message` VARCHAR ) WITH ( 'connector' = 'datagen' ); CREATE TEMPORARY TABLE ASI_UDTF_Sink ( name VARCHAR, place VARCHAR ) WITH ( 'connector' = 'blackhole' ); INSERT INTO ASI_UDTF_Sink SELECT name, place FROM ASI_UDTF_Source, LATERAL TABLE(ASI_UDTF(`message`)) AS T(name, place); En la página Operation Center > Job O&M, busque el trabajo deseado y haga clic en Start en la columna Actions. Una vez iniciado el trabajo, las cadenas del campo
messagedeASI_UDTF_Sourcese dividen con el delimitador|. Las columnas resultantes se insertan enASI_UDTF_Sink.