#1.3 akka streams 背后的设计原则(Desgin Principles behind Akka Streams) 花了相当长的一段时间,直到我们对API的外观和风格以及架构的实施相当满意,同时,凭借直觉引导设计是非常有探索性的研究。这一部分详细的介绍了调查结果,并将在过程呈现的一套原则编纂下来了。
注意,经过详细的介绍请记住,在akka stream的API中和只是描述在个别处理阶段数据传递的实现细节的Reactive stream接口是完全解耦的,
##1.3.1 用户对akka stream报以怎样的期待( What shall users of Akka Streams expect?) akka是建立在有意识的决定提供最小的,一致的而不是简单的或者直观的API上的。该信条是,我们倾向与明确而非魔法,如果我们提供了一个功能,那它必须始终能毫无例外的工作。另一种说法是我们最大限度的降低规则数量而不是试图和我们认为用户期望的规则保持接近。由此得出akka stream实现的原则是:
- 所有的功能都是明确的,没有魔法的
- 最高组合性,组件每一部分的功能都得以保留
- 详尽领域模型的分布式流处理
这意味着我们提供完善的工具来描述任何流处理拓扑中,我们模拟无论用户是否在一个更大的上下文中构建可重用,所有这个领域必要的方面(
back-pressure
、缓存、转化、故障恢复等)。 ###akka stream 不能发送废弃流的元素到死信办公室 akka stream不能确保所有通过处理拓扑的对象都会被处理的限制造成只提供可以依赖的功能的结果。很多原因造成了元素可以被删除: - 普通的用户代码可以在
map(...)
阶段消耗一个元素,并且产生一个完全不同的元素作为结果 - common流操作者有意的丢弃元素,如采取take/drop/filter/conflate/buffer/...
- 流故障将会摧毁流而不必等到进程结束,所有其中的元素都将会被丢弃
- 流取消将被传递到上游,(如take操作者),导致上游处理步骤被终止而不必等待所有的输入被处理 这意味着需要被清理的的那些发送到流的jvm对象的情况需要用户确信发生在akka stream的设备之外。 ###产生实现约束 组合性需要局部流拓扑的可重用性,导致我们取消描述流作为(部分)图,可以作为复合sources、flows(又名管道)和sinks数据的方法。这些构建块应该自由共享,它们具备自由组合,形成更大的图的能力。为了启动流处理,这些零件的表现因此必须是那些以显式介入物化的不可变的蓝图。由此产生的由蓝图所规定的固定拓扑结构的流处理引擎也是不可变的。动态网络需要使用Reactive stream接口接入不同的引擎来显式的建模。进程物化通常会创建在与进程引擎运行时互作用时有用的特定对象,例如关闭或者提取指标。这意味着物化函数产生了一个叫做图的物化值。
##1.3.2 与其他Reactive Streams实现的互操作(Interoperation with other Reactive Streams implementations)
Akka Streams完全贯彻了Reactive Stream的规范,并且与其他符合该标准的实现进行互操作。我们在用户级别的API上选择与Reactive Streams完全剥离,因为我们认为他们(Reactive Stream)意图成为一种SPI而非面向最终用户。为了从akka stream的拓扑结构获得发布者或者订阅者,必须使用相应的Sink.asPublisher
或者Source.asSubscriber
元素。
akka stream的物化产生的流处理器默认为单个订阅者,而拒绝其他订阅者。这样做的的原因是:使用DSL描述的流拓扑从未要求发布者元素的fan-out
行为,通过显式元素实现所有的fan-out
,像Broadcast[T]
。
这意味着在与其他Reactive Stream实现进行互操作如果需要广播行为,那么必须使用Sink.asPublisher(true)
(为了实现fan-out
支持)。
##1.3.3 什么是用户期待的流媒体库 (What shall users of streaming libraries expect?) 我们期望的库必须建立在akka stream之上,其实在akka项目本身,akka http就是这样的一个例子。为了使得用户可以从从akka stream所描述的原理中有所收获,确定了以下规则:
- 库应为其用户提供可重用性的组件,例如,那些返回可以得到完整性的图的实现工厂
- 库任意可选,并且提供消费和物化的图的设备(
facilities
) 第一条规则背后的原因是:如果不同的库指接受图并且期望实现它们:把这两者结合起来使用肯定是不可能的:因为物化不可能只发生一次,结合性将被破坏。其结果是一个库的功必须描述用户来完成的物化,而非库所控制。所述的第二条规则是允许库在通常情况下提供语法糖,例如,在akka http API中为了方便物化而提供了handleWith
方法。
注意:这样做的一个重要影响是一个可重用的flow不能被绑定到一个活跃的源
"live" resources
,任何连接或者分配这种资源都必须推迟物化时间。活跃的资源的例子就像已经存在的TCP连接一样,组播发布者(a multicast Publisher
)等,如果定时器只是在物化创建(这是我们的执行方案)那么TickSource
将不属于此类情况。这种额外的需求完全是合理并且被详细记录的。
###产生实现约束 akka stream必须启用一个库来表示在不可变蓝图方面的任何流处理工具。最常见的构建组件是: *Source:那些只有一个输出流的物件 *Sink:那些只有一个输入流的物件 *Flow:那些只有一个输入流和一个输出流的物件 *BidiFlow:那些只有两个输入流和两个输出流的物件,在概念上像方向相反的两个流 *Graph:打包的流处理拓扑,有着特定形状的对象,暴露一组特定的输入和输出端口
注意 由streams发出的一个stream依然是一个正常的源,所产生的元素没有起到静态拓扑所提出的概念。
##1.3.4 Error和Failure的区别 (The difference between Error and Failure)
这一讨论的出发点是Reactive宣言给出的定义。一个流的译本(Translated to stream
)意味着一个error被作为一个普通的元素在流内部被访问,然而一个failure意味着流自身出现了故障,正在崩溃。具体而言,在Reactive stream接口级别的数据元素通过(包含errors)onNext
信号而failures提升则通过onError
信号。
注意:不幸的是,由于历史原因,到订阅者的失败信号被叫做onError
,始终牢记的是,Reactive Stream接口(发布者/订阅/订阅者)对传递执行单元之间的流被定义在底层,在这个层面的errors
恰恰是我们在更高层面的akka stream中定义的failures
与akka stream中操作者可用的数据元素转化相比,对onError
的处理只是有限的支持。这是上一段的实质。onError
信号表明这个流正在崩溃
其排序的语意和流的完成是不一样的:
*在任何情况下流的转化都可能崩溃,甚至包含隐式或者显式缓冲区。这意味着在数据元素发射之前如果onError
追上它们将造成一个failure
的丢失。
*对于摧毁backpressured
流那么传播failure
的速度必须要超过传播数据元素-
特别是back-pressured
能成为故障模式(由于上游缓冲的跳闸,然后终止,因为它们不能做任何事,或者死锁了)
###流回收的意义 (The semantics of stream recovery)
一个恢复元素(那些吸收了onError
信号并且能将其转化为跟随正常流结束的数据元素)
充当封闭崩溃流的挡板,在崩溃流区域的元素可能会丢失,但外面不受失败的影响。
这种工作方式类似try-catch experssion
:将会标记引发异常的区域,但是在标记failure
区域确切数量的代码会被跳过,可能什么错误都不知道--语句的位置很重要。