zoukankan      html  css  js  c++  java
  • Akka源码分析-Akka-Streams-GraphStage

      上一篇博客中我们介绍了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

  • 相关阅读:
    window.onload 、body.onload 以及 jQuery 等dom加载完成后执行脚本的区别
    HTML5事件-pageshow 和 pagehide
    动态加载script 和 link
    递归 recursive
    HTML5事件-自定义右键菜单
    left与margin-left区别
    偏移量、客户区、滚动大小
    屏幕适配
    KVC和KVO
    HUD总结
  • 原文地址:https://www.cnblogs.com/gabry/p/9553293.html
Copyright © 2011-2022 走看看