Spring Batch 批量處理策略
為了幫助設計和實現批量處理系統,基本的批量應用是通過塊和模式來構建的,同時也應該能夠為程式開發人員和設計人員提供結構的樣例和基礎的批量處理程式。
當你開始設計一個批量作業任務的時候,商業邏輯應該被拆分一系列的步驟,而這些步驟又是可以通過下面的標準構件塊來實現的:
- 轉換應用程式(Conversion Applications):針對每一個從外部系統匯出或者提供的各種型別的檔案,我們都需要建立一個轉換應用程式來講這些型別的檔案和資料轉換為處理所需要的標準格式。這個型別的批量應用程式可以是正規轉換工具模組中的一部分,也可以是整個的轉換工具模組(請檢視:基本的批量服務(Basic Batch Services))。
- 校驗應用程式(Validation Applications):校驗應用程式能夠保證所有的輸入和輸出記錄都是正確和一致的。校驗通常是基於頭和尾進行校驗的,校驗碼和校驗演算法通常是針對記錄的交叉驗證。
- 提取應用(Extract Applications): 這個應用程式通常被用來從資料庫或者文字檔案中讀取一系列的記錄,並對記錄的選擇通常是基於預先確定的規則,然後將這些記錄輸出到輸出檔案中。
- 提取/更新應用(Extract/Update Applications):這個應用程式通常被用來從資料庫或者文字檔案中讀取記錄,並將每一條讀取的輸入記錄更新到資料庫或者輸出資料庫中。
- 處理和更新應用(Processing and Updating Applications):這種程式對從提取或驗證程式 傳過來的輸入事務記錄進行處理。這處理通常包括有讀取資料庫並且獲得需要處理的資料,為輸出處理更新資料庫或建立記錄。
- 輸出和格式化應用(Output/Format Applications):一個應用通過讀取一個輸入檔案,對輸入檔案的結構重新格式化為需要的標準格式,然後建立一個列印的輸出檔案,或將資料傳輸到其他的程式或者系統中。
更多的,一個基本的應用外殼應該也能夠被針對商業邏輯來提供,這個外殼通常不能通過上面介紹的這些標準模組來完成。
另外的一個主要的構建塊,每一個引用通常可以使用下面的一個或者多個標準工具步驟,例如:
- 分類(Sort)- 一個程式可以讀取輸入檔案後生成一個輸出檔案,在這個輸出檔案中可以對記錄進行重新排序,重新排序的是根據給定記錄的關鍵欄位進行重新排序的。分類通常使用標準的系統工具來執行。
- 拆分(Split)- 一個程式可以讀取輸入檔案後,根據需要的欄位值,將輸入的檔案拆分為多個檔案進行輸出。拆分通常使用標準的系統工具來執行。
- 合併(Merge)- 一個程式可以讀取多個輸入檔案,然後將多個輸入檔案進行合併處理後生成為一個單一的輸出檔案。合併可以自定義或者由引數驅動的(parameter-driven)系統實用程式來執行.
批量處理應用程式可以通過下面的輸入資料型別來進行分類:
- 資料庫驅動應用程式(Database-driven applications)可以通過從資料庫中獲得的行或值來進行驅動。
- 檔案驅動應用程式(File-driven applications) 可以通過從檔案中獲得的資料來進行驅動。
- 訊息驅動應用程式(Message-driven applications) 可以通過從訊息佇列中獲得的資料來進行驅動。
所有批量處理系統的處理基礎都是策略(strategy)。對處理策略進行選擇產生影響的因素包括有:預估批量處理需要處理的資料量,線上併發量,和另外一個批量處理系統的線上併發量,可用的批量處理時間視窗(很多企業都希望系統是能夠不間斷執行的,基本上來說批量處理可能沒有處理時間視窗)。
針對批量處理的標準處理選項包括有:
- 在一個批處理視窗中執行常規離線批處理
- 併發批量 / 線上處理
- 併發處理很多不同的批量處理或者有很多批量作業在同一時間執行
- 分割槽(Partitioning),就是在同一時間有很多示例在執行相同的批量作業
- 混合上面的一些需求
上面列表中的順序代表了批處理實現複雜性的排序,在同一個批處理視窗的處理最簡單,而分割槽實現最複雜。
上面的一些選項或者所有選項能夠被商業的任務排程所支援。
在下面的部分,我們將會針對上面的處理選項來對細節進行更多的說明。需要特別注意的是,批量處理程式使用提交和鎖定策略將會根據批量處理的不同而有所不同。作為最佳實踐,線上鎖策略應該使用相同的原則。因此,在設計批處理整體架構時不能簡單地拍腦袋決定,需要進行詳細的分析和論證。
鎖定策略可以僅僅使用常見的資料庫鎖或者你也可以在系統架構中使用其他的自定義鎖定服務。這個鎖服務將會跟蹤資料庫的鎖(例如在一個專用的資料庫表(db-table)中儲存必要的資訊),然後在應用程式請求資料庫操作時授予許可權或拒絕。重試邏輯應該也需要在系統架構中實現,以避免批量作業中的因資源鎖定而導致批量任務被終止。
批量處理作業視窗中的常規處理
針對執行在一個單獨批處理視窗中的簡單批量處理,更新的資料對線上使用者或其他批處理來說並沒有實時性要求,也沒有併發問題,在批處理執行完成後執行單次提交即可。
大多數情況下,一種更健壯的方法會更合適.要記住的是,批處理系統會隨著時間的流逝而增長,包括複雜度和需要處理的資料量。如果沒有合適的鎖定策略,系統仍然依賴於一個單一的提交點,則修改批處理程式會是一件痛苦的事情。 因此,即使是最簡單的批處理系統,也應該為重啟-恢復(restart-recovery)選項考慮提交邏輯。針對下面的情況,批量處理就更加複雜了。
併發批量 / 線上處理
批處理程式處理的資料如果會同時被線上使用者實時更新,就不應該鎖定線上使用者需要的所有任何資料(不管是資料庫還是檔案),即使只需要鎖定幾秒鐘的時間。
還應該每處理一批事務就提交一次資料庫。這減少了其他程式不可用的資料資料量,也壓縮了資料不可用的時間。
另一個可以使用的方案就是使用邏輯行基本的鎖定實現來替代物理鎖定。通過使用樂觀鎖(Optimistic Locking )或悲觀鎖(Pessimistic Locking)模式。
- 樂觀鎖假設記錄爭用的可能性很低。這通常意味著併發批處理和線上處理所使用的每個資料表中都有一個時間戳列。當程式讀取一行進行處理時,同時也獲得對應的時間戳。當程式處理完該行以後嘗試更新時,在 update 操作的 WHERE 子句中使用原來的時間戳作為條件.如果時間戳相匹配,則資料和時間戳都更新成功。如果時間戳不匹配,這表明在本程式上次獲取和此次更新這段時間內已經有另一個程式修改了同一條記錄,因此更新不會被執行。
- 悲觀鎖定策略假設記錄爭用的可能性很高,因此在檢索時需要獲得一個物理鎖或邏輯鎖。有一種悲觀邏輯鎖在資料表中使用一個專用的 lock-column 列。當程式想要為更新目的而獲取一行時,它在 lock column 上設定一個標誌。如果為某一行設定了標誌位,其他程式在試圖獲取同一行時將會邏輯上獲取失敗。當設定標誌的程式更新該行時,它也同時清除標誌位,允許其他程式獲取該行。請注意,在初步獲取和初次設定標誌位這段時間內必須維護資料的完整性,比如使用資料庫鎖(例如,SELECT FOR UPDATE)。還請注意,這種方法和物理鎖都有相同的缺點,除了它在構建一個超時機制時比較容易管理。比如記錄而使用者去吃午餐了,則超時時間到了以後鎖會被自動釋放。
這些模式並不一定適用於批處理,但他們可以被用在併發批處理和線上處理的情況下(例如,資料庫不支援行級鎖)。作為一般規則,樂觀鎖更適合於線上應用,而悲觀鎖更適合於批處理應用。只要使用了邏輯鎖,那麼所有訪問邏輯鎖保護的資料的程式都必須採用同樣的方案。
請注意:這兩種解決方案都只鎖定(address locking)單條記錄。但很多情況下我們需要鎖定一組相關的記錄。如果使用物理鎖,你必須非常小心地管理這些以避免潛在的死鎖。如果使用邏輯鎖,通常最好的解決辦法是建立一個邏輯鎖管理器,使管理器能理解你想要保護的邏輯記錄分組(groups),並確保連貫和沒有死鎖(non-deadlocking)。這種邏輯鎖管理器通常使用其私有的表來進行鎖管理、爭用報告、超時機制 等等。
並行處理
並行處理允許多個批量處理執行(run)/任務(job)同時並行地執行。以使批量處理總執行時間降到最低。如果多個任務不使用相同的檔案、資料表、索引空間時,批量處理這些不算什麼問題。如果確實存在共享和競爭,那麼這個服務就應該使用分割槽資料來實現。另一種選擇是使用控制表來構建一個架構模組以維護他們之間的相互依賴關係。控制表應該為每個共享資源分配一行記錄,不管這些資源是否被某個程式所使用。執行並行作業的批處理架構或程式隨後將查詢這個控制表,以確定是否可以訪問所需的資源。
如果解決了資料訪問的問題,並行處理就可以通過使用額外的執行緒來並行實現。在傳統的大型主機環境中,並行作業類上通常被用來確保所有程序都有充足的 CPU 時間。無論如何,解決方案必須足夠強勁,以確保所有正在執行的程序都有足夠的執行處理時間。
並行處理的其他關鍵問題還包括負載平衡以及一般系統資源的可用性(如檔案、資料庫緩衝池等)。請注意,控制表本身也可能很容易變成一個至關重要的資源(有可能發生嚴重競爭)。
分割槽
分割槽技術允許多版本的大型批處理程式併發地(concurrently)執行。這樣做的目的是減少超長批處理作業過程所需的時間。
可以成功分割槽的過程主要是那些可以拆分的輸入檔案 和/或 主要的資料庫表被分割槽以允許程式使用不同的資料來執行。
此外,被分割槽的過程必須設計為只處理分配給他的資料集。分割槽架構與資料庫設計和資料庫分割槽策略是密切相關的。請注意,資料庫分割槽並不一定指資料庫需要在物理上實現分割槽,儘管在大多數情況下這是明智的。
下面的圖片展示了分割槽的方法:
上圖: 分割槽處理
系統架構應該足夠靈活,以允許動態配置分割槽的數量。自動控制和使用者配置都應該納入考慮範圍。自動配置可以根據引數來決定,例如輸入檔案大小 和/或 輸入記錄的數量。
分割槽方案
面列出了一些可能的分割槽方案,至於具體選擇哪種分割槽方案,要根據具體情況來確定:
固定和均衡拆分記錄集
這涉及到將輸入的記錄集合分解成均衡的部分(例如,拆分為 10 份,這樣每部分是整個資料集的十分之一)。每個拆分的部分稍後由一個批處理/提取程式例項來處理。
為了使用這種方案,需要在預處理時候就將記錄集進行拆分。拆分的結果有一個最大值和最小值的位置,這兩個值可以用作限制每個 批處理/提取程式處理部分的輸入。
預處理可能有一個很大的開銷,因為它必須計算並確定的每部分資料集的邊界。
通過關鍵欄位(Key Column)拆分
這涉及到將輸入記錄按照某個關鍵欄位來拆分,比如一個地區程式碼(location code),並將每個鍵分配給一個批處理例項。為了達到這個目標,也可以使用列值。
通過分割槽表來指派給一個批量處理例項
請檢視下面的詳細說明。
在使用這種方法時, 新值的新增將意味著需要手動重新配置批處理/提取程式,以確保新值被新增到某個特定的例項。
通過資料的部分值指派給一個批量處理例項
例如,值 0000-0999, 1000 - 1999, 等。
使用這種方法的時候,將確保所有的值都會被某個批處理作業例項處理到。然而,一個例項處理的值的數量依賴於列值的分佈(即可能存在大量的值分佈在0000-0999範圍內,而在1000-1999範圍內的值卻很少)。如果使用這種方法,設計時應該考慮到資料範圍的切分。
使用 通過分割槽表來指派 和 通過資料的部分值, 在這兩種方法中,並不能將指定給批處理例項的記錄實現最佳均勻分佈。批處理例項的數量並不能動態配置。
通過檢視(Views)
這種方法基本上是根據鍵列來分解,但不同的是在資料庫級進行分解。它涉及到將記錄集分解成檢視。這些檢視將被批處理程式的各個例項在處理時使用。分解將通過資料分組來完成。
使用這個方法時,批處理的每個例項都必須為其配置一個特定的檢視(而非主表)。當然,對於新新增的資料,這個新的資料分組必須被包含在某個檢視中。也沒有自動配置功能,例項數量的變化將導致檢視需要進行相應的改變。
附加的處理識別器
這涉及到輸入表一個附加的新列,它充當一個指示器。在預處理階段,所有指示器都被標誌為未處理。在批處理程式獲取記錄階段,只會讀取被標記為未處理的記錄,一旦他們被讀取(並加鎖),它們就被標記為正在處理狀態。當記錄處理完成,指示器將被更新為完成或錯誤。批處理程式的多個例項不需要改變就可以開始,因為附加列確保每條紀錄只被處理一次。
使用該選項時,表上的I/O會動態地增長。在批量更新的程式中,這種影響被降低了,因為寫操作是必定要進行的。
提取表到無格式檔案
這包括將表中的資料提取到一個檔案中。然後可以將這個檔案拆分成多個部分,作為批處理例項的輸入。
使用這個選項時,將資料提取到檔案中,並將檔案拆分的額外開銷,有可能抵消多分割槽處理(multi-partitioning)的效果。可以通過改變檔案分割指令碼來實現動態配置。
With this option, the additional overhead of extracting the table into a file, and splitting it, may cancel out the effect of multi-partitioning. Dynamic configuration can be achieved via changing the file splitting script.
使用雜湊列(Hashing Column)
這個計劃需要在資料庫表中增加一個雜湊列(key/index)來檢索驅動(driver)記錄。這個雜湊列將有一個指示器來確定將由批處理程式的哪個例項處理某個特定的行。例如,如果啟動了三個批處理例項,那麼 “A” 指示器將標記某行由例項 1 來處理,“B”將標記著將由例項 2 來處理,以此類推。
稍後用於檢索記錄的過程(procedure)程式,將有一個額外的 WHERE 子句來選擇以一個特定指標標記的所有行。這個表的插入(insert)需要附加的標記欄位,預設值將是其中的某一個例項(例如“A”)。
一個簡單的批處理程式將被用來更新不同例項之間的重新分配負載的指標。當新增足夠多的新行時,這個批處理會被執行(在任何時間,除了在批處理視窗中)。
批處理應用程式的其他例項只需要像上面這樣的批處理程式執行著以重新分配指標,以決定新例項的數量。
資料庫和應用設計原則
如果一個支援多分割槽(multi-partitioned)的應用程式架構,基於資料庫採用關鍵列(key column)分割槽方法拆成的多個表,則應該包含一箇中心分割槽倉庫來儲存分割槽引數。這種方式提供了靈活性,並保證了可維護性。這個中心倉庫通常只由單個表組成,叫做分割槽表。
儲存在分割槽表中的資訊應該是是靜態的,並且只能由 DBA 維護。每個多分割槽程式對應的單個分割槽有一行記錄,組成這個表。這個表應該包含這些列:程式 ID 編號,分割槽編號(分割槽的邏輯ID),一個分割槽對應的關鍵列(key column)的最小值,分割槽對應的關鍵列的最大值。
在程式啟動時,應用程式架構(Control Processing Tasklet, 控制處理微執行緒)應該將程式 id 和分割槽號傳遞給該程式。這些變數被用於讀取分割槽表,來確定應用程式應該處理的資料範圍(如果使用關鍵列的話)。另外分割槽號必須在整個處理過程中用來:
- 為了使合併程式正常工作,需要將分割槽號新增到輸出檔案/資料庫更新
- 向框架的錯誤處理程式報告正常處理批處理日誌和執行期間發生的所有錯誤
死鎖最小化
當程式並行或分割槽執行時,會導致資料庫資源的爭用,還可能會發生死鎖(Deadlocks)。其中的關鍵是資料庫設計團隊在進行資料庫設計時必須考慮儘可能消除潛在的競爭情況。
還要確保設計資料庫表的索引時考慮到效能以及死鎖預防。
死鎖或熱點往往發生在管理或架構表上,如日誌表、控制表、鎖表(lock tables)。這些影響也應該納入考慮。為了確定架構可能的瓶頸,一個真實的壓力測試是至關重要的。
要最小化資料衝突的影響,架構應該提供一些服務,如附加到資料庫或遇到死鎖時的 等待-重試(wait-and-retry)間隔時間。這意味著要有一個內建的機制來處理資料庫返回碼,而不是立即引發錯誤處理,需要等待一個預定的時間並重試執行資料庫操作。
引數處理和校驗
對程式開發人員來說,分割槽架構應該相對透明。框架以分割槽模式執行時應該執行的相關任務包括:
- 在程式啟動之前獲取分割槽引數
- 在程式啟動之前驗證分割槽引數
- 在啟動時將引數傳遞給應用程式
驗證(validation)要包含必要的檢查,以確保:
- 應用程式已經足夠涵蓋整個資料的分割槽
- 在各個分割槽之間沒有遺漏斷代(gaps)
如果資料庫是分割槽的,可能需要一些額外的驗證來保證單個分割槽不會跨越資料庫的片區。
體系架構應該考慮整合分割槽(partitions).包括以下關鍵問題:
- 在進入下一個任務步驟之前是否所有的分割槽都必須完成?
- 如果一個分割槽 Job 中止了要怎麼處理?