如何優化這個 Unix 命令?
以下命令大約需要 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 行。使用gawk
ormawk
,您可以使用fflush()
它。或者你可以添加一個if (++n == 10) exit
inawk
。總結一下:
(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 個文件批次上並行執行 4
zcat | awk
個作業。如果這
20160920100643
是一個時間戳,您可能希望排除在此之前最後修改的文件。對於 GNU 或 BSDfind
,添加一個-newermt '2016-09-20 10:06:42'
.如果行有大量欄位,您會因為
awk
拆分它並分配這麼多$n
欄位而受到懲罰。使用只考慮前 22 個欄位的方法可以加快速度:grep -E '^([^|]*\|){13}20160920100643(\|[^|]*){7}\|567094398953(\||$)'
而不是
awk
命令。使用 GNUgrep
,添加--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 秒的步驟幾乎是沒有用的。
當我查看命令管道時,有三件事引起了我的注意:
find .
- 目錄結構是什麼樣的?每個目錄有多少個文件?該目錄是執行命令的系統的本地目錄嗎?遠端文件系統會慢很多。-name "muc*_*_20160920_*.unl*"
- 目錄結構中的所有文件名有多接近?它們是否都“接近”名稱並且難以匹配/CPU密集型?因為目錄樹中的每個文件都必須從磁碟讀取其名稱並與模式進行比較。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 綁定的,您需要通過使用更快的儲存來解決問題。再一次 - 如果目錄不是您執行命令管道的機器的本地目錄,請嘗試在文件系統實際所在的機器上執行它。