資料處理中提升效能的方法-引入並發但是避免同步

來源:互聯網
上載者:User

背景

只要存在資料庫,就會有後台批量處理資料的需求,比如資料表備份、定期清理、資料替換、資料移轉,對於批量處理來說,往往會涉及大量的查詢、過濾、歸類、彙總計算,在批量指令碼中直接查詢資料庫往往效能太低,甚至會因為一個大型的SQL導致資料庫鎖表出現線上事故,因此一般採用先匯出到檔案,在檔案上計算然後再匯入,比如:

1、使用mysql -e “select * from table” > output.txt的方式,執行SQL,將結果匯出到檔案中;

2、針對檔案,使用各種方式進行彙總、過濾、替換等計算,最後產出成需要使用的格式;

3、發布產出的檔案,或者使用load data命令匯入到資料庫;

由於只是一次性的批量查詢資料庫匯出到檔案,然後針對檔案進行計算,而不是每次都查詢資料庫,大量節省了網路的IO耗費,從而提升處理的速度。

然而得到了匯出的檔案之後,如果檔案過大,或者計算邏輯複雜比如大量的調用了耗費CPU的正則匹配、彙總計算,那麼單線程的處理會耗費大量的時間,這時候就可以引入並發處理,使得機器的CPU、記憶體、IO、網路等資源全部充分利用起來,大幅度降低處理時間。

引入多線程,拆分輸入檔案為多個,每個小檔案啟動一個處理線程

HADOOP的MAP-REDUCE的做法,是先將檔案split成小分區檔案,然後針對每個分區做計算,最後將每個分區的結果彙總在一起,然而由於HADOOP的調度、叢集穩定性等各種原因,對於MB大小層級的檔案處理,會發現速度非常慢,有時候甚至比單機單線程處理速度還慢,將單機單線程改成多線程,往往會發現令人驚訝的效果提升。

直觀的做法,是使用主線程讀取輸入的單個大檔案,然後將讀取的結果分配給子線程處理,然後主線程做整合,這種方式因為多線程共用了單個檔案的IO,需要加入對檔案的同步機制,最後會發現效能瓶頸在這單個檔案的讀取同步之上。

可以將大檔案分區成小檔案,然後每個檔案分配給單個線程單獨處理,避免線程間的資源同步,每個線程會享用單獨的CPU核、記憶體單元、檔案控制代碼,處理速度能達到最快。

使用這種方式,可以用以下的步驟進行:

1、使用SHELL,將輸入檔案拆分成預定線程數目的份數,存放到一個目錄中;

2、以輸入檔案的目錄路徑作為參數,程式設計語言JAVA/PYTHON讀取該目錄的所有檔案,對於每個檔案啟動一個處理線程,進行處理;

3、SHELL將輸出目錄的所有檔案,使用cat file* > output_file的方式,得到最終的計算結果

Shell

將輸入檔案拆分成多個小檔案,啟動多線程進行處理,輸出結果檔案

function run multitask(){ # 開啟多個非同步線程 SPLITS COUNT=20 # 輸入檔案總數 sourcefile linescount=

cat ${input_file} | wc -l
# 計算出拆分的檔案個數 split filelines count=$(( $sourcefile linescount / $SPLITS COUNT )) # 進行檔案拆分 split -l $splitfile linescount -a 3 -d ${input file} ${input

dir}/inputFile_

# 執行JAVA程式$JAVA_CMD -classpath $jar_path "net.crazyant.BackTaskMain" "${input_dir}" "${output_dir}" "${output_err_dir}"# 合并檔案cat ${output_dir}/* > ${output_file}

}

run multitask

## 將輸入檔案拆分成多個小檔案,啟動多線程進行處理,輸出結果檔案#function run_multi_task(){ # 開啟多個非同步線程 SPLITS_COUNT=20 # 輸入檔案總數 source_file_lines_count=`cat ${input_file} | wc -l` # 計算出拆分的檔案個數 split_file_lines_count=$(( $source_file_lines_count / $SPLITS_COUNT )) # 進行檔案拆分 split -l $split_file_lines_count -a 3 -d ${input_file} ${input_dir}/inputFile_  # 執行JAVA程式 $JAVA_CMD -classpath $jar_path "net.crazyant.BackTaskMain" "${input_dir}" "${output_dir}" "${output_err_dir}"  # 合并檔案 cat ${output_dir}/* > ${output_file}} run_multi_task

這裡注意,拆分檔案的時候,不能使用split按照大小進行拆分,因為會把輸入檔案中的行截斷;

對應的JAVA程式,則是通過讀取檔案夾中檔案清單的方法,每個檔案單獨啟動一個線程:

Java

public class BackTaskMain {    public static void main(String[] args) {        String inputDataDir = args[1];        String outputDataDir = args[2];        String errDataDir = args[3];                File inputDir = new File(inputDataDir);        File[] inputFiles = inputDir.listFiles();                // 記錄開啟的線程        List threads = new ArrayList();        for (File inputFile : inputFiles) {            if (inputFile.getName().equals(".") || inputFile.getName().equals("..")) {                continue;            }                        // 針對每個inputFile,產生對應的outputFile和errFile            String outputSrcLiceFpath = outputDataDir + "/" + inputFile.getName() + ".out";            String errorOutputFpath = errDataDir + "/" + inputFile.getName() + ".err";                        // 建立Runnable            BackRzInterface backRzInterface = new BackRzInterface();            backRzInterface.setInputFilePath(inputFile.getAbsolutePath());            backRzInterface.setOutputFilePath(outputSrcLiceFpath);            backRzInterface.setErrorOutputFpath(errorOutputFpath);                        // 建立Thread,啟動線程            Thread singleRunThread = new Thread(backRzInterface);            threads.add(singleRunThread);            singleRunThread.start();        }                for (Thread thread : threads) {            try {                // 使用thread.join(),等待所有的線程執行完畢                thread.join();                System.out.println(thread.getName() + " has over");            } catch (InterruptedException e) {                e.printStackTrace();            }        }        System.out.println("proccess all over");    }}
public class BackTaskMain {    public static void main(String[] args) {        String inputDataDir = args[1];        String outputDataDir = args[2];        String errDataDir = args[3];                FileinputDir = new File(inputDataDir);        File[] inputFiles = inputDir.listFiles();                // 記錄開啟的線程        List threads = new ArrayList();        for (FileinputFile : inputFiles) {            if (inputFile.getName().equals(".") || inputFile.getName().equals("..")) {                continue;            }                        // 針對每個inputFile,產生對應的outputFile和errFile            String outputSrcLiceFpath = outputDataDir + "/" + inputFile.getName() + ".out";            String errorOutputFpath = errDataDir + "/" + inputFile.getName() + ".err";                        // 建立Runnable            BackRzInterfacebackRzInterface = new BackRzInterface();            backRzInterface.setInputFilePath(inputFile.getAbsolutePath());            backRzInterface.setOutputFilePath(outputSrcLiceFpath);            backRzInterface.setErrorOutputFpath(errorOutputFpath);                        // 建立Thread,啟動線程            ThreadsingleRunThread = new Thread(backRzInterface);            threads.add(singleRunThread);            singleRunThread.start();        }                for (Threadthread : threads) {            try {                // 使用thread.join(),等待所有的線程執行完畢                thread.join();                System.out.println(thread.getName() + " has over");            } catch (InterruptedException e) {                e.printStackTrace();            }        }        System.out.println("proccess all over");    }}

通過這種方式,將大檔案拆分成小檔案,啟動多個線程,每個線程處理一個小檔案,最終將每個小檔案的結果彙總,就得到了最終產出,效能上卻大幅提升。

若有依賴的資源,可以按線程先複製、拆分、複製,防止依賴的資源成為效能瓶頸

在上面的代碼中,BackRzInterface是每個線程啟動時要使用的Runnable對象,可以看到用的是每次new的方式建立:

// 建立RunnableBackRzInterface backRzInterface = new BackRzInterface();

這樣每個處理線程依賴的BackRzInterface都是獨立的,對這個Runnable代碼的使用不會存在同步問題。

如果多執行緒中需要使用外部資源,最好想辦法使得每個線程單獨使用自己獨佔的資源,相互之間若不會存在衝突,可以實現最大化並發處理。

其他一些例子,比如:

  • 多線程用到了字典檔案,那麼方法是首先將字典檔案複製多份,每個線程使用自己獨佔的字典,避免並發同步訪問字典;
  • 多線程若需要統一ID發號,可以提前計算出每個輸入檔案的行數,然後依次產生第一個線程需要的ID範圍、第二個線程需要的ID範圍,這些不同的ID範圍也可以分別產生不同的檔案,這樣每個線程會使用各自獨立的ID資源,避免了多個線程單時刻訪問單個ID發號服務,使得發號成為效能瓶頸的可能;
  • 多線程如果依賴相同的Service,如果可以每次new對象就每次new,如果Bean都是在Spring中管理,則將Service加上@Scope(“prototype”),或者將對象每次clone一下得到一個新對象,保證最終每個線程使用自己獨佔的對象。
  • 盡量使用函數式編程的思想,每個函數都不要產生副作用,不要修改入參,結果只能通過return返回,避免增加代碼同步衝突的可能;

通過以上這些類似的方法,每次將可能需要同步訪問的共用資源,通過複製、分區等手段得到不同份,每個線程單獨訪問自己那一份,避免同步訪問,最終實現效能最優。

避免同步的終極方法:使用多進程進行實現資源隔離

如果將檔案拆分成了多份,依賴的ID、詞典等資源也相應提供了多份,但是發現代碼中存在無法解決的代碼層級同步,該怎麼辦呢?

相對於想盡辦法解決代碼中的同步問題來說,多線程和多進程之間的效能差別微乎其微,我們都知道線程會使用進程的資源,所以導致了線程之間存在競爭進程資源,但是對於進程來說,CPU、記憶體等硬體資源是完全隔離的,這時候將程式運行在多進程而不是多線程,反而能更好的提升效能。

對於一些支援多線程不好的語言,比如PHP,直接用這種多進程計算的方法,速度並不比支援多線程的JAVA、PYTHON語言差:

Shell

要拆分的檔案數,也就是要啟動的多進程數

SPLITS_COUNT=20

input splitsdir="${input dir}splits" output splitsdir="${output dir}splits"

輸入檔案行數

source filelines_count=

cat ${input_file} | wc -l

每個檔案拆分的行數=總行數除以要拆分的檔案個數(也就是對應進程的個數)

split filelines count=$(( $sourcefile linescount / ${SPLITS_COUNT} ))

執行拆分,注意這裡使用-l進行行層級拆分更好

split -l $split filelines count -a 3 -d ${inputfile} ${input splitsdir}/inputfile_

process idx=1 for fname in $(ls ${inputsplits dir}); do inputfpath=${input splitsdir}/$fname ouput fpath=${outputsplits dir}/$fname # 後台執行所有進程 php "/php/main.php" "${inputfpath}" "${ouput fpath}" & (( processidx++ )) done

等待所有後台進程執行結束

wait

合并檔案

cat $output splitsdir/* > ${output_file}

# 要拆分的檔案數,也就是要啟動的多進程數SPLITS_COUNT=20 input_splits_dir="${input_dir}_splits"output_splits_dir="${output_dir}_splits"# 輸入檔案行數source_file_lines_count=`cat ${input_file} | wc -l`# 每個檔案拆分的行數=總行數除以要拆分的檔案個數(也就是對應進程的個數)split_file_lines_count=$(( $source_file_lines_count / ${SPLITS_COUNT} ))# 執行拆分,注意這裡使用-l進行行層級拆分更好split -l $split_file_lines_count -a 3 -d ${input_file} ${input_splits_dir}/inputfile_ process_idx=1for fname in $(ls ${input_splits_dir}); do input_fpath=${input_splits_dir}/$fname ouput_fpath=${output_splits_dir}/$fname # 後台執行所有進程 php "/php/main.php" "${input_fpath}" "${ouput_fpath}" & (( process_idx++ )) done # 等待所有後台進程執行結束wait # 合并檔案cat $output_splits_dir/* > ${output_file}

上述代碼中,使用shell的&符號,可以在後台同時啟動多個進程,使用wait文法,可以實現多線程的Thread.join特性,等待所有的進程執行結束。

總結

對於輸入檔案的大小、計算的複雜度處於單機和叢集計算之間的資料處理,使用並發處理最為合適,但是並發的同步處理卻會降低多線程的效能,這時可以藉助於輸入檔案複製拆分、依賴資源複製拆分切片等方法,實現每個線程處理自己的獨佔資源,從而最大化提升計算速度。而對於一些無法避免的代碼同步衝突邏輯,可以退化為多進程處理資料,藉助於SHELL的後台進程支援,實現進程層級的資源獨佔,最終大幅提升處理效能。

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.