Spring batch教程 之 擴展與並行處理

很多批處理問題都可以通過單進程、單線程的工作模式來完成, 所以在想要做一個複雜設計和實現之前,請審查你是否真的需要那些超級複雜的實現。 衡量實際作業(job)的性能,看看最簡單的實現是否能滿足需求: 即便是最普通的硬件,也可以在一分鐘內讀寫上百MB數據文件。


當你準備使用並行處理技術來實現批處理作業時,Spring Batch提供一系列選擇,本章將對他們進行講述,雖然某些功能不在本章中涵蓋。從高層次的抽象角度看,並行處理有兩種模式: 單進程,多線程模式; 或者多進程模式。還可以將他分成下面這些種類:


  • 多線程Step(單個進程)
  • 並行Steps(單個進程)
  • 遠程分塊Step(多個進程)
  • 對Step分區 (單/多個進程)

下面我們先回顧一下單進程方式,然後再看多進程方式.

1.1 多線程 Step


啓動並行處理最簡單的方式就是在 Step 配置中加上一個TaskExecutor , 比如,作爲 tasklet 的一個屬性:

<step id="loading">
    <tasklet task-executor="taskExecutor">...</tasklet>
</step>

上面的示例中, taskExecutor指向了另一個實現 TaskExecutor 接口的Bean. TaskExecutor 是一個標準的Spring接口,具體有哪些可用的實現類,請參考 Spring用戶指南. 最簡單的多線程 TaskExecutor 實現是 SimpleAsyncTaskExecutor.

以上配置的結果就是在 Step 在(每次提交的塊)記錄的讀取,處理,寫入時都會在單獨的線程中執行。請注意,這段話的意思就是在要處理的數據項之間沒有了固定的順序, 並且一個非連續塊可能包含項目相比,單線程的例子。此外executor還有一些限制(例如,如果它是由一個線程池在後臺執行的),有一個tasklet的配置項可以調整,throttle-limit默認爲4。你可能根據需要增加這個值以確保線程池被充分利用,如:

<step id="loading"> 
<tasklet task-executor="taskExecutor"
    throttle-limit="20">...</tasklet>
</step>


還需要注意在step中併發使用連接池資源時可能會有一些限制,例如數據庫連接池 DataSource. 請確保連接池中的資源數量大於或等於併發線程的數量.

在一些常見的批處理情景中,對使用多線程Step有一些實際的限制。Step中的許多部分(如readers 和 writers)是有狀態的,如果某些狀態沒有進行線程隔離,那麼這些組件在多線程Step中就是不可用的。特別是大多數Spring Batch提供的readers 和writers不是爲多線程而設計的。但是,我們也可以使用無狀態或線程安全的readers 和 writers,可以參考Spring Batch Samples中(parallelJob)的這個示例(點擊進入Section 6.12, “Preventing State Persistence”),示例中展示了通過指示器來跟蹤數據庫input表中的哪些項目已經被處理過,而哪些還沒有被處理。

Spring Batch 提供了ItemWriter 和 ItemReader 的一些實現. 通常在javadoc中會指明是否是線程安全的,或者指出在併發環境中需要注意哪些問題. 假若文檔中沒有明確說明,你只能通過查看源代碼來看看是否有什麼線程不安全的共享狀態. 一個並非線程安全的 reader , 也可以在你自己處理了同步的代理對象中高效地使用.

如果你的step中寫操作和處理操作所消耗的時間更多,那麼即使你對read()操作加鎖進行同步,也會比你在單線程環境中執行要快很多.

1.2 並行 Steps


只要需要並行的程序邏輯可以劃分爲不同的職責,並分配給各個獨立的step,那麼就可以在單個進程中並行執行。並行Step執行很容易配置和使用,例如,將執行步驟(step1,step2)和步驟3step3並行執行,則可以向下面這樣配置一個流程:

<job id="job1">
     <split id="split1" task-executor="taskExecutor" next="step4">
      <flow>
        <step id="step1" parent="s1" next="step2"/>
        <step id="step2" parent="s2"/>
      </flow>
      <flow>
        <step id="step3" parent="s3"/>
      </flow>
     </split>
     <step id="step4" parent="s4"/>
</job>
<beans:bean id="taskExecutor" class="org.spr...SimpleAsyncTaskExecutor"/>

可配置的 "task-executor" 屬性是用來指明應該用哪個TaskExecutor實現來執行獨立的流程。默認是SyncTaskExecutor,但有時需要使用異步的TaskExecutor來並行運行某些步驟。請注意,這項工作將確保每一個流程在聚合之前完成.並進行過渡。


1.3 遠程分塊(Remote Chunking)


使用遠程分塊的Step被拆分成多個進程進行處理,多個進程間通過中間件實現通信. 下面是一幅模型示意圖:



Master組件是單個進程,從屬組件(Slaves)一般是多個遠程進程。如果Master進程不是瓶頸的話,那麼這種模式的效果幾乎是最好的,因此應該在處理數據比讀取數據消耗更多時間的情況下使用(實際應用中常常是這種情形)。

Master組件只是Spring Batch Step 的一個實現, 只是將ItemWriter替換爲一個通用的版本,這個通用版本 "知道" 如何將數據項的分塊作爲消息(messages)發送給中間件。 從屬組件(Slaves)是標準的監聽器(listeners),不論使用哪種中間件(如使用JMS時就是 MesssageListeners ), Slaves的作用都是處理數據項的分塊(chunks), 可以使用標準的 ItemWriter 或者是ItemProcessor加上一個 ItemWriter, 使用的接口是 ChunkProcessor interface。使用此模式的一個優點是: reader,processor和 writer 組件都是現成的(就和在本機執行的step一樣)。數據項被動態地劃分,工作是通過中間件共享的,因此,如果監聽器都是飢餓模式的消費者,那麼就自動實現了負載平衡。

中間件必須持久可靠,能保證每個消息都會被分發,且只分發給單個消費者。JMS是很受歡迎的解決方案,但在網格計算和共享內存產品空間裏還有其他可選的方式(如 Java Spaces服務; 爲Java對象提供分佈式的共享存儲器)。

1.4 分區


Spring Batch也爲Step的分區執行和遠程執行提供了一個SPI(服務提供者接口)。在這種情況下,遠端的執行程序只是一些簡單的Step實例,配置和使用方式都和本機處理一樣容易。下面是一幅實際的模型示意圖:



在左側執行的作業(Job)是串行的Steps,而中間的那一個Step被標記爲 Master。圖中的 Slave 都是一個Step的相同實例,對於作業來說,這些Slave的執行結果實際上等價於就是Master的結果。Slaves通常是遠程服務,但也有可能是本地執行的其他線程。在此模式中,Master發送給Slave的消息不需要持久化(durable) ,也不要求保證交付: 對每個作業執行步驟來說,保存在JobRepository 中的Spring Batch元信息將確保每個Slave都會且僅會被執行一次。

Spring Batch的SPI由Step的一個專門的實現( PartitionStep),以及需要由特定環境實現的兩個策略接口組成。這兩個策略接口分別是 PartitionHandler 和 StepExecutionSplitter,他們的角色如下面的序列圖所示:



此時在右邊的Step就是“遠程”Slave,所以可能會有多個對象 和/或 進程在扮演這一角色,而圖中的 PartitionStep 在驅動(/控制)整個執行過程。PartitionStep的配置如下所示:

<step id="step1.master">
   <partition step="step1" partitioner="partitioner">
        <handler grid-size="10" task-executor="taskExecutor"/>
   </partition>
</step>

類似於多線程step的 throttle-limit 屬性, grid-size屬性防止單個Step的任務執行器過載。

在Spring Batch Samples示例程序中有一個簡單的例子在單元測試中可以拷貝/擴展(詳情請參考 *PartitionJob.xml 配置文件)。

Spring Batch 爲分區創建執行步驟,名如“step1:partition0”,等等,所以我們經常把Master step叫做“step1:master”。在Spring 3.0中也可以爲Step指定別名(通過指定 name 屬性,而不是 id 屬性)。

1.4.1 分區處理器(PartitionHandler)


PartitionHandler組件知道遠程網格環境的組織結構。 它可以發送StepExecution請求給遠端Steps,採用某種具體的數據格式,例如DTO.它不需要知道如何分割輸入數據,或者如何聚合多個步驟執行的結果。一般來說它可能也不需要了解彈性或故障轉移,因爲在許多情況下這些都是結構的特性,無論如何Spring Batch總是提供了獨立於結構的可重啓能力: 一個失敗的作業總是會被重新啓動,並且只會重新執行失敗的步驟。

PartitionHandler接口可以有各種結構的實現類: 如簡單RMI遠程方法調用,EJB遠程調用,自定義web服務、JMS、Java
Spaces, 共享內存網格(如Terracotta或Coherence)、網格執行結構(如GridGain)。Spring Batch自身不包含任何專有網格或遠程結構的實現。


但是 Spring Batch也提供了一個有用的PartitionHandler實現,在本地分開的線程中執行Steps,該實現類名爲
TaskExecutorPartitionHandler,並且他是上面的XML配置中的默認處理器。還可以像下面這樣明確地指定:

<step id="step1.master">
    <partition step="step1" handler="handler"/>
</step>

<bean class="org.spr...TaskExecutorPartitionHandler">
    <property name="taskExecutor" ref="taskExecutor"/>
    <property name="step" ref="step1" />
    <property name="gridSize" value="10" />
</bean>

gridSize決定要創建的獨立的step執行的數量,所以它可以配置爲TaskExecutor中線程池的大小,或者也可以設置得比可用的線程數稍大一點,在這種情況下,執行塊變得更小一些。

TaskExecutorPartitionHandler 對於IO密集型步驟非常給力,比如要拷貝大量的文件,或複製文件系統到內容管理系統時。它還可用於遠程執行的實現,通過爲遠程調用提供一個代理的步驟實現(例如使用Spring Remoting)。

1.4.2 分割器(Partitioner)


分割器有一個簡單的職責: 僅爲新的step實例生成執行環境(contexts),作爲輸入參數(這樣重啓時就不需要考慮)。 該接口只有一個方法:

 public interface Partitioner {
    Map<String, ExecutionContext> partition(int gridSize);
}

這個方法的返回值是一個Map對象,將每個Step執行分配的唯一名稱(Map泛型中的 String),和與其相關的輸入參數以
ExecutionContext 的形式做一個映射。 這個名稱隨後在批處理 meta data 中作爲分區 StepExecutions 的Step名字顯示。ExecutionContext僅僅只是一些 名-值對的集合,所以它可以包含一系列的主鍵,或行號,或者是輸入文件的位置。 然後遠程Step 通常使用 #{...}佔位符來綁定到上下文輸入(在 step作用域內的後期綁定),詳情請參見下一節。

step執行的名稱( Partitioner接口返回的 Map 中的 key)在整個作業的執行過程中需要保持唯一,除此之外沒有其他具體要求。要做到這一點,並且需要一個對用戶有意義的名稱,最簡單的方法是使用 前綴+後綴 的命名約定,前綴可以是被執行的Step的名稱(這本身在作業Job中就是唯一的),後綴可以是一個計數器。在框架中有一個使用此約定的 SimplePartitioner。

有一個可選接口 PartitioneNameProvider 可用於和分區本身獨立的提供分區名稱。 如果一個 Partitioner 實現了這個接口,那麼重啓時只有names會被查詢。 如果分區是重量級的,那麼這可能是一個很有用的優化。 很顯
然,PartitioneNameProvider提供的名稱必須和Partitioner提供的名稱一致。


1.4.3 將輸入數據綁定到 Steps


因爲step的輸入參數在運行時綁定到ExecutionContext中,所以由相同配置的PartitionHandler執行的steps是非常高效的。 通過 Spring Batch的StepScope特性這很容易實現(詳情請參考 後期綁定)。 例如,如果 Partitioner 創建 ExecutionContext 實例, 每個step執行都以fileName爲key 指向另一個不同的文件(或目錄),則 Partitioner 的輸出看起來可能像下面這樣:

表 1.1. 由執行目的目錄處理Partitioner提供的的step執行上下文名稱示例

| Step Execution Name (key) | ExecutionContext (value) |
| filecopy:partition0 | fileName=/home/data/one |
| filecopy:partition1 | fileName=/home/data/two |
| filecopy:partition2 | fileName=/home/data/three |

然後就可以將文件名綁定到 step 中, step使用了執行上下文的後期綁定:

<bean id="itemReader" scope="step" class="org.spr...MultiResourceItemReader">
    <property name="resource" value="#{stepExecutionContext[fileName]}/*"/>
</bean>


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章