Kullanıcı tanımlı tablo değerli bir işlev (UDTF), girdi olarak sıfır veya daha fazla skaler değer alır ve çıktı olarak birden çok satır döndürür. Döndürülen her satır bir veya daha fazla sütun içerebilir. Bu durum, girdiyi tek bir çıktı değerine eşleyen skaler işlevlerden farklıdır.
Flink, birkaç farklı işlev türü arasında ayrım yapar:
| İşlev türü | Davranış |
|---|---|
| Skaler işlev | Skaler değerleri yeni bir skaler değere eşler |
| Tablo değerli işlev (UDTF) | Skaler değerleri yeni satırlara eşler |
| Toplama işlevi | Birden çok satırdan gelen skaler değerleri yeni bir skaler değere eşler |
| Tablo toplama işlevi | Birden çok satırdan gelen skaler değerleri yeni satırlara eşler |
Mantığınızın tek bir girdi satırından birden çok çıktı satırı üretmesi gerektiğinde bir UDTF kullanın; örneğin, sınırlayıcıyla ayrılmış bir dizeyi ayrı satırlara bölmek gibi.
Flink kullanıcı tanımlı işlevleri hakkında arka plan bilgisi için bkz. Kullanıcı tanımlı işlevler.
Ön koşullar
Başlamadan önce aşağıdakilere sahip olduğunuzdan emin olun:
Yerel makinenizde kurulu olan Apache Maven
Yerel makinenizde kurulu olan IntelliJ IDEA
Realtime Compute for Apache Flink'te yapılandırılmış bir Flink SQL iş ortamı
Bir UDTF Geliştirme
Realtime Compute for Apache Flink, önceden yapılandırılmış geliştirme ortamları ve kullanıcı tanımlı skaler işlevler, kullanıcı tanımlı toplama işlevleri (UDAF'ler) ve UDTF'ler için örnek uygulamalar içeren bir örnek proje (ASI_UDX_Demo) sağlar. Bu üçüncü taraf GitHub deposuna erişim zaman zaman yavaş olabilir veya kullanılamayabilir.
-
ASI_UDX_Demo örnek projesini yerel makinenize indirin ve sıkıştırılmış dosyayı açın. Sıkıştırma işleminden sonra aşağıdaki dosyaları içeren bir
ASI_UDX-mainklasörü oluşturulur:pom.xml: Projenin koordinatlarını, bağımlılıklarını ve derleme ayarlarını açıklayan Maven proje yapılandırma dosyası.\ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.java: UDTF için örnek Java kodu.
IntelliJ IDEA'da File > Open seçeneklerine tıklayın ve sıkıştırması açılan
ASI_UDX-mainklasörünü açın.-
\ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDTF.javadosyasını açın ve UDTF mantığınızı uygulayın. Bir UDTF, çıktı türününTolduğuTableFunction<T>sınıfından türetilir.eval()yöntemivoiddöndürmelidir; birreturndeyimi kullanmak yerine çıktı satırlarını yaymak içincollect()yöntemini çağırın. Bu durum,returnkullanan skaler işlevlerden farklıdır. Aşağıdaki örnek, bir dizeyi dikey çubuk (|) ayırıcısını kullanarak iki sütuna böler: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)); } }Çıktı türlerini bildirme Flink, bir UDTF'nin çıktı şemasını bildirmek için iki yaklaşımı destekler:
-
@FunctionHintve@DataTypeHintile Satır türü (önerilen): Alan adlarını ve türlerini açıkça bildirerek çıktı şemasının hem Flink hem de okuyucular için net olmasını sağlar.@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())); } } } -
Tuple türü (tür genel türlerden çıkarılır): Yazılması daha basittir ancak sütun adları açık değildir.
TableFunction<Tuple2<String, Integer>>
NotTableFunctiontarafından desteklenen veri türleri ve tür çıkarsama mekanizması, ana Flink sürümleri arasında değişiklik gösterir. Yukarıdaki örnekler Flink 1.15 belgelerini takip eder (Veri türleri ve Tür çıkarsama). Ververica Runtime (VVR) sürümünüzle eşleşen Flink belgelerini kontrol edin. Flink sürümünüzü bulmak için bkz. Depolama yönetimi ve işlemleri. -
-
ASI_UDX-mainklasöründekipom.xmldosyasını açın ve bağımlılıklarınızı yapılandırın. Örnek proje, Flink 1.11 için aşağıdaki Maven bağımlılıklarını içerir:<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>UDTF'nizin listelenenlerin dışında ek bağımlılığı yoksa bir sonraki adıma geçin. Aksi takdirde, gerekli bağımlılık girişlerini
pom.xmldosyasına ekleyin. -
pom.xmldosyasını içeren dizinde projeyi paketlemek için aşağıdaki komutu çalıştırın:mvn package -Dcheckstyle.skipDerleme başarılı olursa
\ASI_UDX-main\target\dizinindeASI_UDX-1.0-SNAPSHOT.jardosyası oluşturulur. UDTF geliştirme işlemi tamamlanmıştır.
Eşzamansız tablo işlevleri (AsyncTableFunction)
Eşzamansız bir tablo işlevi (AsyncTableFunction), Flink SQL veya Tablo API'sinde harici hizmetlere eşzamansız olarak erişir ve her çağrı için sıfır, bir veya birden çok satır döndürür. Bu, bir tür kullanıcı tanımlı tablo değerli işlevdir (UDTF). Tipik senaryolar arasında ürün kimliğine göre birden çok etiketi sorgulama veya bir arama koşulu için birden çok eşleşen kaydı döndürme yer alır. Eşzamansız bir tablo işlevi, SQL'de LATERAL TABLE veya Tablo API'sinde joinLateral tarafından çağrılan ve eşzamansız bir ilişkilendirme işlemi oluşturan bir kullanıcı tanımlı işlev olabilir. Ayrıca, eşzamansız arama birleştirmesi için sorgu uygulaması olarak bir boyut tablosu bağlayıcısı tarafından sağlanabilir.
Yapılandırma parametreleri
Aşağıdaki parametreler eşzamansız tablo işlevlerinin çalışma zamanı davranışını kontrol eder.
| Parametre | Senaryo | Amaç |
|---|---|---|
table.exec.async-table.max-concurrent-operations |
Eşzamansız İlişkilendirme | Eşzamansız tablo işlevinin eşzamanlı istek sayısını kontrol eder. |
table.exec.async-table.timeout |
Eşzamansız İlişkilendirme | Etkin yeniden denemeler dahil olmak üzere bir çağrının toplam bekleme süresini ayarlar. |
table.exec.async-table.retry-strategy |
Eşzamansız İlişkilendirme | İşlev çağrılarının yeniden deneme stratejisini ayarlar. |
table.exec.async-lookup.buffer-capacity |
Eşzamansız Arama Birleştirmesi | Eşzamansız aramaların arabellek kapasitesini kontrol eder. |
table.exec.async-lookup.timeout |
Eşzamansız Arama Birleştirmesi | Eşzamansız boyut tablosu aramalarının zaman aşımını ayarlar. Uygulanabilir bir LOOKUP ipucu, tek bir birleştirme için zaman aşımını da belirtebilir. |
Geliştirme örneği
Bir AsyncTableFunction uygularken, statik olmayan herkese açık bir eval yöntemi bildirin. İlk parametre, sonuçları göndermek için kullanılan bir CompletableFuture<Collection<T>> nesnesidir ve kalan parametreler SQL çağrısı tarafından geçirilen iş parametreleridir.
İşlev, eşzamansız isteği gönderdikten sonra hızlıca döner ve result.complete(rows) çağrısı yaparak tamamlama geri çağrısında sonuçları gönderir veya result.completeExceptionally(error) çağrısı yaparak bir özel durum gönderir. Tek bir çağrının birden çok satırı tek bir koleksiyon olarak gönderilir. Sonuç olmadığında boş bir koleksiyon gönderin.
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 ağ isteklerini eşzamansız olarak yürüttüğünden işlevin kendi iş parçacığı havuzunu oluşturmasına gerek yoktur. Geri çağrı yalnızca kısa metni ayrıştırır ve sonuçları gönderir. Başka bir istemci kullanırken, onun yerel eşzamansız arayüzünü tercih edin. JDK istemcisinin davranışı için bkz. HttpClient API.
Ürün etiketleri zaman içinde değişebileceğinden örnek isDeterministic() değerini false olarak ayarlar. Aynı girdi için her zaman aynı sonucu döndüren bir işlev için varsayılan davranışı koruyun.
Özel zaman aşımı işleme
Varsayılan olarak, bir eval çağrısı zaman aşımı süresi dolmadan ve yapılandırılan yeniden denemeler tükenmeden CompletableFuture nesnesini tamamlamazsa çerçeve bir java.util.concurrent.TimeoutException özel durumu oluşturur ve iş başarısız olur. Bir AsyncTableFunction alt sınıfında eval yöntemiyle eşleşen bir timeout(...) yöntemi bildirerek yedek bir satır döndürebilir veya özel zaman aşımı işleme uygulayabilirsiniz.
Uzak bir LLM kümesine çağrı yapılırken zaman aşımları daha olasıdır ve bir zaman aşımı genellikle işi doğrudan başarısız etmemelidir. Bildirilen bir timeout yöntemi ile zaman aşımı durumunda bir yedek sonuç döndürülür ve iş devam eder.
timeout yöntemi aşağıdaki tüm koşulları karşılamalıdır:
Yöntem bildirimi: Yöntem,
publicbir örnek yöntemi olmalı,staticolmamalı vetimeoutolarak adlandırılmalıdır.**
evalile uyumlu imza**: Parametre listesi, ilgilievalyöntemininkiyle kesinlikle aynı olmalıdır. İlk parametre, genel türüevalile tam olarak aynı olan birCompletableFuturenesnesidir ve kalan parametrelerin türleri ve sırası da kesinlikle aynı olmalıdır.timeoutaşırı yüklemeyi destekler: yedek mantığı gerektiren herevalaşırı yüklemesi için ayrı birtimeoutyöntemi bildirebilirsiniz.-
Eşzamanlı tamamlama (zorunlu): Bu geri çağrı, operatörün posta kutusu iş parçacığında çalışır ve yöntem dönüşünden önce geleceği tamamlamalıdır. Yöntem döndükten hemen sonra çerçeve
future.isDone()değerini kontrol eder. Gelecek tamamlanmamışsa çerçeve onu birIllegalStateExceptionile zorla tamamlar. Bu nedenle:Yöntem gövdesinde başka bir eşzamansız çağrı (örneğin başka bir yeniden deneme veya ikinci bir sorgu) başlatmayın ve geri çağrısının geleceği tamamlamasını beklemeyin. Geri çağrı tetiklendiğinde çerçeve kayıt için zaten vazgeçmiş olur.
Geleceği eşzamansız olarak tamamlamak için yeni bir iş parçacığı başlatmayın, neden aynıdır.
Yöntem gövdesi yalnızca tek bir şey yapar: sabit bir satır, NULL satırı veya boş bir koleksiyon gibi hafif bir yedek sonuç sağlar veya özel bir iş özel durumuyla
completeExceptionally(...)çağrısı yapar.
Özel durum oluşturma güvenlidir: Yöntem gövdesinde eşzamanlı olarak bir özel durum oluşturmak kabul edilebilir. Çerçeve özel durumu aşağı akış
ResultFuturenesnesine iletir; bu, açıkçafuture.completeExceptionally(thrown)çağrısı yapmaya eşdeğerdir.
Bu kısıtlamaların ötesinde çerçeve aşağıdaki senaryolarda da kararlı davranış garantisi verir:
**
timeoutyöntemi bildirilmemiş**: Çerçevenin varsayılanTimeoutExceptionözel durumu kullanılır ve kod oluşturma sırasında hata oluşmaz.Uyumsuz imza:
timeoutyöntemi geçerli görünürlüğe sahip ancak parametre listesi çağrı sitesindeki gerçek bağımsız değişken türleriyle eşleşmiyorsa çerçeve, planlama (kod oluşturma) sırasında birValidationExceptionile başarısız olur. Hata iletisi işlevin tam nitelikli sınıf adını içerir ve hem beklenen hem de gerçek imzaları gösterir.Yedek olarak boş koleksiyon:
future.complete(Collections.emptyList())çağrıldığında, satır INNER arama birleştirmesi için atılır ve LEFT OUTER arama birleştirmesi için sağ tablo alanları NULL ile doldurulur.
Aşağıdaki örnek, eşzamansız bir tablo işlevine bir timeout geri çağrısı ekler ve geleceğin nasıl eşzamanlı olarak tamamlanacağını ve bir yedek satırın nasıl döndürüleceğini gösterir:
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));
}
}
Bir UDTF Kaydetme
Bir UDTF'nin nasıl kaydedileceğine ilişkin talimatlar için bkz. Kullanıcı tanımlı işlevleri (UDF'ler) yönetme.
Bir UDTF Kullanma
UDTF'yi kaydettikten sonra bunu bir Flink SQL işinde kullanın.
-
Bir Flink SQL işi geliştirin. Daha fazla bilgi için bkz. İş geliştirme haritası. Aşağıdaki SQL,
ASI_UDTF_Sourceiçindekimessagealanını|ayırıcısını kullanarak iki sütuna böler ve ardından sonuçlarıASI_UDTF_Sinkiçine ekler.LATERAL TABLEsözdizimi, her girdi satırını UDTF tarafından yayılan satırlarla çapraz birleştirir.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); Operation Center > Job O&M sayfasında hedef işi bulun ve Actions sütunundaki Start seçeneğine tıklayın. İş başladıktan sonra
ASI_UDTF_Sourceiçindekimessagealanındaki dizeler|ayırıcısına göre bölünür ve elde edilen sütunlarASI_UDTF_Sinkiçine eklenir.