为什么Akka应用程序在执行NLP任务时因内存不足错误而失败?

时间:2014-12-28 12:52:15

标签: scala akka stanford-nlp

我注意到我的程序存在严重的内存泄漏(内存消耗螺旋上升)。我必须并行化这个NLP任务(使用StanfordNLP EnglishPCFG Parser和Tregex Matcher)。所以我构建了一个演员管道(每个任务只有6个演员):

   val listOfTregexActors = (0 to 5).map(m => system.actorOf(Props(new TregexActor(timer, filePrinter)), "TregexActor" + m)) 
   val listOfParsers = (0 to 5).map(n => system.actorOf(Props(new ParserActor(timer, listOfTregexActors(n), lp)), "ParserActor" + n)) 
   val listOfSentenceSplitters  = (0 to 5).map(j => system.actorOf(Props(new SentenceSplitterActor(listOfParsers(j), timer)), "SplitActor" + j)) 

我的演员非常标准。他们需要保持活力来处理所有信息(沿途没有毒药)。内存消耗量不断增加,而且我不知道什么是错误的。如果我运行单线程,内存消耗就可以了。我在某个地方读到,如果演员不会死,那么内部的任何东西都不会被释放。我应该手动发布东西吗?

有两个举重演员:

https://github.com/windweller/parallelAkka/blob/master/src/main/scala/blogParallel/ParserActor.scala https://github.com/windweller/parallelAkka/blob/master/src/main/scala/blogParallel/TregexActor.scala

我想知道它是否可能是Scala的关闭或其他保留太多信息的机制,GC无法以某种方式收集它。

这是TregexActor的一部分:

def receive = {
    case Match(rows, sen) =>
      println("Entering Pattern matching: " + rows(0))
      val result = patternSearching(sen)
      filePrinter ! Print(rows :+ sen.toString, result)
  }

  def patternSearching(tree: Tree):List[Array[Int]] = {
    val statsFuture = search(patternFuture, tree)
    val statsPast = search(patternsPast, tree)

    List(statsFuture, statsPast)
  }

  def search(patterns: List[String], tree: Tree) = {
    val stats =  Array.fill[Int](patterns.size)(0)

    for (i <- 0 to patterns.size - 1) {
      val searchPattern = TregexPattern.compile(patterns(i))
      val matcher = searchPattern.matcher(tree)
      if (matcher.find()) {
        stats(i) = stats(i) + 1
      }
      timer ! PatternAddOne
    }
    stats
  }

或者如果我的代码检出,可能是StanfordNLP解析器还是tregex匹配器的内存泄漏?是否存在手动释放内存的策略,或者我是否需要在一段时间后杀死这些actor并将其邮箱任务分配给新的actor以释放内存? (如果是这样,怎么样?)

enter image description here


在使用分析工具进行一些努力之后,我终于能够将VisualVM与IntelliJ一起使用了。这是快照。 GC从未跑过。

enter image description here

另一个是堆转储:

enter image description here


管道摘要:

原始文件 - &gt; SentenceSplit Actors(6) - &gt;解析器演员(6) - &gt; Tregex演员(6) - &gt;文件输出演员(完成)

模式在Entry.scala文件中定义:https://github.com/windweller/parallelAkka/blob/master/src/main/scala/blogParallel/Entry.scala

2 个答案:

答案 0 :(得分:1)

这可能不是正确答案,但我没有足够的空间在评论中写下来。

尝试在Companion对象中移动创建的actor。

  val listOfTregexActors = (0 to 5).map(m => system.actorOf(Props(new TregexActor(timer, filePrinter)), "TregexActor" + m))
  val listOfParsers = (0 to 5).map(n => system.actorOf(Props(new ParserActor(timer, listOfTregexActors(n), lp)), "ParserActor" + n))
  val listOfSentenceSplitters = (0 to 5).map(j => system.actorOf(Props(new SentenceSplitterActor(listOfParsers(j), timer)), "SplitActor" + j))

或者不要使用new来创建你的演员。

我怀疑当您创建应用时,您正在关闭App,这会阻止GC收集任何垃圾。

通过在进行更改后查看Visual VM上的堆,可以轻松验证是否存在此问题。

此外,耗尽内存需要多长时间,最大值是多少。堆内存你给你的JVM?

EDIT 请参阅 - 使用道具创建演员here

很少有其他事情需要考虑:

  1. 确保您的演员没有死亡并自动重启。
  2. 在演员之外创建NLP对象,并在创建演员时传递它们。
  3. 使用Akka router代替散列逻辑在不同的角色之间分配作品。

答案 1 :(得分:0)

我在你的演员中看到了这些代码:

 val cleanedSentences = new java.util.ArrayList[java.util.List[HasWord]]()

这些物品是否曾被释放?如果没有,这可能解释了为什么内存会上升。特别要考虑到您返回这些新创建的对象(例如,在cleanSentence方法中)。

更新:您可以尝试,而不是创建新对象,而是修改您收到的对象,然后将其标记为可用性作为响应(而不是将新对象发回),尽管从线程安全它也可能很时髦。其他选项可能是使用外部存储(例如数据库或Redis键值存储),将得到的句子放在那里并发送&#34;句子清理&#34;作为回复,以便客户端可以从该键值存储或DB访问该句子。

一般情况下,使用可变对象(如java.util.List)可能会泄露在actor中并不是一个好主意,因此可能值得重新设计您的应用程序以使用尽可能不可变的对象

E.g。你的cleanSentence方法看起来像:

def cleanSentence(sentences: List[HasWord]): List[HasWord] = {
    import TwitterRegex._

    sentences.filter(ref =>
        val word = ref.word() // do not call same function several times
        !word.contains("#") && 
        !word.contains("@") &&  
        !word.matches(searchPattern.toString())
      )
  }

您可以通过以下方式将java.util.List转换为Scala列表(在将其发送给actor之前):

import scala.collection.JavaConverters._

val javaList:java.util.List[HasWord] = ...
javaList.asScala