Почему приложение Akka завершается с ошибкой нехватки памяти при выполнении задачи 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 ?? Существует ли стратегия ручного освобождения памяти или мне нужно убить этих акторов через некоторое время и назначить задачи их почтовых ящиков новому актору для освобождения памяти? (Если да, то как?)

введите здесь описание изображения


После некоторой борьбы с инструментами профилирования я наконец смог использовать VisualVM с IntelliJ. Вот снимки. GC никогда не запускался.

введите здесь описание изображения

Другой - Heap Dump:

введите здесь описание изображения


Резюме конвейера:

Необработанные файлы -> Актеры SentenceSplit (6) -> Актеры парсера (6) -> Актеры Tregex (6) -> Актеры вывода файла (сделано)

Шаблоны определены в файле Entry.scala: https://github.com/windweller/parallelAkka/blob/master/src/main/scala/blogParallel/Entry.scala


person windweller    schedule 28.12.2014    source источник
comment
Попробуйте использовать VisualVM, который сообщит вам, какой класс вызывает это. Скорее всего, почтовые ящики вашего Актера переполняются, потому что у разных актеров разное время обслуживания в вашем пайплайне. В общем, представьте себе быстрого производителя и медленного потребителя. Почтовый ящик медленного потребителя вызовет MemoryException.   -  person Soumya Simanta    schedule 28.12.2014
comment
Мне кажется, вы просто перечисляете несколько случайных вещей, которые могут просочиться. Вы исследовали, куда уходит память? например с hprof?   -  person The Archetypal Paul    schedule 28.12.2014
comment
@SoumyaSimanta Ты прав. Это вполне может быть проблемой, но данные, ожидающие обработки, составляют всего 320 МБ. Он не может занимать более 80 ГБ пространства кучи. Я только что скачал VisualVM. Я запустил свою программу Akka с помощью команды sbt не из какого-либо приложения. Я не могу найти свою программу на левой боковой панели VisualVM!   -  person windweller    schedule 28.12.2014
comment
@WindDweller, без каких-либо доказательств того, чем поглощается память, вы (и мы) просто блуждаете в темноте. Хорошая вещь о несоответствии между размером данных и размером виртуальной машины заключается в том, что, вероятно, вся утечка памяти будет во многих экземплярах одного или двух классов. Пожалуйста, попробуйте получить некоторую информацию о том, где / что потребляет память   -  person The Archetypal Paul    schedule 28.12.2014
comment
@ Пол Да, я понимаю. Я пытаюсь использовать VisualVM, но столкнулся с проблемой, упомянутой выше. Все учебники, которые я нашел, учат только запускать VisualVM на Java Server (даже на локальном сервере, таком как Glassfish или Tomcat).   -  person windweller    schedule 28.12.2014
comment
ХОРОШО. А как насчет hprof?   -  person The Archetypal Paul    schedule 28.12.2014
comment
@SoumyaSimanta результат профиля выше!   -  person windweller    schedule 28.12.2014
comment
@Paul Я запустил VisualVM с IntelliJ, и результат выше. Кажется, что два класса regexPattern и lexparser занимали много памяти (хотя и не так много, как float[]). Они принадлежат TregexActor и ParserActor. Также GC никогда не запускался.   -  person windweller    schedule 28.12.2014
comment
Вам нужно копнуть дальше и выяснить, где производятся распределения float[]   -  person tddmonkey    schedule 28.12.2014
comment
@MrWiggles есть предложения как? Я никогда не использовал float[] в своем коде...   -  person windweller    schedule 28.12.2014
comment
Где определены patternFuture и patternsPast? Можете ли вы кратко изложить свой конвейер, т. Е. Какой участник отправляет какое сообщение какому другому участнику в цепочке?   -  person Soumya Simanta    schedule 28.12.2014
comment
Возможно, у вас его нет, но какой-то код, который вы используете, будет выполняться. Профилировщик должен предлагать варианты для изучения того, где были сделаны распределения.   -  person tddmonkey    schedule 28.12.2014
comment
@SoumyaSimanta Я описал свой конвейер (он очень-очень простой). patternFuture и patternPast определены в файле Entry.scala здесь: github.com/windweller/parallelAkka/blob/master/src/main/scala/. Хотя я сомневаюсь, что дело в этом.   -  person windweller    schedule 28.12.2014
comment
Можете ли вы добавить журналирование актеров и регистрировать запуск и остановку ваших актеров? тогда вы узнаете, выходят ли вновь созданные актеры или нет.   -  person Ashalynd    schedule 28.12.2014
comment
@Ashalynd Я не создавал нового актера. В самом начале я создал 18 актеров и продолжал их использовать. В процессе нет убийств или создания актеров.   -  person windweller    schedule 28.12.2014
comment
@MrWiggles Понял. Огромное количество float[] получено от ExhaustivePCFGParser, что означает, что это от Parser Actor. Но что я могу сделать с этой информацией??   -  person windweller    schedule 28.12.2014


Ответы (2)


Возможно, это неправильный ответ, но у меня недостаточно места, чтобы написать его в комментарии.

Попробуйте переместить актера, создающего внутри вашего объекта Companion.

  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, что мешает сборщику мусора собирать мусор.

Вы можете легко убедиться, что это проблема, просмотрев кучу в Visual VM после внесения изменений.

Кроме того, как долго у вас заканчивается память и каков максимальный объем. куча памяти, которую вы даете своей JVM?

РЕДАКТИРОВАТЬ См. Создание актеров с реквизитами здесь

Несколько других вещей, которые следует учитывать:

  1. Убедитесь, что ваш актер не умирает и перезапускается автоматически.
  2. Создавайте свои объекты NLP вне ваших актеров и передайте их при создании своих актеров.
  3. Используйте маршрутизатор Akka вместо логики хеширования, чтобы распределять работу между разными актеры.
person Soumya Simanta    schedule 28.12.2014
comment
Даю огромное количество памяти (около 80гб). Нехватка памяти занимает 2-3 часа. Мне пришлось использовать new, потому что мне нужно передавать аргументы при создании этих актеров. Что вы имеете в виду под переездом в Companion object? Entry.scala — это объект! Это кажется более возможным решением! - person windweller; 28.12.2014
comment
Пожалуйста, смотрите мое редактирование выше. Он рассказывает вам о различных способах создания актеров. 80 ГБ — это много памяти, IMO, поэтому у вас определенно есть утечка в вашей системе. Наконец, из VisualVM попробуйте выполнить явный сборщик мусора (нажав кнопку «Выполнить сборщик мусора»). Посмотрите, не исчезнет ли ваша куча памяти после этого. - person Soumya Simanta; 28.12.2014
comment
Следуя вашему предложению, я смог оптимизировать намного больше (особенно часть НЛП, перемещающуюся из части актера). Благодарю вас! Я заметил, что шаблон использования памяти отличается, когда я запускал программу через IntelliJ (с открытой VisualVM) и когда я запускал SBT из командной строки. Если я запускаю из командной строки, память никогда не уменьшается (около 50 ГБ). Если я запускаю IntelliJ, памяти будет около 40 ГБ. Однако в любом случае у меня закончилось место в куче до того, как задача была завершена. - person windweller; 29.12.2014
comment
Что происходит, когда вы выполняете явный GC ? Уменьшается ли пространство кучи? Можете ли вы дать оценку размера вашего нового поколения и старого поколения? - person Soumya Simanta; 29.12.2014

Я вижу эти фрагменты кода в ваших актерах:

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

Освобождаются ли эти объекты? Если нет, это может объяснить, почему память увеличивается. Особенно с учетом того, что вы возвращаете эти вновь созданные объекты (например, в методе cleanSentence).

ОБНОВЛЕНИЕ: вы можете попробовать вместо создания нового объекта изменить объект, который вы получили, а затем отметить его доступность в качестве ответа (вместо отправки нового объекта обратно), хотя с точки зрения потокобезопасности это также может быть напуганный. Другой вариант может состоять в том, чтобы использовать внешнее хранилище (например, базу данных или хранилище значений ключа Redis), поместить туда полученные предложения и отправить «предложение очищено» в качестве ответа, чтобы клиент мог затем получить доступ к предложению из этого хранилища значений ключа. или БД.

Как правило, использование изменяемых объектов (таких как java.util.List), которые потенциально могут попасть в акторы, не является хорошей идеей, поэтому, возможно, стоит перепроектировать приложение для использования неизменяемых объектов< /strong> по возможности.

Например. тогда ваш метод 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 (перед отправкой его актеру) следующим образом:

import scala.collection.JavaConverters._

val javaList:java.util.List[HasWord] = ...
javaList.asScala
person Ashalynd    schedule 28.12.2014
comment
Это может быть проблемой, но как это освободить? - person windweller; 28.12.2014
comment
Теоретически в Java вы освобождаете объект, если 1) он выходит за рамки или 2) вы явно назначаете ему null. - person Ashalynd; 28.12.2014
comment
Еще одним решением может быть не создание нового объекта, а изменение объекта, который вы получили. - person Ashalynd; 28.12.2014
comment
Спасибо за предложение. Я назначил null этому. Однако я сомневаюсь, что это главная проблема. Я прочитал другой пост, в котором говорилось, что GC не будет выполняться, когда потоков много. Мой профилированный результат также показывает почти 0 активности GC. - person windweller; 28.12.2014
comment
Да, это правда, но присвоение null больше говорит GC, что этот объект можно освободить. Прочтите мое другое предложение, не знаю, насколько легко вам было бы изменить код таким образом (используя отдельное хранилище вместо отправки объектов туда и обратно) - person Ashalynd; 28.12.2014
comment
Еще один вариант: отправлять списки Scala (неизменяемые) вместо изменяемых списков Java. - person Ashalynd; 28.12.2014
comment
Я старался. Stanford NLP Parser принимает только java.util.List. Даже если я использую списки Scala, мне все равно придется конвертировать их в java.util.List в конце. Извиняюсь. - person windweller; 28.12.2014
comment
Преобразование допустимо, но вы должны преобразовать эти списки в списки Scala при отправке их акторам и обратно. Затем вы отправляете неизменяемые объекты, которые легче освободить сборщику мусора. - person Ashalynd; 28.12.2014