Akka stream

68
copyright Fringe81 Co.,Ltd. Akka Stream @mtoyoshi

Transcript of Akka stream

Page 1: Akka stream

copyright Fringe81 Co.,Ltd.

Akka Stream

@mtoyoshi

Page 2: Akka stream

copyright Fringe81 Co.,Ltd.

AmazonKinesis

1行目

2行目

3行目

4行目

・・・

処理

・・・ 処理

Page 3: Akka stream

copyright Fringe81 Co.,Ltd.

AmazonKinesis

1行目

2行目

3行目

4行目

・・・

・・・

Akka Actorで処理

Page 4: Akka stream

copyright Fringe81 Co.,Ltd.

Akka Actor便利ですが

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

・OutOfMemoryに遭遇

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

Page 5: Akka stream

copyright Fringe81 Co.,Ltd.

Akka Stream?

Typesafeより2015.07に1.0リリース

Akka Actor

Akka Stream

Akka HTTP

Page 6: Akka stream

copyright Fringe81 Co.,Ltd.

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

Page 7: Akka stream

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

Page 8: Akka stream

copyright Fringe81 Co.,Ltd.

特徴

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

Page 9: Akka stream

copyright Fringe81 Co.,Ltd.

特徴

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

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

Page 10: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

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

Page 11: Akka stream

copyright Fringe81 Co.,Ltd.

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

Page 12: Akka stream

copyright Fringe81 Co.,Ltd.

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

Page 13: Akka stream

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の構築と実行

Page 14: Akka stream

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()

Page 15: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

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

Page 16: Akka stream

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

Page 17: Akka stream

copyright Fringe81 Co.,Ltd.

利用可能な処理:

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

Page 18: Akka stream

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それぞれがストリームを流れるように出来る。

Page 19: Akka stream

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)

Page 20: Akka stream

copyright Fringe81 Co.,Ltd.

Source#apply

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

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

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

Page 21: Akka stream

copyright Fringe81 Co.,Ltd.

ちょっとハマった

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

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

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

えっ?

Page 22: Akka stream

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

Page 23: Akka stream

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はメッセージ受けて下流になんらかのデータを流していく

Page 24: Akka stream

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

Page 25: Akka stream

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)と同義

Page 26: Akka stream

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()

Page 27: Akka stream

copyright Fringe81 Co.,Ltd.

・Publisher(Reactive Stream)から

Source#apply

Page 28: Akka stream

copyright Fringe81 Co.,Ltd.

実行時の挙動確認

通常のScala Collectionとの違い

Page 29: Akka stream

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

Page 30: Akka stream

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

Page 31: Akka stream

copyright Fringe81 Co.,Ltd.

source map:A map:B sink:C

1

123

2

3

1

2

3

1

2

3

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

Page 32: Akka stream

copyright Fringe81 Co.,Ltd.

source map:A map:B sink:C

1

123

2

3

1

2

3

1

2

3

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

Page 33: Akka stream

copyright Fringe81 Co.,Ltd.

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

Page 34: Akka stream

copyright Fringe81 Co.,Ltd.

Backpressure?(背圧制御)

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

Page 35: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

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

Page 36: Akka stream

copyright Fringe81 Co.,Ltd.

implicit val system = ActorSystem()

implicit val materializer = ActorMaterializer()

Page 37: Akka stream

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}

Page 38: Akka stream

copyright Fringe81 Co.,Ltd.

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

source mapA mapB sinkmapC

heavy!

Page 39: Akka stream

copyright Fringe81 Co.,Ltd.

source mapA mapB mapC

11234567...

2

3

1

1

2

sink

1

2

2

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

3

3

4

3

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

Page 40: Akka stream

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

Page 41: Akka stream

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の処理中も上流は処理が行われる

Page 42: Akka stream

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に達しており

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

Page 43: Akka stream

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つ進む

Page 44: Akka stream

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への大量流入を防ぐ

Page 45: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

Page 46: Akka stream

copyright Fringe81 Co.,Ltd.

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

上流の処理を進める施策

Page 47: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

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

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

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

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

Page 48: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

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

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

Page 49: Akka stream

copyright Fringe81 Co.,Ltd.

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

上流の処理を進める施策

Page 50: Akka stream

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

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

Page 51: Akka stream

copyright Fringe81 Co.,Ltd.

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

重いmapCを改善する施策

Page 52: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

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

mapC

12

34

Page 53: Akka stream

copyright Fringe81 Co.,Ltd.

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

parallelに処理する

重いmapCを改善する施策

Page 54: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

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

balance merge元のmapC元のmapC

Page 55: Akka stream

copyright Fringe81 Co.,Ltd.

新しいmapC

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

balance merge元のmapC元のmapC

balance merge元のmapC元のmapC

新しいmapC

Page 56: Akka stream

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つずつ持つ

要素追加

データフロー定義

Page 57: Akka stream

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を作ることも出来る

Page 58: Akka stream

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

Page 59: Akka stream

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()

Page 60: Akka stream

copyright Fringe81 Co.,Ltd.

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

Page 61: Akka stream

copyright Fringe81 Co.,Ltd.

<Fan-Out>

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

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

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

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

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

Page 62: Akka stream

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型の部品を作るためのベース

Page 63: Akka stream

copyright Fringe81 Co.,Ltd.

Error Handling

Page 64: Akka stream

copyright Fringe81 Co.,Ltd.

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

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

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

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

Supervision Strategies

Page 65: Akka stream

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")

Page 66: Akka stream

copyright Fringe81 Co.,Ltd.

本番用Source

本番用Sink

本番用Flow

本番用Flow

テスト用Source

テスト用Sink

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

Page 67: Akka stream

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で流れてくるデータの確認最後にデータが全て流れ終わったかどうかの確認

Page 68: Akka stream

copyright Fringe81 Co.,Ltd.

ありがとうございました