Slide 1

Slide 1 text

ZIOでサクッとFunctionalにETL 2024/09/06 Scalaわいわい勉強会 #3 wakye5815 1

Slide 2

Slide 2 text

自己紹介 Scalaでバックエンド書いたりEM見習いを始めたり色々やってます 名前:脇田悠介 働いてるところ:株式会社Flinters X :@wakye5815 悩み:ZIOの読み方がZIOかZIOなのかわからない 2

Slide 3

Slide 3 text

とある日 3

Slide 4

Slide 4 text

実装依頼はいつも突然に 大量の広告配信データを〜 ちょろっと圧縮して〜 ストレージに出力する〜 使い捨ての〜 スクリプトお願いします! 4

Slide 5

Slide 5 text

欲しいのは ストリーミング操作が優秀でサクッと書ける 軽量に動かせて 並行処理もいい感じ あわよくばFunctionalな そんなScalaライブラリ 5

Slide 6

Slide 6 text

そんなScalaライブラリあるわけ.... 6

Slide 7

Slide 7 text

7

Slide 8

Slide 8 text

と言うことで今日は軽量ETL by ZIOに ついて話します 8

Slide 9

Slide 9 text

他ライブラリ候補 9

Slide 10

Slide 10 text

Akka/Pekko Stream メリット 様々なデータソースを接続できるAlpakka優秀 ストリーミング操作のオペレーターもかなり揃っている 腐っているので情報が多い デメリット ActorSystemが付いてくる チームでは脱Akkaしていたので改めて採用するのも... 10

Slide 11

Slide 11 text

Spark メリット 分散処理できてスケールする Alpakkaと同じくデータソースサポートが手厚い デメリット クラスタ管理はちょっとしたくない 11

Slide 12

Slide 12 text

そしてZIO ZIOはEffectライブラリ 誤解を恐れずに表現するとZIO文脈のEffectは R => Either[E,A] であり ZIO[R,E,A] として扱われる ScalaらしくFunctionalに設計されているがモナモナしていないので とっつきやすい、またドキュメントも十分 並行処理はJVMのvirtual threadをベースとしたFiberを提供してい る 12

Slide 13

Slide 13 text

Streamingはどうなの? ZIOを元にしたZIO Streamsが提供されている Akka/Pekkoに負けない抽象化され且つ豊富なストリーミング操作 ZIO Streamsはプッシュベースストリームではなくプルベースストリ ームのためバッファリング要らず、バックプレッシャーしなくてOK ただしAlpakkaほどの豊富なデータソースサポートはないので要件 とご相談 13

Slide 14

Slide 14 text

顧客が本当に求めていたもの 14

Slide 15

Slide 15 text

結果としてZIOを採用 Httpな広告媒体APIからデータをとるだけなのでデータソースサポ ートが豊富でなくてもOK 使い捨てなのでSparkではオーバーキル チーム背景も含めてAkka/Pekkoはごめん 15

Slide 16

Slide 16 text

実装していく 16

Slide 17

Slide 17 text

まずは今回のETLについて Extract 広告媒体APIから取得できる配信データをExtract Transform 取得したデータを一定粒度でgzipに圧縮 Load 外部のシステムが読みにくるGCSにLoad 17

Slide 18

Slide 18 text

ZIO Streamsで表現するには ワークフロー定義に3つの抽象化されたモジュールを利用する ZStream[Env,Err,Out] ストリームワークフローのデータソースとして機能 ZPipeline[Env, Err, In, Out] 実態は ZStream[Env,Err,In] => ZStream[Env,Err,Out] ZSink[Env,Err,In,Left,Consumed] ZSink[Env,Err,In,Left,Consumed] => ZIO[Env,Err,Consumed] な ZStream#run に渡して消費する 18

Slide 19

Slide 19 text

組み合わせると def extractionStream(apiEndpoint: URL): ZStream[Any, Throwable, ApiResponse] def transformationPipeline: ZPipeline[Any, Throwable, APIResponse, (String, Array[Byte])] def loadingSink: ZSink[Any, IOException, (String, Array[Byte]), Nothing, Unit] object Main extends ZIOAppDefault override def run = val effects = apiEndpoints.map { apiEndpoint => extractionStream(apiEndpoint) >>> transformationPipeline >>> loadingSink } ZIO.collectAllPar(effects).unit 19

Slide 20

Slide 20 text

Extractの実装 java.util.concurrent.FutureあるいはBlockingなAPI実行をZIO Effectに変換する APIはpagination形式 paginateしつつ変換したZIO EffectをさらにStreamへ変換 20

Slide 21

Slide 21 text

Extractの実装 def extractionStream(apiEndpoint: URL): : ZStream[Any, Throwable, ApiResponse] = ZStream.paginateZIO(apiEndpoint) { apiEndpoint => for response: Task[ApiResponse] <- if(isBlocking(apiEndpoint)) then ZIO.attemptBlocking(callBlockingApi(apiEndpoint)) else ZIO.fromFutureJava(callApi(apiEndpoint)) nextEndpoint = Option(response.nextEndpoint) yield (response, nextEndpoint) } 21

Slide 22

Slide 22 text

Transformの実装 ZStreamで流れてくるApiResponseをArray[Byte]に変換 ArrayButeをgzip圧縮する ついでにuploadする粒度毎にファイル名をつけておく 22

Slide 23

Slide 23 text

Transformの実装 def transformationPipeline: ZPipeline[Any, Throwable, APIResponse, (String, Array[Byte])] = val gzipPipeline = ZPipeline.mapZIO { (response: APIResponse) => ZIO.scoped { for byteArrayOs <- ZIO.fromAutoCloseable(ZIO.attempt(new ByteArrayOutputStream())) gzipOs <- ZIO.fromAutoCloseable(ZIO.attempt(new GZIPOutputStream(byteArrayOs))) _ <- ZIO.attemptBlockingIO(gzipOs.write(response.getRawResponse.getBytes)) yield byteArrayOs }.map(_.toByteArray) } val fileNamePipeline = ZPipeline.mapAccum[Array[Byte], Int, (String, Array[Byte])](1)( (index, data) => (index + 1, (s"data_$index.json.gz", data)) ) gzipPipeline >>> fileNamePipeline 23

Slide 24

Slide 24 text

Loadの実装 あとはGCSに放り込むだけ 24

Slide 25

Slide 25 text

Loadの実装 def loadingSink: ZSink[Any, IOException, (String, Array[Byte]), Nothing, Unit] = ZSink.foreach { (pathWithData: (String, Array[Byte])) => val blobId = BlobId.of(BUCKET_NAME, pathWithData._1) val blobInfo = BlobInfo.newBuilder(blobId).build() ZIO.attemptBlockingIO(storage.create(blobInfo, pathWithData._2)) } 25

Slide 26

Slide 26 text

ZIO Streams万歳 26

Slide 27

Slide 27 text

他にも Streamに対するretry stream.retry(Schedule.exponential(1.second)) timeoutしたら他のStreamに切り替える firstStream.timeoutTo(10.seconds)(secondStream) 27

Slide 28

Slide 28 text

まとめ モナモナせずにFunctionalにETLを組めた ZStream(むしろZIO)の豊富なオペレータでデータ処理のロジックが スマートに Akka/Pekko Streamを使っていた人はZStreamの操作自体は似てい るので移行しやすそう 28

Slide 29

Slide 29 text

We're hiring 絶賛採用中なのでZIOやりたい人、大規模広告データと戯れたい人い らっしゃいましたらお話ししましょう〜 https://www.wantedly.com/companies/flinters 29