通过背压接收逐行文件IO

时间:2016-04-15 21:06:36

标签: scala akka akka-stream

我有一个文件处理作业,目前使用带有手动管理背压的akka​​ actor来处理处理管道,但我从未能够在输入文件读取阶段成功管理背压。

此作业接受一个输入文件,并按每行开头的ID号对行进行分组,然后一旦遇到具有新ID号的行,它就会通过消息将分组的行推送到处理行为者,并且然后继续使用新的ID号,直到它到达文件的末尾。

这似乎是Akka Streams的一个很好的用例,使用File作为接收器,但我仍然不确定三件事:

1)如何逐行读取文件?

2)我如何按每行上的ID分组?我目前使用非常必要的处理,我不认为我在流管道中具有相同的能力。

3)我如何应用背压,这样我就不会更快地将线读入内存,而不是处理下游数据?

2 个答案:

答案 0 :(得分:8)

Akka溪流' groupBy是一种方法。但是groupBy有一个maxSubstreams参数,要求你先知道最大ID范围。所以:下面的解决方案使用scan来识别相同的ID块,并splitWhen分割成子流:

object Main extends App {
  implicit val system = ActorSystem("system")
  implicit val materializer = ActorMaterializer()

  def extractId(s: String) = {
    val a = s.split(",")
    a(0) -> a(1)
  }

  val file = new File("/tmp/example.csv")

  private val lineByLineSource = FileIO.fromFile(file)
    .via(Framing.delimiter(ByteString("\n"), maximumFrameLength = 1024))
    .map(_.utf8String)

  val future: Future[Done] = lineByLineSource
    .map(extractId)
    .scan( (false,"","") )( (l,r) => (l._2 != r._1, r._1, r._2) )
    .drop(1)
    .splitWhen(_._1)
    .fold( ("",Seq[String]()) )( (l,r) => (r._2, l._2 ++ Seq(r._3) ))
    .concatSubstreams
    .runForeach(println)

  private val reply = Await.result(future, 10 seconds)
  println(s"Received $reply")
  Await.ready(system.terminate(), 10 seconds)
}

extractId将行拆分为id - >数据元组。 scan预先添加ID - >具有起始ID范围标志的数据元组。 drop将引物元素丢弃到scansplitwhen为每个范围的起点开始一个新的子流。 fold将子流连接到列表并删除开始的ID范围布尔值,以便每个子流生成一个单独的元素。代替折叠,您可能需要一个自定义SubFlow来处理单个ID的行流,并为ID范围发出一些结果。 concatSubstreams将split生成的per-ID-range子流合并为由runForEach打印的单个流。

使用以下命令运行:

$ cat /tmp/example.csv
ID1,some input
ID1,some more input
ID1,last of ID1
ID2,one line of ID2
ID3,2nd before eof
ID3,eof

输出是:

(ID1,List(some input, some more input, last of ID1))
(ID2,List(one line of ID2))
(ID3,List(2nd before eof, eof))

答案 1 :(得分:0)

似乎在不引入大量修改的情况下向系统添加“背压”的最简单方法是简单地将消耗Actor的输入组的邮箱类型更改为BoundedMailbox

  1. 将使用您的行的Actor类型更改为高mailbox-push-timeout-time的BoundedMailbox:

    bounded-mailbox {
      mailbox-type = "akka.dispatch.BoundedDequeBasedMailbox"
      mailbox-capacity = 1
      mailbox-push-timeout-time = 1h
    }
    
    val actor = system.actorOf(Props(classOf[InputGroupsConsumingActor]).withMailbox("bounded-mailbox"))
    
  2. 从您的文件创建迭代器,从该迭代器创建分组(通过id)迭代器。然后只需循环访问数据,将组发送到消费者Actor。注意,在这种情况下,当Actor的邮箱已满时,send将阻止。

    def iterGroupBy[A, K](iter: Iterator[A])(keyFun: A => K): Iterator[Seq[A]] = {
      def rec(s: Stream[A]): Stream[Seq[A]] =
        if (s.isEmpty) Stream.empty else {
          s.span(keyFun(s.head) == keyFun(_)) match {
          case (prefix, suffix) => prefix.toList #:: rec(suffix)
        }
      }
      rec(iter.toStream).toIterator
    }
    
    val lines = Source.fromFile("input.file").getLines()
    
    iterGroupBy(lines){l => l.headOption}.foreach {
      lines:Seq[String] =>
          actor.tell(lines, ActorRef.noSender)
    }
    
  3. 就是这样! 您可能希望将文件读取内容移动到单独的线程,因为它会阻止。同样通过调整mailbox-capacity,您可以调节消耗的内存量。但是,如果从文件中读取批处理总是比处理更快,那么保持容量较小似乎是合理的,例如1或2。

    使用iterGroupBy实施

    更新 Stream,经过测试不生成StackOverflow