90-character pass for scalability.adoc

I removed extraneous characters from non-code lines and arranged the non-code lines to fit into 90 characters.
This commit is contained in:
Jay Bryant
2017-10-13 16:03:45 -05:00
committed by Michael Minella
parent 3a3abdc28a
commit 0f989d9c9f

View File

@@ -6,48 +6,36 @@
== Scaling and Parallel Processing
Many batch processing problems can be solved with single threaded,
single process jobs, so it is always a good idea to properly check if that
meets your needs before thinking about more complex implementations. Measure
the performance of a realistic job and see if the simplest implementation
meets your needs first. You can read and write a file of several hundred
megabytes in well under a minute, even with standard hardware.
Many batch processing problems can be solved with single threaded, single process jobs,
so it is always a good idea to properly check if that meets your needs before thinking
about more complex implementations. Measure the performance of a realistic job and see if
the simplest implementation meets your needs first. You can read and write a file of
several hundred megabytes in well under a minute, even with standard hardware.
When you are ready to start implementing a job with some parallel
processing, Spring Batch offers a range of options, which are described in
this chapter, although some features are covered elsewhere. At a high level,
there are two modes of parallel processing:
When you are ready to start implementing a job with some parallel processing, Spring
Batch offers a range of options, which are described in this chapter, although some
features are covered elsewhere. At a high level, there are two modes of parallel
processing:
* Single process, multi-threaded
* Multi-process
These break down into categories as well, as
follows:
These break down into categories as well, as follows:
* Multi-threaded Step (single process)
* Parallel Steps (single process)
* Remote Chunking of Step (multi process)
* Partitioning a Step (single or multi process)
First, we review the single-process options. Then we review the
multi-process options.
First, we review the single-process options. Then we review the multi-process options.
[[multithreadedStep]]
=== Multi-threaded Step
The simplest way to start parallel processing is to add a
`TaskExecutor` to your Step configuration. For example, you might add an
attribute of the `tasklet`, as shown in the following example:
The simplest way to start parallel processing is to add a `TaskExecutor` to your Step
configuration. For example, you might add an attribute of the `tasklet`, as shown in the
following example:
[source, xml]
----
@@ -56,25 +44,21 @@ The simplest way to start parallel processing is to add a
</step>
----
In this example, the taskExecutor is a reference to another bean
definition that implements the `TaskExecutor`
interface. https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/task/TaskExecutor.html[`TaskExecutor`] is a standard Spring
interface, so consult the Spring User Guide for details of available
implementations. The simplest multi-threaded
`TaskExecutor` is a
`SimpleAsyncTaskExecutor`.
The result of the above configuration is that the `Step`
executes by reading, processing, and writing each chunk of items
(each commit interval) in a separate thread of execution. Note
that this means there is no fixed order for the items to be
processed, and a chunk might contain items that are
non-consecutive compared to the single-threaded case. In addition
to any limits placed by the task executor (such as whether it is backed by
a thread pool), there is a throttle limit in the tasklet
configuration which defaults to 4. You may need to increase this
to ensure that a thread pool is fully utilized, as shown in the following example:
In this example, the taskExecutor is a reference to another bean definition that
implements the `TaskExecutor` interface.
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/task/TaskExecutor.html[`TaskExecutor`]
is a standard Spring interface, so consult the Spring User Guide for details of available
implementations. The simplest multi-threaded `TaskExecutor` is a
`SimpleAsyncTaskExecutor`.
The result of the above configuration is that the `Step` executes by reading, processing,
and writing each chunk of items (each commit interval) in a separate thread of execution.
Note that this means there is no fixed order for the items to be processed, and a chunk
might contain items that are non-consecutive compared to the single-threaded case. In
addition to any limits placed by the task executor (such as whether it is backed by a
thread pool), there is a throttle limit in the tasklet configuration which defaults to 4.
You may need to increase this to ensure that a thread pool is fully utilized, as shown in
the following example:
[source, xml]
----
@@ -84,49 +68,41 @@ The result of the above configuration is that the `Step`
</step>
----
Note also that there may be limits placed on concurrency by
any pooled resources used in your step, such as
a `DataSource`. Be sure to make the pool in
those resources at least as large as the desired number of
concurrent threads in the step.
Note also that there may be limits placed on concurrency by any pooled resources used in
your step, such as a `DataSource`. Be sure to make the pool in those resources at least
as large as the desired number of concurrent threads in the step.
There are some practical limitations of using multi-threaded `Step` implementations
for some common batch use cases. Many participants in a `Step` (such as readers
and writers) are stateful. If the state is not segregated by thread,
then those components are not usable in a multi-threaded `Step`. In
particular, most of the off-the-shelf readers and writers from Spring Batch
are not designed for multi-threaded use. It is, however, possible to work
with stateless or thread safe readers and writers, and there is a sample
(called `parallelJob`) in the https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples[Spring Batch Samples] that show the use of a process
indicator (see <<readersAndWriters.adoc#process-indicator,Preventing State Persistence>>) to keep
track of items that have been processed in a database input table.
Spring Batch provides some implementations of
`ItemWriter` and
`ItemReader`. Usually, they say in the
Javadoc if they are thread safe or not or what you have to do to
avoid problems in a concurrent environment. If there is no
information in the Javadoc, you can check the implementation to see
if there is any state. If a reader is not thread safe, it may
still be efficient to use it in your own synchronizing delegator.
You can synchronize the call to `read()` and as
long as the processing and writing is the most expensive part of
the chunk, your step may still complete much faster than it would in a
single threaded configuration.
There are some practical limitations of using multi-threaded `Step` implementations for
some common batch use cases. Many participants in a `Step` (such as readers and writers)
are stateful. If the state is not segregated by thread, then those components are not
usable in a multi-threaded `Step`. In particular, most of the off-the-shelf readers and
writers from Spring Batch are not designed for multi-threaded use. It is, however,
possible to work with stateless or thread safe readers and writers, and there is a sample
(called `parallelJob`) in the
https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples[Spring
Batch Samples] that show the use of a process indicator (see
<<readersAndWriters.adoc#process-indicator,Preventing State Persistence>>) to keep track
of items that have been processed in a database input table.
Spring Batch provides some implementations of `ItemWriter` and `ItemReader`. Usually,
they say in the Javadoc if they are thread safe or not or what you have to do to avoid
problems in a concurrent environment. If there is no information in the Javadoc, you can
check the implementation to see if there is any state. If a reader is not thread safe,
it may still be efficient to use it in your own synchronizing delegator. You can
synchronize the call to `read()` and as long as the processing and writing is the most
expensive part of the chunk, your step may still complete much faster than it would in a
single threaded configuration.
[[scalabilityParallelSteps]]
=== Parallel Steps
As long as the application logic that needs to be parallelized can
be split into distinct responsibilities and assigned to individual steps,
then it can be parallelized in a single process. Parallel Step execution
is easy to configure and use, for example, to execute steps
`(step1,step2)` in parallel with
`step3`, as shown in the following example:
As long as the application logic that needs to be parallelized can be split into distinct
responsibilities and assigned to individual steps, then it can be parallelized in a
single process. Parallel Step execution is easy to configure and use, for example, to
execute steps `(step1,step2)` in parallel with `step3`, as shown in the following
example:
[source, xml]
----
@@ -146,91 +122,76 @@ As long as the application logic that needs to be parallelized can
<beans:bean id="taskExecutor" class="org.spr...SimpleAsyncTaskExecutor"/>
----
The configurable "task-executor" attribute is used to specify which
`TaskExecutor` implementation should be used to execute the individual
flows. The default is `SyncTaskExecutor`, but an
asynchronous `TaskExecutor` is required to run the steps in parallel. Note
that the job ensures that every flow in the split completes before
aggregating the exit statuses and transitioning.
The configurable "task-executor" attribute is used to specify which `TaskExecutor`
implementation should be used to execute the individual flows. The default is
`SyncTaskExecutor`, but an asynchronous `TaskExecutor` is required to run the steps in
parallel. Note that the job ensures that every flow in the split completes before
aggregating the exit statuses and transitioning.
See the section on <<step.adoc#split-flows,Split Flows>> for more
detail.
See the section on <<step.adoc#split-flows,Split Flows>> for moredetail.
[[remoteChunking]]
=== Remote Chunking
In remote chunking, the `Step` processing is split across multiple
processes, communicating with each other through some middleware. The following
image shows the pattern:
In remote chunking, the `Step` processing is split across multiple processes,
communicating with each other through some middleware. The following image shows the
pattern:
.Remote Chunking
image::{batch-asciidoc}images/remote-chunking.png[Remote Chunking, scaledwidth="60%"]
The master component is a single process, and the slaves are
multiple remote processes. This pattern works best if the master
is not a bottleneck, so the processing must be more expensive than the
reading of items (as is often the case in practice).
The master component is a single process, and the slaves are multiple remote processes.
This pattern works best if the master is not a bottleneck, so the processing must be more
expensive than the reading of items (as is often the case in practice).
The master is an implementation of a Spring Batch
`Step` with the `ItemWriter` replaced by a generic
version that knows how to send chunks of items to the middleware as
messages. The slaves are standard listeners for whatever middleware is
being used (for example, with JMS, they would be
`MesssageListener` implementations), and their role is to process
the chunks of items using a standard `ItemWriter` or
`ItemProcessor` plus
`ItemWriter`, through the
`ChunkProcessor` interface. One of the advantages of
using this pattern is that the reader, processor, and writer components are
off-the-shelf (the same as would be used for a local execution of the
step). The items are divided up dynamically and work is shared through the
middleware, so that, if the listeners are all eager consumers, then load
balancing is automatic.
The master is an implementation of a Spring Batch `Step` with the `ItemWriter` replaced
by a generic version that knows how to send chunks of items to the middleware as
messages. The slaves are standard listeners for whatever middleware is being used (for
example, with JMS, they would be `MesssageListener` implementations), and their role is
to process the chunks of items using a standard `ItemWriter` or `ItemProcessor` plus
`ItemWriter`, through the `ChunkProcessor` interface. One of the advantages of using this
pattern is that the reader, processor, and writer components are off-the-shelf (the same
as would be used for a local execution of the step). The items are divided up dynamically
and work is shared through the middleware, so that, if the listeners are all eager
consumers, then load balancing is automatic.
The middleware has to be durable, with guaranteed delivery and a
single consumer for each message. JMS is the obvious candidate, but other
options (such as Java Spaces exist in the grid computing and shared memory product space.
The middleware has to be durable, with guaranteed delivery and a single consumer for each
message. JMS is the obvious candidate, but other options (such as Java Spaces exist in
the grid computing and shared memory product space.
[[partitioning]]
=== Partitioning
Spring Batch also provides an SPI for partitioning a `Step` execution
and executing it remotely. In this case, the remote participants are
`Step` instances that could just as easily have been configured and used for
local processing. The following image shows the pattern:
Spring Batch also provides an SPI for partitioning a `Step` execution and executing it
remotely. In this case, the remote participants are `Step` instances that could just as
easily have been configured and used for local processing. The following image shows the
pattern:
.Partitioning
image::{batch-asciidoc}images/partitioning-overview.png[Partitioning Overview, scaledwidth="60%"]
The `Job` runs on the left-hand side as a sequence of `Step` instances,
and one of the `Step` instances is labeled as a master. The slaves in this picture
are all identical instances of a `Step`, which could in fact take the place
of the master, resulting in the same outcome for the `Job`. The slaves are
typically going to be remote services but could also be local threads of
execution. The messages sent by the master to the slaves in this pattern
do not need to be durable or have guaranteed delivery. Spring Batch
metadata in the JobRepository ensures that
each slave is executed once and only once for each `Job` execution.
The `Job` runs on the left-hand side as a sequence of `Step` instances, and one of the
`Step` instances is labeled as a master. The slaves in this picture are all identical
instances of a `Step`, which could in fact take the place of the master, resulting in the
same outcome for the `Job`. The slaves are typically going to be remote services but
could also be local threads of execution. The messages sent by the master to the slaves
in this pattern do not need to be durable or have guaranteed delivery. Spring Batch
metadata in the JobRepository ensures that each slave is executed once and only once for
each `Job` execution.
The SPI in Spring Batch consists of a special implementation of `Step`
(called the `PartitionStep`) and two strategy interfaces
that need to be implemented for the specific environment. The strategy
interfaces are `PartitionHandler` and
`StepExecutionSplitter`, and their role is shown in
the following sequence diagram:
The SPI in Spring Batch consists of a special implementation of `Step` (called the
`PartitionStep`) and two strategy interfaces that need to be implemented for the specific
environment. The strategy interfaces are `PartitionHandler` and `StepExecutionSplitter`,
and their role is shown in the following sequence diagram:
.Partitioning SPI
image::{batch-asciidoc}images/partitioning-spi.png[Partitioning SPI, scaledwidth="60%"]
The `Step` on the right in this case is the "remote" slave, so,
potentially, there are many objects and or processes playing this role, and
the `PartitionStep` is shown driving the execution. The following example shows the `PartitionStep`
configuration:
The `Step` on the right in this case is the "remote" slave, so, potentially, there are
many objects and or processes playing this role, and the `PartitionStep` is shown driving
the execution. The following example shows the `PartitionStep` configuration:
[source, xml]
----
@@ -241,53 +202,44 @@ The `Step` on the right in this case is the "remote" slave, so,
</step>
----
Similar to the multi-threaded step's `throttle-limit`
attribute, the `grid-size` attribute prevents the task executor from
being saturated with requests from a single step.
Similar to the multi-threaded step's `throttle-limit` attribute, the `grid-size`
attribute prevents the task executor from being saturated with requests from a single
step.
There is a simple example that can be copied and extended in the
unit test suite for https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples/src/main/resources/jobs[Spring Batch Samples] (see
`Partition*Job.xml` configuration).
There is a simple example that can be copied and extended in the unit test suite for
https://github.com/spring-projects/spring-batch/tree/master/spring-batch-samples/src/main/resources/jobs[Spring
Batch Samples] (see `Partition*Job.xml` configuration).
Spring Batch creates step executions for the partitions called
"step1:partition0", and so on. Many people prefer to call the master step
"step1:master" for consistency. You can use an
alias for the step (by specifying the `name` attribute
instead of the `id` attribute).
Spring Batch creates step executions for the partitions called "step1:partition0", and so
on. Many people prefer to call the master step "step1:master" for consistency. You can
use an alias for the step (by specifying the `name` attribute instead of the `id`
attribute).
[[partitionHandler]]
==== PartitionHandler
The `PartitionHandler` is the component that
knows about the fabric of the remoting or grid environment. It is able
to send `StepExecution` requests to the remote
`Step` instances, wrapped in some fabric-specific format, like a DTO. It does not
have to know how to split the input data or how to aggregate the
result of multiple `Step` executions. Generally speaking, it probably also
does not need to know about resilience or failover, since those are
features of the fabric in many cases. In any case, Spring Batch always
provides restartability independent of the fabric. A failed `Job` can
always be restarted and only the failed `Steps` are
re-executed.
The `PartitionHandler` is the component that knows about the fabric of the remoting or
grid environment. It is able to send `StepExecution` requests to the remote `Step`
instances, wrapped in some fabric-specific format, like a DTO. It does not have to know
how to split the input data or how to aggregate the result of multiple `Step` executions.
Generally speaking, it probably also does not need to know about resilience or failover,
since those are features of the fabric in many cases. In any case, Spring Batch always
provides restartability independent of the fabric. A failed `Job` can always be restarted
and only the failed `Steps` are re-executed.
The `PartitionHandler` interface can have
specialized implementations for a variety of fabric types, including simple
RMI remoting, EJB remoting, custom web service, JMS, Java Spaces, shared
memory grids (like Terracotta or Coherence), and grid execution fabrics
(like GridGain). Spring Batch does not contain implementations for any
proprietary grid or remoting fabrics.
Spring Batch does, however, provide a useful implementation of
`PartitionHandler` that executes `Step` instances locally in
separate threads of execution, using the
`TaskExecutor` strategy from Spring. The
implementation is called
`TaskExecutorPartitionHandler`, and it is the
default for a step configured with the XML namespace shown previously. It can
also be configured explicitly, as shown in the following example:
The `PartitionHandler` interface can have specialized implementations for a variety of
fabric types, including simple RMI remoting, EJB remoting, custom web service, JMS, Java
Spaces, shared memory grids (like Terracotta or Coherence), and grid execution fabrics
(like GridGain). Spring Batch does not contain implementations for any proprietary grid
or remoting fabrics.
Spring Batch does, however, provide a useful implementation of `PartitionHandler` that
executes `Step` instances locally in separate threads of execution, using the
`TaskExecutor` strategy from Spring. The implementation is called
`TaskExecutorPartitionHandler`, and it is the default for a step configured with the XML
namespace shown previously. It can also be configured explicitly, as shown in the
following example:
[source, xml]
----
@@ -302,27 +254,23 @@ Spring Batch does, however, provide a useful implementation of
</bean>
----
The `gridSize` attribute determines the number of separate
step executions to create, so it can be matched to the size of the
thread pool in the `TaskExecutor`. Alternatively, it can
be set to be larger than the number of threads available, which makes
the blocks of work smaller.
The `gridSize` attribute determines the number of separate step executions to create, so
it can be matched to the size of the thread pool in the `TaskExecutor`. Alternatively, it
can be set to be larger than the number of threads available, which makes the blocks of
work smaller.
The `TaskExecutorPartitionHandler` is
useful for IO-intensive `Step` instances, such as copying large numbers of files or
replicating filesystems into content management systems. It can also be
used for remote execution by providing a `Step` implementation that is a
proxy for a remote invocation (such as using Spring Remoting).
The `TaskExecutorPartitionHandler` is useful for IO-intensive `Step` instances, such as
copying large numbers of files or replicating filesystems into content management
systems. It can also be used for remote execution by providing a `Step` implementation
that is a proxy for a remote invocation (such as using Spring Remoting).
[[stepExecutionSplitter]]
==== Partitioner
The `Partitioner` has a simpler responsibility: to generate
execution contexts as input parameters for new step executions only (no
need to worry about restarts). It has a single method, as shown in the following interface definition:
The `Partitioner` has a simpler responsibility: to generate execution contexts as input
parameters for new step executions only (no need to worry about restarts). It has a
single method, as shown in the following interface definition:
[source, java]
----
@@ -331,57 +279,40 @@ public interface Partitioner {
}
----
The return value from this method associates a unique name for
each step execution (the `String`) with input
parameters in the form of an `ExecutionContext`.
The names show up later in the Batch metadata as the step name in the
partitioned `StepExecutions`. The
`ExecutionContext` is just a bag of name-value
pairs, so it might contain a range of primary keys, line numbers, or
the location of an input file. The remote `Step`
then normally binds to the context input using `#{...}`
placeholders (late binding in step scope), as illustrated in the next
section.
The return value from this method associates a unique name for each step execution (the
`String`) with input parameters in the form of an `ExecutionContext`. The names show up
later in the Batch metadata as the step name in the partitioned `StepExecutions`. The
`ExecutionContext` is just a bag of name-value pairs, so it might contain a range of
primary keys, line numbers, or the location of an input file. The remote `Step` then
normally binds to the context input using `#{...}` placeholders (late binding in step
scope), as illustrated in the next section.
The names of the step executions (the keys in the
`Map` returned by
`Partitioner`) need to be unique amongst the step
executions of a `Job` but do not have any other specific requirements.
The easiest way to do this (and to make the names meaningful for users)
is to use a prefix+suffix naming convention, where the prefix is the
name of the step that is being executed (which itself is unique in the
`Job`), and the suffix is just a counter. There is
a `SimplePartitioner` in the framework that uses
this convention.
The names of the step executions (the keys in the `Map` returned by `Partitioner`) need
to be unique amongst the step executions of a `Job` but do not have any other specific
requirements. The easiest way to do this (and to make the names meaningful for users) is
to use a prefix+suffix naming convention, where the prefix is the name of the step that
is being executed (which itself is unique in the `Job`), and the suffix is just a
counter. There is a `SimplePartitioner` in the framework that uses this convention.
An optional interface called
`PartitionNameProvider` can be used to
provide the partition names separately from the partitions
themselves. If a `Partitioner` implements
this interface, then, on a restart, only the names are queried.
If partitioning is expensive, this can be a useful optimization.
The names provided by the
`PartitionNameProvider` must match those
provided by the `Partitioner`.
An optional interface called `PartitionNameProvider` can be used to provide the partition
names separately from the partitions themselves. If a `Partitioner` implements this
interface, then, on a restart, only the names are queried. If partitioning is expensive,
this can be a useful optimization. The names provided by the `PartitionNameProvider` must
match those provided by the `Partitioner`.
[[bindingInputDataToSteps]]
==== Binding Input Data to Steps
It is very efficient for the steps that are executed by the
`PartitionHandler` to have identical configuration and for their input
parameters to be bound at runtime from the `ExecutionContext`. This is
easy to do with the StepScope feature of Spring Batch (covered in more
detail in the section on <<step.adoc#late-binding,Late Binding>>). For example,
if the `Partitioner` creates
`ExecutionContext` instances with an attribute key called
`fileName`, pointing to a different file (or
directory) for each step invocation, the
`Partitioner` output might resemble the content of the following table:
It is very efficient for the steps that are executed by the `PartitionHandler` to have
identical configuration and for their input parameters to be bound at runtime from the
`ExecutionContext`. This is easy to do with the StepScope feature of Spring Batch
(covered in more detail in the section on <<step.adoc#late-binding,Late Binding>>). For
example, if the `Partitioner` creates `ExecutionContext` instances with an attribute key
called `fileName`, pointing to a different file (or directory) for each step invocation,
the `Partitioner` output might resemble the content of the following table:
.Example step execution name to execution context provided by `Partitioner` targeting directory processing
|===============
|__Step Execution Name (key)__|__ExecutionContext (value)__
|filecopy:partition0|fileName=/home/data/one
@@ -389,10 +320,8 @@ It is very efficient for the steps that are executed by the
|filecopy:partition2|fileName=/home/data/three
|===============
Then the file name can be bound to a step using late binding to
the execution context, as shown in the following example:
Then the file name can be bound to a step using late binding to the execution context, as
shown in the following example:
[source, xml]
----