上一篇博客中我们介绍了ActorMaterializer的一小部分源码,其实分析的还是非常简单的,只是初窥了Materializer最基本的初始化过程及其涉及的基本概念。我们知道在materialize过程中,对Graph进行了某种遍历,然后创建了actor,最终graph运行起来。那Graph相关的概念我们其实是没有进行深入研究的。但Graph定义又非常抽象,乍一看非常难于理解。但我在阅读官方文档的时候发现了自定义流处理过程的章节,这应该有助于我们理解Graph,此处对其做简要分析。

  GraphStage抽象可以通过任意数量的输入输出端口,来创建任意操作。它是GraphDSL.create()方法的对应部分,这个方法是通过组合其他操作来创建新的流处理操作的。GraphStage不同之处在于,它创建一个不能分割的操作并且以安全的方式操作内部状态,怎么样是不是很像一个actor?嗯,没错其实在很久很久以前,GraphStage这个抽象是用actor来代替的。别问我为啥知道,看代码喽。

@deprecated("Use `akka.stream.stage.GraphStage` instead, it allows for all operations an Actor would and is more type-safe as well as guaranteed to be ReactiveStreams compliant.", since = "2.5.0")
trait ActorSubscriber extends Actor
@deprecated("Use `akka.stream.stage.GraphStage` instead, it allows for all operations an Actor would and is more type-safe as well as guaranteed to be ReactiveStreams compliant.", since = "2.5.0")
trait ActorPublisher[T] extends Actor

  上面源码显示,在2.5.0版本之前,GraphStage被分为ActorSubscriber、ActorPublisher两个抽象,在2.5.0之后,这两个概念统一用GraphStage替换。那其实意味着,GraphStage既可以定义输出端口,也可以定义输入端口。

/**
* A GraphStage represents a reusable graph stream processing operator.
*
* A GraphStage consists of a [[Shape]] which describes its input and output ports and a factory function that
* creates a [[GraphStageLogic]] which implements the processing logic that ties the ports together.
*/
abstract class GraphStage[S <: Shape] extends GraphStageWithMaterializedValue[S, NotUsed]

  官方注释显示,GraphStage代表一个可重用的图的流式处理操作(我们姑且成为算子吧)。它有一个Shape和一个工厂函数组成,Shape描述它的输入输出端口,工厂函数用来创建一个GraphStageLogic,而GraphStageLogic实现了与端口绑定的处理逻辑。

  其实我们可以简单的把GraphStage理解为一个算子或操作,对数据处理的一个步骤,或简单的理解为面向过程编程中的一个函数。它有输入、输出、对数据的操作逻辑。

class NumbersSource extends GraphStage[SourceShape[Int]] {
val out: Outlet[Int] = Outlet("NumbersSource")
override val shape: SourceShape[Int] = SourceShape(out) override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
new GraphStageLogic(shape) {
// All state MUST be inside the GraphStageLogic,
// never inside the enclosing GraphStage.
// This state is safe to access and modify from all the
// callbacks that are provided by GraphStageLogic and the
// registered handlers.
private var counter = 1 setHandler(out, new OutHandler {
override def onPull(): Unit = {
push(out, counter)
counter += 1
}
})
}
}

  NumbersSource是官方的一个demo,这个Source是用来从1产生递增序列的,但可以在反压机制下停止产生数据。官网的注释也比较清楚,所有在GraphStageLogic里面的状态都是线程安全的,但仅仅相对于GraphStageLogic内部的回调函数。

  NumbersSource还覆盖了一个shape字段,这个字段哪里来的呢?其实根据GraphStage的继承关系来看,它最终还继承了Graph这个trait,而这个trait是具有shape字段的,代表当前Graph的“形状”,这个形状的类型是GraphStage的类型参数决定的,也就是SourceShape[Int]。SourceShape[Int]代表一个只有输出没有输入的形状,且输出的数据类型是Int。

  下面我们来看GraphStageLogic的定义,这个类还是比较重要的,因为它决定了数据的处理逻辑。

/**
* Represents the processing logic behind a [[GraphStage]]. Roughly speaking, a subclass of [[GraphStageLogic]] is a
* collection of the following parts:
* * A set of [[InHandler]] and [[OutHandler]] instances and their assignments to the [[Inlet]]s and [[Outlet]]s
* of the enclosing [[GraphStage]]
* * Possible mutable state, accessible from the [[InHandler]] and [[OutHandler]] callbacks, but not from anywhere
* else (as such access would not be thread-safe)
* * The lifecycle hooks [[preStart()]] and [[postStop()]]
* * Methods for performing stream processing actions, like pulling or pushing elements
*
* The operator logic is completed once all its input and output ports have been closed. This can be changed by
* setting `setKeepGoing` to true.
*
* The `postStop` lifecycle hook on the logic itself is called once all ports are closed. This is the only tear down
* callback that is guaranteed to happen, if the actor system or the materializer is terminated the handlers may never
* see any callbacks to `onUpstreamFailure`, `onUpstreamFinish` or `onDownstreamFinish`. Therefore operator resource
* cleanup should always be done in `postStop`.
*/
abstract class GraphStageLogic private[stream] (val inCount: Int, val outCount: Int)

  GraphStageLogic定义了GraphStage背后的处理逻辑,粗略的说,GraphStageLogic的子类就是下面的集合:

  • InHandler和OutHandler实例的集合,以及他们给Inlet和Outlet的赋值。
  • 可变状态(不必须),被InHandler和OutHandler回调函数存取,其他地方不能存取(否则就不是线程安全)。
  • 生命周期hook,对preStart/postStop的hook。
  • 实施流处理动作的方法,比如pull和push元素。

  一旦输入输出端口完毕,算子逻辑就确定了。

  final protected def setHandler(out: Outlet[_], handler: OutHandler): Unit = {
handlers(out.id + inCount) = handler
if (_interpreter != null) _interpreter.setHandler(conn(out), handler)
}

  setHandler方法也比较简单,就是把OutHandler添加到handlers数组里面。_interpreter这个拦截器我们没有设置,所以应该是null。

/**
* Collection of callbacks for an input port of a [[GraphStage]]
*/
trait InHandler {
/**
* Called when the input port has a new element available. The actual element can be retrieved via the
* [[GraphStageLogic.grab()]] method.
*/
@throws(classOf[Exception])
def onPush(): Unit /**
* Called when the input port is finished. After this callback no other callbacks will be called for this port.
*/
@throws(classOf[Exception])
def onUpstreamFinish(): Unit = GraphInterpreter.currentInterpreter.activeStage.completeStage() /**
* Called when the input port has failed. After this callback no other callbacks will be called for this port.
*/
@throws(classOf[Exception])
def onUpstreamFailure(ex: Throwable): Unit = GraphInterpreter.currentInterpreter.activeStage.failStage(ex)
} /**
* Collection of callbacks for an output port of a [[GraphStage]]
*/
trait OutHandler {
/**
* Called when the output port has received a pull, and therefore ready to emit an element, i.e. [[GraphStageLogic.push()]]
* is now allowed to be called on this port.
*/
@throws(classOf[Exception])
def onPull(): Unit /**
* Called when the output port will no longer accept any new elements. After this callback no other callbacks will
* be called for this port.
*/
@throws(classOf[Exception])
def onDownstreamFinish(): Unit = {
GraphInterpreter
.currentInterpreter
.activeStage
.completeStage()
}
}

  上面是InHandler和OutHandler的定义。OutHandler定义了一个onPull回调函数,根据注释,它之后在输出端口收到一个pull请求时才会被调用。还记得Akka Streams的设计哲学么,它是基于Reactive Streams的API来做抽象的,而且实现了背压机制,而且还不需要缓存数据,这个机制怎么实现呢?当然是一拉一推喽?啥意思?简单来说就是,下游消费者,会定期向上游pull一批数据,然后上游把指定数量的消息发送给下游,下游消费完这批数据后,根据自身的压力(或者消息的平均处理时间),计算下一次请求消息的数量。如果自身压力很小,那就一次性多请求一些数据,如果压力很大,那就把请求数据的数值设小一点。这样就可以实现背压机制了,而且无需缓存数据。所以这才有了pull和push。

  在NumberSoure的OutHandler中收到pull请求时,也是通过调用push把数据发送给out端口的,然后计数器加1,就达到了生成自增数列的功能。那么push在哪里实现的呢?OutHandler并没有对应的方法啊。其实如果你对Java比较熟悉就知道在哪里定义了。

  /**
* Emits an element through the given output port. Calling this method twice before a [[pull()]] has been arrived
* will fail. There can be only one outstanding push request at any given time. The method [[isAvailable()]] can be
* used to check if the port is ready to be pushed or not.
*/
final protected def push[T](out: Outlet[T], elem: T): Unit = {
val connection = conn(out)
val it = interpreter
val portState = connection.portState connection.portState = portState ^ PushStartFlip if ((portState & (OutReady | OutClosed | InClosed)) == OutReady && (elem != null)) {
connection.slot = elem
it.chasePush(connection)
} else {
// Restore state for the error case
connection.portState = portState // Detailed error information should not add overhead to the hot path
ReactiveStreamsCompliance.requireNonNullElement(elem)
if (isClosed(out)) throw new IllegalArgumentException(s"Cannot push closed port ($out)")
if (!isAvailable(out)) throw new IllegalArgumentException(s"Cannot push port ($out) twice, or before it being pulled") // No error, just InClosed caused the actual pull to be ignored, but the status flag still needs to be flipped
connection.portState = portState ^ PushStartFlip
}
}

  push通过给定的输出端口,把元素给发送刚出去。而且在收到下一个pull请求之前,重复调用push会失败。也就是说一个push对应一个pull请求。这段代码逻辑也比较清晰,其实就是获取一个connection,然后判断connection的状态是不是OutReady,如果是就把待发送的数据赋值给connection的slot字段。

  // Using common array to reduce overhead for small port counts
private[stream] val portToConn = new Array[Connection](handlers.length)

  通过跟踪我们发现,connection其实就是通过OutLet的id从上面这个数组中获取了一个值,但可惜的是,我们没有找到这个数组赋值的逻辑。其实这个也可以理解,毕竟我们都graph还没有编译,相关的参数没有很正常,关于这一点我们后面再分析。

  /**
* INERNAL API
*
* Contains all the necessary information for the GraphInterpreter to be able to implement a connection
* between an output and input ports.
*
* @param id Identifier of the connection.
* @param inOwner The operator logic that corresponds to the input side of the connection.
* @param outOwner The operator logic that corresponds to the output side of the connection.
* @param inHandler The handler that contains the callback for input events.
* @param outHandler The handler that contains the callback for output events.
*/
final class Connection(
var id: Int,
var inOwner: GraphStageLogic,
var outOwner: GraphStageLogic,
var inHandler: InHandler,
var outHandler: OutHandler) {
var portState: Int = InReady
var slot: Any = Empty override def toString =
if (GraphInterpreter.Debug) s"Connection($id, $inOwner, $outOwner, $inHandler, $outHandler, $portState, $slot)"
else s"Connection($id, $portState, $slot, $inHandler, $outHandler)"
}

  Connection其实可以理解成一个JavaBean,用来对相关的参数进行封装,而push仅仅是把待发送的数据赋值给slot,这就算发出去了?复制给slot之后,数据什么时候才被下游取走呢?

class StdoutSink extends GraphStage[SinkShape[Int]] {
val in: Inlet[Int] = Inlet("StdoutSink")
override val shape: SinkShape[Int] = SinkShape(in) override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
new GraphStageLogic(shape) { // This requests one element at the Sink startup.
override def preStart(): Unit = pull(in) setHandler(in, new InHandler {
override def onPush(): Unit = {
println(grab(in))
pull(in)
}
})
}
}

  其实官网,下面还有一个类,是一个Sink,可以看到在Sink的GraphStageLogic中,它是调用了grab获取了对应的数据。

 /**
* Once the callback [[InHandler.onPush()]] for an input port has been invoked, the element that has been pushed
* can be retrieved via this method. After [[grab()]] has been called the port is considered to be empty, and further
* calls to [[grab()]] will fail until the port is pulled again and a new element is pushed as a response.
*
* The method [[isAvailable()]] can be used to query if the port has an element that can be grabbed or not.
*/
final protected def grab[T](in: Inlet[T]): T = {
val connection = conn(in)
val it = interpreter
val elem = connection.slot // Fast path
if ((connection.portState & (InReady | InFailed)) == InReady && (elem.asInstanceOf[AnyRef] ne Empty)) {
connection.slot = Empty
elem.asInstanceOf[T]
} else {
// Slow path
if (!isAvailable(in)) throw new IllegalArgumentException(s"Cannot get element from already empty input port ($in)")
val failed = connection.slot.asInstanceOf[Failed]
val elem = failed.previousElem.asInstanceOf[T]
connection.slot = Failed(failed.ex, Empty)
elem
}
}

  grap其实就是通过Inlet获取了Connection然后取得了Connection的slot值,作为返回值。

  这样大概就能梳理一下GraphStage的处理逻辑了。GraphStage是通过Connection作为“全局变量”来传递数据的,简单来说就是,source把待发送的数据设置给某个Connection的slot字段,sink从这个Connection的slot字段获取值,那么Source和Sink是如何绑定的呢?那就是ActorMaterializer的作用了,编译之后,Source和Sink才通过Connection进行绑定,而绑定的依据就是InPort和OutPort的ID,即具有相同ID的InPort和OutPort的Connection相同,这样就可以传递数据了。麻蛋,有点绕啊,究竟是不是这样,还得后续分析啊。

  其实分析到这里,GraphStage的作用就已经很明显了,它是用来定义流处理中的算子的,可以把GraphStage理解成一个函数,它通过Shape定义输入输出的类型,通过GraphStageLogic定义函数体,通过Connection.slot返回值供其他函数访问。而Graph可以理解成函数的一连串调用,只不过调用逻辑比较复杂,不是线性那么简单,可能是一个DAG图。

  为了与算子的端口(Inlet、Outlet)交互,我们需要可以接收和产生属于对应端口的事件。GraphStageLogi的输出端口可以做以下操作:

  • push(out,elem)。推送数据到输出端口,前提是下游端口发送了pull请求。
  • complete(out)。正常关闭输出端口。
  • fail(out,exception)。关闭输出端口,并提供一个失败的异常信息。
  • isAvailable(out)。判断当前端口是否可以推送数据。
  • isClosed(out)。判断当前端口是否已经关闭。关闭状态,端口不能推送数据也不能拉取数据。

  与输出端口关联的事件可以在一个OutHandler实例中接收到。

  输入端口可以进行的操作包括:

  • pull(in)。从熟读端口请求一个数据,前提是上游端口已经推送过一个数据。
  • grab(in)。在onPush回调时,获取一个数。不能重复调用。
  • cancel(in)。关闭输入端口
  • isAvailable(in)。判断当前端口是否可以获取(grab)数据。
  • hasBeenPulled(in)。判断当前端口是否已经拉取过数据。此状态无法调用pull拉取数据。
  • isClosed(in)。判断当前端口是否已经关闭。

  当然了还有两个操作是输入和输出端口都可以进行的操作:

  • completeStage()。等同于关闭所有的输出端口,取消所有的输入端口。
  • failStage(exception)。等同于关闭所有的输出端口,取消所有的输入端口,并提供对应的失败异常信息。
class Map[A, B](f: A ⇒ B) extends GraphStage[FlowShape[A, B]] {

  val in = Inlet[A]("Map.in")
val out = Outlet[B]("Map.out") override val shape = FlowShape.of(in, out) override def createLogic(attr: Attributes): GraphStageLogic =
new GraphStageLogic(shape) {
setHandler(in, new InHandler {
override def onPush(): Unit = {
push(out, f(grab(in)))
}
})
setHandler(out, new OutHandler {
override def onPull(): Unit = {
pull(in)
}
})
}
}

  上面是官网的一个稍微复杂点的demo它实现了map的功能,其实就是把指定的函数f应用于流入该stage的数据,然后push给下游。可以看到,这里同时设置了InHandler和OutHandler。

  好了,由于时间关系,GraphStage就分析到这里,可以看到GraphStage是最终承担算子定义以及图的链接等功能的,可以说还是非常重要的一个概念,但离我们完全理解akka Stream各个概念的关系还比较远,加油吧,骚年。

  

Custom stream processing

Akka源码分析-Akka-Streams-GraphStage的更多相关文章

  1. Akka源码分析-Akka Typed

    对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...

  2. Akka源码分析-Akka-Streams-概念入门

    今天我们来讲解akka-streams,这应该算akka框架下实现的一个很高级的工具.之前在学习akka streams的时候,我是觉得云里雾里的,感觉非常复杂,而且又难学,不过随着对akka源码的深 ...

  3. Akka源码分析-Cluster-DistributedData

    上一篇博客我们研究了集群的分片源码,虽然akka的集群分片的初衷是用来解决actor分布的,但如果我们稍加改造就可以很轻松的开发出一个简单的分布式缓存系统,怎么做?哈哈很简单啊,实体actor的id就 ...

  4. Akka源码分析-Cluster-Metrics

    一个应用软件维护的后期一定是要做监控,akka也不例外,它提供了集群模式下的度量扩展插件. 其实如果读者读过前面的系列文章的话,应该是能够自己写一个这样的监控工具的.简单来说就是创建一个actor,它 ...

  5. Akka源码分析-Cluster-Distributed Publish Subscribe in Cluster

    在ClusterClient源码分析中,我们知道,他是依托于“Distributed Publish Subscribe in Cluster”来实现消息的转发的,那本文就来分析一下Pub/Sub是如 ...

  6. Akka源码分析-Cluster-Singleton

    akka Cluster基本实现原理已经分析过,其实它就是在remote基础上添加了gossip协议,同步各个节点信息,使集群内各节点能够识别.在Cluster中可能会有一个特殊的节点,叫做单例节点. ...

  7. Akka源码分析-Persistence

    在学习akka过程中,我们了解了它的监督机制,会发现actor非常可靠,可以自动的恢复.但akka框架只会简单的创建新的actor,然后调用对应的生命周期函数,如果actor有状态需要回复,我们需要h ...

  8. Akka源码分析-local-DeathWatch

    生命周期监控,也就是死亡监控,是akka编程中常用的机制.比如我们有了某个actor的ActorRef之后,希望在该actor死亡之后收到响应的消息,此时我们就可以使用watch函数达到这一目的. c ...

  9. Akka源码分析-Cluster-ActorSystem

    前面几篇博客,我们依次介绍了local和remote的一些内容,其实再分析cluster就会简单很多,后面关于cluster的源码分析,能够省略的地方,就不再贴源码而是一句话带过了,如果有不理解的地方 ...

随机推荐

  1. Leetcode 213.大家劫舍II

    打家劫舍II 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金.这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的.同时,相邻的房屋装有相互连通的防盗系统,如果两 ...

  2. 在workbench中导入.sql文件!(导入数据库文件)

    第一步,登陆mysql workbench 第二步,打开自己的数据 ,此处默认(root) 打开数据库后页面 : 第三步,新建一个schema ,随便给个名字,这里起名为test : 可以看到test ...

  3. Uva - 11181 Probability|Given (条件概率)

    设事件B为一共有r个人买了东西,设事件Ai为第i个人买了东西. 那么这个题目实际上就是求P(Ai|B),而P(Ai|B)=P(AiB)/P(B),其中P(AiB)表示事件Ai与事件B同时发生的概率,同 ...

  4. HDU——2647 Reward

    Reward Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Sub ...

  5. Mybatis 最强大的动态sql <where>标签

    <select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHER ...

  6. Elasticsearch学习系列之配置文件详解

    ################################### Cluster ################################### #定义集群名称,默认是elasticse ...

  7. 消息驱动bean(MDB)实例

    到眼下为止前面介绍的有关JavaEE的东西都是同步的.也就是说调用者调用某个方法.那么这种方法必须马上运行并返回运行结果. 用官方一些的语言来说就是"client通过业务接口调用一个方法,在 ...

  8. tomcat用80port能够启动,可是浏览器不显示tomcat首页

    一.打开执行(ctrl+r)->输入cmd->确定->输入netstat -ano 结果检測到 :80port被system 占用,如图所看到的 打开进程发现确实被 PID为 4 的 ...

  9. keepalived + lvs marster 与 backup 之间的 高可用

    简介 keepalived 是linux下一个轻量级的高可用解决方案,它与HACMP实现功能类似,都可以实现服务或者网络的高可用,但是又有差别:hacmp是一个专业的.功能完善的高可用软件,它提供了H ...

  10. 工作总结 default Console.WriteLine(default(Guid));

    泛型代码中的默认关键字 在泛型类和泛型方法中产生的一个问题是,在预先未知以下情况时,如何将默认值分配给参数化类型 T: T 是引用类型还是值类型. 如果 T 为值类型,则它是数值还是结构. 给定参数化 ...