외부 명령어 마스터하기: Bash 스크립트 성능 최적화

Bash 스크립트에서 외부 명령어 사용을 마스터하여 숨겨진 성능 향상을 얻으세요. 이 가이드는 `grep`이나 `sed` 같은 프로세스를 반복적으로 생성할 때 발생하는 상당한 오버헤드를 설명합니다. 효율적인 Bash 내장 함수로 외부 호출을 대체하고, 강력한 유틸리티를 사용한 배치 작업, 파일 읽기 루프 최적화를 통해 고처리량 자동화 작업에서 실행 시간을 극적으로 줄이는 실용적이고 실행 가능한 기술을 배우세요.

외부 명령어 마스터하기: Bash 스크립트 성능 최적화

가장 빠른 Bash 스크립트는 종종 더 적은 프로그램을 시작하는 스크립트입니다.

Bash는 접착 작업에 능숙합니다: 파일 읽기, 결정 내리기, 다른 도구 시작하기, 종료 상태 확인하기, 그리고 계속 진행하기. 하지만 고성능 데이터 처리 언어는 아닙니다. 함정은 모든 작은 문자열 작업에 sed가 필요하고, 모든 비교에 expr이 필요하며, 모든 파일 루프에 새로운 grep이 필요하다고 생각하며 Bash를 사용하는 것입니다. 그런 스타일은 열 줄에서는 작동합니다. 200,000줄에서는 고통스러워집니다.

비용은 프로세스 시작입니다. 스크립트가 grep, sed, awk, cut, tr, date, 또는 basename을 실행할 때, 셸은 다른 프로세스를 생성하고 기다려야 합니다. 한 번 호출하는 것은 문제가 아닙니다. 큰 루프 안에서 한 번 호출하는 것은 고칠 가치가 있는 패턴입니다.

루프 안의 명령어를 찾는 것으로 시작하세요:

grep -nE 'for |while ' script.sh
grep -nE 'grep|sed|awk|cut|tr|expr|basename|dirname|cat' script.sh

모든 일치가 나쁘다는 의미는 아닙니다. 전체 파일에 대한 단일 awk는 일반적으로 괜찮습니다. 줄당 한 번 실행되는 sed는 유지보수 스크립트를 배포 중 신비한 중단으로 바꾸는 종류입니다.

작은 외부 호출을 Bash 자체로 대체하기

가장 쉬운 이점은 산술, 문자열 길이, 접두사, 접미사, 간단한 대체입니다. Bash는 이미 이러한 작업을 수행하는 방법을 알고 있습니다.

외부 산술:

# 외부 'expr' 유틸리티 사용
RESULT=$(expr $A + $B)

내장 산술:

RESULT=$((A + B))

외부 문자열 대체:

MY_STRING="hello world"
NEW_STRING=$(echo "$MY_STRING" | sed 's/world/universe/')

매개변수 확장:

MY_STRING="hello world"
NEW_STRING=${MY_STRING/world/universe}
printf '%s\n' "$NEW_STRING"
작업 비효율적인 방법 (외부) 효율적인 방법 (내장)
부분 문자열 추출 `echo "$STR" cut -c 1-5`
길이 확인 expr length "$STR" ${#STR}
접미사 제거 basename "$file" .log ${file%.log}
경로 제거 basename "$path" ${path##*/}
파일 이름 제거 dirname "$path" ${path%/*}
첫 번째 일치 대체 sed 's/foo/bar/' ${value/foo/bar}
모든 일치 대체 sed 's/foo/bar/g' ${value//foo/bar}

Bash 조건문에는 [[ ... ]]를 선호하세요. 셸 키워드이며, 패턴 매칭을 깔끔하게 처리하고 [ ... ]에서 발생할 수 있는 인용 문제를 피합니다.

if [[ $name == *.log && -s $name ]]; then
  printf '비어 있지 않은 로그: %s\n' "$name"
fi

이것을 너무 강요하지 마세요. Bash 패턴 대체는 완전한 정규 표현식 엔진이 아닙니다. 규칙이 진정으로 복잡하다면, 하나의 awk 또는 perl 패스가 영리한 셸 확장보다 더 깔끔하고 일반적으로 더 빠릅니다.

작업을 반복하지 말고 배치로 처리하기

도구가 한 번에 여러 입력을 처리할 수 있다면, 여러 입력을 제공하세요. 이는 grep, awk, sed, find, 압축 도구, 업로드 클라이언트, 네트워크 서비스에 연결하는 모든 것에 가장 중요합니다.

이 루프는 파일당 하나의 grep을 시작합니다:

for file in *.log; do
  grep "ERROR" "$file" > "${file}.errors"
done

하나의 결합된 결과만 필요하다면, 하나의 grep을 사용하세요:

grep "ERROR" *.log > all_errors.txt

파일별 출력이 필요하다면, 분할이 정말 필요한지 생각해보세요. 때로는 다운스트림 도구가 grep -H에서 파일 이름 접두사를 읽을 수 있습니다:

grep -H "ERROR" *.log > errors-with-filenames.txt

줄 지향 변환의 경우, 간단한 grep | awk 체인을 하나의 awk 프로그램으로 축소하세요:

awk '/data/ {print $1}' input.txt | sort > output.txt

여전히 sort를 실행하며, 그것은 괜찮습니다. 정렬은 정확히 외부 도구가 해야 할 작업입니다. 유용한 변경은 쓸모없는 cat과 별도의 grep을 제거하는 것입니다.

cat 없이 파일 읽기

표준 줄 읽기 루프는 이유가 있어 지루합니다:

while IFS= read -r line; do
  printf '처리 중: %s\n' "$line"
done < file.txt

IFS=는 앞뒤 공백을 보존합니다. -rread가 백슬래시를 이스케이프로 처리하지 못하게 합니다. 리디렉션은 루프를 현재 셸에 유지하며, 이는 루프가 나중에 필요한 변수를 업데이트하는 경우 중요합니다.

이 버전은 무해해 보이지만 일반적으로 더 나쁩니다:

cat file.txt | while read -r line; do
  count=$((count + 1))
done
printf '%s\n' "$count"

Bash에서 파이프라인 세그먼트는 일반적으로 서브셸에서 실행되므로, count가 부모 셸에서 업데이트되지 않을 수 있습니다. 또한 이점 없이 cat을 시작합니다.

입력이 실제로 명령어에 의해 생성된 경우 프로세스 대체를 사용하세요:

while IFS= read -r file; do
  printf '큰 파일: %s\n' "$file"
done < <(find /var/log -type f -size +100M)

여기서 find는 실제 작업을 수행합니다. 루프를 현재 셸에 유지하는 것은 여전히 유용합니다.

find -exec ... +xargs 신중하게 사용하기

파일 루프는 우발적인 느림의 일반적인 원인입니다:

for file in $(find . -name '*.tmp'); do
  rm "$file"
done

이것은 공백에서 깨지고 반복적으로 rm을 시작합니다. 배치 실행을 사용하세요:

find . -name '*.tmp' -exec rm -f {} +

+ 형식은 각 rm 호출에 여러 경로를 전달합니다. 이전 \; 형식은 경로당 한 번 명령어를 실행합니다.

동시성의 이점을 얻는 명령어의 경우, xargs -P는 벽시계 시간을 줄일 수 있습니다:

xargs -n 1 -P 4 curl -fsS -O < urls.txt

파일 이름이 관련된 경우 -0을 사용하세요:

find uploads -type f -print0 | xargs -0 -n 50 -P 4 ./process-file

병렬 처리는 무료가 아닙니다. 네 개의 curl 작업은 하나보다 빠를 수 있습니다. 40개는 API에 의해 제한되거나 작은 호스트를 포화시킬 수 있습니다.

모든 것을 다시 작성하기 전에 측정하기

올바른 최적화는 시간이 어디에 소비되는지에 달려 있습니다. 먼저 간단한 타이밍을 사용하세요:

time ./script.sh

프로세스가 많은 스크립트의 경우, Linux에서 strace -c는 스크립트가 프로세스 생성, 파일 열기, 또는 I/O 대기에 시간을 소비하는지 보여줄 수 있습니다:

strace -f -c ./script.sh

셸 추적은 반복되는 명령어를 드러낼 수 있습니다:

PS4='+ $SECONDS ${BASH_SOURCE}:${LINENO}: '
bash -x ./script.sh

스크립트가 95%의 시간을 데이터베이스 내보내기를 기다리는 데 소비한다면, ${value/foo/bar}를 대체하는 것은 중요하지 않습니다. sed를 300,000번 실행한다면 중요할 것입니다.

외부 도구가 더 나은 경우 알기

목표 최고의 도구 (일반적으로) 참고
필드 추출 및 필터링 awk 표 형식 텍스트에 대해 Bash 루프보다 낫습니다.
스트림 편집 sed 파일에 대한 한 번의 패스에 좋습니다.
파일 탐색 find ls를 구문 분석하는 것보다 안전합니다.
JSON jq cut으로 JSON을 구문 분석하지 마세요.
병렬 작업 xargs -P 또는 GNU parallel 제한을 추가하고 실패를 처리하세요.
대용량 텍스트 처리 awk, perl, Python 종종 영웅적인 Bash보다 명확합니다.

Bash 내장 함수는 빠르지만, 유지보수성이 여전히 승리합니다. 나는 40줄의 깨지기 쉬운 매개변수 확장보다 하나의 명확한 awk 스크립트를 유지하는 것을 선호합니다.

실용적인 검토 체크리스트

Bash 스크립트가 느리게 느껴질 때, 이 순서대로 검토하세요:

  1. 루프 안의 외부 명령어를 찾으세요.
  2. 간단한 산술 및 문자열 작업을 Bash 확장으로 대체하세요.
  3. 쓸모없는 cat 호출을 제거하세요.
  4. grep, awk, sed, find -exec ... +, 또는 xargs로 파일 인수를 배치 처리하세요.
  5. 변수가 루프를 넘어 살아남아야 할 때 줄 읽기 루프를 현재 셸에 유지하세요.
  6. 다시 측정하세요.

모든 스크립트를 벤치마크 연습으로 만들 필요는 없습니다. 큰 이점은 일반적으로 몇 가지 명백한 지점에서 옵니다: 줄당 하나의 명령어, 파일당 하나의 명령어, 또는 API 항목당 하나의 명령어. 그것들을 수정하고, 스크립트를 읽기 쉽게 유지하며, 실행 시간이 더 이상 문제가 되지 않을 때 멈추세요.