"Functional Data Engineering — a modern paradigm for batch data processing"の要約と感想

概要

Functional Data Engineering — a modern paradigm for batch data processing | by Maxime Beauchemin | Medium https://maximebeauchemin.medium.com/functional-data-engineering-a-modern-paradigm-for-batch-data-processing-2327ec32c42a

関数型プログラミングパラダイムをデータエンジニアリングに適用することで、複雑なETLを明快にする方法。

筆者はAirflowの開発者であり、Yahoo、Fafebook、Airbnbなどで長年培った経験に基づいた構成になっている。

ざっくり要約する。

関数型プログラミング

関数型プログラミングは、関数を主軸にしたプログラミングを行うスタイルである[1]。ここでの関数は、数学的なものを指し、引数の値が定まれば結果も定まるという参照透過性を持つものである[1]参照透過性とは、数学的な関数と同じように同じ値を返す式を与えたら必ず同じ値を返すような性質である[1]

関数型プログラミング - Wikipedia

関数型プログラミングは明確性をもたらす。関数が”Pure”であるとは「外部のコンテキストや、実行を取り巻くイベントとは関係なく、単独で記述、デバッグ、テストができる」状態を指す。

ETLの複雑性が増すに伴って、明確なパイプラインを記述することは必須となっている。

Reproducibility

データの信頼性を高めるには、プロセスに再現性が伴わなければならない。

例えばある企業からソースデータを受け取り、それを加工するパイプラインを組んでいたとする。ある時データに誤りがあることがわかり、すべてのデータを再生成する必要が生じた。

このときパイプラインでの変更は、ソースデータのみであることが保証できないといけない。他のデータに差分があると計算するたびに異なる結果を得る可能性がある。

またビジネスロジックを変更した場合、パイプラインの中でロジックが唯一の変更点であることを保証できなければならない。

データの不変性と、バージョン管理されたビジネスロジックが再現性の鍵となる。

Pure Tasks

Pure Taskとは「外部のコンテキストや、実行を取り巻くイベントとは関係なく、単独で記述、デバッグ、テスト可能なタスク」を指す。

冪等性はパイプラインの操作性を強める。冪等性が保証されれば、パイプラインを何度実行しても同じ結果が得られる。タスクが失敗した、あるいはビジネスロジックに何らかの変更が加わった場合でも、安全なbackfillを保証できる。

SQLの文脈では、Partitionへの上書きがPure Taskとみなせる。上書後のレコードを、関数が返すイミュータブルなオブジェクトとみなせる。

INSERT, DELETE, UPDATEは使わなくて良い。データの状態を変えてしまう。

関数型プログラミング言語では、イミュータブルなオブジェクトの強制によりパラダイムを実現している。ETLの文脈ではPartitionをイミュータブルなオブジェクトとして扱い、OVERWRITEのみを許容することで、Pure Taskを機能させることができる。

関数のローカル変数と同じように、Pure Tasksの状態は独立していることも重要。一時テーブルを使用する場合は、インスタンスが互いに干渉できない方法で実装するべき。

Changing logic over time

ビジネスロジックは時間とともに変わる。遡及的な変更を適用するには、ロジックをアップデートしbackfillを行う。そうでない場合は、条件付きのロジックを適用すると良い。

例えば2018年から税率が変わったとする。単純に新しい税率で上書きするのは危うい。2017年のデータをbackfillする際、コードが変更されたことを知らずに2018年の税率を適用してしまうかもしれない。日付に応じた条件付きのロジックを実装することで、危険性を排除できる。

また多くの場合、ビジネスルールはデータで表現できると良い。例えば、ハードコーディングされた税率とは対象的に、税率と対象期間が伴ったテーブルとして管理できる。

感想

確かにテーブルでロジックを管理すると見通しがよくなりそう。パターンが一つしかない時点では、わざわざテーブルに切り出す必要はないと思う。分岐が増える保証もない。

But what about dimensions?

ディメンションはゆっくり変わる。それゆえUPSERTやSCD (Slowly Changed Dimension methodology) が適用される事も多いが、関数型パラダイムではどう扱えるだろうか?

ETL毎にディメンションのスナップショットを取れば良い。ディメンションテーブルはスナップショットのコレクションになる。データ量の無駄かと思うかもしれないが、テーブルサイズは比較的小さく、シンプルさと再現性も兼ね備えている。エンジニアリングにかかる工数と天秤にかけると理にかなっている。

SCDでは、ディメンションの履歴を追いにくく、エラーも起きやすい。ディメンションスナップショットは非正規化の一種とも考えられる。

感想

ストレージ・コンピューティングのコストと、エンジニアリングのコストを天秤にかける視点は見習おうと思った。エンジニアリングのコストは定量化しづらく、マネージャー層が理解しやすいストレージやコンピューティングのリソースを優先しがちな現場は少なくないと思う。PJに大きな影響を与えるのはエンジニアリングコストだと思うので、エンジニアが責任を持って考慮に含めたい。

Past dependencies

複雑性が増す典型的なパターンは、同じテーブルの過去のpartitionを参照すること。長期間に渡るほど複雑性が跳ね上がる。

数ヶ月間のデータをbackfillする際、何百ものparititonを並列化無しで再生成しなければいけない。backfilは一般的であり、過去の参照は並列実行不可能な深いDAGに繋がる。できるだけ過去への依存は避けた方が良い。

累積的な指標が必要な場合の解決策の一つは、特別化されたフレームワーク、もしくは、独立かつ最適化されたモデルを使うことである。累計計算モデルは一つの記事にできるほどのトピックなので、ここでは深く追求しない。

感想

明確な解決策が提示されておらず残念。累計値を出す以外にも、window関数を使った過去データの参照や、factテーブルを辿る場面よくあると思うので、プラクティスを知りたい。

Late arriving facts

遅延データはイミュータブルなポリシーに反しやすい。モバイルアプリの浸透もあって、データの遅延一般的である。

うまく扱う第一歩は、イベント時刻と、処理時刻を分離することにある。遅延ログを扱う際は、partitionはいつも処理時刻で切るべきである。こうすることで遅延とは無縁のイミュータブルなブロッグを設けられる。

処理時刻でpartitionを切った場合、イベント時刻に基づくpartitionからの恩恵を受け取れなくなる。これは明らかなトレードオフである。回避策として、両方の時刻でparititonを切る、window関数を使って分析するなどの方法がある。

SLAと不変性のトレードオフになる。通常前者が優先されることが多い。その場合、将来的なbackfiilを行った際、同じ結果を得られないことに注意しよう。

感想

遅延データのpartitionをイベント処理時刻で切ると、確かにpartition単位での不変性は保てる。ただ逆に複雑性が増す気もする。場合によっては、ETLキック時点でのテーブルのスナップショットを取り、遅延ログを無視することも選択肢の一つとしてあると思う。

さいごに

関数型プログラミングの思想、関数型プログラミングの思想をデータエンジニアリングに適用する具体的方法(partitionをイミュータブルなオブジェクトとして扱う)が述べられたあと、いくつかのプラクティスが述べられていた。

最も重要なのはタスクがPureであること、つまり

外部のコンテキストや、実行を取り巻くイベントとは関係なく、単独で記述、デバッグ、テストができる

であること。

データエンジニアリング界隈では新しいツールやSaaSが凄まじい速度でリリースされているが、例えばDagstarなど上記の指針に則ったツールは多いと思う。実現方法が変わったとしても、上記の観点が根底にあるのは変わらないはず。

とはいえ「Late arriving facts」でも述べられていたように、Pureは他の要素とのトレードオフになる。メリデメを把握しながら、ケースごとに最適な選択をできるよう視野を広く保っておきたい。