Command-Line

如何優化這個 Unix 命令?

  • September 23, 2016

以下命令大約需要 10 分鐘才能輸出結果

find . -name "muc*_*_20160920_*.unl*" | xargs zcat |
   awk -F "|" '{if($14=="20160920100643" && $22=="567094398953") print $0}'| head

我怎樣才能提高它的性能?

這已經相當優化了。如果不了解更多細節,很難知道瓶頸是什麼:

  • 儲存類型(HD、SSD、網路、RAID)
  • 匹配文件的數量和平均大小
  • 目錄和其他不匹配文件的數量
  • 每行中的欄位數
  • 線的平均長度

在任何情況下你都可以做的事情:

  • 替換-print | xargs-exec cmd {} +-print0 | xargs -r0如果您的find/xargs支持它。-print | xargs不僅錯誤而且成本更高,因為xargs需要對字元進行解碼以找出哪些是空白並進行一些昂貴的報價處理。
  • 將語言環境修復為 C ( export LC_ALL=C)。由於此處涉及的所有字元(|以及文件內容和拉丁字母的十進制數字、文件名的句點和下劃線)都是可移植字元集的一部分,如果您的字元集是 UTF-8 或其他一些其他多字節字元集,則切換到具有單字節字元集的 C 將為find和進行大量工作awk
  • 將部分簡化awk為:awk -F "|" '$14 == "20160920100643" && $22 == "567094398953"'.
  • 由於您將輸出通過管道傳輸到head,因此您可能希望禁用輸出緩衝,awk以便它儘早輸出這 10 行。使用gawkor mawk,您可以使用fflush()它。或者你可以添加一個if (++n == 10) exitin awk

總結一下:

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -exec zcat {} + |
 awk -F "|" '$14 == "20160920100643" && $22 == "567094398953" {
   print; if (++n == 10) exit}')

如果 CPU 是瓶頸,在多核 GNU 系統上,您可以嘗試:

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -print0 |
 xargs -r0P 4 -n 100 sh -c '
   zcat "$@" | 
     awk -F "|" "\$14 == "20160920100643" && \$22 == "567094398953" {
       print; fflush()}"' sh | head)

在 100 個文件批次上並行執行 4zcat | awk個作業。

如果這20160920100643是一個時間戳,您可能希望排除在此之前最後修改的文件。對於 GNU 或 BSD find,添加一個-newermt '2016-09-20 10:06:42'.

如果行有大量欄位,您會因為awk拆分它並分配這麼多$n欄位而受到懲罰。使用只考慮前 22 個欄位的方法可以加快速度:

grep -E '^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)'

而不是awk命令。使用 GNU grep,添加--line-buffered選項以在並行方法中儘早輸出行,或者-m 10在非並行方法中在 10 次匹配後停止。

總而言之,如果 CPU 是瓶頸,並且您的系統上至少有 4 個 CPU 核心並且至少有 400 個 muc* 文件並且您在 GNU 系統上(grep通常比 GNU 快得多awk):

(export LC_ALL=C
find . -name "muc*_*_20160920_*.unl*" -newermt '2016-09-20 10:06:42' -print0 |
 xargs -r0P 4 -n 100 sh -c '
   zcat "$@" | 
     grep --line-buffered -E \
       "^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)"
 ' sh | head)

請注意,在並行方法中,您可能會得到grep混合在一起的命令輸出(儘管使用行緩衝並且提供的行小於幾千字節,但應保留行邊界)。

@Stéphane Chazelas 的回答提供了很多關於如何優化命令管道的細節

find . -name "muc*_*_20160920_*.unl*" | xargs zcat |
   awk -F "|" '{if($14=="20160920100643" && $22=="567094398953") print $0}'| head

我將提供另一種方法來解決您實際衡量您花費最多時間的問題的方法。一旦你找到時間花在哪裡,你就可以決定如何處理它。如果你想提高你的 10 分鐘執行時間,優化一個需要 2 秒的步驟幾乎是沒有用的。

當我查看命令管道時,有三件事引起了我的注意:

  1. find .- 目錄結構是什麼樣的?每個目錄有多少個文件?該目錄是執行命令的系統的本地目錄嗎?遠端文件系統會慢很多。
  2. -name "muc*_*_20160920_*.unl*"- 目錄結構中的所有文件名有多接近?它們是否都“接近”名稱並且難以匹配/CPU密集型?因為目錄樹中的每個文件都必須從磁碟讀取其名稱並與模式進行比較。
  3. xargs zcat- 在xargs我看來,這不會是一個太大的性能問題,尤其是與上述find問題及其zcat本身相比。即使是 10,000 甚至 10,000,000 個文件名,與查找名稱然後打開和解壓縮所有文件本身所花費的時間相比,傳遞和解析名稱所用的時間幾乎可以忽略不計。文件有多大?因為您正在解壓縮與您的文件名模式匹配的每個文件的全部內容。find

您如何確定主要的性能問題是什麼?測量管道中每個命令的性能。(有關定時整個管道的詳細資訊,請參閱https://stackoverflow.com/questions/13294554/how-to-use-gnu-time-with-pipeline。)您可以執行以下命令並查看每個步驟貢獻了多少時間到整個管道的處理時間:

/usr/bin/time find .- 這告訴您執行目錄樹需要多長時間。如果這很慢,您需要一個更好的儲存系統。 刷新文件系統記憶體$$ s $$在對此進行計時以獲得最壞情況的測量之前,然後find再次執行計時並查看記憶體對性能的影響程度。如果該目錄不是本地目錄,請嘗試在文件所在的實際系統上執行該命令。

/usr/bin/time find . -name "muc*_*_20160920_*.unl*"- 這將告訴您模式匹配文件名需要多長時間。再次刷新文件系統記憶體

$$ s $$並執行兩次。 /usr/bin/time bash -c "find . -name 'muc*_*_20160920_*.unl*' | xargs zcat > /dev/null"- 這是我懷疑是管道長時間執行的主要組成部分。如果這是問題所在,zcat按照 Stéphane Chazelas 的答案並行化命令可能是最好的答案。

繼續將原始命令管道中的步驟添加到正在測試的命令管道中,直到找到您花費大部分時間的位置。再次,我懷疑這是zcat一步。如果是這樣,也許zcat@Stéphane Chazelas 發布的並行化會有所幫助。

並行化zcat可能無濟於事——它甚至可能會損害性能並減慢處理速度。一次只zcat執行一個,IO 可能處於一種很好的流模式,可以最大限度地減少磁碟尋軌。由於多個zcat程序同時執行,IO 操作可能會競爭並實際上減慢處理速度,因為磁碟磁頭需要尋找,並且任何預讀都變得不那麼有效。

如果該zcat步驟是您的主要性能瓶頸,並且zcat一次執行多個程序對您沒有幫助或實際上減慢了您的速度,那麼您的管道是 IO 綁定的,您需要通過使用更快的儲存來解決問題。

再一次 - 如果目錄不是您執行命令管道的機器的本地目錄,請嘗試在文件系統實際所在的機器上執行它。

引用自:https://unix.stackexchange.com/questions/311522