Spring Batch: Tasklet с многопоточным исполнителем имеет очень плохие характеристики, связанные с алгоритмом дросселирования

используя Spring batch 2.2.1, я настроил весеннее пакетное задание, я использовал этот подход:

конфигурация следующая:

  • Tasklet использует ThreadPoolTaskExecutor, ограниченный 15 потоками

  • дроссель-предел равен количеству потоков

  • Чанк используется с:

    • 1 синхронизированный адаптер JdbcCursorItemReader позволяет использовать его многими потоками в соответствии с рекомендацией документации Spring Batch

      вы можете синхронизировать вызов read () и до тех пор, пока обработка и запись являются самой дорогой частью фрагмента, ваш шаг может выполняться намного быстрее, чем в однопоточной конфигурации.

    • saveState является ложным на JdbcCursorItemReader

    • пользовательский ItemWriter на основе JPA. заметьте что своя обработка одного деталя может поменять оперируя понятиями времени обработки, его может принять немного Миллис к немного секунд ( > 60С).

    • commit-interval установлен в 1 (я знаю, что это может быть лучше, но это не проблема)

  • все пулы jdbc в порядке, относительно Spring Batch doc рекомендация

запуск пакета приводит к очень странным и плохим результатам из-за следующего:

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

глядя на весенний пакетный код, первопричина, похоже, находится в этом пакет:

  • org / springframework / batch / repeat / support/

Это способ работы функции или это ограничение/ошибка ?

Если это функция, каков способ настройки, чтобы сделать все потоки без голодания длительной обработки без необходимости переписывать все ?

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

Примечание я открыл этот вопрос:

4 ответов


Как сказал Алекс, похоже, это поведение является контрактом согласно javadocs :

подклассы просто должны предоставить метод, который получает следующий результат * и тот, который ждет всех результатов, которые будут возвращены из параллельных * процессов или потоков

посмотреть:

TaskExecutorRepeatTemplate#waitForResults

другой вариант для вас будет использовать разделение:

  • A TaskExecutorPartitionHandler, который будет выполнять элементы из Partitionned ItemReader, см. ниже
  • реализация разделителя, которая дает диапазоны для обработки ItemReader, см. ColumnRangePartitioner ниже
  • CustomReader, который будет считывать данные, используя то, что будет заполнено разделителем, см. конфигурацию myItemReader ниже

Майкл Минелла объясняет это в Главе 11 своей книги Pro Весна Партия:

<batch:job id="batchWithPartition">
    <batch:step id="step1.master">
        <batch:partition  partitioner="myPartitioner" handler="partitionHandler"/>
    </batch:step>       
</batch:job>
<!-- This one will create Paritions of Number of lines/ Grid Size--> 
<bean id="myPartitioner" class="....ColumnRangePartitioner"/>
<!-- This one will handle every partition in a Thread -->
<bean id="partitionHandler" class="org.springframework.batch.core.partition.support.TaskExecutorPartitionHandler">
    <property name="taskExecutor" ref="multiThreadedTaskExecutor"/>
    <property name="step" ref="step1" />
    <property name="gridSize" value="10" />
</bean>
<batch:step id="step1">
        <batch:tasklet transaction-manager="transactionManager">
            <batch:chunk reader="myItemReader"
                writer="manipulatableWriterForTests" commit-interval="1"
                skip-limit="30000">
                <batch:skippable-exception-classes>
                    <batch:include class="java.lang.Exception" />
                </batch:skippable-exception-classes>
            </batch:chunk>
        </batch:tasklet>
</batch:step>
 <!-- scope step is critical here-->
<bean id="myItemReader"    
                        class="org.springframework.batch.item.database.JdbcCursorItemReader" scope="step">
    <property name="dataSource" ref="dataSource"/>
    <property name="sql">
        <value>
            <![CDATA[
                select * from customers where id >= ? and id <=  ?
            ]]>
        </value>
    </property>
    <property name="preparedStatementSetter">
        <bean class="org.springframework.batch.core.resource.ListPreparedStatementSetter">
            <property name="parameters">
                <list>
 <!-- minValue and maxValue are filled in by Partitioner for each Partition in an ExecutionContext-->
                    <value>{stepExecutionContext[minValue]}</value>
                    <value>#{stepExecutionContext[maxValue]}</value>
                </list>
            </property>
        </bean>
    </property>
    <property name="rowMapper" ref="customerRowMapper"/>
</bean>

разметки.java:

 package ...;
  import java.util.HashMap;  
 import java.util.Map;
 import org.springframework.batch.core.partition.support.Partitioner;
 import org.springframework.batch.item.ExecutionContext;
 public class ColumnRangePartitioner  implements Partitioner {
 private String column;
 private String table;
 public Map<String, ExecutionContext> partition(int gridSize) {
    int min =  queryForInt("SELECT MIN(" + column + ") from " + table);
    int max = queryForInt("SELECT MAX(" + column + ") from " + table);
    int targetSize = (max - min) / gridSize;
    System.out.println("Our partition size will be " + targetSize);
    System.out.println("We will have " + gridSize + " partitions");
    Map<String, ExecutionContext> result = new HashMap<String, ExecutionContext>();
    int number = 0;
    int start = min;
    int end = start + targetSize - 1;
    while (start <= max) {
        ExecutionContext value = new ExecutionContext();
        result.put("partition" + number, value);
        if (end >= max) {
            end = max;
        }
        value.putInt("minValue", start);
        value.putInt("maxValue", end);
        System.out.println("minValue = " + start);
        System.out.println("maxValue = " + end);
        start += targetSize;
        end += targetSize;
        number++;
    }
    System.out.println("We are returning " + result.size() + " partitions");
    return result;
}
public void setColumn(String column) {
    this.column = column;
}
public void setTable(String table) {
    this.table = table;
}
}

вот что я думаю:

  • как вы сказали, Ваш ThreadPoolTaskExecutor ограничен 15 потоками
  • "чанк" фреймворка вызывает выполнение каждого элемента в JdbcCursorItemReader (до предела потока) в другом потоке
  • но Spring Batch framework также ожидает, что каждый из потоков (т. е. все 15) завершит свой индивидуальный поток чтения/процесса/записи перед переходом на следующий фрагмент, учитывая вашу фиксацию интервал 1. Иногда это приводит к тому, что 14 потоков ждут почти 60 секунд на родственном потоке, который длится вечно.

другими словами, для этого многопоточного подхода в весенней партии, чтобы быть полезным, каждый поток должен обрабатывать примерно за то же время. Учитывая ваш сценарий, где существует огромное несоответствие между временем обработки определенных элементов, вы испытываете ограничение, когда многие из ваших потоков завершены и ждут длительный родственный поток, чтобы иметь возможность перейти на следующий кусок обработки.

мое предложение:

  • как правило, я бы сказал, что увеличение интервала фиксации должно помочь, так как оно должно позволить обрабатывать более одного элемента курсора в одном потоке между фиксациями, даже если один из потоков застрял на длительной записи. Однако, если вам не повезло, несколько длинных транзакций могут произойти в одном потоке и ухудшить ситуацию (например,, 120 сек. между фиксациями в одном потоке для интервала фиксации 2).
  • в частности, я бы предложил увеличить размер пула потоков до большого числа, даже превышая ваши максимальные подключения к базе данных на 2x или 3x. Что должно произойти, так это то, что, хотя некоторые из ваших потоков будут блокировать попытку получить соединение( из-за большого размера пула потоков), вы фактически увидите увеличение пропускной способности, поскольку ваши длительные потоки больше не останавливают другие потоки от принятия новых элементы из курсора и продолжение работы пакетного задания в то же время (в начале фрагмента количество ожидающих потоков значительно превысит количество доступных подключений к базе данных. Таким образом, планировщик ОС будет немного сбивать, поскольку он активирует потоки, которые заблокированы при получении соединения с базой данных, и должен деактивировать поток. Однако, поскольку большинство ваших потоков завершат свою работу и освободят соединение с базой данных относительно быстро, вы должны увидеть, что в целом ваша пропускная способность улучшается, поскольку многие потоки продолжают получать соединения с базой данных, выполнять работу, освобождать соединения с базой данных и разрешать дальнейшим потокам делать то же самое, даже когда ваши длительные потоки делают свое дело).

в моем случае, если я не устанавливаю ограничение дроссельной заслонки, то только 4 потока входят в метод read() ItemReader, который также является количеством потоков по умолчанию, если не указано в теге tasklet согласно документации Spring Batch.

Если я укажу больше потоков e.g 10 или 20 или 100, тогда только 8 потоков входят в метод read () ItemReader


ограничение 8 активных потоков независимо от значения ограничения дроссельной заслонки может быть вызвано конфликтом в репозитории пакетных заданий Spring. Каждый раз, когда обрабатывается кусок, некоторая информация записывается в репозиторий заданий. Увеличьте размер пула, чтобы разместить необходимое количество потоков!