在很多應用場景中,我們都會面臨著排序需求,可以說是見怪不怪。我們也看過許多的排序演算法:從最簡單的氣泡排序、選擇排序,到稍微好點的插入排序、希爾排序,再到有點理論的堆排序、快速排序,再到高階的歸併排序、桶排序、基數排序。
而實際工作中我們可能用到的排序有哪些呢?而且,大部分時序,相信大家都是使用一個現有庫API直接就完成了排序功能。所以,講真,大家還真不一定會很好排序。
不過本文的目的不是基礎排序演算法,而是如何處理資料量的檔案的內容排序問題?
1. 多大的檔案算大檔案?
多大的檔案算大檔案?這是個定義的問題,當我每天處理的都是幾百幾千的資料,那麼我遇到幾萬的資料後,我可以認為這個資料量是大的。
但總體來說,我們還是需要定義一個量級的,不然無法評估處理能力問題。
比如我們定義超過200M的檔案算大檔案可以不?我們定義超過5000w行的資料算大檔案可以不?
好了,基於這樣大的資料量的時候,也許我們就不能簡單的呼叫幾個庫函式就解決問題了,至少你load到記憶體也將存在一定的風險了。
所以,是時候想想優化的事了!
2. 如何利用好現有的工具?
針對一些問題,我們可以自己做,也可以找別人幫忙做。具體誰來做,這是個問題!
比如,你自己花個一兩天的時間,寫了個排序演算法,搞定了。但是,你能保證你的穩定性嗎?你能經受住生產環境複雜的環境考驗嗎?
再比如,你可以現有的工具進行操作,如果有人提供了穩定的api函式供呼叫的話,你可以這麼幹。如果你的服務是跑在linux環境下,那麼,我們有必要試一下系統提供的排序功能。 sort . 這工具絕對是經過無數的考驗的,可以放心使用。它也有豐富的引數供選擇,這對我們的日常工作非常有幫助,但對於一個普通的排序也許我們並不需要。
比如最簡單的,自然排序:
sort 1-merged.txt -o 1-sorted.txt
就可以將檔案排好序了。但是當資料非常大的時候,比如我使用 7000w+ 的行數(約1.2G)進行排序時,就花費了 6min+ . 也許是我硬體不怎麼好,但是實際上它就是會很慢啊!
$ time sort 1-merged.txt -o 1-sorted.txt real 8m5.709s user 25m19.652s sys 0m4.523s
這種效能,在當今大資料橫行的時代,基本就是胎死腹中了。大資料應對的都是TB/PB 級別的數量,而我們僅為GB級並且沒有做其他業務就已經耗費了這麼長時間,這是沒辦法繼續了。讓我進一步深入。
看到文件裡有說,系統本地化配置影響排序,實際就是存在一個編解碼的問題,它會依據本地的配置來進行轉換字元然後再進行排序。這個開銷可是不小哦,比如我們設定都是中文環境。而要去除這個影響,則可以使用新增 LC_ALL=C 之後就會使用原始的值進行排序,具體影響就是省去轉換編碼的開銷了。那麼,我們用這個引數試試。
*** WARNING *** The locale specified by the environment affects sort order. Set LC_ALL=C to get the traditional sort order that uses native byte values. $ time LC_ALL=C sort 1-merged.txt -o 1-sorted.txt real 2m52.663s user 2m7.577s sys 0m5.146s
哇,從8分鐘降到了3分鐘,雖然不是數量級的提升,但至少下降了一半以上的時間消耗,還是非常可觀的。到這個地步,也許能滿足我們的場景了。
但是,請注意一個問題,這裡的 LC_ALL=C 之後,就會使用預設的進行處理了,那麼,會有什麼影響呢?實際上就是一些本地相關的東西,就會失效了。
最直接的,就是中文處理的問題了,比如我有一個檔案內容是這樣的:
床前看月光,
疑是地上霜。
舉頭望山月,
低頭思故鄉。
天子呼來不上船,
自稱臣是酒中仙。
紅酥手,
黃藤酒,
滿城春色宮牆柳。
那麼,我們使用 LC_ALL=C 設定來排序後,將會得到如下結果:
$ LC_ALL=C sort 1.txt -o 1-s1.txt $ cat 1-s1.txt 舉頭望山月, 低頭思故鄉。 天子呼來不上船, 滿城春色宮牆柳。 疑是地上霜。 紅酥手, 自稱臣是酒中仙。 黃藤酒, 床前明月光,
額,看不懂啥意思?中文咋排序的我也給整忘了(而且各自機器上得到的結果也可能不一樣)。好吧,沒關係,我們去掉 LC_ALL=C 來看看結果:
$ sort 1.txt -o 1-s1.txt $ cat 1-s1.txt 床前明月光, 低頭思故鄉。 紅酥手, 黃藤酒, 舉頭望山月, 滿城春色宮牆柳。 天子呼來不上船, 疑是地上霜。 自稱臣是酒中仙。
這下看懂了吧,這是按照拼音順序來排序的。所以,你說 LC_ALL=C 重不重要,如果沒有本地化設定,很多東西都是不符合情理的。所以,有時候我們還真不能這麼幹咯。
如果真想這麼幹,除非你確認你的檔案裡只有英文字元符號和數字,或者是 ASCII 的127 個字元。
3. 繞個路高階一下
前面的方法,不是不能解決問題,而是不能解決所有問題。所以,我們還得繼續想辦法。想想當下對大檔案的處理檔案都有哪些?實際也不多,平行計算是根本,但我們也許做不了平行計算,但我們可以拆分檔案嘛。一個檔案太大,我們就檔案拆小排序後再合併嘛!就是不知道效能如何?
split -l 100000 -d ../1-merged.txt -a 4 sp_; for file in sp_*.txt; do; sort -o $file sorted_$file; done; sort -m sp_*.txt -o targed.txt; # 一行化後的格式 $ time for file in sp_*; do sort -o sorted_$file $file; done; sort -m sorted_* -o targetd.txt; real 12m15.256s user 10m11.465s sys 0m18.623s # 以上時間僅是單個檔案的排序時間還不算歸併的時間,下面這個程式碼可以統一計算 $ time `for file in sp_1_*; do sort $file -o sorted_$file; done; sort -m sorted_* -o targetd.txt;` real 14m27.643s user 11m13.982s sys 0m22.636s
看起來切分小檔案後,排序太耗時間了,看看能不能用多程式輔助下!(還是回到了平行計算的問題上了)
# shell 非同步執行就是在其後面新增 & 就可以了, 但是最後的歸併是同步的. $ time `split -l 100000 -d ../1-merged.txt -a 4 sp_ ; for file in sp_* ; do {sort $file -o $file} &; done; wait; sort -m sp_* -o target.txt ; ` # 多處計時監控 $ time `time split -l 100000 -d ../1-merged.txt -a 4 sp_; time for file in sp_1_*; do { sort $file -o $file } & ; done; time wait; time sort -m sp_* -o target.txt;` # 以上報錯,因為命令列下不允許使用 & 操作, 只能自己寫shell指令碼,然後執行了 # sort_merge.sh time split -l 100000 -d ../1-merged.txt -a 4 sp_; i=0 for file in sp_*; do { #echo "sort -o $file $file"; sort -o $file $file; } & done; time wait; time sort -m sp_* -o target.txt; # 以上指令碼的確是會以非同步進行排序,但會開啟非常多的程式,從而導致程式排程繁忙,機器假死 # 需要修復下 # sort_merge.sh split_file_prefix='sp_' rm -rf ${split_file_prefix}*; time split -l 1000000 -d ../short-target.csv -a 4 ${split_file_prefix}; i=0 for file in ${split_file_prefix}*; do { sort -o $file $file; } & # 每開5個程式,就等一下 (( i=$i + 1 )) b=$(( $i % 10 )) if [ $b = 0 ] ; then # 小優化: 只要上一個程式退出就繼續,而不是等到所有程式退出再繼續 time wait $! # time wait fi; done; time wait; time sort -m ${split_file_prefix}* -o target.txt; # 以上執行下來,耗時9min+, 比未優化時還要差, 尷尬! real 9m54.076s user 19m1.480s sys 0m36.016s
看起來沒啥優勢啊, 咋整? 我們們試著調下參試試!
# 1. 將單個檔案設定為50w行記錄, 耗時如下: # 額, 沒跑完, 反正沒啥提升, 單個檔案排序由之前的2s左右, 上升到了11s左右 # 2. 將單個設定為20w行記錄試試: # 單個檔案排序上升到了4.xs, 也不理想啊; real 9m2.948s user 21m25.373s sys 0m27.067s
增加下並行度試試!
# 增加並行度到10 real 9m3.569s user 21m4.346s sys 0m27.519s # 單檔案行數 500000, 並行10個程式 real 8m12.916s user 21m40.624s sys 0m20.988s
看起來效果更差了,或者差不多. 難道這引數咋調整也沒用了麼? 算了, 不搞了.
4. 換個效能好的機器試試
前面的機器太差了,也沒了信心。乾脆換一個試試。直接進入優化引數環節:
# 50w行, 5程式 real 5m6.348s user 5m38.684s sys 0m44.997s # 100w行, 5程式 real 2m39.386s user 3m34.682s sys 0m23.157s # 100w行, 10程式 real 2m22.223s user 3m41.079s sys 0m25.418s # 以上結論是行數更容易影響結果, 因排序是計算型密集型任務, 程式數也許等於CPU核數比較優的選擇 # 不過也有一個干擾項:即檔案的讀取寫入是IO開銷,此時2倍以上的CPU核數程式可能是更好的選擇 # 100w行, 10程式, 7100w總數(1.6G) # 使用原始排序 sort real 6m14.485s user 5m10.292s sys 0m13.042s # 使用 LC_ALL=C 測試結果 real 2m1.992s user 1m18.041s sys 0m11.254s # 使用分治排序, 100w行, 10程式 real 2m53.637s user 4m22.170s sys 0m29.979s
好吧,linux的優化估計只能到這裡了。
5. 自行實現大檔案排序
看起來shell幫不了太多忙了,咋整?用java實現?執行緒池可以很好利用佇列先後問題;但到底有多少優勢呢?試試就試試!但執行緒可以很方便地並行,比如在分片檔案的同時,其他執行緒就可以同進行排序了,也許等分片完成,排序就ok了呢!
簡單時序可表示為: split -> f1 -> submit(f1) -> sort(f1) -> merge(f1) -> wait (所有merge都完成)。也就是說所有過程都並行化了,拆分一個檔案就可以進行排序檔案,排序完一個檔案就可以合併一個檔案。
執行緒模型是這樣的: 1個拆分執行緒 -> n個排序執行緒 -> 1個歸併執行緒;任務執行完成的前提是:n個排序執行緒完成 + n個歸併任務完成;
大體思路是這樣,但還可以優化的是,歸併執行緒是否可以維護一個指標,代表最後一次插入的結果,以便可以快速比較插入;(有點困難,還要寫新檔案,這裡可能是個瓶頸點,因為如果有1000次歸併,就會存在讀寫大檔案的過程,如果前面的檔案分片存在速度慢的問題,那麼此處的反覆寫更是一個瓶頸點,即使是用linux的sort進行歸併也要花2min+,更不用說一次次地寫讀了)
程式碼實現:待完善!
。。。