Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scalaで作るデータパイプライン

 Scalaで作るデータパイプライン

B8ab2e500b13247ede10b3ed06012b3a?s=128

laughingman7743

August 30, 2018
Tweet

Transcript

  1. 2018/08/30 Merpay インフラ Talk! Merpay Data Platform@laughingman7743 Scalaで作るデータパイプライン

  2. 2 自己紹介 Copyright © Merpay, Inc. All Rights Reserved. @laughingman7743

    Data Platform Team at Merpay
  3. 今日の目次 アーキテクチャ 01 ログフォーマット 02 パイプライン処理 03 テストのススメ 04 まとめ

    05
  4. アーキテクチャ

  5. PubSub, Dataflow • マネージド・サービスを中心とした構成 • スピードレイヤ(ストリーミング処理)で PubSub, Dataflow を利用 •

    Stackdriver Logging から PubSub に Sync (Stackdriver Logging は使わなくなるかも)
  6. Scio, ScalaPB Scio • Spotify が OSS で公開している ApacheBeam の

    Scala ラッパー • コレクション操作する感覚で Dataflow を書くことができる (DoFn, Apply 不要) ScalaPB • Proto定義 から Scala の case class を生成する sbtプラグイン (protoc は Scala に対応していない) • 『ScalaにおけるProtocol Buffers事情』に詳しく解説してあります (xuwei-x 神)     独自の Parser を利用していない, version3 に対応, Java との相互変換メソッド, Lens サポート, gRPC 対応 • ログフォーマットを Protocol Buffers で定義
  7. ログフォーマット

  8. syntax = "proto3"; package xxx; import "google/protobuf/any.proto"; message Log {

    Header header = 1; google.protobuf.Any body = 2; } Proto定義 import "google/protobuf/timestamp.proto"; message Header { google.protobuf.Timestamp time = 1; string field1 = 2; uint64 field2 = 3; } message Body1 { enum Enum1 { TYPE1 = 0; TYPE2 = 1; } Enum1 field1 = 1; string field2 = 2; } message Body2 { enum Enum2 { TYPE1 = 0; TYPE2 = 1; } Enum2 field1 = 1; string field2 = 2; } and more... 共通部分 ログ毎に異なる部分 ログ定義 • 共通部分と別の Proto 定義がなんでも入れられる Any 型を利用 • ログ毎に異なる Any 型の部分は必要に応じて定義
  9. case class の生成 • Proto 定義から ScalaPB で case class

    をCIで自動生成 • 生成した case class をライブラリとしてJarにビルドして Artifactory にパブリッシュ
  10. パイプライン処理

  11. Any型のハンドリング • リフレクションを使ってデシリアライズ 型安全にデシリアライズできるが, Any 型に入る Proto 定義が増えるとコードの修正が必要となる TypeUrl からリフレクションでデシリアライズ

    Proto 定義から生成した case class ライブラリのアップデートで対応できるように • shapeless で case class のフィールド値を取得, TableRowを生成 以下のようなメソッド & PValue 型を BigQuery の型に変換するパターンマッチ TypeSafe な BigQuery への書き込み def parseFrom(payload: Array[Byte], typeUrl: String): GeneratedMessage = { val clazz = typeUrl.split("/").lastOption.getOrElse(typeUrl) val runtimeMirror = runtime.universe.runtimeMirror(getClass.getClassLoader) val cmp = runtimeMirror.reflectModule(runtimeMirror.staticModule(clazz)) .instance.asInstanceOf[GeneratedMessageCompanion[_ <: GeneratedMessage]] cmp.parseFrom(payload) } def toMap[A, R <: HList, K, V](a: A) (implicit gen: LabelledGeneric.Aux[A, R], toMap: ToMap.Aux[R, K, V]): Map[String, Any] = { toMap(gen.to(a)).map { case (a: Symbol, b) => a.name -> v } }
  12. BigQueryへの書き込み • ApacheBeam の DynamicDestinations クラスを実装 Any 型の中身に応じてテーブル振り分け, BigQueryIO でストリーミング書き込み

    • スキーマ自動生成 (DynamicDestinations#getSchema の実装) case class の descriptor のフィールド定義から BigQuery のスキーマ生成 • 自動パーティショニング (DynamicDestinaitons#getTable の実装) ログの共通項目の日付から自動的にパーティショニング (table_name$yyyymmdd を返却) テーブル名は Any 型の TypeUrl を利用 • フィールド追加ぐらいであれば対応できるように, スキーママイグレーション処理も実装 Scio の BigQueryClient で実装されているキャッシュ機構をうまく利用 • shapless を使った TableRow の生成 (FormatFunction の実装) 前頁 『Any型のハンドリング』 output.saveAsCustomOutput("output", BigQueryIO.write() .to(new MyDynamicDestinations[T](args)) .withFormatFunction(formatFunction) .withCreateDisposition(CREATE_IF_NEEDED) .withWriteDisposition(WRITE_APPEND))
  13. エラーハンドリング • リフレクション時に対応する case class がない場合等 • サイドアウトプットを利用して, エラーとなったログを別テーブルに出力 •

    変換処理を Either でラップ, Right は BigQuery の所定のテーブルに, Left はエラーログ格納用のテーブルにストア • エラーログ格納用のテーブル定義はログを格納する String型 のフィールドのみ • エラーとなってもログをロストしない&リカバリができるように
  14. テストのススメ

  15. テストのススメ • scio-test パッケージに ScalaTest をベースとしたライブラリが整備されている • PipelineSpec でパイプラインの output

    のテスト テスト用の input/output に簡単に入れ替え可能 • FlatSpec でモデルやユーティリティメソッドのユニットテスト • カバレッジは8割以上 class WordCountTest extends PipelineSpec { val inData = Seq("a b c d e", "a b a b", "") val expected = Seq("a: 3", "b: 3", "c: 1", "d: 1", "e: 1") "WordCount" should "work" in { JobTest[com.spotify.scio.examples.WordCount.type] .args("--input=in.txt", "--output=out.txt") .input(TextIO("in.txt"), inData) .output(TextIO("out.txt"))(_ should containInAnyOrder (expected)) .run() } }
  16. まとめ

  17. Scalaで書くメリット 簡潔な記述で複雑な処理を書くことができる 01 豊富なコレクション操作メソッド, 強力なパターンマッチング 02 Option, Try, Either 等エラーハンドリングがしやすい

    03 ジェネリックプログラミング (型安全で再利用しやすいクラス) 04 関数型プログラミングはほどほどに (ベターJavaとしての利用) 05
  18. Q&A