Akka stream

  • View
    3.390

  • Download
    2

  • Category

    Software

Preview:

Citation preview

copyright Fringe81 Co.,Ltd.

Akka Stream

@mtoyoshi

copyright Fringe81 Co.,Ltd.

AmazonKinesis

1行目

2行目

3行目

4行目

・・・

処理

・・・ 処理

copyright Fringe81 Co.,Ltd.

AmazonKinesis

1行目

2行目

3行目

4行目

・・・

・・・

Akka Actorで処理

copyright Fringe81 Co.,Ltd.

Akka Actor便利ですが

・メッセージ(データ)が型安全でない

・OutOfMemoryに遭遇

・メッセージ送受信の仕組み、汎用的 

copyright Fringe81 Co.,Ltd.

Akka Stream?

Typesafeより2015.07に1.0リリース

Akka Actor

Akka Stream

Akka HTTP

copyright Fringe81 Co.,Ltd.

Migration Guide 1.0 to 2.0https://github.com/drewhk/akka/pull/30/files

copyright Fringe81 Co.,Ltd.

RxJava

Reactive Streams(JEP266)

Vert.xAkka

Stream

・・・

Slick3 mongoDB

a standard for asynchronous stream processing

with non-blocking back pressure

その他OSS

copyright Fringe81 Co.,Ltd.

特徴

・バックプレッシャーによりバッファ溢れの危険を回避しつつパフォーマンスにも配慮・ReactiveStreams規格のものと接続可能・ストリームを構成する豊富な部品群・部品群の合成性、拡張性・ビジュアルなグラフDSL

copyright Fringe81 Co.,Ltd.

特徴

・API変更はこれからも続く(1.0->2.0)

・複数ノードにまたがった ストリームの構築は未対応・モナってはいない

copyright Fringe81 Co.,Ltd.

今日はOverview的な話・Akka Streamの構成要素は?

・どういうふうにプログラミングする?

・バックプレッシャーが特徴みたい 概念レベルの理解から一歩進めたい

copyright Fringe81 Co.,Ltd.

部品群を組み合わせて RunnableGraphを作る※1つ以上のSourceと1つ以上のSinkが必要

copyright Fringe81 Co.,Ltd.

部品群を組み合わせて RunnableGraphを作る※1つ以上のSourceと1つ以上のSinkが必要

copyright Fringe81 Co.,Ltd.

val source = Source(1 to 10)val filter = Flow[Int].filter(_ % 2 == 0)val map = Flow[Int].map(_ * 2)val sink = Sink.foreach[Int](println)

val runnableGraph = source.via(filter).via(map).to(sink)

runnableGraph.run()

RunnableGraphの構築と実行

copyright Fringe81 Co.,Ltd.

Source(1 to 10) .filter(_ % 2 == 0) .map(_ * 2) .runForeach(println)

RunnableGraphの構築と実行

こう書くことも出来る

val source = Source(1 to 10)val filter = Flow[Int].filter(_ % 2 == 0)val map = Flow[Int].map(_ * 2)val sink = Sink.foreach[Int](println)

val runnableGraph =source.via(filter).via(map).to(sink)

runnableGraph.run()

copyright Fringe81 Co.,Ltd.

Source[Int] - Flow[Int,String] - Sink[String]

Source[Int] - Flow[String,Long] Sink[String]

Function1のようにInとOutの型が合えば合成可能

copyright Fringe81 Co.,Ltd.

implicit val system = ActorSystem()implicit val materializer = ActorMaterializer()

val source = Source(1 to 10)val filter = Flow[Int].filter(_ % 2 == 0)val map = Flow[Int].map(_ * 2)val sink = Sink.foreach[Int](println)

val runnableGraph = source.via(filter).via(map).to(sink)

runnableGraph.run()

materializer

WHAT

HOW

copyright Fringe81 Co.,Ltd.

利用可能な処理:

map filter collect take / takeWhile drop / dropWhile flatten fold scan grouped / groupBy recoverなどなど

copyright Fringe81 Co.,Ltd.

val future: Future[List[Int]] = ...

val src: Source[List[Int], Unit] = Source(future)

src.mapConcat(identity).map(_ * 2)

def mapConcat[T](f: Out => Iterable[T])

Source[List[Int]]]だとList[Int]が1つ、ストリームを流れる事になる。mapConcatを使う事でList要素のIntそれぞれがストリームを流れるように出来る。

copyright Fringe81 Co.,Ltd.

zipWithIndexを使おうと思った。が、用意されてなかった。...作る!

case class ZipWithIndex[T]() extends PushStage[T, (T, Int)] { var i = -1

override def onPush(elem: T, ctx: Context[(T, Int)]): SyncDirective = { i += 1 ctx.push((elem, i)) }}

Source(List("A", "B", "C")) .transform(() => ZipWithIndex()) .runForeach(println) // (A,0) (B,1) (C, 2)

copyright Fringe81 Co.,Ltd.

Source#apply

使用頻度が多そう(?)なもの・Iterableから・Iteratorから・Futureから・Fileから↓

SynchronousFileSource(new java.io.File("..."))Source[ByteString]が出来る。

※Akka2.4ベースになればJava7追加のAsynchronousFileChannel等のNIO API使いたいとのこと。

copyright Fringe81 Co.,Ltd.

ちょっとハマった

IterableからSourceを作ることが出来る

// Compile Errorval src = Source(Seq(1,2,3))

// Compile Successval src = Source(List(1, 2, 3))

えっ?

copyright Fringe81 Co.,Ltd.

ちょっとハマった

Iterable とは collection.Immutable.Iterable// Compile Errorval src = Source(Seq(1,2,3))

// Compile Successval src = Source(List(1, 2, 3))

Seq は collection.Seq、つまりcollection.Iterabletype Seq[+A] = scala.collection.Seq[A]val Seq = scala.collection.Seq scala/package.scala

copyright Fringe81 Co.,Ltd.

Source#apply

Source(initialDelay=1.second, interval=100.millis, tick="msg")

100ms毎にmsgというStringを下流に永遠に流す

Tcp().bind("127.0.0.1", 8888)

こういうSourceも作れる

TCP connectionを待ちByteStringをストリームとして処理する

Source(Props[MyActor])

Actorはメッセージ受けて下流になんらかのデータを流していく

copyright Fringe81 Co.,Ltd.

val src: Source[String, Cancellable] = Source(initialDelay=0.second, interval=100.millis, tick="msg")

val sink: Sink[String, Future[Int]] = Sink.fold[Int, String](0){ case (sum, _) => sum + 1 }

src sink

100ms毎に"msg"を送出 msgを受信する度に件数カウント※foldは上流のデータが完了して集計終了となる

Cancellable Future[Int]

ストリームの実行者に渡される値Materialized Value

copyright Fringe81 Co.,Ltd.

val rg1: RunnableGraph[Cancellable] = src.to(sink)

val rg2: RunnableGraph[Future[Int]] = src.toMat(sink)(Keep.right)

val rg3: RunnableGraph[(Cancellable, Future[Int])] = src.toMat(sink)(Keep.both)

val (cancellable, futureInt) = rg3.run()

※src.toMat(sink)(Keep.left)と同義

copyright Fringe81 Co.,Ltd.

val src: Source[String, Cancellable] = Source(initialDelay = 0.second, interval = 100.millis, tick = "msg")val sink: Sink[String, Future[Int]] = Sink.fold[Int, String](0){ case (sum, _) => sum + 1 }

val rg: RunnableGraph[(Cancellable, Future[Int])] = src.toMat(sink)(Keep.both)

val (cancellable, futureInt) = rg.run()

futureInt.foreach(println)

Thread.sleep(1000 * 5)cancellable.cancel()

copyright Fringe81 Co.,Ltd.

・Publisher(Reactive Stream)から

Source#apply

copyright Fringe81 Co.,Ltd.

実行時の挙動確認

通常のScala Collectionとの違い

copyright Fringe81 Co.,Ltd.

(1 to 3) .map{ i => println(s"A: $i"); i } .map{ i => println(s"B: $i"); i } .foreach(i => println(s"C $i"))

A: 1A: 2A: 3B: 1B: 2B: 3C: 1C: 2C: 3

Scala Collection

copyright Fringe81 Co.,Ltd.

Source(1 to 3) .map{ i => println(s"A: $i"); i } .map{ i => println(s"B: $i"); i } .runForeach(i => println(s"C: $i"))

A: 1A: 2B: 1A: 3B: 2C: 1B: 3C: 2C: 3

Akka Stream

copyright Fringe81 Co.,Ltd.

source map:A map:B sink:C

1

123

2

3

1

2

3

1

2

3

各ステージの処理は並行に実行される

copyright Fringe81 Co.,Ltd.

source map:A map:B sink:C

1

123

2

3

1

2

3

1

2

3

ステージ内では一件ずつ逐次処理

copyright Fringe81 Co.,Ltd.

Backpressureの挙動確認スレッドとバッファの関係

copyright Fringe81 Co.,Ltd.

Backpressure?(背圧制御)

上流と下流のデータ流量制御の仕組みバッファ溢れを防ぐ

copyright Fringe81 Co.,Ltd.

PushModel 上流が下流にデータを流し続ける 下流側で処理追いつかずバッファ溢れの可能性

Pull Model 下流から上流にリクエストするとデータが流れる 溢れないが下流側の待ちが大きくなる

dynamic Push/Pull Model 下流から上流にn件リクエストする 上流は下流に要求分流す  initial-buffer-size(4), max-buffer-size(16)

copyright Fringe81 Co.,Ltd.

implicit val system = ActorSystem()

implicit val materializer = ActorMaterializer()

copyright Fringe81 Co.,Ltd.

// スレッドプールの定義

implicit val system = ActorSystem()

// バッファの定義

implicit val materializer = ActorMaterializer()

akka.actor.default-dispatcher.fork-join-executor.parallelism-max = 1

akka.stream.materializer { initial-input-buffer-size = 1 max-input-buffer-size = 1}

copyright Fringe81 Co.,Ltd.

このうちmapCはかなり重い処理とする

source mapA mapB sinkmapC

heavy!

copyright Fringe81 Co.,Ltd.

source mapA mapB mapC

11234567...

2

3

1

1

2

sink

1

2

2

スレッド = 1バッファ = 1

3

3

4

3

※ は各ステージ上での処理実行を表す

copyright Fringe81 Co.,Ltd.

source mapA mapB mapC

11234567...

2

3

1

sink

1

2

2

スレッド = 1バッファ = 2

3

2

4

1

2

4

5

6

copyright Fringe81 Co.,Ltd.

source mapA mapB mapC

11234567...

2

3

1

sink

1

スレッド = 2バッファ = 2

2

4

1

2

4

5

6

3

スレッドは2本あるので1の処理中も上流は処理が行われる

copyright Fringe81 Co.,Ltd.

source mapA mapB mapC

11234567...

2

3

1

sink

1

スレッド = 2バッファ = 2

2

4

1

2

4

5

6

3

スレッドは1本余っているがバッファ = 2に達しており

バックプレッシャーが効いて上流は処理が行われない

copyright Fringe81 Co.,Ltd.

source mapA mapB mapC

11234567...

2

3

1

sink

1

スレッド = 2バッファ = 2

2

4

1

2

4

5

6

3

mapCでは1の処理が終わって2の処理が始まった。

これにより上流のバッファに1つ空きが出来たので上流では処理が1つ進む

copyright Fringe81 Co.,Ltd.

source mapA mapB mapC

11234567...

2

3

1

sink

1

スレッド = 2バッファ = 2

2

4

1

2

4

5

6

3

スレッドは1本余っているがバッファ = 2に達しており

バックプレッシャーが効いて上流は処理が行われない

mapCへの大量流入を防ぐ

copyright Fringe81 Co.,Ltd.

バックプレッシャーが効いて上流がストップしている状態を回避/改善しようとすると?

案1:上流の処理を進める為の施策案2:重いmapCを改善する施策

copyright Fringe81 Co.,Ltd.

案1-1:bufferステージを置く

上流の処理を進める施策

copyright Fringe81 Co.,Ltd.

重い処理の前にbufferステージを設けることで上流の処理を進めることが出来る。

ストリーム全体では各ステージのバッファは2としていても

bufferステージのバッファは4といったように異なる値を設定することが出来る。

※なおbufferステージ以外でも個別にバッファ数を指定可能

val buffer = Flow[Int].buffer(4, OverflowStrategy.backpressure)

... mapB.via(buffer).via(mapC) ...

copyright Fringe81 Co.,Ltd.

Flow[Int].buffer(4, OverflowStrategy.dropNew)

ただし設定したバッファ値に達した場合はBPが効く

bufferの前後で極端な処理速度の差がある場合はあまり効果ない

捨てる指示をすれば上流の処理は続行

copyright Fringe81 Co.,Ltd.

案1-2:conflateステージを置く

上流の処理を進める施策

copyright Fringe81 Co.,Ltd.

def conflate[S](seed: Out => S)(aggregate: (S, Out) => S)

... .conflate(List(_)){ (elems, elem) => elem :: elems }...

← 要素を捨てて良いなら... .conflate(identity){ (e, _) => e }...

BPが効いている間、aggregate関数が実行される

※下流へはList[T]

※下流へはT

まとめあげ効果で下流へのデータ数が減る下流が要素数に応じて遅くなるなら効果はない

copyright Fringe81 Co.,Ltd.

案2-1:mapAsyncステージに変える

重いmapCを改善する施策

copyright Fringe81 Co.,Ltd.

val mapC = Flow[Int].mapAsync(4) { n => Future { 重い処理 } }

処理の終了を待たずに次の処理を開始する

※入力と出力の順序は保証される

mapC

12

34

copyright Fringe81 Co.,Ltd.

案2-2:Fan-Outな部品を用い

parallelに処理する

重いmapCを改善する施策

copyright Fringe81 Co.,Ltd.

Balanceは入力1、出力NなFan-Outな部品均等に下流に流す

Mergeは入力N、出力1なFan-Inな部品同期はしない来たものから下流に流す

※順序は保証されなくなる

balance merge元のmapC元のmapC

copyright Fringe81 Co.,Ltd.

新しいmapC

新しいFlowとしてmapCを定義出来る

balance merge元のmapC元のmapC

balance merge元のmapC元のmapC

新しいmapC

copyright Fringe81 Co.,Ltd.

val mapC = Flow() { implicit builder => import FlowGraph.Implicits._

val balance = builder.add(Balance[Int](2)) val merge = builder.add(Merge[Int](2))

val map = Flow[Int].map(重い処理)

balance ~> map ~> merge balance ~> map ~> merge

(balance.in, merge.out) }

Flowは入力と出力のポートを1つずつ持つ

要素追加

データフロー定義

copyright Fringe81 Co.,Ltd.

val runnableGraph = FlowGraph.closed() { implicit builder => import FlowGraph.Implicits._

val balance = builder.add(Balance[Int](2)) val merge = builder.add(Merge[Int](2))

val src = Source(1 to 10) val mapFlow = Flow[Int].map(_ * 2) val sink = Sink.foreach[Int](println)

src ~> balance ~> map ~> merge ~> sink balance ~> map ~> merge }

RunnableGraphを作ることも出来る

copyright Fringe81 Co.,Ltd.

val runnableGraph = FlowGraph.closed() { implicit builder => import FlowGraph.Implicits._

val balance = builder.add(Balance[Int](2)) val merge = builder.add(Merge[Int](2))

val src = Source(1 to 10) val mapFlow = Flow[Int].map(_ * 2) val sink = Sink.foreach[Int](println)

src ~> balance ~> map ~> merge ~> sink balance ~> map ~> merge }

RunnableGraphを作ることも出来る

実際はコードフォーマッタに潰されるので

こう書いています

src ~> balancebalance ~> map ~> mergebalance ~> map ~> mergemerge ~> sink

copyright Fringe81 Co.,Ltd.

FlowGraphとMat値

val sink: Sink[Int, Future[Int]] = Sink.fold(0){_ + _}

val rg: RunnableGraph[Future[List[Int]]] = FlowGraph.closed(sink, sink) ((f1,f2) => Future.sequence(f1 :: f2 :: Nil)) { implicit builder => (sink1, sink2) => import FlowGraph.Implicits._

val balance = builder.add(Balance[Int](2))

Source(1 to 10) ~> balance ~> sink1 balance ~> sink2 }

val ret: Future[List[Int]] = rg.run()

copyright Fringe81 Co.,Ltd.

その他のFan-Out, Fan-Inな部品

copyright Fringe81 Co.,Ltd.

<Fan-Out>

Balance 入力を均等に出力に振り分ける

Broadcast 入力を全出力に等しく流す

Unzip (A,B)の入力をAの出力とBの出力に流す

UnZipWith 任意の型の入力をタプルにして出力

FlexiRoute Fan-Out型の部品を作るためのベース

copyright Fringe81 Co.,Ltd.

<Fan-In>

Merge 複数入力を1本に同期することなしに来たものから出力する

Zip 2つの入力AとBを(A,B)にして出力する同期する

ZipWith 2つの入力AとBを(A,B)にして出力する同期する(A,B)を任意の型に加工して出力する

Concat 1つ目の入力を流し終えたら2つ目の入力を流す

FlexiMerge Fan-In型の部品を作るためのベース

copyright Fringe81 Co.,Ltd.

Error Handling

copyright Fringe81 Co.,Ltd.

・null要素は流せない・例外が起きるとストリームは失敗として終了

Stop ストリーム失敗終了(default)

Resume 該当の要素を捨てて次の処理を再開

Restart ・該当の要素を捨てる・そのステージを再作成する・処理を再開 ※fold等状態を持つものは状態がクリアされてしまうので注意

Supervision Strategies

copyright Fringe81 Co.,Ltd.

Test

libraryDependencies += Seq( …, "com.typesafe.akka" % "akka-testkit_2.11" % "2.3.14" % "test", "com.typesafe.akka" % "akka-stream-testkit-experimental_2.11" % "1.0" % "test")

copyright Fringe81 Co.,Ltd.

本番用Source

本番用Sink

本番用Flow

本番用Flow

テスト用Source

テスト用Sink

SourceやSinkは外部環境との接続点になりがちでテストしづらい事が多い。テスト時はテスト用のデータを流すSourceとつなげたり、akka-stream-testkitに用意されているTestSinkとつなげて期待通りの結果が流れてくるかを確認したりする。

copyright Fringe81 Co.,Ltd.

val probe = source.runWith(TestSink.probe[Result])

probe .request(2) .expectNext(Result(1),Result(2)) .request(100) .expectNext(Result(3)) .expectComplete()

requestで下流から上流へデータを要求できるexpectNextで流れてくるデータの確認最後にデータが全て流れ終わったかどうかの確認

copyright Fringe81 Co.,Ltd.

ありがとうございました