├── imgs ├── jet-workflow.png ├── source-sink.png ├── broadcast-zip.png ├── graph-stage-map.png ├── materialization.png ├── merge-balance.png ├── tubi-workflow.png ├── basic-composition.png ├── jet-workflow-gui.png ├── graph-stage-concept.png ├── graph-stage-filter.png └── graph-stage-duplicator.png ├── README.md ├── .gitignore ├── manuscript ├── ch09.md ├── ch10.md ├── ch08.md ├── ch01.md ├── ch05.md ├── ch07.md ├── ch04.md ├── ch06.md ├── ch02.md └── ch03.md └── LICENSE /imgs/jet-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/jet-workflow.png -------------------------------------------------------------------------------- /imgs/source-sink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/source-sink.png -------------------------------------------------------------------------------- /imgs/broadcast-zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/broadcast-zip.png -------------------------------------------------------------------------------- /imgs/graph-stage-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/graph-stage-map.png -------------------------------------------------------------------------------- /imgs/materialization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/materialization.png -------------------------------------------------------------------------------- /imgs/merge-balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/merge-balance.png -------------------------------------------------------------------------------- /imgs/tubi-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/tubi-workflow.png -------------------------------------------------------------------------------- /imgs/basic-composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/basic-composition.png -------------------------------------------------------------------------------- /imgs/jet-workflow-gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/jet-workflow-gui.png -------------------------------------------------------------------------------- /imgs/graph-stage-concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/graph-stage-concept.png -------------------------------------------------------------------------------- /imgs/graph-stage-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/graph-stage-filter.png -------------------------------------------------------------------------------- /imgs/graph-stage-duplicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YingLiu4203/Akka-Streams-Missing-Manual/HEAD/imgs/graph-stage-duplicator.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Akka Streams: A Missing Manual 2 | 3 | ## 目的 4 | 5 | Akka Streams 需要一个方便学习的书。就大言不惭称之为丢失的文档之一。随着使用了解的增加,会逐步充实内容,欢迎提 PR 一起改进开发生态。 6 | 7 | ## 目录 8 | 9 | - [Chapter 1: Why and What](manuscript/ch01.md) 10 | - [Chapter 2: Basic Concepts](manuscript/ch02.md) 11 | - [Chapter 3: Operator Composition](manuscript/ch03.md) 12 | - [Chapter 4: Backpressure](manuscript/ch04.md) 13 | - [Chapter 5: Graph DSL](manuscript/ch05.md) 14 | - [Chapter 6: Custom Shape](manuscript/ch06.md) 15 | - [Chapter 7: GraphStage](manuscript/ch07.md) 16 | - [Chapter 8: Operators](manuscript/ch08.md) 17 | - [Chapter 9: Future Interop](manuscript/ch09.md) 18 | - [Chapter 10: Dynamic Streams](manuscript/ch10.md) 19 | 20 | ## 其它概念 21 | 22 | 下面这些概念或实践,对于开发者来说也需要有自己的深刻理解。 23 | 24 | - Error Handling 25 | - Log 26 | - Test 27 | - Flow Control Patterns 28 | - Integration Services 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # WebStorm/IntelliJ 2 | target/ 3 | .idea/ 4 | .idea_modules/ 5 | .classpath/ 6 | .project/ 7 | .settings/ 8 | RUNNING_PID 9 | *.ipr 10 | *.iml 11 | 12 | # Logs 13 | logs/ 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Optional npm cache directory 19 | .npm 20 | 21 | # Dependency directories 22 | node_modules/ 23 | jspm_packages/ 24 | bower_components/ 25 | 26 | # Yarn Integrity file 27 | .yarn-integrity 28 | 29 | # Optional eslint cache 30 | .eslintcache 31 | 32 | # dotenv environment variables file(s) 33 | .env 34 | .env.* 35 | 36 | #Build generated 37 | dist/ 38 | build/ 39 | 40 | ### VisualStudioCode ### 41 | .vscode/* 42 | !.vscode/settings.json 43 | !.vscode/tasks.json 44 | !.vscode/launch.json 45 | !.vscode/extensions.json 46 | 47 | # The Scala compiler used by VS Code 48 | .metals/ 49 | .bloop/ 50 | 51 | ### Vim ### 52 | *.sw[a-p] 53 | 54 | 55 | ### System Files ### 56 | *.DS_Store 57 | 58 | # Windows thumbnail cache files 59 | Thumbs.db 60 | ehthumbs.db 61 | ehthumbs_vista.db 62 | 63 | # Folder config file 64 | Desktop.ini 65 | 66 | # Recycle Bin used on file shares 67 | $RECYCLE.BIN/ 68 | 69 | # Thumbnails 70 | ._* 71 | 72 | # Files that might appear in the root of a volume 73 | .DocumentRevisions-V100 74 | .fseventsd 75 | .Spotlight-V100 76 | .TemporaryItems 77 | .Trashes 78 | .VolumeIcon.icns 79 | .com.apple.timemachine.donotpresent 80 | -------------------------------------------------------------------------------- /manuscript/ch09.md: -------------------------------------------------------------------------------- 1 | # Future Interop 2 | 3 | `Future` 是 Scala 自带的异步并发机制。如果数据处理非常费时或是远程的,那么通过 `Future` 来运行这些操作是非常常见的做法。Akka Streams 提供了方便的 API 来执行这种异步操作。由于 Akka Streams 是基于 Akka Actor 的,另外一种常用的异步操作是和 Actor 的互操作,其机制与 `Future` 的互操作原理相近,不做单独介绍。 4 | 5 | ## 1 单独的线程池 6 | 7 | 对于异步操作,最好根据应用的特点使用单独定制的线程池。[Daniel Spiewak 的文章](https://gist.github.com/djspiewak/46b543800958cf61af6efa8e072bfd5c) 给出了很好的建议。 8 | 9 | 本文例子假设异步服务是阻塞式调用,首先配置一个适合阻塞服务调用的线程池。一个应用的所有阻塞式服务可以共享这个线程池。在 `src/main/resources/appliation.conf` 文件加入如下内容。 10 | 11 | ```text 12 | blocking-dispatcher { 13 | executor = "thread-pool-executor" 14 | thread-pool-executor { 15 | core-pool-size-min = 2 16 | core-pool-size-max = 10 17 | } 18 | } 19 | ``` 20 | 21 | 可以提供隐含参数 `implicit val dispatcher = system.dispatchers.lookup("blocking-dispatcher")` 来使用这个线程池,避免使用缺省的 Akka Actor 线程池。 22 | 23 | ## 2 `mapAsync` 24 | 25 | Akka streams 为 `Source` 和 `Flow` 提供了 `mapAsync` 扩展方法用于执行异步操作。其第一个参数是并发的线程数,第二个参数为返回 `Future[T]` 的异步函数。当异步函数执行完成时,其返回值(类型为 `T`,而不是 `Future[T]`)会作为流的数据单元发送到下游。下面是一个完整的例子。 26 | 27 | ```scala 28 | import scala.concurrent.Future 29 | import akka.actor.ActorSystem 30 | import akka.stream.scaladsl.{Sink, Source} 31 | 32 | object FutureDemo { 33 | def main(args: Array[String]) { 34 | implicit val system = ActorSystem("testStreams") 35 | implicit val dispatcher = system.dispatchers.lookup("blocking-dispatcher") 36 | 37 | object Processor { 38 | def process(number: Int) = Future { 39 | 40 | println(s"Process $number") 41 | Thread.sleep(500) 42 | number * 10 43 | } 44 | } 45 | 46 | val source = Source(1 to 7) 47 | val sink = Sink.foreach[Int](value => println(s"Sink: $value")) 48 | val result = source.mapAsync(3)(Processor.process).runWith(sink) 49 | } 50 | } 51 | 52 | /* 输出结果 53 | Process 3 54 | Process 1 55 | Process 2 56 | Sink: 10 57 | Sink: 20 58 | Process 4 59 | Sink: 30 60 | Process 5 61 | Process 6 62 | Sink: 40 63 | Sink: 50 64 | Sink: 60 65 | Process 7 66 | Sink: 70 67 | */ 68 | ``` 69 | 70 | 从输出结果可以看到,并发的异步执行阶段处理的次序可能不同,但是传到下游的数据单元和上游到达的次序一致。如果不需要保持原来的次序,通常可以加快数据处理速度。此时只需要把 `mapAsync` 换成 `mapAsyncUnordered` 即可。把上面代码改为使用 `source.mapAsyncUnordered(3)(Processor.process).runWith(sink)` 后,一种可能的输出结果如下: 71 | 72 | ```text 73 | Process 1 74 | Process 3 75 | Process 2 76 | Sink: 10 77 | Process 4 78 | Sink: 30 79 | Process 5 80 | Sink: 20 81 | Process 6 82 | Sink: 50 83 | Sink: 40 84 | Process 7 85 | Sink: 60 86 | Sink: 70 87 | ``` 88 | 89 | ## 3 和 `map` 及 `async` 的区别 90 | 91 | `Source` 和 `Flow` 的 `map` 操作通常是 Fusion 聚合后同步执行的。可是通过属性可以指定异步执行的线程池。比如这个例子:`Source(1 to 7).map(_ + 1).withAttributes(ActorAttributes.dispatcher("blocking-dispatcher")).runWith(Sink.ignore)` 中的操作是在指定的线程池中异步执行的。 92 | 93 | 这种异步和 `mapAsync` 的区别在于参数和并发度。 `mapAsync` 支持多个并发执行和返回 `Future[T]` 的异步函数。而 `map` 每次只处理一个数据单元并且对传入的函数没有太多限制。 94 | 95 | `async` 和上面的 `map` 一样,在缺省的 Akka Streams 全局线程池异步执行,每次只处理一个数据单元,不支持并发。`async` 可以作为任何一个 `Graph` 的操作创建一个有缓存和回压的异步执行的新边界。 96 | -------------------------------------------------------------------------------- /manuscript/ch10.md: -------------------------------------------------------------------------------- 1 | # Dynamic Streams 2 | 3 | Akka Streams 的实体化值可以是任何数据类型,理解了这一点就比较容易理解动态的流处理:就是用实体化值对流进行一些有意思的操作比如动态的创建 Source 或 Sink。此处的动态是指创建的流在实体化运行后,可以接入多个不同的输入或输出。 4 | 5 | ## 1 `MergeHub` 6 | 7 | `MergeHub` 是一个动态的多入口构件。其作用相当于一个多入口的 `Sink`,多个数据源可以把数据发送给 `MergeHub` 并共享相同的下游数据处理逻辑。下面是个基于 [MergeHub 官方文档](https://doc.akka.io/docs/akka/current/stream/stream-dynamic.html#using-the-mergehub) 的例子: 8 | 9 | ```scala 10 | import akka.actor.ActorSystem 11 | import akka.NotUsed 12 | import akka.stream.scaladsl.{MergeHub, RunnableGraph, Source, Sink} 13 | 14 | object DynamicTest { 15 | def main(args: Array[String]) { 16 | implicit val system = ActorSystem("testStreams") 17 | implicit val ec = scala.concurrent.ExecutionContext.global 18 | 19 | val consumer = Sink.foreach(println) 20 | 21 | val runnableGraph: RunnableGraph[Sink[String, NotUsed]] = 22 | MergeHub.source[String].to(consumer) 23 | 24 | val toConsumer: Sink[String, NotUsed] = runnableGraph.run() 25 | 26 | Source.single("Hello!").runWith(toConsumer) 27 | Source.single("Hub!").runWith(toConsumer) 28 | } 29 | } 30 | ``` 31 | 32 | 从类型签名可以看出,把 `MergeHub` 当成 Source 创建 RunnableGraph 并运行后,其实体化值的类型是一个 `Sink` 对象。此时可以把这个实体化值当成一个 Sink 来创建新的 RunnableGraph,所有这些新的 RunnableGraph 会根据保持到达次序传给原来最初的下游数据处理组件。 33 | 34 | ## 2 `BroadcastHub` 35 | 36 | `BroadcastHub` 是一个动态的多出口构件。其作用相当于一个多出口的 `Source`,可以把一个数据源发送给多个下游处理组件。例子如下: 37 | 38 | ```scala 39 | import akka.actor.ActorSystem 40 | import akka.NotUsed 41 | import akka.stream.scaladsl.{BroadcastHub, Keep, RunnableGraph, Source} 42 | 43 | object DynamicTest { 44 | def main(args: Array[String]) { 45 | implicit val system = ActorSystem("testStreams") 46 | implicit val ec = scala.concurrent.ExecutionContext.global 47 | 48 | import scala.concurrent.duration._ 49 | val producer = Source.tick(1.second, 1.second, "New message") 50 | 51 | val runnableGraph: RunnableGraph[Source[String, NotUsed]] = 52 | producer.toMat(BroadcastHub.sink)(Keep.right) 53 | 54 | val fromProducer: Source[String, NotUsed] = runnableGraph.run() 55 | 56 | fromProducer.runForeach(msg => println("consumer1: " + msg)) 57 | fromProducer.runForeach(msg => println("consumer2: " + msg)) 58 | } 59 | } 60 | ``` 61 | 62 | ## 发布-注册服务 63 | 64 | 当把 `MergeHub` 和 `BroadcastHub` 组合起来是会形成一个多入口多出口的发布-注册服务组件。其作用类似一个广播消息总线。 65 | 66 | ```scala 67 | mport akka.actor.ActorSystem 68 | import akka.NotUsed 69 | import akka.stream.scaladsl.{BroadcastHub, Keep, MergeHub, RunnableGraph, Sink, Source} 70 | 71 | object DynamicTest { 72 | def main(args: Array[String]) { 73 | implicit val system = ActorSystem("testStreams") 74 | implicit val ec = scala.concurrent.ExecutionContext.global 75 | 76 | val (sinkHub, sourceHub) = 77 | MergeHub 78 | .source[String] 79 | .toMat(BroadcastHub.sink)(Keep.both) 80 | .run() 81 | 82 | // 多个数据发布者 83 | val source1 = Source.single("Source1") 84 | val source2 = Source.single("Source2") 85 | source1.runWith(sinkHub) 86 | source2.runWith(sinkHub) 87 | 88 | // 多个数据注册者 89 | val sink1 = Sink.foreach[String](data => println(s"Sink1: $data")) 90 | val sink2 = Sink.foreach[String](data => println(s"Sink2: $data")) 91 | sourceHub.runWith(sink1) 92 | sourceHub.runWith(sink2) 93 | } 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /manuscript/ch08.md: -------------------------------------------------------------------------------- 1 | # Operators 2 | 3 | Flow DSL 使用各种 Operators 以及 `viaMat`, `via`, `to`, `toMat` 这些连接构件来灵活的连接各种组件。其底层的功能则是通过 Graph DSL 以及 `GraphStage` (图步)来完成。`GraphStage` 是多数 Operator 的具体实现。本文通过给出一个 Operator 的实现例子,然后总结 GraphStage 的创建过程。最后给出 Operators 的大致分类。 4 | 5 | 所有的例子和图形都基于或来自 [Custom stream processing 官方文档](https://doc.akka.io/docs/akka/current/stream/stream-customize.html),下面不另做说明。 6 | 7 | ## 1 Operator 的实现 8 | 9 | 利用 Scala 的 `implicit class`,可以很方便的把一些 GraphStage 转换为其它 Graph 的扩展方法。比如上一章的的 Duplicator 可以简单地转换为 `Soruce` 或 `Flow` 的一个扩展方法。 10 | 11 | ```scala 12 | 13 | implicit class SourceDuplicator[Out, Mat](s: Source[Out, Mat]) { 14 | def duplicateElements: Source[Out, Mat] = s.via(new Duplicator) 15 | } 16 | 17 | implicit class FlowDuplicator[In, Out, Mat](s: Flow[In, Out, Mat]) { 18 | def duplicateElements: Flow[In, Out, Mat] = s.via(new Duplicator) 19 | } 20 | 21 | Source(1 to 3).duplicateElements.runForeach(println) 22 | 23 | val flow = Flow[Int].duplicateElements 24 | Source(1 to 3).via(flow).runForeach(println) 25 | 26 | ``` 27 | 28 | 很多的 Soruce, Flow 以及 Sink 的方法(Operator)都是采用类似的实现机制:先定制一个 GraphStage,再通过 implicit class 把 Source/Flow/Sink 转换为使用定制 GraphStage 的一个隐含类。 29 | 30 | ## 2 定制 `GraphStage` 小结 31 | 32 | ### 2.1 定制的基本步骤 33 | 34 | - 如果不需要实体化值,继承 `GraphStage` 并定义重载的 `createLogic` 方法。需要实体化值的需要继承 `GraphStageWithMaterializedValue` 并定义重载的 `createLogicAndMaterializedValue` 方法。 35 | - 定义一个包含用到的入口或出口的 Shape 36 | - 定制入口和出口的处理逻辑 37 | - 可以通过定义 `implict class` 将定制的处理功能变成 Graph 的扩展方法。 38 | - 如果有实体化值,生成 `Success` 或 `Failure` 的相应值。 39 | 40 | ### 2.2 定制入口和出口的处理逻辑 41 | 42 | 有二种 API 来定制出口和入口的逻辑。一种是使用 `InHandler` 和 `OutHandler` 的处理逻辑,另一种是临时一次性取代现有 `InHandler` 和 `OutHandler` 逻辑。 43 | 44 | - 使用 `InHandler` 和 `OutHandler` 的处理逻辑 45 | - 定制入口 `InHandler` 处理逻辑 46 | - 在 `onPush()` 时入口收到上游数据 47 | - `onUpstreamFinish()` 是上游正常结束的回调。`onUpstreamFailure()` 是上游出错的回调。按需要加入处理逻辑。 48 | - 定制出口 `OutHandler` 处理逻辑 49 | - 在 `onPull()` 时接到下游数据请求。 50 | - `onDownstreamFinish()` 是下游结束时的回调。`onDownstreamFinish(cause: Throwable)` 是下游出错时的回调。 51 | - 使用 `emit` 和 `read` 52 | - `read(in)(andThen)` 和 `readN(in, n)(andThen)` 暂时替换当前的 `InHandler`,读取一个或多个数据单元。完成后在下一次上游 `onPush` 的时候恢复替换前的 `InHandler`。`andthen` 是恢复了 `InHandler` 后的操作。缺省是不做事。 53 | - `emit(out, elem, andThen)` 和 `emitMultiple(out, Iterable(elem1, elem2), andThen)` 暂时替换当前的 `OutHandler`,读取一个或多个数据单元。完成后在下一次下游 `pull` 的时候恢复替换前的 `OutHandler`。`andthen` 是在恢复 `OutHandler`之后的操作。缺省是不做事。 54 | - `abortEmitting()` 和 `abortReading()` 55 | 56 | ### 2.2 入口(`in: InLet`) 的操作方法 57 | 58 | - 可以用 `val element = grab(in)` 取得到达的数据。一个 `onPush()` 只能调用一次 `grab(in)`, 否则报错。检查可否 `grab` 的状态用 `isAvailable(in)`。 59 | - 请求下一个数据 `pull(in)`。 在收到数据之前只能有一个 `pull` 请求。检查是否已经发送请求 `hasBeenPulled(in)`。 60 | - 关闭入口 `cancel(in)`。是否关闭 `isClosed(in)`。 61 | 62 | ### 2.3 出口(`out: OutLet`) 的操作方法 63 | 64 | - 发送数据 `push(out, element)`。检查是否可发送数据 `isAvailable(out)`。 65 | - 正常关闭出口 `complete(out)`。出口是否关闭 `isClosed(out)`。 66 | - 非正常关闭出口 `fail(out,exception)`。 67 | 68 | ### 2.4 图步的操作 69 | 70 | - `completeStage()`: 正常关闭所有出口,取消所有入口。 71 | - `failStage()`:异常关闭所有出口,取消所有入口。 72 | 73 | ### 2.5 运行环境 74 | 75 | - 每次实体化都生成新的 `GraphStageLogic` 对象。 76 | - 定义在 `GraphStageLogic` 内的代码都是单线程运行。定义的可变状态都是线程安全。 77 | - 资源清理应该定义在 `GraphStageLogic.postStop` 里面。 78 | - 实体化值是相对独立的异步运算结果,可以是和流数据相关或无关的任何数据类型。 79 | 80 | ## 3 基本 Operators 81 | 82 | 基本的 Operators 是指那些操作基本的单入口/单出口形状,包含 `SourceShape`, `SinkShape` 以及 `FlowShape`,的那些定制的 GraphStage 类及其衍生的 Operators。这些 Operators 是 Flow DSL 的常用方法。Akka Streams 的文档把这些基本操作符分为二大类:[simple operators 简单操作符](https://doc.akka.io/docs/akka/current/stream/operators/index.html#simple-operators)和 [detached operators 分离操作符](https://doc.akka.io/docs/akka/current/stream/operators/index.html#backpressure-aware-operators)。分离操作符又称为 backpressure aware operator 背压察觉操作符。因为二种操作符都基于背压,而分离操作符在整个文档只出现过一次,也没有给出定义,所以令人困惑。其实这二类的差别在于入口和出口的数据请求/发送方式。 83 | 84 | 背压的工作方式如下图所示: 85 | 86 | ![graph stage concept](../imgs/graph-stage-concept.png) 87 | 88 | 在实体化开始时,从 Sink 开始往上,下游首先调用 `GraphStageLogic` 的 `pull(in)`, 触发上游的的 `OutHandler.onPull()` 方法,此时该上游方法应该调用 `pull(in)` 来触发更上游的 `OutHandler` 的 `onPull()`,连锁触发最后一直到 Source。此时 Source 的 `onPull()` 应该把产生的数据通过 `GraphStageLogic` 的 `push(out, elem)` 发送出去。这个上游的发送会触发下游的 `InHandler` 的 `onPush()` 方法,每个下游的 `onPush()` 方法按照背压规则,应该调用 `push(out, elem)` 发送数据给下游,触发更下游的 `onPush()`,连锁触发一直到 Sink 的 `onPush()`,此时完成来一个完整的数据请求发送过程。下图中的 `Map` GraphStage 就是一个不改变流速率的例子,完全遵从回压规则。 89 | 90 | ![graph stage map](../imgs/graph-stage-map.png) 91 | 92 | 可是有时候 GraphStage 对上游出入口进行处理会改变流速率,比如不发给下游或发多个给下游。如果其操作没有连接下游的出入口,则这些 Operators 称为 `Simple Operator`。比如下图的 Filter Operator。 93 | 94 | ![graph stage filter](../imgs/graph-stage-filter.png) 95 | 96 | 常用的 Simple Operators 有 `collect`,`drop`,`filter`,`fold`,`grouped`,`lazyFlow`,`limit`,`take`,`takeWhile`,`map`,`recover`,`reduce`,`scan`,`throttle` 等。 97 | 98 | 如果 GraphStage 对下游一侧的出入口处理改变流速率,比如不发给下游或发多个给下游。如果其操作连接了连接了下游的出入口,类似于短路状态,和上游会偶尔分离,则称为 `Detached Operator` 分离操作符。比如下图的 Duplicator Operator。 99 | 100 | ![graph stage duplicator](../imgs/graph-stage-duplicator.png) 101 | 102 | Detached Operators 的一个特殊之处是有时候需要在实体化时,不等 Sink 触发就自己发送 `pull(in)` 请求。比如 Buffer 就会预先缓存一些数据。常用的 Detached Operators 有 `batch`,`buffer`,`conflate`,`expand`,`extraplote` 等。 103 | 104 | ## 其它 Operators 105 | 106 | [Akka Streams Operators 文档](https://doc.akka.io/docs/akka/current/stream/operators/index.html#operators) 在上面基本 Operators 的基础上给出了更多的分类。大致柯分为 107 | 108 | - Source:产生 Source 的操作符,比如 `single`, `tick`, `range` 等。 109 | - Sink: 产生 Sink 的操作符,比如 `fold`, `foreach`, `head` 等。 110 | - Flow 组合: 从 Sink 和 Source 产生新的 Flow,包括 `fromSinkAndSource`, `fromSinkAndSourceCoupled`。 111 | - Async: 异步执行的操作符。包括 `ask`, `mapAsync`,`mapAsyncUnordered`。 112 | - File IO Sinks and Sources:`FileIO` 的方法 如 `fromPath`, `toPath`。 113 | - 时间相关的: `delay`, `dropWithin`, `groupedWithin`,`idleTimeout`, `keepAlive` 等。 114 | - 子流 Substreams:比如 `groupBy`, `flatMapConcat`, `splitWhen` 等。 115 | - 多入口或多出口: `concat`, `zip`, `balance`, `unzip` 等。 116 | - 状态检测: `monitor`, `watchTermination` 117 | - Actor 互操作:`actorRef`, `ask` 等。 118 | - 错误处理:`withBackoff`, `onFailuresWithBackoff` 等。 119 | 120 | 所以的简单操作符和上面这些操作符构成了 Flow DSL 的各种组合操作方法。当处理的拓扑形状不能满足需要,则用 Graph DSL 来定制,如果现有的处理不满足需求,则用 GraphStage 来增加处理功能。 121 | -------------------------------------------------------------------------------- /manuscript/ch01.md: -------------------------------------------------------------------------------- 1 | # Whay and What 2 | 3 | 随着分布式应用的普及,Akka Streams 成为 Akka 生态里面很受欢迎的开源库。作为一个程序员,做任何一个决定之前都会问三个问题:为什么(why)? 是什么 (what)?怎么做 (how)?作为 Akka Streams 系列文章的第一篇,本文尝试从应用层面回答 Akka Streams 的 why 和 how 的问题,并在最后给出一个具体例子,眼见为实。 4 | 5 | ## 为什么需要 Akka Streams 6 | 7 | 所有的技术都是业务驱动的,当现有的技术不能满足业务发展要求时,会出现新一代的技术。Akka Streams 是由于分布式业务系统的普及以及现有技术不能满足分布式软件开发的产物。 8 | 9 | ### 业务系统的需求 10 | 11 | 一项优秀的技术其好处应该显而易见。如果简单的回答为什么需要 Akka Streams,答案也非常简单:Akka Streams 提供的流处理(stream processing)方式是业务应用系统的一个非常合适的抽象模型:既涵盖了业务相关的数据和处理逻辑,又可以在不同的层次抽象组合。既适合描述细粒度的本地处理逻辑,又提供了灵活简单的组合能力来描述端到端的分布式业务流程。 12 | 13 | 多数业务应用系统都很可以很自然的用流程图 [Flowchart](https://en.wikipedia.org/wiki/Flowchart) 或数据流程图 [Data-flow Diagram DFD](https://en.wikipedia.org/wiki/Data-flow_diagram) 来表达, 而 Akka Streams 就是这类流程图的软件实现。软件是现实世界的一个模型,越能描述真实世界的特性的软件模型,就越容易正确的理解和实现。下图来自[C4 软件架构模型](https://c4model.com/) 14 | 15 | ![c4 model](https://c4model.com/img/c4-overview.png) 16 | 17 | 可以看到软件架构的本质是不同层次的模块及其交互。C4 模型只是一种纸上的软件架构,流处理是其具体实现。下面是二个基于不同技术栈的真实流处理例子。 18 | 19 | - [Tubi 的视频广告处理流程片段](https://code.tubitv.com/a-fully-reactive-ad-serving-platform-using-scala-akka-streams-13299e7ea04e):基于 Akka Streams 的流处理 20 | 21 | ![Tubi 的视频广告处理流程片段](../imgs/tubi-workflow.png) 22 | 23 | - [Jet.com 的订单处理流程](https://medium.com/jettech/microservices-to-workflows-expressing-business-flows-using-an-f-dsl-d2e74e6d6d5e):采用 F# 自己开发的流处理 24 | 25 | ![Jet 的订单处理流程片段](../imgs/jet-workflow.png) 26 | 27 | 上述例子是简化了的架构模型,只描述了业务流程的核心处理步骤,其中隐含了很多真实业务系统的处理需求: 28 | 29 | - 业务流程是分布式的,各个业务模块可以运行在不同的系统甚至来自企业外部。 30 | - 各个处理步骤都是实时响应式的,一旦数据到达或相关事件发生,都会立刻触发下一步的处理。 31 | - 为保证资源利用率,各个处理步骤都是异步独立运行。 32 | - 各个步骤之间有流量控制协议来解决上游和下游的处理速度不匹配问题。 33 | - 需要具备业务异常和系统失效处理机制。 34 | - 业务流程可以有任意的处理拓扑结构。 35 | - 支持基于分形 [Fractal](https://en.wikipedia.org/wiki/Fractal) 的组合:每个小模块可以简单灵活的组成大模块。组合后的大模块仍保持相同的特质。换言之,各个处理步骤分拆或组合后仍保持相同的运行与组合特质。分形组合的意义在于,一旦了解了各个小模块的功能以及组合的规则,那么大模块的功能就可以很好的理解而无需额外的信息。如果软件的模块及其组合具有分形的特质,那么软件开发的效率与可维护性会有很大的提升。 36 | 37 | 以上每一条要求实现起来都有很大的工作量而且需要时间来成熟,不是每个开发团队都有相应的技能和时间来打造一个满足上述所有要求的新轮子。当使用的软件开发工具功能和真实业务特质需求高度吻合上,软件系统的设计、实现、测试和维护都会变得容易而且高效。流处理模型的最大特点就在于把上面这些特质需求包含在模型里面。相对而言,当前流行的微服务是一种服务拆分的理念和粗旷的点对点交互模型,缺乏对端到端的多段处理流程支持和不同层次的抽象手段。而反应式流处理模型就是为了提供高性能的分布式端到端流程处理能力,并且追求不同层次的抽象组合能力,这二个特点非常适合作为企业应用架构模型。[Jet.com](https://jet.com/) 是沃尔玛并购的一家以技术闻名的电子商务公司。是比较早认识到流处理的价值并把架构重心从微服务转移到工作流(workflow)处理模型。其自己研发的流处理框架加上图形式定义操作管理界面,使得业务开发变得透明、灵活、而且高效。下图来自其博客[从微服务到工作流](https://medium.com/jettech/microservices-to-workflows-the-evolution-of-jets-order-management-system-9e5669bd53ab)。可以看到他们做到了以简单的拖拉方式定义和改变工作流,实现这种软件能力相信是很多研发团队的梦想。 38 | 39 | ![workflow GUI](../imgs/jet-workflow-gui.png) 40 | 41 | 幸运的是 Akka Streams 是一套比较成熟的开源流处理库,对上面的每个业务需求都有比较好的支持,因而对基于反应式流处理模型的业务软件开发提供了强大的帮助。 42 | 43 | ### 技术趋势 44 | 45 | 开发 Akka Streams 的目的是为了达成[反应式宣言](https://www.reactivemanifesto.org/zh-CN)中列出的及时响应、耐挫性、可伸缩性、以及消息驱动的目标. 作为反应式编程的倡导者和先驱,[Erik Meijer]()发现了 `Enumerable` 模式和 `Observable` 模式作为数据生产者和消费者的二元性,参见 [setter and getter duality](https://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Keynote-Duality),并把二者统一到一套包含流量控制的异步编程 API,即后来的反应式流(reactive streams)编程 [ReactiveX](http://reactivex.io/) API。 其不同语言实现比如 Rx.NET, RxJava 以及 RxJs 在前后端开发都得到广泛的应用。 46 | 47 | 看到反应式流编程的普及和优点,Java 9 将其理念纳入标准 API。可是 Java 9 的流编程 API 只提供了几个底层 API,适合不多的简单应用场景。RxJava 则提供了比较丰富的反应式 API,但是对于流编程的支持仅限于进程内的 API 接口,是事件驱动(event-driven)而非消息驱动,且很长时间都不支持回压 back pressure 这种基本需求。复杂的拓扑结构以及灵活的模块组合方式则是踪影皆无。分布式,流量控制,灵活的拓扑与分形组合是成熟可用的流处理框架的紧迫要求。Akka Streams 的开发符合了这种技术发展的趋势。从技术角度看,反应式流处理编程的发展过程包括了下面多个维度的扩展: 48 | 49 | - 从单一的数据处理进展到包含多个甚至无限数量的数据流, 数据流中的数据先后次序及产生速度都隐含了时间的概念。 50 | - 从同步处理发展到异步处理。 51 | - 从单线程单进程处理到分布式处理。 52 | - 从线性结构到任意拓扑结构。 53 | - 从单一处理流程到不同抽象层次的分形组合处理流程。 54 | 55 | 自 2014 年 4 月[发布预览版](https://www.lightbend.com/blog/typesafe-announces-akka-streams) 至今,Akka Streams 逐渐发展为成熟且有丰富功能的反应式流编程库。今天看到的 Akka Streams 的功能特点是其开发团队多年开发高性能、高可用的分布式系统经验的结果。 56 | 57 | ## Akka Streams 是什么 58 | 59 | 明白了为什么会有 Akka Streams, 也就比较好理解 Akka Streams 是什么了。简单说,Akka Streams is an open source library that provides asychronous stream processing with non-blocking back pressure。就是一个流处理开源库,提供了异步流处理以及非阻塞的回压功能。其主要特点如下: 60 | 61 | - 面向应用开发者的的高级流处理 API 62 | - 丰富的流量控制功能 63 | - 原生异步处理能力 64 | - 跨进程分布式处理 65 | - 丰富的数据流处理机制 66 | - 提供显式的错误处理机制 67 | - 支持包含多入口/多出口以及闭环在内的任意处理拓扑结构 68 | - 多个小的处理模块可以灵活组合为大的处理模块,这些组合后的模块可以仍像最初的原子模块那样在不同层次任意组合。 69 | 70 | 从更抽象的层次讲,所有的程序都可以看成包含二个核心概念的系统:数据结构和函数。Akka streams 的数据结构是基于回压的数据流组合成的各种图,其函数则是对图中数据流的操作。 71 | 72 | ### 面向开发者的流处理 API 73 | 74 | 基于 JVM 的 [Rective Streams API](https://www.reactive-streams.org/) 是一个仅有 4 个接口和一个类的定义。这是一个支持回压的异步流处理最小接口集合,是 [Service Provider Interface (SPI)](https://en.wikipedia.org/wiki/Service_provider_interface), 需要第三方来提供具体实现。 Java 9 reactive streams SPI 的主要目的是提供不同编程语言及其实现的互操作,而不是为开发者提供很多马上可用的功能。Akka Streams 则完全不同,在封装了底层 Rective Streams SPI 的基础上为开发者提供了有丰富功能高级 API,同时支持 Scala 和 Java 二种接口。在操作符 [Operators 页面](https://doc.akka.io/docs/akka/current/stream/operators/) 可以看数以百计的接口及其实现。涵盖了 Source、Flow 以及 Sink 的各种操作、组合、管理以及外部集成等功能。 75 | 76 | ### 丰富的流量控制功能 77 | 78 | Non-blocking back pressure 非阻塞回压是指下游的消费者通过异步方式来控制上游发送数据的流量。出了最基本的回压, Akka Streams 提供了丰富的流量控制功能:包括缓存 buffer、 溢出策略、节流 throttle、延时、甚至数据的插值生成 (extrapolate) 等功能。涵盖了生产者和消费者的各种速度不匹配情况。 79 | 80 | ### 异步处理 81 | 82 | Akka Streams 提倡异步处理并提供原生的 (native) 支持。任何一个处理步骤都可以通过调用 `async` 方法异步并发运行。数据流之外的回压流量控制也是异步非阻塞运行。流处理的运行是基于 Akka Actor,对于异步执行有消息顺序以及线程安全的各种保证,使得异步并发程序的开发变得简单。 83 | 84 | ### 分布式处理 85 | 86 | 这个特点也是继承了 Akka Actor 的[分布式设计理念](https://doc.akka.io/docs/akka/current/general/remoting.html#distributed-by-default):所有的 Akka Actor 功能都是基于分布式的异步操作。其设计理念是以远程分布为出发点,对本地操作进行优化,而不是先按本地设计再推广到远程分布。Akka Streams 的处理单元可以通过简单的编码和配置分布到 Akka Cluster 的不同节点运行。这种分布式不像 Apach Spark 或 Apache Flink 那种更加广泛的分布式处理方式,只是一种基于 Akka Cluster 的紧凑的方式。对其它系统的分布式调用可以在流处理过程中通过异步远程调用实现。 87 | 88 | ### 丰富的流处理机制 89 | 90 | 从常见的 `map`, `filter`, `fold`, `reduce`, `collect`, `limit`, `take`, `drop` 等操作,到流的各种静态组合分散方式 `merge`, `concat`, `broadcast`, `groupBy`, `splitAfter`, `zip` 等,再到动态的组合比如 `MergHub`, `BroadcastHub`, `queue` 等,Akka Steams 提供了非常丰富的流处理机制。 91 | 92 | ### 显式的错误处理机制 93 | 94 | 错误处理是任何异步分布式系统的组成部分。Akka Streams 提供了很多内置的显式处理功能。既包含数据操作错误的处理机制比如重试、停止、替换等,也包括基于 Akka Actor 的错误监控处理策略。 95 | 96 | ### 任意拓扑 97 | 98 | 除了个别业务流程,多数业务场景都不是简单的单入单出线性处理。Akka Streams 提供了很多常用的多入口/多出口模块,也提供了一套 Graph DSL 允许程序员定义任意拓扑结构的处理流程。 99 | 100 | ### 分形组合 (fractal composition) 101 | 102 | 这也是 Akka Streams 最重要的功能之一。 Akka Streams 提供的 Graph DSL 允许灵活组装与复用各种形状的模块。组装后的模块仍可以相同的方式复用和组合。这种分形组合带来极大的灵活性和复用能力。 103 | 104 | ## 一个简单例子 105 | 106 | 使用了一年 Akka Streams API 之后,基于实际经验,Colin Breck 给出了一个为什么要采用 Akka Streams 的[假想例子](https://blog.colinbreck.com/akka-streams-a-motivating-example/)。这个例子需要完成的功能是从 HTTP 接口读取一些消息,解析消息然后异步写入数据库。这是一个常见的应用场景。 107 | 108 | 最开始的版本基于 Akka Actor 实现,对每个到达消息都会立刻解析然后写入数据库,性能不佳。改为缓存 1000 个消息批量写入数据库可以大大提高性能。此时需要引入一个缓存区和记录缓存消息数量。但是这种方式有个比较大的问题: HTTP 到达的消息可能很快也可能很慢,在很慢的情况下,很长时间都无法缓存到 1000 个消息。此时为了提高处理的实时性,需要至少每秒钟写入数据库一次。此时需要引入一个定时器,每秒钟触发和重置一次。考虑到缓存 1000 个消息会随时触发写库操作,此时需要在定时触发或缓存 1000 消息时相应改变缓存和设置定时器状态。当消息到达速率比较大的时候,很可能会有多个异步写数据库的操作。此时需要引入新的状态变量来限制同时写入数据库的操作数。可是即使如此也无法解决大量消息到达可能引起的系统缓存内存不足问题。 109 | 110 | 批量处理,定时调度,并发限制,流量控制是常见的可靠、高性能业务处理需求。虽然 Akka Actor 这类的并发技术使程序员不用考虑并发控制,但是上面这些各种状态的管理随着状态数目的增加,实现难度也是指数增长,测试和维护会越来越困难。相应的 Akka Streams 代码如下: 111 | 112 | ```scala 113 | messageFlow 114 | .groupedWithin(1000, 1 second) 115 | .mapAsync(10)(database.bulkInsertAsync) 116 | ``` 117 | 118 | `groupedWithin(1000, 1 second)` 实现了批量处理与定时调度。 `mapAsync(10)(database.bulkInsertAsync)` 限制数据库并发数目不超过 10。Akka Streams 内置的回压自动限制到达流量。如有必要,可以简单设置溢出的消息处理策略,可以简单丢弃一些消息或给客户端返回超载信息。无论如何,数据库的操作不会超过限定值,保证了可靠稳定的处理能力。同时上述代码仅需要集成测试,因为相应的流处理功能来自久经测验的 Akka Streams 库。 119 | 120 | ## 小结 121 | 122 | 本文试着从业务与技术的角度回答了为什么需要 Akka Steams 这样一个库,同时描述了 Akka Streams 是怎样的一个反应式流处理库。可以看到 Akka Streams 非常符合异步分布式软件开发的趋势,同时又提供了业界领先的成熟技术。其丰富的流处理功能和分形组合对于软件的开发效率、灵活性以及可维护性有非常大的提升。 123 | 124 | 可以预见响应式流处理会取代微服务成为企业应用架构的着眼点。夸张一点说,使用 Akka Streams 的开发者会有一种使用超前能力的幸福感。 125 | -------------------------------------------------------------------------------- /manuscript/ch05.md: -------------------------------------------------------------------------------- 1 | # Graph DSL 2 | 3 | 可以灵活的组合处理单元是 Akka Streams 的另外一个核心功能。当处理流程的图形不是从一个 Source 到零个或多个串行的 Flow 再到一个 Sink 的简单线性结构时,需要用 Akka Streams 提供的 Graph DSL(图形领域特定语言)来构造复杂非线性的处理拓扑结构。Graph DSL 提供了二种方式来构建非线性处理:利用已有的非线性 Shape 和定制有任意数量的入口和出口的 Shape。本文给出第一种的使用方法,定制 Shape 在下文介绍。 4 | 5 | ## 1 构建一个 `RunnableGraph` 可执行图 6 | 7 | 一个 `RunnableGraph` 可执行图如其名字所言,是一个可以实体化的 `Graph`。如前所述,一个 runnable(可执行)Graph 的基本要求是必须始于始于一个或多个 Source 而终于一个或多个 Sink 且所有的输入输出端口都按规则完全连接。本例中假设有二个快慢不一 `Source` 的数据单元需要发送到二个 `Sink` 打印。常见的场景是需要平衡工作量,二个简单的线性流程显然无法满足要求。为了平衡负载,需要先合并所有的输入数据再平衡的分发,如下图所示。 8 | 9 | ![basic composition](../imgs/merge-balance.png) 10 | 11 | 在 Akka Streams 里,所有的 `Graph` 图都从更基础的 `Shape` 形状来构造。图的构造过程自顶向下,可以分解为如下步骤. 12 | 13 | ### 1.1 组件及其组合形状 14 | 15 | Akka Streams 可以灵活组合业务处理组件。这些业务处理组件在具体应用中可以是非常复杂耗时的。下面给出简单的演示组件,目的是演示灵活的组合方式。 16 | 17 | ```scala 18 | // Step 1 基本组件 19 | import scala.concurrent.duration._ 20 | val source1 = Source.repeat("Repeat").throttle(3, 1.second).take(7) 21 | val source2 = Source.tick(0.second, 1.second, "Tick").take(3) 22 | 23 | val sink1 = Sink.foreach[String](message => println(s"Sink 1: ${message}")) 24 | val sink2 = Sink.foreach[String](message => println(s"Sink 2: ${message}")) 25 | ``` 26 | 27 | 二个不同速率的数据源需要发送到二个处理终点。简单的线性 Flow DSL 无法满足上图的非线性处理需求。需要借助 Graph DSL 来定制。 28 | 29 | ### 1.2 构建的模版 30 | 31 | Akka Streams 提供了 `RunnableGraph.fromGraph(g)` 方法来构建一个可执行图。这个方法的参数类型为 `Graph[ClosedShape, Mat]`,是个内含 `ClosedShape` 的 `Graph` 对象。 32 | 33 | Akka Streams 提供了 `GraphDSL.create` 方法创建这个 `Graph` 参数。这个 `create` 方法是个 overloaded(重载)方法,有多个函数签名分别接受不同的参数。最简单的一个是 `def create[S <: Shape]()(buildBlock: GraphDSL.Builder[NotUsed] => S): Graph[S, NotUsed]`,这里只接受一个函数对象为参数 `buildBlock: GraphDSL.Builder[NotUsed] => S`。这个函数的参数类型是 `GraphDSL.Builder`,返回值类型用了 upper type bound 类型 `S <: Shape` 表示 `S` 是 `Shape` 的一个子类。 34 | 35 | 一个简单的基础构建模版如下: 36 | 37 | ```scala 38 | // Step 2 基本构建模版 39 | val graph = RunnableGraph.fromGraph( 40 | GraphDSL.create() { implicit builder: GraphDSL.Builder[NotUsed] => 41 | // 构建一个 Shape 42 | } 43 | ) 44 | ``` 45 | 46 | ### 1.3 构建形状 47 | 48 | `Shape` 顾名思义,就是定义了图的形状,进而决定了所用的基本构件、构建操作符以及连接方式。根据需求,需要一个双输入单输出的 `Merge` 合并构件和一个单输入双输出的 `Balance` 平衡构件。一个 runnable(可执行)Graph 需要一个 `ClosedShapte`, 就是必须始于一个或多个 Source 而终于一个或多个 Sink,且所有处理组件的输入输出端口都按规则完全连接。其相关代码如下: 49 | 50 | ```scala 51 | // Step 3 导入构建操作符 52 | import GraphDSL.Implicits._ 53 | 54 | // Step 4 创建连接构件 55 | val merge = builder.add(Merge[String](2)) 56 | val balance = builder.add(Balance[String](2)) 57 | 58 | // Step 5 连接功能组件和构件,所有的输入输出接口都全连接 59 | source1 ~> merge ~> balance ~> sink1 60 | source2 ~> merge 61 | balance ~> sink2 62 | 63 | // Step 6 返回形状类型 64 | ClosedShape 65 | ``` 66 | 67 | 可以看到构建形状包含了多个步骤,`import GraphDSL.Implicits._` 的目的是导入构建操作符。这些操作符比如用到的 `~>` 用于连接不同的**构件**或**组件**。其作用类似于 `viaMat` 或 `toMat` 这些方法,但是用于连接更基础的 Shape 形状构件。 68 | 69 | 特别声明一下,这里用了中文`构件`来指代那些类型为 `Shape` 的低级对象(object),`组件`则指代那些类型为 `Graph` 的高级数据处理对象(object)。 70 | 71 | `builder.add(Merge[Int](2))` 和 `builder.add(Balance[Int, Int])` 分别创建了一个合并构件和一个平衡构件。`~>` 是个重载方法,既可以连接二个构件,也可以连接一个构件和一个组件,但是不可以两边都是组件。 72 | 73 | 最后一步是指明返回的形状类型,这里是 `ClosedShape`,表示一个全连接的可执行图。里面所有构件和组件的输入输出端口都完全连接。本例子中 `Merge` 有两个输入连接,一个输出连接,`Balance` 则有一个输入连接和二个输出连接。同样的,所有的 Source 和 Sink 也都完全连接。如果有任何一个未连接的端口会产生编译错误。 74 | 75 | ### 1.4 实体化运行 76 | 77 | 上面构建的可执行图可以实体化运行。完整的代码和输出如下。 78 | 79 | ```scala 80 | import scala.util.Success 81 | import akka.actor.ActorSystem 82 | import akka.NotUsed 83 | import akka.stream.ClosedShape 84 | import akka.stream.scaladsl.{Balance, GraphDSL, Merge, RunnableGraph, Sink, Source, Zip} 85 | 86 | object MergeBalance { 87 | def main(args: Array[String]) { 88 | implicit val system = ActorSystem("testStreams") 89 | implicit val ec = scala.concurrent.ExecutionContext.global 90 | 91 | // Step 1 基本组件 92 | import scala.concurrent.duration._ 93 | val source1 = Source.repeat("Repeat").throttle(3, 1.second).take(7) 94 | val source2 = Source.tick(0.second, 1.second, "Tick").take(3) 95 | 96 | val sink1 = Sink.foreach[String](message => println(s"Sink 1: ${message}")) 97 | val sink2 = Sink.foreach[String](message => println(s"Sink 2: ${message}")) 98 | 99 | // Step 2 基本构建模版 100 | val graph = RunnableGraph.fromGraph( 101 | GraphDSL.create() { implicit builder: GraphDSL.Builder[NotUsed] => 102 | // Step 3 导入构建操作符 103 | import GraphDSL.Implicits._ 104 | 105 | // Step 4 创建连接构件 106 | val merge = builder.add(Merge[String](2)) 107 | val balance = builder.add(Balance[String](2)) 108 | 109 | // Step 5 连接功能组件和构件,所有的输入输出接口都全连接 110 | source1 ~> merge ~> balance ~> sink1 111 | source2 ~> merge 112 | balance ~> sink2 113 | 114 | // Step 6 返回形状类型 115 | ClosedShape 116 | } 117 | ) // RunnableGrpah 118 | 119 | graph.run() 120 | } 121 | } 122 | 123 | /* 输出结果 124 | Sink 1: Repeat 125 | Sink 2: Tick 126 | Sink 1: Repeat 127 | Sink 2: Repeat 128 | Sink 1: Tick 129 | Sink 2: Repeat 130 | Sink 1: Repeat 131 | Sink 2: Repeat 132 | Sink 1: Repeat 133 | Sink 2: Tick 134 | */ 135 | ``` 136 | 137 | ## 2 构建一个 `FlowShape` 图 138 | 139 | 利用 Graph DSL 不仅仅可以构建 `RunnableGraph`, 也可以构件其它用于 Flow DSL 的组件,比如 一个 `FlowShape` 图是一个可用于线性组合处理的单输入单输出组件。本例中假设每个数据单元都需要发送到二个不同的业务组件进行处理,然后把结果组合为一个 `Tuple` 数据。如下图所示: 140 | 141 | ![basic composition](../imgs/broadcast-zip.png) 142 | 143 | 在 Akka Streams 里,所有的 `Graph` 图都从更基础的 `Shape` 形状来构造。`FlowShape` 图的构造也不例外,可以分解为如下步骤. 144 | 145 | ### 2.1 基本业务组件 146 | 147 | 本例中假设每个数据单元都需要发送到二个独立的 `Flow` 分别处理。这二个组件定义如下: 148 | 149 | ```scala 150 | // Step 1 基本组件 151 | val flow1 = Flow[Int].map(_ * 10) 152 | val flow2 = Flow[Int].map(_ * 100) 153 | ``` 154 | 155 | ### 2.2 构建的模版 156 | 157 | Akka Streams 提供了 `Flow.fromGraph(g)` 方法来构建一个 Flow 图。这个方法的参数类型为 `Graph[FlowShape[I, O], M]`,是个内含 `FlowShape` 的 `Graph` 对象。同样,可以用 `GraphDSL.create` 方法创建这个 `Graph` 参数。 158 | 159 | 一个简单的基础构建模版如下: 160 | 161 | ```scala 162 | // Step 2 基本构建模版 163 | val graph = Flow.fromGraph( 164 | GraphDSL.create() { implicit builder: GraphDSL.Builder[NotUsed] => 165 | // 构建一个 Shape 166 | } 167 | ) 168 | ``` 169 | 170 | ### 2.3 构建形状 171 | 172 | `Shape` 定义了图的形状,也决定了所用的基本构件、构建操作符以及连接方式。根据需求,需要一个单输入双输出的 `Broadcast` 广播组件和一个双输入单输出的 `Zip` 拉链组件。一个 Flow Graph 需要一个 `FlowShape`, 就是必须有一个输入口和一个输出口,其内部的所有处理组件的输入输出端口都按规则完全连接。其相关代码如下: 173 | 174 | ```scala 175 | // Step 3 导入构建操作符 176 | import GraphDSL.Implicits._ 177 | 178 | // Step 4 创建连接构件 179 | val broadcast = builder.add(Broadcast[Int](2)) 180 | val zip = builder.add(Zip[Int, Int]) 181 | 182 | // Step 5 连接功能组件和构件,连接相应的输入输出接口 183 | broadcast ~> flow1 ~> zip.in0 184 | broadcast ~> flow2 ~> zip.in1 185 | 186 | // Step 6 返回形状类型 187 | FlowShape(broadcast.in, zip.out) 188 | ``` 189 | 190 | 此处有个小细节就是需要具体指定 `Zip` 的输入端口 `in0` 或 `in1`。这是因为作为一个 `FanInShape2`,构件操作符 `~>` 没有相应的重载方法。背后的考量估计是因为数据的位置决定了输出的类型。但是 `Broadcast`, `Balance`, `Meger` 这些形状的端口是一致的(Uniform),连接的顺序无关紧要,所以不需要指定具体端口。如果想指定,也可以如下所示,但是此处没有必要: 191 | 192 | ```scala 193 | broadcast.out(0) ~> flow1 ~> zip.in0 194 | broadcast.out(1) ~> flow2 ~> zip.in1 195 | ``` 196 | 197 | ### 2.4 实体化运行 198 | 199 | 构造的 Flow Graph 可以像其它任何一个 `Flow` 对象一样用在线性的 Flow DSL 灵活组合。下面是完整的源码。 200 | 201 | ```scala 202 | import akka.actor.ActorSystem 203 | import akka.NotUsed 204 | import akka.stream.FlowShape 205 | import akka.stream.scaladsl.{Broadcast, Flow, GraphDSL, Sink, Source, Zip} 206 | 207 | object FlowDemo { 208 | def main(args: Array[String]) { 209 | implicit val system = ActorSystem("testStreams") 210 | implicit val ec = scala.concurrent.ExecutionContext.global 211 | 212 | // Step 1 基本组件 213 | val flow1 = Flow[Int].map(_ * 10) 214 | val flow2 = Flow[Int].map(_ * 100) 215 | 216 | // Step 2 基本构建模版 217 | val flow = Flow.fromGraph( 218 | GraphDSL.create() { implicit builder: GraphDSL.Builder[NotUsed] => 219 | // Step 3 导入构建操作符 220 | import GraphDSL.Implicits._ 221 | 222 | // Step 4 创建连接构件 223 | val broadcast = builder.add(Broadcast[Int](2)) 224 | val zip = builder.add(Zip[Int, Int]) 225 | 226 | // Step 5 连接功能组件和构件,连接相应的输入输出接口 227 | broadcast ~> flow1 ~> zip.in0 228 | broadcast ~> flow2 ~> zip.in1 229 | 230 | // Step 6 返回形状类型 231 | FlowShape(broadcast.in, zip.out) 232 | } 233 | ) // Flow Grpah 234 | 235 | val source = Source(1 to 3) 236 | val sink = Sink.foreach[(Int, Int)](println) 237 | source.via(flow).runWith(sink) 238 | } 239 | } 240 | 241 | /* 输出结果 242 | (10,100) 243 | (20,200) 244 | (30,300) 245 | */ 246 | ``` 247 | -------------------------------------------------------------------------------- /manuscript/ch07.md: -------------------------------------------------------------------------------- 1 | # GraphStage 2 | 3 | Flow DSL 和 Graph DSL 的目的是灵活的连接各种组件,主要是通过 Shape 来操作的图的连接拓扑结构。如果需要实现类似 `Merge`, `Zip` 这种内置了数据处理功能的构件或操作符,需要定制 `GraphStage` (翻译成“图步”)来完成。`GraphStage` 是 `Graph` 的子类,在其形状和属性基础上定义了入口和出口的数据处理逻辑。从下面的例子可以看到,Akka Streams 的基本处理逻辑是 pull-based back pressure 拉驱动的回压机制。通过实现定制 GraphStage 有助于深刻理解 Akka Streams 的实现机制。本文用由简到繁的几个例子加以演示和说明,总结放在下一章。 4 | 5 | 所有的例子和图形都基于或来自 [Custom stream processing 官方文档](https://doc.akka.io/docs/akka/current/stream/stream-customize.html),下面不另做说明。 6 | 7 | ## 1 一个定制的数据源点 8 | 9 | 创建一个 GraphStage 类需要给出 Shape 的定义和处理逻辑。Shape 用于指定用到的入口 (`Inlet`) 和出口 (`Outlet`)。一个数据源没有入口,通常也只有一个出口。利用已有的 `SourceShape`,可以定义如下: 10 | 11 | ```scala 12 | val out: Outlet[Int] = Outlet("NumbersSource") 13 | override val shape: SourceShape[Int] = SourceShape(out) 14 | ``` 15 | 16 | 数据处理逻辑需要重载 `createLogic()` 方法来创建一个新的 `GraphStageLogic` 对象。这是非常关键的一步,因为每次实体化的时候都会调用这个方法来产生新的`GraphStageLogic` 对象,所有的相关数据和处理逻辑都需要封装在这个对象里面。对于数据源,只需要指定出口的 `onPull` callback 回调函数即可。完整的演示代码如下: 17 | 18 | ```scala 19 | import akka.actor.ActorSystem 20 | import akka.stream.{Attributes, Outlet, SourceShape} 21 | import akka.stream.scaladsl.Source 22 | import akka.stream.stage.{GraphStage, GraphStageLogic, OutHandler} 23 | 24 | class NumbersSource extends GraphStage[SourceShape[Int]] { 25 | val out: Outlet[Int] = Outlet("NumbersSource") 26 | override val shape: SourceShape[Int] = SourceShape(out) 27 | 28 | override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = 29 | new GraphStageLogic(shape) { 30 | private var counter = 1 31 | val outHandler = new OutHandler { 32 | override def onPull(): Unit = { 33 | push(out, counter) 34 | counter += 1 35 | } 36 | } 37 | setHandler(out, outHandler) 38 | } 39 | } 40 | 41 | object Test { 42 | def main(args: Array[String]) { 43 | implicit val system = ActorSystem("testStreams") 44 | implicit val ec = scala.concurrent.ExecutionContext.global 45 | 46 | val source = Source.fromGraph(new NumbersSource) 47 | source.take(3).runForeach(println) 48 | 49 | } 50 | } 51 | /* output 52 | 1 53 | 2 54 | 3 55 | */ 56 | ``` 57 | 58 | 本例子中在 `new GraphStageLogic(shape) {...}` 的表达式中定义了一个计数器,然后在 `OutHandler` 里面的 `onPull()` 方法里调用 `push` 方法数据推出,然后计数器加一。下游的 `pull` 请求会触发上游的 `push` 方法调用来发送数据。这里可以看到回压机制的实现。因为 GraphStage 是 Graph 的子类,没有运行时需要的 Attribute 属性配置,所以用 `Sink.fromGraph` 方法来创建一个新的数据源点。 59 | 60 | ## 2 定制数据终点 61 | 62 | 终点的 Shape 没有出口,只有入口。其处理逻辑在于重载 `onPush` 方法,用 `grab(in)` 方法从入口取得数据后,需要调用 `pull(in)` 请求后续数据。需要注意的是,Akka Streams 的流是 pull-based,所以数据终点需要在开始时先 `pull(in)` 来启动整个数据流。 一个简单的打印收到数据的定制终点程序如下: 63 | 64 | ```scala 65 | import akka.actor.ActorSystem 66 | import akka.stream.{Attributes, Inlet, SinkShape} 67 | import akka.stream.scaladsl.{Source, Sink} 68 | import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler} 69 | 70 | class PrintSink extends GraphStage[SinkShape[Int]] { 71 | val in: Inlet[Int] = Inlet("PrintSink") 72 | override val shape: SinkShape[Int] = SinkShape(in) 73 | 74 | override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = 75 | new GraphStageLogic(shape) { 76 | 77 | // 启动时发出初始请求 78 | override def preStart(): Unit = pull(in) 79 | 80 | setHandler(in, new InHandler { 81 | override def onPush(): Unit = { 82 | println(grab(in)) 83 | pull(in) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | object SinkTest { 90 | def main(args: Array[String]) { 91 | implicit val system = ActorSystem("testStreams") 92 | implicit val ec = scala.concurrent.ExecutionContext.global 93 | 94 | val sink = Sink.fromGraph(new PrintSink) 95 | Source(1 to 3).runWith(sink) 96 | } 97 | } 98 | 99 | /* output 100 | 1 101 | 2 102 | 3 103 | */ 104 | ``` 105 | 106 | 创建的 GraphStage 是 Graph 的子类,没有运行时需要的 Attribute 属性配置,所以用 `Sink.fromGraph` 方法来创建一个新的数据终点。 107 | 108 | ## 3 一个定制的 Duplicator Flow 109 | 110 | 一个 Flow 具有一个入口和一个出口。内置的处理逻辑需要接受入口数据和产生出口数据,同时可能还要处理上下游完成或出错的情况。比如一个定制的 Duplicator Flow,复制上游的每个数据。当上游完成时,也需要尝试把缓存的数据发送给下游。下面的例子很好的解释来入口和出口的处理逻辑及其交互。类似的,创建的 GraphStage 是 Graph 的子类,没有运行时需要的 Attribute 属性配置,所以用 `Flow.fromGraph` 方法来创建一个新 Flow。 111 | 112 | ```scala 113 | import akka.actor.ActorSystem 114 | 115 | import akka.stream.{Attributes, FlowShape, Inlet, Outlet} 116 | import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler} 117 | import akka.stream.scaladsl.{Flow, Source} 118 | 119 | class Duplicator[A] extends GraphStage[FlowShape[A, A]] { 120 | 121 | val in = Inlet[A]("Duplicator.in") 122 | val out = Outlet[A]("Duplicator.out") 123 | 124 | val shape = FlowShape.of(in, out) 125 | 126 | override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = 127 | new GraphStageLogic(shape) { 128 | // 可变状态必须定义在 GraphStageLogic 里面 129 | var lastElem: Option[A] = None 130 | 131 | val inHandler = new InHandler { 132 | override def onPush(): Unit = { 133 | val elem = grab(in) 134 | lastElem = Some(elem) 135 | push(out, elem) 136 | } 137 | 138 | // 上游结束时尝试发送缓存的数据 139 | override def onUpstreamFinish(): Unit = { 140 | if (lastElem.isDefined) emit(out, lastElem.get) 141 | complete(out) 142 | } 143 | 144 | } 145 | setHandler(in, inHandler) 146 | 147 | val outHanlder = new OutHandler { 148 | override def onPull(): Unit = { 149 | if (lastElem.isDefined) { 150 | push(out, lastElem.get) 151 | lastElem = None 152 | } else { 153 | pull(in) 154 | } 155 | } 156 | } 157 | setHandler(out, outHanlder) 158 | } 159 | } 160 | 161 | object DuplicatorTest { 162 | def main(args: Array[String]) { 163 | implicit val system = ActorSystem("testStreams") 164 | implicit val ec = scala.concurrent.ExecutionContext.global 165 | 166 | val source = Source(1 to 3) 167 | val flow = Flow.fromGraph(new Duplicator[Int]) 168 | source.via(flow).runForeach(println) 169 | } 170 | } 171 | 172 | /* output 173 | 1 174 | 1 175 | 2 176 | 2 177 | 3 178 | 3 179 | */ 180 | ``` 181 | 182 | ## 4 返回实体化值 183 | 184 | 当需要返回一个实体化值时,需要重载 `GraphStageWithMaterializedValue` 类的 `createLogicAndMaterializedValue` 方法。改方法返回一个包含二个元素的 tuple:`(GraphStageLogic, Future[A])`。下面的例子把流中的第一个数据单元作为实体化值。 185 | 186 | ```scala 187 | import scala.concurrent.{Future, Promise} 188 | import akka.actor.ActorSystem 189 | 190 | import akka.stream.{Attributes, FlowShape, Inlet, Outlet} 191 | import akka.stream.stage.{ 192 | GraphStageLogic, 193 | GraphStageWithMaterializedValue, 194 | InHandler, 195 | OutHandler 196 | } 197 | import akka.stream.scaladsl.{Flow, Keep, Source, Sink} 198 | 199 | class FirstValue[A] 200 | extends GraphStageWithMaterializedValue[FlowShape[A, A], Future[A]] { 201 | 202 | val in = Inlet[A]("FirstValue.in") 203 | val out = Outlet[A]("FirstValue.out") 204 | val shape = FlowShape.of(in, out) 205 | 206 | override def createLogicAndMaterializedValue( 207 | inheritedAttributes: Attributes 208 | ): (GraphStageLogic, Future[A]) = { 209 | val promise = Promise[A]() 210 | val logic = new GraphStageLogic(shape) { 211 | 212 | val inHandler = new InHandler { 213 | override def onPush(): Unit = { 214 | val elem = grab(in) 215 | promise.success(elem) 216 | push(out, elem) 217 | 218 | // 第一次之后就直接转发数据 219 | setHandler(in, new InHandler { 220 | override def onPush(): Unit = { 221 | push(out, grab(in)) 222 | } 223 | }) 224 | } 225 | } 226 | setHandler(in, inHandler) 227 | 228 | setHandler(out, new OutHandler { 229 | override def onPull(): Unit = { 230 | pull(in) 231 | } 232 | }) 233 | 234 | } 235 | 236 | (logic, promise.future) 237 | } 238 | } 239 | 240 | object MatGraphTest { 241 | def main(args: Array[String]) { 242 | implicit val system = ActorSystem("testStreams") 243 | implicit val ec = scala.concurrent.ExecutionContext.global 244 | 245 | val source = Source(1 to 3) 246 | val flow = Flow.fromGraph(new FirstValue[Int]) 247 | val result = 248 | source.viaMat(flow)(Keep.right).toMat(Sink.ignore)(Keep.left).run() 249 | 250 | result.onComplete(println) 251 | } 252 | } 253 | 254 | /* output 255 | Success(1) 256 | */ 257 | ``` 258 | 259 | 本例子中在实体化时可以很早就拿到这个数据。如果取最后一个数据单元作为实体化值,则只有在整个流结束后,`Future` 才会有结果。所以实体化值是相对独立的异步运算结果,可以是和流数据相关或无关的任何数据类型。 260 | -------------------------------------------------------------------------------- /manuscript/ch04.md: -------------------------------------------------------------------------------- 1 | # Backpressure 2 | 3 | Backpressure(回压)是 Akka Streams 最为核心的功能,是其区别于其它系统的标志性特征。回压主要用于处理数据生产者和消费者的速度不匹配情况,通过不同的数据溢出处理策略来提高系统的稳定性。本文首先介绍 async boundary(异步边界)的概念,然后用代码演示了回压的功能和溢出策略。 4 | 5 | ## 1 Async Boundar 6 | 7 | Akka Streams 的一个流处理中的 Operators 可以在运行在不同的线程甚至不同的 JVM 里面,一个线程就是一个 async boundary(异步边界)。从 Akka Streams 2.5 开始,当组合 operators 时,如果不调用 `async()` 方法,这些 operators 会运行在同一个 async boundary,也就是同一个线程里面。这种优化 Akka Streams 称其为 Fusion (聚合)。Fusion 模式时上下游 Operators 之间直接用共享内存访问 elements,每一个 element 处理完成后再处理下一个 element,不要流量控制机制。比如下面的例子: 8 | 9 | ```scala 10 | import akka.actor.ActorSystem 11 | import akka.stream.scaladsl.{Flow, Sink, Source} 12 | import java.time.format.DateTimeFormatter 13 | import java.time.LocalTime 14 | 15 | object TestStreams extends App { 16 | implicit val system = ActorSystem("testStreams") 17 | implicit val ec = scala.concurrent.ExecutionContext.global 18 | val dataFormat = DateTimeFormatter.ofPattern("hh:mm:ss:SSS") 19 | 20 | val source = Source(1 to 3) 21 | 22 | private def getTime() = { 23 | LocalTime.now().format(dataFormat) 24 | } 25 | 26 | private def flowFactory(name: String) = { 27 | Flow[Int].map(element => { 28 | Thread.sleep(1000) 29 | val threadName = Thread.currentThread().getName() 30 | val now = getTime() 31 | println(s"Flow-${name}: ${threadName} [${element}] ${now}") 32 | element 33 | }) 34 | } 35 | 36 | val flowA = flowFactory("A") 37 | val flowB = flowFactory("B") 38 | val flowC = flowFactory("C") 39 | 40 | println(s"Start at ${getTime()}") 41 | val result = source 42 | .via(flowA) 43 | .via(flowB) 44 | .via(flowC) 45 | .runWith(Sink.ignore) 46 | 47 | result.onComplete(_ => { 48 | println(s"End at ${getTime()}") 49 | system.terminate() 50 | }) 51 | } 52 | ``` 53 | 54 | 其输出为: 55 | 56 | ```text 57 | Start at 07:20:47:823 58 | Flow-A: testStreams-akka.actor.default-dispatcher-7 [1] 07:20:48:847 59 | Flow-B: testStreams-akka.actor.default-dispatcher-7 [1] 07:20:49:852 60 | Flow-C: testStreams-akka.actor.default-dispatcher-7 [1] 07:20:50:855 61 | Flow-A: testStreams-akka.actor.default-dispatcher-7 [2] 07:20:51:858 62 | Flow-B: testStreams-akka.actor.default-dispatcher-7 [2] 07:20:52:862 63 | Flow-C: testStreams-akka.actor.default-dispatcher-7 [2] 07:20:53:862 64 | Flow-A: testStreams-akka.actor.default-dispatcher-7 [3] 07:20:54:864 65 | Flow-B: testStreams-akka.actor.default-dispatcher-7 [3] 07:20:55:865 66 | Flow-C: testStreams-akka.actor.default-dispatcher-7 [3] 07:20:56:869 67 | End at 07:20:56:871 68 | ``` 69 | 70 | 上例中用 `Thread.sleep` 来模拟长时间的处理,生产代码中应该尽可能避免这种 blocking call 阻塞调用。可以看到所有 Operators 运行在同一个线程,处理完一个 element 再处理下一个,没有流量控制的必要。对于程序中模拟的长时间处理,由于只用到一个线程,一个接一个串行处理每个元素。处理一个需要 3 秒,总耗时 9 秒。如果各个 Operator 运行在不同的异步边界,则各个 Operator 异步并发在不同的线程执行。创建异步边界非常简单,只需要在 Operator 后面调用 `async` 即可。如下面程序所示: 71 | 72 | ```scala 73 | val result = source 74 | .via(flowA) 75 | .async 76 | .via(flowB) 77 | .async 78 | .via(flowC) 79 | .async 80 | .runWith(Sink.ignore) 81 | ``` 82 | 83 | 此时的运行结果如下: 84 | 85 | ```text 86 | Start at 07:24:42:062 87 | Flow-A: testStreams-akka.actor.default-dispatcher-9 [1] 07:24:43:091 88 | Flow-A: testStreams-akka.actor.default-dispatcher-9 [2] 07:24:44:097 89 | Flow-B: testStreams-akka.actor.default-dispatcher-7 [1] 07:24:44:097 90 | Flow-B: testStreams-akka.actor.default-dispatcher-7 [2] 07:24:45:101 91 | Flow-C: testStreams-akka.actor.default-dispatcher-8 [1] 07:24:45:101 92 | Flow-A: testStreams-akka.actor.default-dispatcher-9 [3] 07:24:45:101 93 | Flow-C: testStreams-akka.actor.default-dispatcher-8 [2] 07:24:46:101 94 | Flow-B: testStreams-akka.actor.default-dispatcher-6 [3] 07:24:46:102 95 | Flow-C: testStreams-akka.actor.default-dispatcher-7 [3] 07:24:47:105 96 | End at 07:24:47:106 97 | ``` 98 | 99 | 每一个 `async` 调用创建一个新的异步边界,底层的实现是运行在不同的 Akka Actor 上。可以看到每个 Operator 运行在不同的线程并发处理收到的数据。总耗时从大约 9 秒降低到大约 5 秒。值得说明的是,如同其它的多线程编程,需要仔细衡量异步边界切换的开销和任务的性质决定是否采用异步并发。比如上面例子中,当去掉 `Thread.sleep` 和打印,对一百万 elements,在我的 Macbook Pro 笔记本电脑上,Fusion 单线程费时 0.22 秒而多线程版本费时约 2 秒。 100 | 101 | 重要的是,对 Akka Streams 而言,当不同 Operators 在不同的异步边界时,Operator 之间的连接会引入 buffer 和 backpressure。数据单元的相对顺序保持不变。 102 | 103 | ## 2 Buffer 和 Backpressure 104 | 105 | Akka Streams 的回压机制是 pull-based(拉驱动),每次数据发送都由下游消费者发起请求,由下游的数据消费者指定上游的数据发送者每次可以发送的数据量。如上节所述,这种回压机制只存在于不同的异步边界上,通过缓存和异步控制消息实现。Akka Streams 中每个异步边界运行在不同的 Akka Actor 上。二个异步边界的连接在下游一侧有一个缺省值为 16 的 buffer。这个值是 element 的个数,大的 element 数据类型会占用更多内存。可以通过参数设置改变这个值 `akka.stream.materializer.max-input-buffer-size = 16`。 106 | 107 | 在开始运行时,上游的 Operator 会等待下游的 Operator 给出明确的 Pull 特定数目的 element 请求时才 Push 推送相应数目的 elements。初始时下游会请求其缓存大小的 elements,只有当缓存有空余时(缺省是有一半空了)才会向上游发出新的请求。这种回压控制是通过异步消息方式进行的,和正常的数据流向相反,这是 **backpressure(回压)**中 **back(回)**字的由来。回压控制是端到端的,从最后的数据终点(Sink)出发,从下游到上游,依次决定各个中间处理环节直到数据源点(Source)的数据发送量。所以整体的数据发送速率是整个流中最慢节点的数据请求速率。这种回压控制是底层机制,对应用程序员是透明的。唯一的例外就是当慢的消费者面对快的数据生产者而无法控制数据发送速率时,需要指明对来不及处理的数据单元的处理策略,Akka Streams 对此提供了丰富的处理策略。 108 | 109 | 为了方便演示,我们把 Buffer Size 值调整为 4。在 `src/main/resources/application.conf` 里设置:`akka.stream.materializer.max-input-buffer-size = 4`。下面的程序演示了在二个异步边界的回压机制。 110 | 111 | ```scala 112 | import akka.actor.ActorSystem 113 | import akka.stream.scaladsl.{Flow, Sink, Source} 114 | import java.time.format.DateTimeFormatter 115 | import java.time.LocalTime 116 | 117 | object TestStreams { 118 | def main(args: Array[String]) { 119 | implicit val system = ActorSystem("testStreams") 120 | implicit val ec = scala.concurrent.ExecutionContext.global 121 | val dataFormat = DateTimeFormatter.ofPattern("hh:mm:ss:SSS") 122 | 123 | def getTime() = { 124 | LocalTime.now().format(dataFormat) 125 | } 126 | 127 | val source = Source(1 to 10) 128 | val flow = Flow[Int].map { element => 129 | println(s"Flow: [${element}] ${getTime()}") 130 | element 131 | } 132 | val sink = Sink.foreach[Int](element => { 133 | Thread.sleep(1000) 134 | println(s"Sink: [${element}] ${getTime()}") 135 | }) 136 | 137 | println(s"Start at ${getTime()}") 138 | val result = source.async.via(flow).async.runWith(sink) 139 | 140 | result.onComplete(_ => { 141 | println(s"End at ${getTime()}") 142 | system.terminate() 143 | }) 144 | } 145 | } 146 | ``` 147 | 148 | 上面程序的输出如下,为了方便分析,人工加入了空行分隔输出内容: 149 | 150 | ```text 151 | Start at 02:22:51:908 152 | 153 | Flow: [1] 02:22:51:937 154 | Flow: [2] 02:22:51:938 155 | Flow: [3] 02:22:51:938 156 | Flow: [4] 02:22:51:938 157 | 158 | Sink: [1] 02:22:52:941 159 | 160 | Flow: [5] 02:22:52:942 161 | Flow: [6] 02:22:52:942 162 | 163 | Sink: [2] 02:22:53:944 164 | Sink: [3] 02:22:54:950 165 | 166 | Flow: [7] 02:22:54:951 167 | Flow: [8] 02:22:54:951 168 | 169 | Sink: [4] 02:22:55:951 170 | Sink: [5] 02:22:56:957 171 | 172 | Flow: [9] 02:22:56:957 173 | Flow: [10] 02:22:56:958 174 | 175 | Sink: [6] 02:22:57:958 176 | Sink: [7] 02:22:58:960 177 | Sink: [8] 02:22:59:966 178 | Sink: [9] 02:23:00:969 179 | Sink: [10] 02:23:01:972 180 | 181 | End at 02:23:01:975 182 | ``` 183 | 184 | 数据的流动始于 `sink` 在实体化时开始请求数据。可以看到,除了最初时一次收到四个单元 (`max-input-buffer-size = 4`),`flow` 随后每次只收到 Buffer Size 的一半,即二个数据单元,而且都是在 `sink` 处理了二个数据单元之后。一个例外是因为有数据缓存,`sink` 第一次只处理了一个数据单元后,`flow` 就接到通知并开始请求二个数据单元。此后都是`sink` 处理了二个,再发通知给 `flow` 请求二个数据单元,`flow` 随后再发通知给 `source` 请求二个数据单元。 185 | 186 | ## 3 溢出策略 187 | 188 | 回压只在快的生产者和慢的消费者之间起作用。在慢的生产者和快的消费者之间则无需担心。其实对于快的生产者和慢的消费者引起的数据溢出问题,回压只是其中的一种策略,是 Akka Streams 的所有处理单元的一种缺省的溢出处理策略。 189 | 190 | Akka Streams 的每一个异步边界都运行在一个单独的 Akka Actor 里面。其 `inlet`(入口)都有一个 `InputBuffer` 输入缓存。`akka.stream.materializer.init-input-buffer-size` 和 `akka.stream.materializer.max-input-buffer-size` 这二个参数指定了初始的缓存大小和最大的缓存大小,尺寸是数据单元的个数,其缺省值分别为 4 和 16。每一个 Runnable Graph 或处理单元也可以分别指定缓存的尺寸以及其溢出策略。 191 | 192 | 比如上面的代码中,如果如下设定 `flow` 的缓存和溢出策略,其它代码不变,则产生不同的输出。 193 | 194 | ```scala 195 | val result = source.async 196 | .via(flow.buffer(2, OverflowStrategy.dropHead)) 197 | .async 198 | .runWith(sink) 199 | ``` 200 | 201 | 其输出如下: 202 | 203 | ```text 204 | Start at 11:04:43:518 205 | Flow: [1] 11:04:43:553 206 | Flow: [2] 11:04:43:553 207 | Flow: [3] 11:04:43:553 208 | Flow: [4] 11:04:43:554 209 | Flow: [5] 11:04:43:554 210 | Flow: [6] 11:04:43:554 211 | Flow: [7] 11:04:43:555 212 | Flow: [8] 11:04:43:555 213 | Flow: [9] 11:04:43:555 214 | Flow: [10] 11:04:43:556 215 | 216 | Sink: [1] 11:04:44:556 217 | Sink: [2] 11:04:45:557 218 | Sink: [3] 11:04:46:562 219 | Sink: [4] 11:04:47:567 220 | 221 | Sink: [9] 11:04:48:568 222 | Sink: [10] 11:04:49:573 223 | End at 11:04:49:576 224 | ``` 225 | 226 | `flow` 不再使用回压策略,而采用了 `dropHead`,即丢弃旧数据的策略。其缓存尺寸为 2。最开始的 4 个数据都基于 `sink`(其缓存尺寸保持系统设定的 4 个数据单元)的第一个请求全部发送出去了。随后的数据,除了最新的 2 个,其它全部丢弃了。 227 | 228 | 下面是 Akka Streams 支持的溢出策略 229 | 230 | - `OverflowStrategy.backpressure`:回压。这是缺省的溢出策略。当缓存满了则不从上游 pull(拉)数据,或者说不请求新数据。当缓存有一半为空时,再从上游请求新数据,请求个数为缓存尺寸的一半。 231 | - `OverflowStrategy.dropBuffer`:丢弃缓存。当缓存满了,有新的数据单元,则丢弃整个缓存数据。 232 | - `OverflowStrategy.dropHead`:丢弃缓存旧数据。当缓存满了,有新的数据单元,则丢弃最老的数据单元。 233 | - `OverflowStrategy.dropNew`:丢弃新数据单元。 234 | - `OverflowStrategy.dropTail`: 丢弃缓存新数据。当缓存满了,有新的数据单元,则丢弃缓存中最新的数据单元。 235 | - `OverflowStrategy.fail`:失败。当缓存满了,有新的数据单元时,completes the streams(结束流)并返回一个错误。 236 | 237 | ## 小结 238 | 239 | 除了包含了 backpressure(回压)的 overflow strategy(溢出策略),Akka Streams 还提供了丰富的流量控制功能,包括 rate limit(限速),aggretation(聚合),throttle(节流阀),以及 extrapolate(插值)等。这些额外的流量控制功能在 `Source` 或 `Flow` 的 API 文档都有详细的说明和例子。 240 | -------------------------------------------------------------------------------- /manuscript/ch06.md: -------------------------------------------------------------------------------- 1 | # Custom Shape 2 | 3 | Akka Streams 提供了比较丰富的 Shpae 形状构件来连接数据处理组件。其组合方法比较类似,都是通过创建不同形状的构件之后再按需要连接输入输出端口进行组合。如果已有的 Shape 形状不能满足要求,Graph DSL 允许自定义 Shape 来任意组合组件。同 Flow DSL 一样,Graph DSL 也提供机制指定要保留的实体化值。 4 | 5 | ## 1 Shape 6 | 7 | Akka Streams 的 Shpae 形状用于定义连接的输入输出端口。从 [`Shape` 文档](https://doc.akka.io/api/akka/current/akka/stream/Shape.html) 可以看出只是包含了 `Seq[Inlet[_]]` 一组入口和 `Seq[OutLet[_]]` 一组出口。其本身没有什么处理逻辑,需要在内部通过其它构件提供功能,对外则给出了定义的一组入口和一组出口。所有的 Graph 图都包含了一个 `Shape` 对象,用于指定其在流中的连接方式。比起 `Shape`, Graph 还包含了很多属性,比如异步边界属性,缓存属性以及日志属性等。下面是 Akka Streams 提供的形状及其对应的 Graph 组件: 8 | 9 | - `ClosedShape`: 无输入,无输出。对应一个 `RunnableGraph` 图。 10 | - `SourceShape[T]`: 无输入,单输出。对应一个 `Source` 图。 11 | - `FlowShape[I, O]`: 单输入,单输出。 对应一个 `Flow` 图。 12 | - `SinkShape[T]`: 单输入,无输出。 对应一个 `Sink` 图。 13 | - `UniformFanOutShape[I, O]`: 单输入,多输出(同类型)。 对应的图有 `Broadcast`, `Partition`, `Balance`。 14 | - `UniformFanInShape[I, O]`: 多输入(同类型),单输出。对应的图有:`Merge`, `MergePreferred` `MergePrioritiezed`, `InterLeave`, `Concat` 等。 15 | - `FanInShape2`, `FanInShape3`,,,: 多输入(多类型),单输出。对应的图有 `Zip`。 16 | - `FanOutShape2`,`FanOutShape3`,,,: 单输入,多输出(多类型)。对应的图有 `Unzip`。 17 | - `BidiShape`: 双输入(不同类型),双输出(不同类型)。对应的图有 `BidiFlow`。 18 | 19 | 具有 `SourceShape[T]`, `FlowShape[I, O]` 和 `SinkShape[T]` 形状的图可以通过 Flow DSL 的线性组合方式 (`viaMat` or `toMat`) 来使用。如果一个 Graph (已有的或应用定制的) 有多个的输入或输出,除了一些特例,通常无法通过 Flow DSL 的线性组合方式来使用。有多个输入或多个输出的图很多时候需要使用 Graph DSL 通过非线性组合方式生成 RunnableGraph ,然后实体化运行。这也是我们区分这二种 DSL 的初衷。 20 | 21 | 用 Flow DSL 处理多输入或多输出的特例情况是可以进行简单组合的一些场景,其本质还是 Graph DSL 一些常用模式的的简化。比如 [`Source.combine` 方法的文档](https://doc.akka.io/docs/akka/current/stream/operators/Source/combine.html) 给出了下面的例子: 22 | 23 | ```scala 24 | val source1 = Source(1 to 3) 25 | val source2 = Source(8 to 10) 26 | val source3 = Source(12 to 14) 27 | val combined = Source.combine(source1, source2, source3)(Merge(_)) 28 | combined.runForeach(println) 29 | ``` 30 | 31 | 该方法的签名为 32 | 33 | ```scala 34 | def combine[T, U](first: Source[T, _], second: Source[T, _], rest: Source[T, _]*)(strategy: Int => Graph[UniformFanInShape[T, U], NotUsed]): Source[U, NotUsed] 35 | ``` 36 | 37 | 可以看到,上面方法的第二个参数都是 `strategy: Int => Graph[UniformFanInShape[T, U], NotUsed]`,即用到一个多输入单输出的图。可以使用的构件包括 `Merge`, `Concat`, `ZipN`, `ZipWithN` 等。除了 `ZipWithN` 可以带一个函数对象,其它的都是简单的合并或拉链操作。 38 | 39 | ## 2 定制 Shape 40 | 41 | 可以看到系统提供的 Shape,除了 `ClosedShape` 和 `BidiShape` 这二个特例外, 都是单输入或单输出的。当系统提供的 Shape 不能满足要求时,Akka Streams 允许开发者定义多输入多输出的 Shape 来连接组件。例如一个二个输入口,三个输出口的形状就只能定制。Akka Streams 提供了简单的定制方法: 只需要继承抽象的 `Shape` class 并直截了当给出三个抽象成员的实现即可。示例代码如下: 42 | 43 | ```scala 44 | case class Shape2By3( 45 | in0: Inlet[Int], 46 | in1: Inlet[Int], 47 | out0: Outlet[Int], 48 | out1: Outlet[Int], 49 | out2: Outlet[Int] 50 | ) extends Shape { 51 | 52 | override def inlets: Seq[Inlet[_]] = List(in0, in1) 53 | override def outlets: Seq[Outlet[_]] = List(out0, out1, out2) 54 | override def deepCopy(): Shape = Shape2By3( 55 | in0.carbonCopy(), 56 | in1.carbonCopy(), 57 | out0.carbonCopy(), 58 | out1.carbonCopy(), 59 | out2.carbonCopy() 60 | ) 61 | } 62 | ``` 63 | 64 | 其使用方法和系统提供的 Shape 一样,用 Graph DSL 将其输入和输出端口通过其它构件连接起来。下面例子用一个二个输入口的 `Merge` 和一个三个输出口的 `Balance` 在内部连接,将二个输入流均衡的分配到三个输出口,对外则形成一个带有二个输入口和三个输出口的构件。最后再通过构建 RunnableGraph 来运行。 完整的代码如下: 65 | 66 | ```scala 67 | import scala.util.Success 68 | import akka.actor.ActorSystem 69 | import akka.NotUsed 70 | import akka.stream.{ClosedShape, Inlet, Outlet, Shape} 71 | import akka.stream.scaladsl.{Balance, GraphDSL, Merge, RunnableGraph, Sink, Source, Zip} 72 | 73 | // Step 1 定制形状 74 | case class Shape2By3( 75 | in0: Inlet[String], 76 | in1: Inlet[String], 77 | out0: Outlet[String], 78 | out1: Outlet[String], 79 | out2: Outlet[String] 80 | ) extends Shape { 81 | 82 | override def inlets: Seq[Inlet[_]] = List(in0, in1) 83 | override def outlets: Seq[Outlet[_]] = List(out0, out1, out2) 84 | override def deepCopy(): Shape = Shape2By3( 85 | in0.carbonCopy(), 86 | in1.carbonCopy(), 87 | out0.carbonCopy(), 88 | out1.carbonCopy(), 89 | out2.carbonCopy() 90 | ) 91 | } 92 | 93 | object Demo2By3 { 94 | def main(args: Array[String]) { 95 | implicit val system = ActorSystem("testStreams") 96 | implicit val ec = scala.concurrent.ExecutionContext.global 97 | 98 | // Step 2 创建连接构件 99 | val graph2By3 = GraphDSL.create() { 100 | implicit builder: GraphDSL.Builder[NotUsed] => 101 | import GraphDSL.Implicits._ 102 | val merge = builder.add(Merge[String](2)) 103 | val balance = builder.add(Balance[String](3)) 104 | 105 | merge ~> balance 106 | 107 | Shape2By3( 108 | merge.in(0), 109 | merge.in(1), 110 | balance.out(0), 111 | balance.out(1), 112 | balance.out(2) 113 | ) 114 | } 115 | 116 | // Step 3 基本组件 117 | import scala.concurrent.duration._ 118 | val source1 = Source.repeat("Repeat").throttle(3, 1.second).take(7) 119 | val source2 = Source.tick(0.second, 1.second, "Tick").take(3) 120 | 121 | val sink1 = Sink.foreach[String](message => println(s"Sink 1: ${message}")) 122 | val sink2 = Sink.foreach[String](message => println(s"Sink 2: ${message}")) 123 | val sink3 = Sink.foreach[String](message => println(s"Sink 3: ${message}")) 124 | 125 | // Step 4 创建 RunnableGraph 126 | val graph = RunnableGraph.fromGraph( 127 | GraphDSL.create() { implicit builder: GraphDSL.Builder[NotUsed] => 128 | import GraphDSL.Implicits._ 129 | 130 | val graph2By3Shape = builder.add(graph2By3) 131 | source1 ~> graph2By3Shape.in0 132 | source2 ~> graph2By3Shape.in1 133 | graph2By3Shape.out0 ~> sink1 134 | graph2By3Shape.out1 ~> sink2 135 | graph2By3Shape.out2 ~> sink3 136 | 137 | ClosedShape 138 | } 139 | ) // RunnableGrpah 140 | 141 | graph.run() 142 | } 143 | } 144 | 145 | /* 输出结果 146 | Sink 1: Repeat 147 | Sink 2: Tick 148 | Sink 3: Repeat 149 | Sink 1: Repeat 150 | Sink 2: Tick 151 | Sink 3: Repeat 152 | Sink 1: Repeat 153 | Sink 2: Repeat 154 | Sink 3: Repeat 155 | Sink 1: Tick 156 | */ 157 | 158 | ``` 159 | 160 | ## 3 生成实体化值 161 | 162 | 用于创建图的 `GraphDSL.create` 方法是一个重载方法,除了第一个签名(固定了实体化值为 `NotUsed`), 后续的签名在创建函数参数列表之前都有二个非空的参数列表: 163 | 164 | ```scala 165 | def create[S <: Shape]()(buildBlock: GraphDSL.Builder[NotUsed] => S): Graph[S, NotUsed] 166 | 167 | def create[S <: Shape, Mat](g1: Graph[Shape, Mat])(buildBlock: GraphDSL.Builder[Mat] => (g1.Shape) => S): Graph[S, Mat] 168 | 169 | def create[S <: Shape, Mat, M1, M2](g1: Graph[Shape, M1], g2: Graph[Shape, M2])(combineMat: (M1, M2) => Mat)(buildBlock: GraphDSL.Builder[Mat] => (g1.Shape, g2.Shape) => S): Graph[S, Mat] 170 | 171 | // 直到 g1, g2, ..., g22 172 | ``` 173 | 174 | 多出来的参数列表一个用于指定传入的一个或多个 Graph 参数,另一个指定相应的实体化值转换函数。下面是一个具体例子,关键的不同就是 `GraphDSL.create(sink1, sink2)(saveMats){ implicit builder => (sum, count) =>...}`。可以看到,Graph DSL 可以利用传入的图的实体化值生成新的实体化值。注意在创建时需要使用作为内部函数参数的 `sum` 和 `count` 变量,否则会因为端口没有连接产生编译错误。 175 | 176 | ```scala 177 | import scala.concurrent.Future 178 | import akka.actor.ActorSystem 179 | 180 | import akka.stream.scaladsl.{Broadcast, GraphDSL, Keep, Sink, Source} 181 | import akka.stream.SinkShape 182 | 183 | object MatDemo { 184 | def main(args: Array[String]) { 185 | 186 | implicit val system = ActorSystem("testStreams") 187 | implicit val ec = scala.concurrent.ExecutionContext.global 188 | 189 | val sink1 = Sink.reduce[Int](_ + _) 190 | val sink2 = Sink.fold[Int, Int](0)((count, _) => count + 1) 191 | 192 | def saveMats(mat1: Future[Int], mat2: Future[Int]) = 193 | for { 194 | v1 <- mat1 195 | v2 <- mat2 196 | } yield (v1.toDouble / v2) 197 | 198 | val sink = Sink.fromGraph(GraphDSL.create(sink1, sink2)(saveMats) { 199 | implicit builder => (sum, count) => 200 | import GraphDSL.Implicits._ 201 | 202 | val broadcast = builder.add(Broadcast[Int](2)) 203 | broadcast ~> sum 204 | broadcast ~> count 205 | SinkShape(broadcast.in) 206 | }) 207 | 208 | val mats = Source(1 to 10).runWith(sink) 209 | 210 | mats.onComplete { value => 211 | println(value) 212 | system.terminate() 213 | } 214 | } 215 | } 216 | 217 | // Output: Success(5.5) 218 | ``` 219 | 220 | ## 4 把实体化值转为数据单元 221 | 222 | Akka Streams 还有一个很有意思的功能:可以在创建 Graph 的时候把实体化值转换为输出的流数据单元。把上面例子稍做改变,就可以把实体化值转换为流数据,相关部分的代码如下: 223 | 224 | ```scala 225 | val flow = Flow.fromGraph(GraphDSL.create(sink1, sink2)(saveMats) { 226 | implicit builder => (sum, count) => 227 | import GraphDSL.Implicits._ 228 | 229 | val broadcast = builder.add(Broadcast[Int](2)) 230 | broadcast ~> sum 231 | broadcast ~> count 232 | 233 | // 把实体化值生成一个输出口,多次调用会生成多个 234 | val materialOut: Outlet[Future[Double]] = builder.materializedValue 235 | FlowShape(broadcast.in, materialOut.outlet) 236 | }) 237 | 238 | val mats = Source(1 to 10) 239 | .via(flow) 240 | .runForeach(_.onComplete(println)) 241 | 242 | mats.onComplete { 243 | case _ => system.terminate() 244 | } 245 | ``` 246 | 247 | 可以看到,`val materialOut: Outlet[Future[Double]] = builder.materializedValue` 每次调用会产生一个 `Outlet`,即一个输出端口,其值为当前 Graph 的实体化值 `Future[Double]`。 由于多出了一个输出端口,产生的图类型也变成了一个 `Flow` 类型。[Graph 实体化文档](https://doc.akka.io/docs/akka/current/stream/stream-graphs.html#accessing-the-materialized-value-inside-the-graph) 给出了更多说明和例子。 如文档所言,这个输出值是实体化的值,要避免内部引用形成循环。 248 | -------------------------------------------------------------------------------- /manuscript/ch02.md: -------------------------------------------------------------------------------- 1 | # Basic Concepts 2 | 3 | Akka Streams 是基于 Akka Actor 的反应式流处理 (reactive stream processing) 开源库。Akka Streams 库不仅提高了丰富的流数据处理功能,更为重要的是其提供了具有分形组合 Fractal Composition 能力的 Graph DSL,让开发者可以定制组合任意拓扑形状和功能的模块。设计和实现具有分形组合功能的 API 实现绝非易事,在最初的几年里 Akka Streams 的开发者重写了六遍才成为我们今天看到的样子。参见 [Reactive Streams, j.u.concurrent, & beyond!](https://www.infoq.com/presentations/streams-jdk/), 23 分 20 秒的视频。另一个贡献者 [Kerr](https://github.com/hepin1989) 告诉我实际是重写了七遍。其强大灵活的功能让初学者也容易发生见树不见林的情况。解锁的钥匙和学习其它复杂软件系统一样:建立清晰的概念,了解其基本实现机制,外加从简到繁的反复编码。本文试着对 Akka Streams 的一些主要概念给出定义,解释其背后的设计理念与实现机理,并在此基础上写出 Akka Streams 版的 Hello World 程序。 4 | 5 | ## 1 主要概念 6 | 7 | Akka Streams 有很多重要的概念,理解其含义、设计理念和运作机理对于开发很有帮助。可是 [Akka Streams 文档](https://doc.akka.io/docs/akka/current/stream/) 没有准确地定义诸多基本概念。为了方便理解和致敬写了六次才定型的系统,这些概念又可以分为六个核心概念和建立在核心概念上的六个基础概念(六个纯属巧合)。核心概念比较抽象,而基础概念通常有具体实现。 8 | 9 | ### 1.1 核心概念 10 | 11 | 下面列表是核心概念的定义。概念后面括弧里的为中文翻译,没有统一规范,列出来仅仅是帮助理解。为了避免词不达意,Lost in Translation,以后这些概念出现时还是用其英文单词以避免歧义。 12 | 13 | - Element (数据单元): 一个单位的数据。可以是几乎任何类型的数据,比如一个数字、字母或一条包含所有订单信息的记录。但是数据不可以是 `null`。应用程序决定数据的类型和大小。因为需要在网络间传输,element 应支持 serialization(序列化)和 deseerialization(反序列化)。当谈到流量控制和缓存大小时,指的是多少个 Element,而不是具体的 byte 字节的多少,因而和数据类型紧密相关。 14 | - Stream 或 Data Stream(流或数据流):一串按相对时间先后次序排列的有限或无限的 element。 15 | - Stream 或 Processing Stream(流或处理流):一串数据流经各个处理模块。可以看到,Stream 有时代表数据流,有时代表处理流。多数情况下都可以从上下文区分其指代的概念。 16 | - Backpressure(回压):Stream 按照数据流向可以看成为从上到下的一个流。相连的二个处理模块之间的数据发送是由下游驱动。如果下游没有 pull (拉)请求,如同产生逆向的压力,则上游的数据不应该 push(推)到下游。 17 | - Shape(形状):Akka streams 用图形描述处理流,因而每个处理模块都会有 Shape Shape 包含了所有的输入端口和所有的输出端口。输入端口和输出端口的数据类型及其连接方式决定了数据的处理路径。 18 | - Graph (图):一个流处理拓扑图中的基本模块或其组合。一个 Graph 包含了描述数据处理路径的 Shape,增加了处理属性(Attribute),比如定义异步边界、缓存参数等。多个小的 Graph 按照一定规则连接其输入输出端口可以 composite (组合)成各种拓扑结构的大 Graph。多个大 Graph 可以组合成更大的 Graph。Graph 可以看成是 Akka Streams 的 Fractal 分形。 19 | - GraphStage(图步):GraphStage 是 Graph 的子类,增加了具体数据处理逻辑。在 Akka Streams 的文档里面,很多时候说 Graph 的数据处理其实是指 GraphStage 的处理功能。 20 | - Operator(操作符):GraphStage 的处理功能可以通过 Scala 的 `implicit class` 成为 Graph 的扩展方法,这些方法称为 Operator。 21 | 22 | 可以看到上面这些核心概念涵盖了数据流(element, stream)、流的组合(shape, graph)、流数据处理(GraphStage, Operator),以及流量控制(backpressure)。所有的程序的可以看成包含二个核心概念:数据结构和函数。Akka streams 的数据结构是基于回压的数据流组合成的各种图,其函数则是对图中数据流的操作。 23 | 24 | ### 1.2 基础概念 25 | 26 | 在核心概念的基础上,Akka Streams 开发了流处理库的各种功能实现。一个简单的线性处理流程可以简化成三个基础概念: 27 | 28 | - Source (源点):只有一个输出端口的 Graph/Operator,代表各种形态的数据生产者。比如文件,网络,定时器,鼠标操作等。Source 代表数据的发布者(Publisher),会异步的产生数据,可以终止或永不终止。 29 | - Sink(终点):只有一个输入端口的 Graph/Operator,代表各种形态的数据消费者,比如打印,显式,结果计算等。Sink 代表数据的最终订阅者(Subscriber)。通常随着 Source 的终止而终止,也可以取消订阅。 30 | - Flow(管道):只有一个输入端口和一个输出端口的 Graph/Operator,代表了一个流处理的中间环节。 31 | 32 | 从定义可以看出,Source 的输出和 Flow 的输入连接后的组合是一个 Souce。同理,Flow 和 Sink 的组合是一个 Sink,而多个 Flow 的组合是一个 Flow。 一个流有二个方向:从 Sink 指向 Source 的方向是 `upstream` (上游),从 Source 指向 Sink 为 `downstream` (下游)。 33 | 34 | Akka Streams 把 Graph 的创建和执行分开,因而引出下面三个基础概念: 35 | 36 | - RunnableGraph(可执行图):一个 runnable(可执行)Graph 的基本要求是必须始于一个或多个 Source 而终于一个或多个 Sink 且所有的输入输出端口都按规则完全连接。在 Source 和 Sink 中间有多少 Graph,其连接形状如何则没有关系。完全连接的要求是指在创建 Graph 的过程中需要按照数据流向和数据类型连接所有的输入端口和输出端口。因此 RunnableGraph 中不存在没有输入连接的输入端口,也不存在没有输出连接的输出端口。 37 | - Materialization(实体化):分配系统资源并运行一个 RunnableGraph 来运行流处理的过程。RunnableGraph 是一个 blueprint(蓝图),如同函数的定义一样,只是定义了数据的处理方式,但是需要 Materializatioin 来运行实际数据处理。 38 | - Materialized Value(实体化值):当 materialize 一个 RunnableGraph 时,每个 operator/graph 会产生一个 materialized value。不同于流中的正常数据 element,这个 materialized value 可以是任何 materializaion 感兴趣的值:比如处理时间,处理的个数,处理的结果,运行的状态等等任何结果。实体化值可以和处理的数据有关或无关,有时候甚至是 `NotUsed`,代表不需要这个值。 39 | 40 | 一个 RunnableGraph 一旦生成,是 immutable (不可变)但是可以多次 Materializing 实体化执行。每次执行之间都是独立的。Materializing 负责准备运行所需要的各种资源比如文件、网络、内存和线程等,并启动运行。Materializaion 可以看成是运行环境和控制系统,因而有时候需要数据来管理运行。Akka Streams 设计成每个 Graph 都会产生一个 materialized value 来满足这方面的要求。因此一个 Source 需要指定二个数据类型 `Source[Out, Mat]`:输出数据类型和 materialized value 类型。同理,一个 Sink 需要指定一个输入数据类型和一个 materialized value 类型 `Sink[In, Mat]`;一个 Flow 则需要三个数据类型 `Flow[In, Out, Mat]`: 分别对应输入、输出和 materialized value。一个 RunnableGraph 是不可变的,但是可以多次实体化处理不同数据并生成不同的实体化值。 41 | 42 | 为了方便起见,一个 Stream(流)可以堪称包含了一系列 Element(数据单元)以及在各个 Segment 的 Processing(处理)。当不需要个别区分时,Source, Sink, Flow, Graph, Runnable Graph,以及 Operator 统称为处理单元 (processing unit)。 43 | 44 | ### 1.3 API 层次 45 | 46 | Akka Streams 的文档对初学者并不友好的一个原因是一开始在 [Streams Quickstart Guide](https://doc.akka.io/docs/akka/current/stream/stream-quickstart.html) 就把低层次的 API 以及复杂的拓扑结构用到入门的代码例子中,因而让学习曲线变得没有必要的陡峭。其实 Akka Streams 库有很好的 API 层次设计。其 API 可以分成三大类 47 | 48 | - 高级的 Flow DSL API 49 | - 中级的 Graph DSLAPI 50 | - 低级的 GraphStage DSL。 51 | 52 | Akka Streams 文档中并没有 Flow DSL 这个概念,是本文创造的一个帮助理解的词。那些只使用现有的 Operators,通过简单的线性组合方式使用的 API 统称为 Flow DSL 高级 API。由于 Akka Streams 提供了数以百计各种功能的 Operators,利用这些 Opertor 的简单线性组合已经能满足很多的日常数据处理要求,而且不容易出错,因此高级 API 是开发人员的首选。 53 | 54 | 当线性结构或已有的简单数据处理拓扑结构不能满足要求时,可以采用 Graph DSL 来组合已有的 Operators 组成任意拓扑结构的 Graph。这是中级 API。中级 API 提供了建立各种拓扑结构的工具。 55 | 56 | 当需要定制有新的处理功能的 Operator 时,采用 GraphStage DSL 来定制 Graph 的内部处理逻辑和输入输出接口处理。GraphStage DSL 提供了状态维护、流量控制、接口定义等各种低级 API。这些 API 有复杂的处理模式但是提供了最大的灵活性。 57 | 58 | 明白这种分层的好处是初学者可以从简单易学的高级 Flow DSL 开始,掌握了一些基本概念之后,再接触中级 Graph DSL 和低级 GraphStage DSL 会变得相对容易。 59 | 60 | 这种层次结构也解释了 Akka Streams 文档里面经常交替使用的三个相似的概念: Graph, Operator,Stage。Graph 偏重模块的图形拓扑维度,Operator 偏重模块的数据处理维度,Stage 则强调模块在流处理中的可组合属性。多数情况下这些概念可以互换,都是用于描述流处理中的可以被灵活组合的一个 Component(组件)。 61 | 62 | ## 2 Stream 和 Backpressure 63 | 64 | 最简单的流处理包含一个 Source 和一个 Sink。其交互模式如下: 65 | 66 | ![source-and-sink](../imgs/source-sink.png) 67 | 68 | 首先可以看到在 Source 和 Sink 之间有二条通信通道,一条是单向的 Elements 数据通道,从 Source 到 Sink。 另一条是双向控制信号通道。Sink 可以发送 Pull 请求和 Cancel 请求。Source 可以发送 Completion 请求 和 Failure 请求。这三种控制信号概括了所有的上下游的交互模式,可以推广到包含 Flow 在内各种 Graph/Operator 交互。Cancel, Completion, Failure 中任何一个信号的出现都意味着上下游连接关系的正常或非正常结束。 69 | 70 | Akka Streams 采用 backpressure 流量控制机制。当 Sink 发出 Pull 请求时,Source 才会 Push element 给 Sink。由于 Akka Streams 的分布式设计,Source 和 Sink 可能在不同的线程甚至不同计算机,考虑到效率,接收端会有一个 Buffer 来批量请求和接收数据。缺省为缓存 16 个 elements。初始运行时,Sink 会发出一个 Pull 请求 Buffer 尺寸大小的 Elements,Source 在数据可用时会持续发送。如果 Sink 处理速度慢而 Buffer 已满,则 Source 未接到新的 Pull 请求时停止发送。如果 Sink 处理了一半的 Buffer,则会发送下一个 Pull 请求 Source 发送 eleemnts 填满完成的那一半。Buffer 的尺寸以及溢出策略在组合 Graph 时可以设置。这种 backpressure 机制做用于 RunnableGraph 里面的所有连接,每一个上游都会受其下游的 backpressure 流量控制。这样保证了端到端的流量控制策略。如果 Sink 是用户屏幕,在用户滚动到下一屏之前,Source 不会发送超过接收端 Buffer 尺寸的数据。很难想象,如此简单的 backpressure 机制,再配合几种简单的 overflow 溢出处理策略竟然优雅高效地完成了看似复杂而非常有用的端到端流量控制。 71 | 72 | 有必要说明,Operators 可以在不同的线程甚至不同 JVM 里面,这种称为不同的 async boundary(异步边界)。最初的 Akka Streams 版本里每个 Operator 都在不同的 Akka Actor 里面运行。Operators 在不同线程运行有些情况下可以增加并发度,进而提高处理效率和吞吐量。从 Akka Streams 2.5 开始,当组合 operators 时,如果不调用 `async()` 方法,这些 operators 会运行在同一个 async boundary,也就是同一个 Akka Actor 里面。作为一个优化手段,如果上下游的 operators 在同一个 async boundary 里面,也就是运行在同一个线程里面,那么上述的 Buffer 和流处理机制就简化掉了,Akka Streams 称其为 Fusion (聚合)。Fusion 模式时上下游 Operators 之间直接用共享内存访问 elements,省却了流量控制的麻烦。 73 | 74 | ## 3 Materialization 和 Materialized Value 75 | 76 | Materialization 就是分配系统资源来运行一个始于 Source 终于 Sink 的 RunnableGraph。其中的一个主要任务就是提供运行 RunnableGrapsh 的线程池。`RunnableGraph` 类有一个 `def run()(implicit materializer: Materializer): Mat = materializer.materialize(this)` 方法。可见其用一个 implicit `Materializer` 来运行一个 RunnableGraph。从 Akka Streams 2.6 版开始,当创建一个 implicit `ActorSystem` 的时候,Akka Streams 的缺省配置会用一个 [`Akka.actor.Extension`](https://doc.akka.io/api/akka/current/akka/actor/Extension.html) 自动创建一个 implicit `Materializer`。因此只要在当前环境中创建了一个 implicit `ActorSystem` 就可以运行 Materialization 了。运行时候,RunnableGraph 里面的每一个 Operator 都会在 stream 的正常 elements 之外产生一个 Materialized value, 如下图所示: 77 | 78 | ![materialization](../imgs/materialization.png) 79 | 80 | 图中包含三个 Operators:Source, Flow, 和 Sink。 每一个都会产生一个 materialized value。当组合二个 Operator 时需要指定保留哪个值。每个组合方法会额外要求一个形式为 `(Mat, Mat2) => Mat3` 的函数作为参数,从二个 materialized values 产生一个新的值。通常这个值可以是第一个,第二个,二个的组合或返回 `NotUsed`。 Akka Streams 提供了四个帮助函数: `Keep.left`, `Keep.right`, `Keep.both`, `Keep.none`。 81 | 82 | 对 `Source`, `Flow` 和 `Sink` 这三种基本 Graph,Akka Streams 提供了二种基本组合方法:用 `viaMat` 连接下一个 `Flow`, 用 `toMat` 连接最终的 `Sink`。最后组合 Sink 的 `toMat` 方法返回的 materialized value 会作为整个 RunnableGraph 的 materialized value。 83 | 84 | ## 4 Hello World 85 | 86 | 到此终于有足够的理论基础写 Akka Streams 版本的 “Hello World”,代码如下: 87 | 88 | ```scala 89 | implicit val system = ActorSystem("helloWorld") 90 | 91 | val source: Source[String, NotUsed] = Source.single("Hello World") 92 | val sink: Sink[String, Future[Done]] = Sink.foreach[String](println) 93 | 94 | val runnableGraph: RunnableGraph[Future[Done]] = source.toMat(sink)(Keep.right) 95 | val matValue: Future[Done] = runnableGraph.run() 96 | 97 | implicit val ec = scala.concurrent.ExecutionContext.Implicits.global 98 | matValue.onComplete(_ => system.terminate()) 99 | ``` 100 | 101 | Akka Streams 运行在 Akka Actor 系统上,因此需要创建一个 implicit `ActorSystem`。`ActorSystem("helloWorld")` 的调用同时也创建了一个 implicit `Materializer` 实例,该实例提供了后面 `runnableGraph.run()` 方法的第二个隐含参数。 102 | 103 | `Source.single("Hello World")` 是产生一个单一 element 的 Soruce, 可以看到其返回的结果是一个类型为 `Source[String, NotUsed]` 的值,输出的数据类型为 `String`, materialized value 类型为 `NotUsed`,代表一个没用的值。 104 | 105 | `Sink.foreach[String](println)` 则生成一个 Sink 值。该实例打印每一个输入的字符串,在整个流处理运行完成之后会产生一个类型为 `Future[Done]` 的 materialized value。所以其对应的数据类型为 `Sink[String, Future[Done]]`。 106 | 107 | 下一语句中的 `source.toMat(sink)(Keep.right)` 则通过 `Source.toMat`方法把 Source 的输出和 Sink 的输入连接起来,从而创建了一个 RunnableGraph 值,其类型为 `RunnableGraph[Future[Done]]`。因为 `toMat` 方法右边是 Sink,其 materialized value 类型是 `Future[Done]` 而 `toMat` 的第二个参数是 `Keep.right`,因此 RunnableGraph 最终的 materialized value 类型是 `Future[Done]`。 如果 `toMat` 的第二个参数是 `Keep.left`, 则最终 materialized value 类型会成为 `NotUsed`。如果 `toMat` 的第二个参数是 `Keep.both`, 则最终 materialized value 类型会成为一个 tuple 值 `(NotUsed, Future[Done])`。在本例中,因为 Sink 的 materialized value 可以用于知道打印完成的时机用于清理 Akka Actor 的系统资源,所以采用了 `Keep.left` 来获得这个值。 108 | 109 | `runnableGraph.run()` 利用 `Materializer` 提供的线程池来异步运行数据处理功能。会和定义组合 Graph 的代码不在同一线程。 110 | 111 | `Future.onComplete()` 回调函数第二个参数是一个隐含的 ExectuionContext 执行环境参数。`implicit val ec = scala.concurrent.ExecutionContext.Implicits.global` 提供了所需的参数值。另外一个可用的选择是 `implicit val ec = system.dispatcher`,即使用现成的 ActorSytem 自带的 ExectuionContext。最后一句 `matValue.onComplete(_ => system.terminate())` 终止程序运行。 112 | 113 | 如 `source.toMat(sink)(Keep.right)` 所展示的,因为连接到 Sink 并保留 Sink 的 materialized value 是个常见的模式, Akka Streams 提供了一个简单的方法 `runWith()` 来合并连接与运行。也就是说 `source.runWith(sink)` 是 `source.toMat(sink)(Keep.right).run()` 的简化版。相关程序简化如下: 114 | 115 | ```scala 116 | val source: Source[String, NotUsed] = Source.single("Hello World") 117 | val sink: Sink[String, Future[Done]] = Sink.foreach[String](println) 118 | 119 | val matValue: Future[Done] = source.runWith(sink) 120 | ``` 121 | 122 | 更进一步,因为 `Sink.foreach` 常用于数据的最终处理,Source 提供了简捷的 `runForeach` 方法来做上述繁琐的步骤。也就是说 `source.runForeach(println)` 等同于 `source.runWith(Sink.foreach(println))`, 也等同于更长的 `source.toMat(Sink.foreach(println))(Keep.right).run()`. 这一个 `runForEach` 方法完成了创建 Sink, 连接 Sink 以及运行整个 RunnableGrpah 的诸多步骤。类似的快捷模式在 Akka Streams API 里面很常见。一旦理解其实现原理,可以看到 Akka Streams API 的系统设计具有高度一致性。 123 | 124 | 去掉中间变量和类型标注,完整的可运行简化版如下: 125 | 126 | ```scala 127 | package com.sample 128 | 129 | import akka.actor.ActorSystem 130 | import akka.stream.scaladsl.Source 131 | 132 | object HelloWorld extends App { 133 | implicit val system = ActorSystem("helloWorld") 134 | 135 | val matValue = Source.single("Hello World").runForeach(println) 136 | 137 | implicit val ec = scala.concurrent.ExecutionContext.Implicits.global 138 | matValue.onComplete(_ => system.terminate()) 139 | } 140 | ``` 141 | 142 | 值得注意的是,作为面向分布式异步执行的系统,上面四行代码牵涉到三个 `ExecutionContext`: Akka Actor 的 ActorSystem,Akka Streams 的 Materializer,以及 Future 的回调执行。Akka Streams 的 Materializer 缺省使用 ActorSystem 的运行环境,所以本例中实际用到二个执行环境。使用 `scala.concurrent.ExecutionContext.Implicits.global` 而不是复用 `system.dispatcher`, 在本例这种简单情况没有必要。之所以这么做是为了表明在实际系统开发中,为了避免干扰 Akka Streams 的本身运作,应用程序自身用到的异步操作应该配置使用额外的、适合其本身特点(CPU 密集或 IO 密集)的线程池作为执行环境。 143 | 144 | Hello world,欢迎来到一个异步、并发、不会过劳而崩的世界! 145 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /manuscript/ch03.md: -------------------------------------------------------------------------------- 1 | # Operator Composition 2 | 3 | 一个 Stream 通常会有多个处理环节。往下,每个处理环节可以分解为更细的处理步骤。往上,每个处理环节也可以看成是更大的处理环节的一部分。因此 Stream 处理会有不同层次的抽象。多个 Stream 的处理流程或者不同的抽象层次会共享一些处理功能,处理模块必须可以按照业务需要进行组合。强大、灵活的模块组合是 Akka Streams 最重要的功能之一。本文介绍了基本的线性组合以及给出相应代码例子。 4 | 5 | ## 1 基本 Operator 6 | 7 | Akka Streams 有三个基本的 Opertors: 8 | 9 | - Source (源点):只有一个输出端口的 Graph/Operator,代表各种形态的数据生产者。比如文件,网络,定时器,鼠标操作等。 10 | - Sink(终点):只有一个输入端口的 Graph/Operator,代表各种形态的数据消费者,比如打印,显式,结果计算等。 11 | - Flow(管道):只有一个输入端口和一个输出端口的 Graph/Operator,代表了一个流处理的中间环节。 12 | 13 | Akka Streams 区分不可变的流处理蓝图(blueprint)和其运行(materialization)。每个 Operator 在 Materaialization 时会产生一个 materialized value。在后面的例子可以看到,这个 materialized value 可以是各种与 stream 中的 element 有关或无关的任何值,包括无用值 `NotUsed`。 14 | 15 | ### 1.1 Source 16 | 17 | Source 代表了数据源或生产者。Akka Streams 在 Source 的伴生对象里定义了很多创建 Source 的工厂方法。下面是一些常用于生产演示数据的方法: 18 | 19 | - `Source.apply`: 其类型为 `Iterable[T] => Source[T, NotUsed]`, 即从一个 `Iterable` 参数创建一个 Source 对象,其中的 element 数据类型和 Iterable 一致,materialized value 则为无用值 `NotUsed`。`Source(1 to 10)` 创建了一个 `Source[Int, NotUsed]` 的对象,会产生从 1 到 10 的 10 个整数。`Source(LazyList.from(1))` 利用 `LazyList` 产生从 1 开始的无穷序列整数。 20 | - `Soruce.single`: 其类型为 `[T] => Source[T, NotUsed]`, 即从单个数据参数创建一个只有一个 element 的 Source 对象。比如 `Source.single("Hello World")` 会产生一个 `"Hello World"` 字符串。 21 | - `Soruce.repeat`: 其类型为 `[T] => Source[T, NotUsed]`, 即从单个数据参数创建一个无限重复输入参数的 Source 对象。比如 `Source.repeat(42)` 会产生一个无限的数据流 `42 42 42 ....`。 22 | - `Source.tick`: 其类型为 `[FiniteDuration, FiniteDuration, T] => Source[T, Cancellable]`, 即从单个数据参数创建一个定时产生 element 的 Source 对象。比如 `Source.tick(1.second,3.seconds, 42)` 会从 1 秒后开始每隔 3 秒钟产生一个 42。注意,此时的 materialized value 是一个可以用于取消该数据源的 `Cancellable` 对象。 23 | 24 | ### 1.2 Flow 25 | 26 | Flow 代表了有一个输入和一个输出的 Operator,因而有代表输入,输出和 maeterialized value 的三个数据类型 `Flow[In, Out, Mat]`。 Flow 定义了很多方法,可以通过 `Flow[T]` 来调用,`T` 是输入端口的 element 的数据类型。stream 很多情况下可以看成一个数据集合,因此可以看到常见的集合方法比如: 27 | 28 | - `map[T](f: Out => T)`: 用转换函数处理每一个 element。此处 `T` 是处理后的输出端口的 element 数据类型。比如 `Flow[Int](_ + 1)` 给每个 element 加一。 29 | - `filter(p: Out => Boolean)`: 用过滤函数处理每一个 element,只有过滤函数返回 `true` 时才把收到的 element 送往下游。比如 `Flow[Int].filter(_ % 2 == 0)` 只让偶数通过。 30 | - `take(n: Long)`: 只取前 n 个 element,然后向上游发出 cancel 信号并结束处理。 31 | - `fold[T](zero: T)(f: (T, Out) => T)`: 类似集合的 `fold` 方法,以 `zero` 为初始值用指定的方法处理每个 element 直到上游完成。此处 `T` 是处理后的输出端口的 element 数据类型。比如 `Flow[Int].fold(0)(_ + _)` 计算总和。如果第一个元素作为初始值,则可以用简化的 `Flow[Int].reduce(_ + _)`。 32 | - `log(name: String)`: 日志记录 element 以及完成和错误信号。element 和 完成用 debug 级别,错误信号用 error 级别。日志级别以及日志对象都可以配置。 33 | 34 | 在具体实现中,`Flow.apply[T]` 返回一个类型为 `Flow[T, T, NotUsed]` 对象,该对象只是简单的把 element 传给下游,不做任何处理。Flow 类的很多方法其实是生成了新的 Operator 来处理数据,所以输出以及 materialized value 的类型也会发生变化。上面所列方法的 materialized value 值是 `NotUsed`。 35 | 36 | ### 1.3 Sink 37 | 38 | `Sink[In, Mat]` 通常代表数据处理的终点,其定义包含了一个输入数据类型和 materialized value 数据类型。其数据处理方法多定义在伴生对象上。常用的方法如下: 39 | 40 | - `foreach[T](f: T => Unit): Sink[T, Future[Done]]`: 对每个输入的 element 运行指定的函数。因为是数据处理的终点,所以指定的函数也返回空值 `Unit`。常用的例子是 `foreach(println)` 打印最终的结果。Materialized value 是一个 `Future` 值。stream 正常结束时, Future 返回 `Success(Done)`, 否则返回 `Failure(error)`。这个返回值可以用于判断 Materialization 完成的时机和状态。 41 | - `ignore: Sink[Any, Future[Done]]`: 丢弃收到的 element。 42 | - `head[T]: Sink[T, Future[T]]`: 把第一个 element 放入 `Future` 作为运行完成后的 materializaed value。 43 | - `last[T]: Sink[T, Future[T]]`: 把最后一个 element 放入 `Future` 作为运行完成后的 materializaed value。 44 | - `fold[U, T](zero: U)(f: (U, T) => U): Sink[T, Future[U]]` 把 fold 的结果放入 `Future` 作为运行完成后的 materializaed value。 45 | - `onComplete[T](callback: Try[Done] => Unit): Sink[T, NotUsed]`: Materializaion 完成时,以完成的状态 `Try[Done]` 为参数运行回调函数。 46 | 47 | ### 1.4 例子 48 | 49 | 下面给出一些 Operator 例子,在后面用于说明不同的组合方式。 50 | 51 | ```scala 52 | // a source generate numbers from 1 to 10, 也可以写成 Source[Int](1 to 10) 53 | val oneToTenSource: Source[Int, NotUsed] = Source[Int](1 to 10) 54 | val helloSource: Source[String, NotUsed] = Source.single("Hello World") 55 | 56 | // print element of Int type, the foreach method comes with an Int type 57 | val printIntSink: Sink[Int, Future[Done]] = Sink.foreach[Int](println) 58 | 59 | // print element of Any type, notice that foreach method doesn't have a type 60 | val printAnySink: Sink[Any, Future[Done]] = Sink.foreach(println) 61 | 62 | // it is recommended to give the data type. notice that foreach method has a type tag 63 | val printStrSink: Sink[String, Future[Done]] = Sink.foreach[String](println) 64 | 65 | // calculate the sum of all elements 66 | val sumSink: Sink[Int, Future[Int]] = Sink.fold[Int, Int](0)(_ + _) 67 | 68 | // add one to each element 69 | val addOneFlow: Flow[Int, Int, NotUsed] = Flow[Int].map[Int](_ + 1) 70 | 71 | // convert an Int into a string 72 | val intToStrFlow: Flow[Int, String, NotUsed] = 73 | Flow[Int].map[String](_.toString) 74 | 75 | // get string length 76 | val strLengthFlow: Flow[String, Int, NotUsed] = 77 | Flow[String].map[Int](_.length) 78 | ``` 79 | 80 | 上面每个 Opertor 都给出来相应的输入,输出以及 MAT 类型。多数的方法都可以标注类型说明,比如 `Source(1 to 10)` 可以写成 `Source[Int](1 to 10)`,`Flow[String].map(_.length)` 可以写成 `Flow[String].map[Int](_.length)`。这二例中的方法(第一个其实是`Source.apply[T]`)的方法类型都可以省略是因为 Scala 编译器可以推断出正确的数据类型。有时候省略数据类型会得到不同的结果。比如 `Sink.foreach[Int](println)` 和 `Sink.foreach(println)` 创建了不同类型的结果:一个是 `Sink[Int, Future[Done]]` 打印整数,而另一个是 `Sink[Any, Future[Done]]` 可以接受任何类型的参数。谨慎起见,最好还是在方法上给出具体的数据类型。 81 | 82 | ## 2 基本组合 83 | 84 | Akka Streams 提供了二个基本组合方法:`viaMat` 和 `toMat`。其使用方法可以参考下图: 85 | 86 | ![basic composition](../imgs/basic-composition.png) 87 | 88 | Akka Streams 流处理的最小可运行单位称为一个 runnable Grpah(可执行图)。一个 runnable Graph 的基本要求是必须始于一个只有一个输出的 Source 而终于一个只有一个输入的 Sink。如上图所示,通常一个 runnable Graph 中间还包括一个或多个只有一个输入和一个输出的 Flow(管道),且所有的输入输出端口都按完全连接。 89 | 90 | `viaMat` 用于连接 Source 或 Flow 的输出端口到 Flow 的输入端口,`toMat` 用于连接一个 Source 或 Flow 的输出端口到 Sink 的输入端口。Stream 中的 Elements 通过相连的输入输出端口流动,Materialization 运行时按照 runnable Graph 蓝图描述的处理逻辑被各个 Operator 处理。`viaMat` 与 `toMat` 由于连接了二个 Opertors,会需要一个额外的参数 `combine: (Mat, Mat2) => Mat3` 从其连接的二个 Operator 的 materializaed value 生成组合后的 materialized value。 91 | 92 | `toMat` 方法的结果是 `RunnableGraph[Mat]`, 其中的 `Mat` 是整个 runnable Graph 的 materialized value 的数据类型。调用 `RunnableGraph.run()` 来运行 Materialization。 93 | 94 | 下面例子中的所有的 Operators 都来自前面的定义。 95 | 96 | ### 2.1 Source + Sink = RunnableGraph 97 | 98 | 一个 runnable Grpah(可执行图)需要一个 Source 和一个 Sink。 99 | 100 | ```scala 101 | // Source + Sink = RunnableGraph 102 | val helloRunnable: RunnableGraph[Future[Done]] = 103 | helloSource.toMat(printAnySink)(Keep.right) 104 | val helloMaterialized: Future[Done] = helloRunnable.run() 105 | 106 | val sumRunnable: RunnableGraph[Future[Int]] = 107 | oneToTenSource.toMat(sumSink)(Keep.right) 108 | val sumMaterialized: Future[Int] = sumRunnable.run() 109 | 110 | // type mismatch, doesn't compile 111 | // helloSource.viaMat(printIntSink) 112 | ``` 113 | 114 | 在 `val helloRunnable: RunnableGraph[Future[Done]] = helloSource.toMat(printAnySink)(Keep.right)` 这个简单的组合有二个需要注意的地方。第一,相连的输入与输出需要有匹配的数据类型。`helloSource` 的输出是 `String`, 而 `printAnySink` 的输入是 `Any`,所以组合没有问题。如果改成 `helloSource.viaMat(printIntSink)`,则因为输出是 `String`, 而 `printIntSink` 的输入是 `Int` 造成数据类型不匹配而无法编译。其次,`toMat(sink)(combine)` 有二个参数,第一个参数是连接的 Sink,第二个参数 `combine` 是一个组合函数。 `combine` 的类型是 `(Mat, Mat2) => Mat3`,该函数从 `toMat` 左边 Operator 和右边 Operator 的二个 materialized value 组合生成最终的 materialized value。此处保留了右边也就是 Sink 的 materialized value 因而最终的结果是 `RunnableGraph[Future[Done]]`。运行后的结果就是 `val helloMaterialized: Future[Done] = helloRunnable.run()`。这个 `Future[Done]` 结果可以用于二个目的:Materialization 结束是可以运行 callback(回调函数),返回结果是个 `Try[Done]` 数据类型可以用于判断运行的状态是 `Success` 还是 `Failure`。 115 | 116 | 在 `val sumRunnable: RunnableGraph[Future[Int]] = oneToTenSource.toMat(sumSink)(Keep.right)` 例子中,Materialization 的执行是 `val sumMaterialized: Future[Int] = sumRunnable.run()`,其结果是数据的总和。需要返回什么样的 materialized value 完全是按照应用的需要来定义。 117 | 118 | ### 2.2 Source + Flow = Flow 119 | 120 | 一个 Source 和一个 Flow 组合成一个新的 Source,输出数据类型也变成 Flow 的输出数据类型。如下例所示: 121 | 122 | ```scala 123 | val addOneSource: Source[Int, NotUsed] = oneToTenSource.viaMat(addOneFlow)(Keep.left) 124 | val oneToTenStrSource: Source[String, NotUsed] = oneToTenSource.viaMat(intToStrFlow)(Keep.left) 125 | ``` 126 | 127 | 可以看到 `oneToTenSource.viaMat(intToStrFlow)` 组合中, 数据类型由 `Source[Int, NotUsed]` 变成 `Source[String, NotUsed]`。由于 `viaMat` 二边的 materialized value 类型都是 `NotUsed`,所以用 `Keep.left` 和用 `Keep.right` 没有差别。 128 | 129 | ### 2.3 Flow + Sink = Sink 130 | 131 | 一个 Flow 和一个 Sink 组合成一个新的 Sink,输入数据类型也变成 Flow 的输入数据类型。 132 | 133 | ```scala 134 | val addOneSink: Sink[Int, NotUsed] = addOneFlow.toMat(printIntSink)(Keep.left) 135 | val intToStrSink: Sink[Int, Future[Done]] = intToStrFlow.toMat(printStrSink)(Keep.right) 136 | ``` 137 | 138 | 组合后的 `addOneSink` 输入类型为 `addOneFlow` 的输入类型 `Int`。其 Mat 类型为左边 `addOneFlow` 的 Mat 类型 `NotUsed`。组合后的 `intToStrSink` 输入类型为 `intToStrFlow` 的输入类型 `String`。其 Mat 类型为右边 `printStrSink` 的 Mat 类型 `Future[Done]`。 139 | 140 | ### 2.4 Flow + Flow = Flow 141 | 142 | 一个 Flow 和一个 Sink 组合成一个新的 Flow。第一个 Flow 的输入和第二个 Flow 的输出成为新 Flow 的输入输出。 143 | 144 | ```scala 145 | val addOneStrFlow: Flow[Int, String, NotUsed] = 146 | addOneFlow.viaMat(intToStrFlow)(Keep.left) 147 | 148 | val intLenFlow: Flow[Int, Int, NotUsed] = 149 | intToStrFlow.viaMat(strLengthFlow)(Keep.left) 150 | ``` 151 | 152 | ### 2.5 Source + Flow(s) + Sink = RunnableGraph 153 | 154 | 通常的流处理包含一个 Source, 一个或多个 Flow,终结于一个 Sink,最终是一个可以执行 `run()` 来完成 Materialization 的 RunnableGraph。最终的 Mat 数据类型是 `toMat` 组合最终的 Sink 时生成的 Mat 数据类型。 155 | 156 | ```scala 157 | val addOneRunnable: RunnableGraph[NotUsed] = 158 | oneToTenSource.viaMat(addOneFlow)(Keep.left).toMat(printIntSink)(Keep.left) 159 | 160 | val addOneSumRunnable: RunnableGraph[Future[Int]] = 161 | oneToTenSource 162 | .viaMat(addOneFlow)(Keep.left) 163 | .viaMat(addOneFlow)(Keep.left) 164 | .toMat(sumSink)(Keep.right) 165 | ``` 166 | 167 | 第一个例子的 RunnableGraph 的 Mat 值用 `Keep.Left` 选择 `toMat` 左边 `addOneFlow` 的值。第二个则用 `Keep.right` 选择右边 `sumSink` 的值。同时因为所有的 Operator 都是 Immutable 的数据处理蓝图,可以在一个或多个 RunnableGraph 里面重复使用。 168 | 169 | ## 3 简化 API 170 | 171 | 可以看到很多的 Flow 的 Mat 值是 `NotUsed`,还有很多时候都在 RunnableGraph 中处理数据的正常或非正常情况,也不关心 最终 Materialzation 的结果。所以 Akka Streams 提供了一些简化 API 来减少代码量。常见的有: 172 | 173 | - `via(flow)`: 作为 Source 或 Flow 的方法,等同于 `viaMat(flow)(Keep.left)` 174 | - `to(sink)`: 作为 Source 或 Flow 的方法,等同于 `toMat(sink)(Keep.left)` 175 | - `source.runWith(sink)`: 作为 Source 的方法,等同于 `source.toMat(sink)(Keep.right).run()` 176 | 177 | 稍复杂一些的还有: 178 | 179 | - `sink.runWith(source)`: 等同于 `source.to(sink).run()` 180 | - `sink.foreach[T](f: T => Unit)`: 等同于 `Flow[T].map(f).toMat(Sink.ignore)(Keep.right)` 181 | - `sink.fold(zero: U)(f: (U, T) => U)`: 等同于 `Flow[T].fold(zero)(f).toMat(Sink.head)(Keep.right)` 182 | - `source.runForeach(f: T => Unit)`: 等同于 `source.runWith(Sink.foreach(f)` 183 | - `source.runFold(zero: U)(f: (U, Out)`: 等同于 `source.runWith(Sink.fold(zero)(f))` 184 | - `flow.runWith(source, sink)`: 等同于 `source.via(flow).toMat(sink)(keep.both).run()` 185 | 186 | 可以看到,这些稍微复杂的 API 无非还是各种简单组合的组合。这些方法通常会创建需要的 Soruce, Flow,或 Sink 来组合成新的 Operator。一旦明白了基本组合,输入输出数据的接口匹配,以及 Mat 值的生成,简化的 API 和更复杂的组合也不难理解。下面是一些代码例子: 187 | 188 | ```scala 189 | // shortcut for oneToTenSource.viaMat(addOneFlow)(Keep.left) 190 | oneToTenSource.via(addOneFlow) 191 | 192 | // shortcut for toMat(printIntSink)(Keep.left) 193 | addOneFlow.to(printIntSink) 194 | 195 | // shortcut for oneToTenSource.toMat(printIntSink)(Keep.right).run() 196 | oneToTenSource.runWith(printIntSink) 197 | 198 | // shortcut for 199 | // oneToTenSource.viaMat(addOneFlow)(Keep.left).viaMat(addOneFlow)(Keep.left).toMat(printStrSink)(Keep.left) 200 | oneToTenSource.via(addOneFlow).via(intToStrFlow).to(printStrSink) 201 | 202 | // shortcut for 203 | // oneToTenSource.via(addOneFlow).toMat(printIntSink)(Keep.both).run() 204 | val result: (NotUsed, Future[Done]) = 205 | addOneFlow.runWith(oneToTenSource, printIntSink) 206 | ``` 207 | 208 | 最后一例中的 `result` 类型为 `(NotUsed, Future[Done])`。这是因为 `Keep.both` 组合了二边的 Mat 值。 209 | 210 | ## 4 完整代码 211 | 212 | 本文用的所有代码如下: 213 | 214 | ```scala 215 | package com.sample 216 | 217 | import akka.actor.ActorSystem 218 | import akka.{Done, NotUsed} 219 | import akka.stream.scaladsl.{Flow, Keep, RunnableGraph, Sink, Source} 220 | 221 | import scala.concurrent.Future 222 | 223 | object Main extends App { 224 | 225 | implicit val system = ActorSystem("demo") 226 | implicit val ec = system.dispatcher 227 | 228 | // Sources 229 | 230 | // a source generate numbers from 1 to 10, 也可以写成 Source[Int](1 to 10) 231 | val oneToTenSource: Source[Int, NotUsed] = Source[Int](1 to 10) 232 | // a single string source 233 | val helloSource: Source[String, NotUsed] = Source.single("Hello World") 234 | 235 | // Sinks 236 | 237 | // print element of Int type, the foreach method comes with an Int type 238 | val printIntSink: Sink[Int, Future[Done]] = Sink.foreach[Int](println) 239 | 240 | // print element of Any type, notice that foreach method doesn't have a type 241 | val printAnySink: Sink[Any, Future[Done]] = Sink.foreach(println) 242 | 243 | // it is recommended to give the data type. notice that foreach method has a type tag 244 | val printStrSink: Sink[String, Future[Done]] = Sink.foreach[String](println) 245 | 246 | // calculate the sum of all elements 247 | val sumSink: Sink[Int, Future[Int]] = Sink.fold[Int, Int](0)(_ + _) 248 | 249 | // Flows 250 | 251 | // add one to each element 252 | val addOneFlow: Flow[Int, Int, NotUsed] = Flow[Int].map[Int](_ + 1) 253 | 254 | // convert an Int into a string 255 | val intToStrFlow: Flow[Int, String, NotUsed] = 256 | Flow[Int].map[String](_.toString) 257 | 258 | // get string length 259 | val strLengthFlow: Flow[String, Int, NotUsed] = 260 | Flow[String].map[Int](_.length) 261 | 262 | // Composition examples 263 | 264 | // Source + Sink = RunnableGraph 265 | val helloRunnable: RunnableGraph[Future[Done]] = 266 | helloSource.toMat(printAnySink)(Keep.right) 267 | val helloMaterialized: Future[Done] = helloRunnable.run() 268 | 269 | val sumRunnable: RunnableGraph[Future[Int]] = 270 | oneToTenSource.toMat(sumSink)(Keep.right) 271 | val sumMaterialized: Future[Int] = sumRunnable.run() 272 | 273 | // type mismatch, doesn't compile 274 | // helloSource.viaMat(printIntSink) 275 | 276 | // Source + Flow = Flow 277 | // 一个 Source 和一个 Flow 组合成一个新的 Source,输出数据类型也变成 Flow 的输出数据类型。 278 | val addOneSource: Source[Int, NotUsed] = 279 | oneToTenSource.viaMat(addOneFlow)(Keep.left) 280 | 281 | val oneToTenStrSource: Source[String, NotUsed] = 282 | oneToTenSource.viaMat(intToStrFlow)(Keep.left) 283 | 284 | // Flow + Sink = Sink 285 | // 一个 Flow 和一个 Sink 组合成一个新的 Sink,输入数据类型也变成 Flow 的输入数据类型。 286 | val addOneSink: Sink[Int, NotUsed] = addOneFlow.toMat(printIntSink)(Keep.left) 287 | val intToStrSink: Sink[Int, Future[Done]] = 288 | intToStrFlow.toMat(printStrSink)(Keep.right) 289 | 290 | // Flow + Flow = Flow 291 | // 第一个 Flow 的输入和第二个 Flow 的输出成为新 Flow 的输入输出 292 | val addOneStrFlow: Flow[Int, String, NotUsed] = 293 | addOneFlow.viaMat(intToStrFlow)(Keep.left) 294 | 295 | val intLenFlow: Flow[Int, Int, NotUsed] = 296 | intToStrFlow.viaMat(strLengthFlow)(Keep.left) 297 | 298 | // Source + Flow(s) + Sink = RunnableGraph 299 | val addOneRunnable: RunnableGraph[NotUsed] = 300 | oneToTenSource.viaMat(addOneFlow)(Keep.left).toMat(printIntSink)(Keep.left) 301 | 302 | val addOneSumRunnable: RunnableGraph[Future[Int]] = 303 | oneToTenSource 304 | .viaMat(addOneFlow)(Keep.left) 305 | .viaMat(addOneFlow)(Keep.left) 306 | .toMat(sumSink)(Keep.right) 307 | 308 | addOneSumRunnable.run().onComplete(println) 309 | 310 | // API shortcuts 311 | 312 | // shortcut for oneToTenSource.viaMat(addOneFlow)(Keep.left) 313 | oneToTenSource.via(addOneFlow) 314 | 315 | // shortcut for toMat(printIntSink)(Keep.left) 316 | addOneFlow.to(printIntSink) 317 | 318 | // shortcut for oneToTenSource.toMat(printIntSink)(Keep.right).run() 319 | oneToTenSource.runWith(printIntSink) 320 | 321 | // shortcut for 322 | // oneToTenSource.viaMat(addOneFlow)(Keep.left).viaMat(addOneFlow)(Keep.left).toMat(printStrSink)(Keep.left) 323 | oneToTenSource.via(addOneFlow).via(intToStrFlow).to(printStrSink) 324 | 325 | // shortcut for 326 | // oneToTenSource.via(addOneFlow).toMat(printIntSink)(Keep.both).run() 327 | val result: (NotUsed, Future[Done]) = 328 | addOneFlow.runWith(oneToTenSource, printIntSink) 329 | 330 | } 331 | ``` 332 | --------------------------------------------------------------------------------