Приложение Groovy с gpars замедляется после многих итераций

Я использую gpars для параллельной обработки 250-мегапиксельной таблицы базы данных MySQL. Я создаю 8 потоков gpars, 8 независимых подключений к базе данных и разделяю данные таким образом, чтобы каждый поток работал независимо с разными диапазонами строк... что-то вроде дешевой концепции MapReduce. В основе логика такая:

  withExistingPool(pool)
  {
    connection_array.collectParallel()         
    {
       // Figure out which connection this thread can use. 
       // We use the index into the array to figure out 
       // which thread we are, and this tells us where to
       // read data.

       int i
       for (i = 0; i < connection_array.size(); i++)
          if (it == connection_array[i])
             break

       // Each thread runs the same query, with LIMIT controlling 
       // the position of rows it will read...if we have 8 threads
       // reading 40000 rows per call to this routine, each thread
       // reads 5000 rows (thread-0 reads rows 0-4999, thread-1 reads 
       // 5000-9999 and so forth). 

       def startrow = lastrow + (i * MAX_ROWS)
       def rows = it.rows( "SELECT * ... LIMIT ($startrow, $MAX_ROWS)")  

       // Add our rows to the result set we will return to the caller
       // (needs to be serialized since many threads can be here)

       lock.lock()
       if (!result) 
          result = rows
       else
          result += rows
       lock.unlock()
   }
 }

Сначала код работает отлично, давая мне более 10 000 строк в секунду при запуске. Но после нескольких миллионов строк он начинает тормозить. К тому времени, когда мы получаем 25 миллионов строк, вместо 10 000 строк в секунду мы получаем только 1000 строк в секунду. Если мы завершим приложение и перезапустим его с того места, где мы остановились, оно снова вернется к 10 000 строк в секунду на некоторое время, но оно всегда замедляется по мере продолжения обработки.

Доступной вычислительной мощности достаточно — это 8-процессорная система, а база данных находится в сети, так что время ожидания остается приличным, несмотря ни на что. Во время работы процессоры обычно загружены не более чем на 25-30%. Утечек памяти также не наблюдается — мы отслеживаем статистику памяти и не видим никаких изменений во время обработки. Сервер MySQL, похоже, не испытывает стресса (первоначально он загружен примерно на 30%, уменьшаясь по мере замедления работы приложения).

Есть ли какие-нибудь хитрости, которые помогут таким вещам работать более последовательно с большим количеством итераций?


person Valerie R    schedule 07.04.2016    source источник
comment
Вероятно, это потому, что вы постоянно изменяете размер списка результатов. Вы пытались указать начальный размер для результата? Вы не показываете, как/где он инициализируется   -  person tim_yates    schedule 07.04.2016
comment
Вы также, вероятно, можете использовать (0..<connection_array.size()).collectParallel вместо вашего массива, тогда вам не нужно будет искать текущий индекс   -  person tim_yates    schedule 07.04.2016
comment
@tim - спасибо за предложения ... ваш первый комментарий хороший - поскольку мы знаем, сколько строк извлекается, мы можем предварительно выделить результат и передать его в качестве параметра, а не динамически создавать его каждый раз. . Это помогло производительности почти на 5%. Второе предложение оказалось на самом деле немного медленнее, чем наш первоначальный подход - я думаю, поиск в массиве из 8 элементов не займет много времени. И, к сожалению, первоначальная проблема все еще существует... процедура становится все медленнее и медленнее, чем больше записей она обрабатывает.   -  person Valerie R    schedule 07.04.2016
comment
На самом деле, вы собираете (который будет строить результаты) и сами собираете результаты... Измените на results = connection_array.collectParallel() и избавьтесь от своего последнего блока if... Или попробуйте сохранить его и изменить на eachParallel   -  person tim_yates    schedule 07.04.2016


Ответы (2)


Хорошо, мы думаем, что нашли проблему - похоже, что это связано с открытием соединения JDBC в другом потоке, чем там, где оно используется. Первоначально открыв соединение в потоке, где оно будет использоваться, а затем убедившись, что ТОЛЬКО этот поток обращается к этому соединению, проблема с производительностью исчезла.

Мы также немного переработали логику, чтобы использовать подход на основе курсора, а не множественные запросы с LIMIT. Были сообщения, что LIMIT с высоким значением start_row может быть медленным, но мы не увидели большой разницы только после внесения этого изменения (курсоры стали быстрее, но производительность по-прежнему снижалась по мере обработки строк).

Тем не менее, между этим и некоторыми изменениями, предложенными tim_yates, мы работаем на 30% быстрее, чем раньше, и теперь это стабильно быстро, независимо от того, сколько строк мы обрабатываем.

person Valerie R    schedule 07.04.2016
comment
Не забудьте собрать/каждую вещь в моем комментарии выше - person tim_yates; 07.04.2016

LIMIT и OFFSET не так эффективны, как хотелось бы большинству людей.

При выполнении LIMIT 1000,20 будет прочитано 1000 строк, но пропущено, затем будет прочитано и доставлено 20 строк. То есть по мере роста OFFSET запрос становится медленнее.

Техника «исправить» это «вспомнить, где вы остановились». Это особенно легко сделать с первичным ключом AUTO_INCREMENT, но это можно сделать с любым ключом PRIMARY KEY или UNIQUE.

Это обсуждается далее в моем блоге "Разбивка на страницы". Он предназначен для кнопок «Далее» на веб-странице, поэтому некоторые обсуждения можно игнорировать.

person Rick James    schedule 08.04.2016