協助調教批次程式處理緩慢與OOM問題

處理效能與OOM問題

前情提要

上週接到了一個工作任務,說某一直批次執行緩慢,以及在寫檔的時候會發生OOM(Out of memory),想請我協助查看問題,並優化程式。就這樣一周的時間我都在處理這件事情。

發現問題

舊程式的寫法其實挺髒的,加上資料庫查詢結果全部返回的是Map物件型態…這是一個多可怕的事情,會造成要追查欄位很難追查,尤其是用Map物件直接傳入Method的時候,在裡面的某處又去Set某個一值,這真的要查起來真的是難上加難,這就是楊藝為什麼要用一個禮拜來好好調整的原因之一。

但以上的問題還不足以造成處理效能與OOM問題,楊藝不能否認,查詢資料庫的結果直接使用Map的速度會比再次封裝成物件的速度快,小資料量很無感,但是面對非常大的資料量時,在封裝成物件的過程會耗費大量時間,反而成為效能問題之一,但…即便是有這樣的情形也該控制在一定範圍,不可發散到各個地方去。

真正造成問題的原因是以下幾點:

  1. 用了不是批次更新的方式批次更新資料

    針對這一點在舊程式裡面一直是用另一個List物件蒐集要Inert到資料庫的資料,使用for迴圈的方式將處理好的資料一一的加入List物件,等到一定數量後,再將這個物件傳入更新資料的Method中,然後繼續使用for迴圈將資料一筆一筆寫入資料庫…這樣的方式完全不是批次更新,而且會讓原本迴圈執行全數多上一倍,還不如直接將資料寫入資料庫會比較快。

  2. 使用單線執行緒處理資料

    在面對大量資料的時候,基本上會希望提高系統資源的使用率,最基礎的就是CPU以及記憶體,在可能的情況下當筆資料處理變成非同步的處理多筆資料,最後再將結果收集起來做後續處理。

  3. 將大檔讀進程式欲傳輸給另一台伺服器

    AP伺服器基本上不會提供太多的記憶體,因此如果要把一個大檔讀進AP直接包成傳輸給另一個AP的物件,好一點是自己掛掉,衰一點是自己沒掛掉,對方伺服器也沒設定上傳容量限制,結果把人家給搞死了…

處理方式

既然照出了問題根源,那楊藝就來一個一個解決,主要解決方案如下。

  1. 重構程式碼

    這是得先執行的第一步,舊埕式邏輯雜亂不堪,所有程式都寫在同一支批次檔案內,因此我得先讀懂程式目的,然後一一地將它分離出來,資料庫操作的部分搬進DAO,該是Util的部分拆出Util類別,並且解除原本邏輯中許多Site Effect問題。簡化舊程式邏輯,舊程式內很多邏輯寫得很長,其實往往一兩行就可以完成原本十幾二十行的目的了。

  2. 改寫資料庫操作為真批次

    將原本邏輯釐清之後,資料庫邏輯原本一筆一筆寫入的方式也得修正,我在DAO內改是用JdbcTemplate更新資料的方式處理,一來加快效能,二來原本是用自己組裝字串的方式是非常消耗記憶體的,改用了正規的方法,記憶體用量也瞬間減少許多。

  3. 使用分頁

    面對大量資料處理,若把資料一次全部讀進程式,那不就跟把大檔案一次讀進去一樣,最終會導致OOM,因此我再次設定可調參數,可以指定分頁筆數大小,把原本一筆一筆的處理改為一批一批的處理。

  4. 使用分片

    AP處理資料的技巧之一,就是將資料蒐集完成之後,切成多分交給其他執行緒處理,也可能是其他AP進行處理,最後再將結果蒐集起來,進行後續處理。

  5. 使用非同步將讀寫分離

    來源資料表跟寫入的資料表示完全不同的資料表,如果同步處理,撈取資料得花時間,寫資料也需要時間,這樣一批處理的時間至少得是撈取資料時間+資料處理時間+寫資料時間,因此為了加速處理時間,我決定在資料處理完之後就丟給另一個執行緒進行資料寫入動作,如此一來在讀取資料的時候,可以一邊寫入資料,等資料讀取的過程,早已把資料寫入完成,等待下一批資料的到來,如此一來資料處理時間就會變成撈取資料時間+資料處理時間。

  6. 不在迴圈裡面查資料庫

    原本邏輯中,將原本查回來的資料在迴圈裡面進行處理,這會造成每一筆資料至少都得多一次資料庫查詢的往返時間,在資料少的情況下,看不出太大差異,但資料量一大,累積起來的時間是非常驚人的。

  7. 加入快取機制

    在資料處理的過程中,會有一些資料是可以直接從資料庫撈回來的,這樣的話就不需要再去查詢資料庫了,或是有些資料邏輯處理的結果,許多資料只要符合某種樣態就會有一樣的結果,因此我在程式中加入快取機制,將撈回來的資料放進Map物件中,然後在處理的時候直接從Map物件中取得資料,並實作資料限制的方法,維持一定快取資料即可,無須存下所有資料。

目前處理結果

目前還沒全部處理完成,但是以模擬差不多的資料量原本需要處理7至8個鐘頭的批次已經被我縮減到1個鐘頭出頭可以處理完成,依照下面的VisualVM監控圖來看,其實這支批次真的使用到的CPU資源不多,在控制記憶體穩定用量的情況下,最後的瓶頸就是資料庫的分頁撈取資料以及批次資料更新上。

VisualVM監控圖

後續處理

楊藝並還沒有完全完成效能優化,主要是整理完邏輯測試可行性之後,其實有一個地方的調整可以在讓速度再縮短大概一半的處理時間,也就是半個鐘頭左右可以完成,但我得下週工作再去釐清一些問題,才能決策出最佳的解決方案。