ユーザー定義スカラー関数 (UDSF) は、0、1、または複数のスカラー値を新しいスカラー値にマッピングし、入力行と出力値の間に 1 対 1 の関係を生成します。組み込みの Flink SQL 関数でカスタムロジックを表現できない場合は、UDSF を使用します。
すべてのユーザー定義関数 (UDF) の種類の概要については、「ユーザー定義関数」をご参照ください。
仕組み
UDSF は Apache Flink の ScalarFunction クラスを継承し、1 つ以上の eval() メソッドを実装します。Flink は入力行ごとに eval() を 1 回呼び出し、戻り値をスカラー出力として使用します。
public class SubstringFunction extends ScalarFunction {
public String eval(String s, Integer begin, Integer end) {
return s.substring(begin, end);
}
}
eval() メソッドの要件:
-
publicとして宣言する必要があります -
メソッドのオーバーロードをサポートします。つまり、異なる入力タイプに対して複数の
eval()シグネチャを定義できます -
可変長引数をサポートします (例:
eval(Integer...)) -
NULL入力を正しく処理するために、ボックス化されたプリミティブ (例:intの代わりにInteger) を使用します
前提条件
開始する前に、以下が準備されていることを確認してください。
-
IntelliJ IDEA がインストール済みであること
-
Apache Maven がインストール済みであること
-
Realtime Compute for Apache Flink のワークスペースへのアクセス権
非同期スカラー関数 (AsyncScalarFunction)
通常のスカラー関数とは異なり、カスタムの非同期スカラー関数は AsyncScalarFunction を継承し、CompletableFuture を介して結果を返す必要があります。
非同期スカラー関数は、外部データベースのクエリや HTTP インターフェースの呼び出しなど、I/O 待機を伴う操作を実行する場合に使用します。この関数は複数のレコードを同時に処理するため、待機がジョブのスループットに与える影響を軽減します。文字列の切り捨てや数値計算など、外部サービスを待機しない操作の場合は、通常、同期スカラー関数で十分です。
設定パラメーター
以下のパラメーターは、非同期スカラー関数のランタイム動作を制御します。クエリを送信したり、実行計画を生成したりする前に、これらのパラメーターを設定してください。
|
パラメーター |
デフォルト値 |
説明 |
|
|
|
各非同期オペレーターインスタンスで許可される同時非同期操作の最大数。値を大きくすると、外部サービスへの同時実行の負荷が増加します。 |
|
|
|
リトライを含む、単一レコードの非同期処理のタイムアウト期間。 |
|
|
|
|
|
|
|
固定間隔リトライ間の待機時間。このパラメーターは、リトライ戦略が |
|
|
|
リトライの最大試行回数。このパラメーターは、リトライ戦略が |
|
|
|
|
順序不同の出力の設定
デフォルトでは、非同期関数は同時に実行されますが、後続のレコードの完了した結果は、先行するレコードが完了した後にのみ出力されます。外部リクエストのレイテンシーが大幅に異なり、ビジネス上、順序不同の結果が許容される場合は、順序不同の出力を有効にすることで、遅いリクエストによる待機を減らすことができます。
非同期呼び出しと順序不同の出力は、2 つの独立した概念です。デフォルトでは、非同期スカラー関数は同時に呼び出され、入力順序で結果を出力します。順序不同の出力を許可した後、システムは入力データの変更タイプに基づいてそれを有効にするかどうかを決定します。
クエリを実行する前に、次の設定を追加します。
SET 'table.exec.async-scalar.output-mode' = 'ALLOW_UNORDERED';
カスタム非同期スカラー関数の例
次の例では、製品サービスの HTTP インターフェースを介して製品名を照会します。この関数は、open で HttpClient を作成して再利用し、eval で sendAsync() を呼び出してリクエストを送信し、完了コールバックを介して結果を返します。sendAsync() は CompletableFuture を返すため、HTTP 呼び出しを非同期スカラー関数に接続するのに適しています。詳細については、「JDK HttpClient documentation」をご参照ください。
この例では、接続タイムアウトを 3 秒、リクエストタイムアウトを 5 秒に設定しており、これはレスポンスボディが小さい製品名のクエリに適しています。実際のインターフェースの応答時間に基づいて値を調整してください。
package com.example.udf;
import org.apache.flink.table.functions.AsyncScalarFunction;
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.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
/**
* 非同期 HTTP インターフェースを介して製品名を照会します。
*
* <p>インターフェースの規約: 200 は UTF-8 の製品名を返し、404 は製品が存在しないことを示します。
* この例は、短いテキストを返す読み取り専用のクエリに適しています。他のステータスコードは呼び出しの失敗として扱われ、エンジンに渡されます。
*/
public class AsyncProductName extends AsyncScalarFunction {
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() {
// 製品情報は変更される可能性があります。同じ ID でも、異なる時点で異なる結果が返されることがあります。
return false;
}
public void eval(CompletableFuture<String> result, Long productId) {
if (productId == null) {
result.complete(null);
return;
}
try {
HttpRequest request =
HttpRequest.newBuilder(
serviceBaseUri.resolve("products/" + productId + "/name"))
.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();
}
if (response.statusCode() == 404) {
return null;
}
throw new CompletionException(
new IOException(
"Failed to query the product. HTTP status code: " + response.statusCode()));
})
.whenComplete(
(name, error) -> {
if (error != null) {
result.completeExceptionally(error);
} else {
result.complete(name);
}
});
} catch (RuntimeException e) {
result.completeExceptionally(e);
}
}
}
開発中は、以下の項目に注意してください。
-
結果を返すには、
result.complete(value)を呼び出します。SQL のNULLを返すには、result.complete(null)を呼び出します。 -
呼び出しが失敗した場合は、
result.completeExceptionally(exception)を呼び出して例外を返します。エンジンはリトライ戦略に基づいてそれを処理します。 -
evalはリクエストが送信されるとすぐに戻ります。sendAsync()の完了コールバックを介して結果を渡します。ブロッキングメソッドであるsend()、get()、またはjoin()を呼び出したり、evalまたはコールバック内で時間のかかる計算を実行したりしないでください。 -
各関数インスタンスは、
openでクライアントを初期化して再利用することで、レコードごとにクライアントを作成するのを回避します。JDK 11 および JDK 17 のHttpClientには public なclose()メソッドがなく、その内部リソースは JDK によって管理されます。close メソッドを提供する別の非同期クライアントを使用する場合は、関数のcloseメソッドでクライアントを解放してください。 -
製品情報は時間とともに変化するため、この例では
isDeterministic()をfalseに設定しています。更新ストリームを扱う場合は、クエリのセマンティクスに基づいて非決定的な結果を処理してください。
UDSFの開発
Flink は、開発環境を事前設定した UDF のサンプルを提供しています。このサンプルには、UDSF、ユーザー定義集計関数 (UDAF)、およびユーザー定義テーブル値関数 (UDTF) が含まれています。
-
ASI_UDX_Demo のサンプルをダウンロードして、ローカルマシンに解凍します。解凍後、
ASI_UDX-mainフォルダーが次の構造で作成されます。-
pom.xml:Maven プロジェクトの設定ファイルです。プロジェクトの座標、依存関係、ビルドルール、および関連するメタデータを定義します。 -
\ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDF.java:UDSF のサンプル Java 実装です。
ASI_UDX_Demo はサードパーティのウェブサイトでホストされています。アクセスに失敗したり、遅延が発生したりする場合があります。
-
-
IntelliJ IDEA で、[File] > [Open] をクリックし、
ASI_UDX-mainフォルダーを選択します。 -
\ASI_UDX-main\src\main\java\ASI_UDF\ASI_UDF.javaを開き、eval()メソッドをカスタムロジックで更新します。サンプル実装では、各入力文字列のインデックスbeginからインデックスendの直前までの部分文字列を抽出します。package ASI_UDF; import org.apache.flink.table.functions.ScalarFunction; public class ASI_UDF extends ScalarFunction { public String eval(String s, Integer begin, Integer end) { return s.substring(begin, end); } } -
\ASI_UDX-main\pom.xmlを開き、ご使用の Flink バージョンに合わせて Maven の依存関係を設定します。次の例は、Flink 1.11 の主な JAR パッケージの依存関係を示しています。UDSF が追加の JAR パッケージに依存しない場合は、この手順をスキップしてください。<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> -
pom.xmlファイルが含まれているディレクトリで、次のコマンドを実行してプロジェクトをパッケージ化します。mvn package -Dcheckstyle.skipビルドが完了すると、
\ASI_UDX-main\target\ディレクトリにASI_UDX-1.0-SNAPSHOT.jarが作成されます。
UDSFの登録
JAR パッケージを UDSF として登録する方法については、「ユーザー定義関数 (UDF) の管理」をご参照ください。
UDSFの使用
登録後、Flink SQL ジョブで UDSF を呼び出します。
-
Flink SQL ジョブを作成します。手順については、「ジョブ開発マップ」をご参照ください。次のサンプルSQLは、
ASI_UDSF_Sourceのaフィールドの文字列から3番目と4番目の文字を抽出し、その結果をASI_UDSF_Sinkに書き込みます。CREATE TEMPORARY TABLE ASI_UDSF_Source ( a VARCHAR, b INT, c INT ) WITH ( 'connector' = 'datagen' ); CREATE TEMPORARY TABLE ASI_UDSF_Sink ( a VARCHAR ) WITH ( 'connector' = 'blackhole' ); INSERT INTO ASI_UDSF_Sink SELECT ASI_UDSF(a, 2, 4) FROM ASI_UDSF_Source; -
[運用センター] > [ジョブ運用保守] ページで、対象のジョブを見つけ、[Actions] 列の [Start] をクリックします。ジョブが開始されると、
ASI_UDSF_Sourceの各行のaフィールドから3番目と4番目の文字がASI_UDSF_Sinkに挿入されます。