我有一个文件处理作业,目前使用带有手动管理背压的akka actor来处理处理管道,但我从未能够在输入文件读取阶段成功管理背压。
此作业接受一个输入文件,并按每行开头的ID号对行进行分组,然后一旦遇到具有新ID号的行,它就会通过消息将分组的行推送到处理行为者,并且然后继续使用新的ID号,直到它到达文件的末尾。
这似乎是Akka Streams的一个很好的用例,使用File作为接收器,但我仍然不确定三件事:
1)如何逐行读取文件?
2)我如何按每行上的ID分组?我目前使用非常必要的处理,我不认为我在流管道中具有相同的能力。
3)我如何应用背压,这样我就不会更快地将线读入内存,而不是处理下游数据?
答案 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
将引物元素丢弃到scan
。 splitwhen
为每个范围的起点开始一个新的子流。 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。
将使用您的行的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"))
从您的文件创建迭代器,从该迭代器创建分组(通过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)
}
就是这样!
您可能希望将文件读取内容移动到单独的线程,因为它会阻止。同样通过调整mailbox-capacity
,您可以调节消耗的内存量。但是,如果从文件中读取批处理总是比处理更快,那么保持容量较小似乎是合理的,例如1或2。
iterGroupBy
实施 更新 Stream
,经过测试不生成StackOverflow
。