Spring Batch:具有多线程执行程序的Tasklet具有与Throttling算法相关的非常糟糕的性能

时间:2013-08-15 22:20:32

标签: java multithreading performance spring spring-batch

使用Spring批处理2.2.1,我已经配置了一个Spring Batch Job,我使用了这种方法:

配置如下:

  • Tasklet使用ThreadPoolTask​​Executor限制为15个线程

  • throttle-limit等于线程数

  • Chunk与:

    一起使用
    • JdbcCursorItemReader的1个同步适配器,允许它按照Spring Batch文档推荐由许多线程使用

        

      您可以将调用同步到read(),只要处理和写入是块中最昂贵的部分,您的步骤可能仍然比单线程配置快得多。

    • 在JdbcCursorItemReader

    • 上,saveState为false
    • 基于JPA的Custom ItemWriter。 请注意,对一个项目的处理可能会因处理时间而异,可能需要几毫秒到几秒(> 60秒)。

    • 将commit-interval设置为1(我知道它可能会更好,但不是问题)

  • 关于Spring Batch doc recommandation

  • ,所有jdbc池都没问题

由于以下原因,运行批处理会导致非常奇怪和糟糕的结果:

  • 在某个步骤,如果这些项目需要一些时间来处理,那么线程池中的几乎所有线程最终都不会执行任何操作而只会处理,只有慢速编写器正在工作。

查看Spring Batch代码,根本原因似乎在这个包中:

  • 组织/ springframework的/批次/重复/支撑/

这种工作方式是一种功能还是限制/错误?

如果它是一个功能,配置的方式是什么方式使所有线程不受长处理工作的影响而不必重写所有内容?

请注意,如果所有项目都占用相同的时间,一切正常,多线程就可以了,但如果其中一项处理需要花费更多时间,那么多线程在慢速进程工作时几乎无用。

注意我打开了这个问题:

4 个答案:

答案 0 :(得分:5)

正如亚历克斯所说,似乎这种行为是根据javadocs的契约:

  

子类只需要提供一个获取下一个结果*的方法,以及一个等待从并发*进程或线程返回所有结果的方法

看看:

  

TaskExecutorRepeatTemplate#waitForResults

另一个选择是使用分区:

  • 将从Partitionned ItemReader执行项目的TaskExecutorPartitionHandler,见下文
  • 分区程序实现,它提供了由ItemReader处理的范围,请参阅下面的ColumnRangePartitioner
  • 将使用分区程序填充的内容读取数据的CustomReader,请参阅下面的myItemReader配置

Michael Minella在他的书Pro Spring Batch 的第11章中解释了这一点:

<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>

Partitioner.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;
}
}

答案 1 :(得分:3)

以下是我的想法:

  • 正如您所说,您的ThreadPoolTask​​Executor仅限于15个线程
  • 框架的“块”导致JdbcCursorItemReader中的每个项目(直到线程限制)在不同的线程中执行
  • 但Spring Batch框架也在等待每个线程(即全部15个)完成各自的读取/处理/写入流程,然后移动到下一个块,给定您的提交间隔为1.有时,这个导致14个线程在兄弟线程上等待将近60秒,这个线程将永远完成。

换句话说,为了使Spring Batch中的这种多线程方法有所帮助,每个线程需要在大约相同的时间内处理。鉴于您的场景中某些项目的处理时间之间存在巨大差异,您遇到了一个限制,其中许多线程已完成并等待长时间运行的兄弟线程以便能够进入下一个处理块。

我的建议:

  • 一般来说,我会说增加你的提交间隔应该有所帮助,因为它应该允许在提交之间的单个线程中处理多个游标项,即使其中一个线程卡在长时间运行写。但是,如果您运气不好,可能会在同一个线程中发生多个长事务并使事情变得更糟(例如,在提交间隔为2的情况下,单个线程中的提交之间为120秒。)
  • 具体来说,我建议将线程池大小增加到一个大数字,甚至超过最大数据库连接2倍或3倍。应该发生的是,即使你的一些线程会阻止尝试获取连接(因为线程池大小很大),你实际上会看到吞吐量增加,因为长时间运行的线程不再停止其他线程从光标中获取新项目并在此期间继续批处理作业的工作(在块的开头,您的待处理线程数将大大超过可用数据库连接的数量。因此OS调度程序将在激活线程时稍微改变一下在获取数据库连接时被阻止并且必须停用该线程。但是,由于大多数线程将完成其工作并相对快速地释放其数据库连接,您应该看到总体上您的吞吐量得到改善,因为许多线程继续获取数据库连接,做工作,释放数据库连接,并允许更多的线程做同样的事情,即使你的长期运行的线程正在做他们的事情)

答案 2 :(得分:1)

在我的情况下,如果我没有设置限制限制,那么只有4个线程进入ItemReader的read()方法,这也是默认的线程数,如果未按照Spring Batch文档在tasklet标记中指定的话

如果我指定更多线程,例如10或20或100,那么只有8个线程进入ItemReader的read()方法

答案 3 :(得分:1)

无论throttle限制的值如何,8个活动线程的限制可能是由Spring Batch Job存储库上的争用引起的。每次处理一个块时,一些信息都会写入作业存储库。增加其池大小以适应您需要的线程数量!