ForEach シンクを使用してパイプラインの各行を書き込みます
ForEach シンクを使用して、外部 REST API やキー値ストアなど、組み込みシンクがない宛先にストリーミングデータを書き込みます。
ForEach シンクは、一度に 1 行ずつストリームを処理します。各行に対して、パイプラインは、Apache Spark Structured Streaming の foreach シンクと同様に、カスタム Python 関数またはライターオブジェクトを呼び出します。
このページで説明されている ForEach シンクは、パイプライン用です。Apache Spark Structured Streamingはforeachもサポートしています。Structured Streamingでカスタム出力を書き込む方法に関する情報については、foreachBatchを使用して任意のデータシンクに書き込むを参照してください。
ForEachシンクを使用するタイミング
外部 API の呼び出しや各レコードのキーバリューストアの更新など、組み込みのシンク形式がない宛先に各レコードを個別に書き込む必要がある場合は、ForEach シンクを使用します。ロジックが Delta Lake テーブルへのマージや DataFrame 操作による複数の宛先への書き込みなど、マイクロバッチ全体で機能する場合は、代わりに ForEachBatch シンクを使用してください。
この表は、2つのシンクを比較しています:
characteristic |
|
|
|---|---|---|
処理の単位 | 一度に1行 | 一度に1つのマイクロバッチDataFrame |
コードの実行場所 | エグゼキューターで | ドライバー上 |
Python API |
|
|
組み込みシンクの情報については、LakeFlow Pipelinesのシンクを参照してください。@dp.foreach_sink() Python API リファレンスについては、foreach_sink を参照してください。
ForEach シンクの定義
関数またはライターオブジェクトからForEachシンクを定義できます。
関数からシンクを定義するには、関数に@dp.foreach_sink()を適用します。パイプラインは、各行に対して関数を呼び出します。name を省略した場合、シンクは関数名を使用します:
from pyspark import pipelines as dp
@dp.foreach_sink()
def my_row_sink(row):
# Custom per-row logic here.
...
ライター オブジェクトからシンクを定義するには、writer キーワード引数を使用してオブジェクトを渡します。ライターは process(row) を実装する必要があり、open(partition_id, epoch_id) と close(error) を実装できます。各パーティションに対して 1 つの接続を開き、行ごとに再利用する場合にライターを使用します。name を省略した場合、シンクはライターのクラス名を小文字で使用します。
from pyspark import pipelines as dp
class MyRowWriter:
def open(self, partition_id, epoch_id):
# Open a connection for this partition.
return True
def process(self, row):
# Write the row by using the connection.
...
def close(self, error):
# Close the connection.
...
dp.foreach_sink(name="my_row_sink", writer=MyRowWriter())
各メソッドの詳細については、Writerメソッドを参照してください。
イベントログエントリ
ForEach シンクを定義すると、パイプラインイベントログは、そのシンクに対して "format": "foreach" を持つ SinkDefinition イベントを記録します。
ForEach シンクへの書き込み
追加フローまたは更新フローから ForEach シンクに書き込むことができます。
追加フローで書き込み
追加フローから ForEach シンクに書き込むには、シンク名を target として参照します。
@dp.append_flow(target="my_row_sink")
def my_flow():
return spark.readStream.table("my_catalog.my_schema.source_table")
同じForEachシンクを対象とする複数の追加フローを定義できます。各フローは独自のチェックポイントを保持します。
更新フローで書き込む
更新フローからForEachシンクに書き込むには、@dp.update_flowデコレータを使用します。
@dp.update_flow(target="my_row_sink")
def my_stateful_flow():
return spark.readStream.table("my_catalog.my_schema.events").groupBy("user_id").count()
更新フローでは、後続のマイクロバッチで新しい値とともに同じキーをシンクに再度送信できます。古い値を置き換えるには、挿入する代わりにキーで各行を宛先にアップサートします。前の例では、キーとして user_id を使用して、ユーザーの新しいカウントが前のカウントを置き換えるようにします。
低レイテンシー処理の場合、ForEach シンクに書き込む更新フローをリアルタイムモードで実行できます。LakeFlow Pipelinesでリアルタイムモードを使用するを参照してください。
エグゼキューターでライブラリと依存関係をインポートする
関数またはライターはドライバー上ではなく、エグゼキューター上で実行されます。次のプラクティスに従ってください。
- コードに必要なパッケージをパイプライン環境に追加します。パイプライン環境のパッケージは、エグゼキューター上のコードから利用できます。パイプラインの Python 依存関係の管理を参照してください。
- コードが関数またはライターメソッド内で使用するライブラリは、パイプラインソースファイルの先頭だけでなく、インポートしてください。
- ファイルの先頭で開くクライアントや接続など、ドライバー上で作成するオブジェクトに依存しないでください。関数内、またはライターの
openメソッド内で作成してください。
障害と重複行の処理
コードで例外が発生すると、フローは失敗し、パイプラインのイベントログに例外メッセージを含むエラーイベントが記録されます。
マイクロバッチが失敗した後に再試行されると、シンクが再度いくつかの行を受け取ることがあります。例えば、行のユニークキーを使用して宛先にアップサートするなどして、行ごとの書き込みを冪等にするようにします。LakeFlow Pipelinesでの処理保証を参照してください。
ライター オブジェクトを使用する場合、open は partition_id と epoch_id を受け取ります。これらを合わせると、1つのマイクロバッチ内の1つのパーティションの行を識別できるため、それを使用して行の重複を排除できます。関数はこれらのIDを受け取りません。
IDは、1つのフローのチェックポイント内でのみ一意です。同じペアは、以下の場合に繰り返すことができます。
- 複数のフローが同じシンクに書き込みます。
- 完全更新はチェックポイントをResetします。
これらのIDで重複を排除するには、単一のフローからシンクに書き込み、完全更新を実行するときに重複排除の状態をクリアしてください。
フルリフレッシュ
完全更新時に、パイプラインは ForEach シンクに書き込む各フローのチェックポイントをResetします。パイプラインはシンクが書き込むデータを追跡またはクリーンアップしないため、宛先の保持とクリーンアップはユーザーが管理します。クリーンな状態からやり直す必要がある場合は、そのデータを自分で削除してください。
制限
- 行メトリクス :パイプラインは、
ForEachシンクが書き込む行数をレポートしません。 - 行ごとの呼び出し : コードが行ごとに外部システムを呼び出すと、外部システムの throughput が制限される可能性があります。オーバーヘッドを削減するには、ライターの
openメソッドで接続を開いて各行で再利用するか、ForEachBatchシンクを使用して一括で書き込みます。
例
これらの例は、ForEach シンクを使用する一般的な方法を示しています。
基本構文の例
この例では、関数から ForEach シンクを定義し、追加フローからそこに書き込みます。
from pyspark import pipelines as dp
# Create a ForEach sink
@dp.foreach_sink(name="my_foreach_sink")
def handle_row(row):
# Custom per-row logic here.
...
# Create source data for the example
@dp.table()
def example_source_data():
return spark.range(5)
# Write to the sink from an append flow
@dp.append_flow(target="my_foreach_sink")
def my_flow():
return spark.readStream.table("example_source_data")
各行を外部APIに書き込む
この例では、ライターオブジェクトを使用して各オーダーを外部 REST API に送信します。ライターはパーティションごとに 1 つの HTTP セッションを開きます。URL 内の注文 ID を使用するため、再試行された行は重複を作成するのではなく、同じレコードを更新します。
from pyspark import pipelines as dp
class OrdersApiWriter:
def open(self, partition_id, epoch_id):
import requests # Import on the executor
self.session = requests.Session()
return True
def process(self, row):
response = self.session.put(
f"https://api.example.com/orders/{row.order_id}",
json={"order_id": row.order_id, "status": row.status},
timeout=10,
)
# Raise on HTTP errors so that the flow fails instead of dropping the row.
response.raise_for_status()
def close(self, error):
self.session.close()
dp.foreach_sink(name="orders_api_sink", writer=OrdersApiWriter())
@dp.append_flow(target="orders_api_sink")
def orders_to_api():
return spark.readStream.table("my_catalog.my_schema.orders")
複数のフローから単一のシンクに書き込む
この例では、2つのソースを同じForEachシンクに書き込みます。
from pyspark import pipelines as dp
@dp.foreach_sink(name="events_sink")
def handle_event(row):
# Custom per-row logic here.
...
@dp.append_flow(target="events_sink")
def web_events():
return spark.readStream.table("my_catalog.my_schema.web_events")
@dp.append_flow(target="events_sink")
def mobile_events():
return spark.readStream.table("my_catalog.my_schema.mobile_events")