Profilowanie skryptów i unikanie zbędnych podpowłok
Mierz czas działania skryptów i zastępuj wzorce wymagające wielu forków, takie jak potoki cat-grep, alternatywami wbudowanymi w powłokę.
Profilowanie skryptów i unikanie zbędnych podpowłok to bezpłatna lekcja DevOps Bootcamp na CoddyKit. To lekcja 1 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DevOps Bootcamp, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DevOps Bootcamp zawiera 4 lekcji w sumie.
Dlaczego wydajność skryptów ma znaczenie
Wolno działające skrypty Bash marnują czas CI, blokują zadania cron i frustrują użytkowników. Większość problemów z wydajnością nie wynika ze złożonej logiki — ich przyczyną są niepotrzebne rozwidlenia procesów: każde wywołane polecenie zewnętrzne uruchamia nowy proces potomny.
W tej lekcji nauczą się Państwo:
- mierzyć, na co faktycznie zużywany jest czas, za pomocą
timeibash -x - identyfikować antywzorce powodujące liczne rozwidlenia, takie jak bezużyteczne użycie cat
- zastępować polecenia zewnętrzne szybszymi konstrukcjami wbudowanymi powłoki
- świadomie używać podpowłok i unikać ich, gdy nie wnoszą żadnej wartości
Celem jest pisanie skryptów, które wykonują tę samą pracę przy użyciu mniejszej liczby procesów potomnych i w krótszym czasie rzeczywistym.
Pomiar czasu działania skryptu za pomocą wbudowanego polecenia time
Najprostszym narzędziem do profilowania jest wbudowane polecenie powłoki time. Umieść je przed dowolnym poleceniem lub potokiem, aby uzyskać trzy pomiary:
- real — rzeczywisty czas, który upłynął (czyli czas faktycznego oczekiwania)
- user — czas procesora zużyty przez kod w przestrzeni użytkownika
- sys — czas procesora zużyty w jądrze (wywołania systemowe, operacje wejścia-wyjścia)
Duża różnica między real a user+sys zwykle oznacza, że skrypt czeka na operacje wejścia-wyjścia lub uruchamia wiele procesów potomnych. Najpierw uruchom time dla całego skryptu, aby potwierdzić występowanie problemu, zanim zoptymalizujesz jakikolwiek jego fragment.
#!/usr/bin/env bash
# Time a whole script block
time {
for i in $(seq 1 1000); do
echo "line $i"
done | grep -c "5"
}
# Output example:
# 271
# real 0m0.045s
# user 0m0.038s
# sys 0m0.012sŚledzenie wykonania za pomocą bash -x i PS4
bash -x wyświetla każde polecenie przed jego wykonaniem — jest to śledzenie wykonania. Pokazuje, które wiersze są wykonywane najczęściej i czy programy zewnętrzne nie są wywoływane częściej, niż oczekiwano.
Domyślnie każdy śledzony wiersz jest poprzedzony znakiem +. Prefiks można rozszerzyć za pomocą PS4, dodając znaczniki czasu, co zmienia śledzenie w lekkie narzędzie do profilowania:
PS4jest rozwijane przed każdym śledzonym poleceniem- Dodanie
$EPOCHREALTIME(bash 5+) lub$(date +%s%N)zapewnia rozdzielczość nanosekundową - Przekieruj standardowe wyjście błędów do pliku i przetwórz go później, aby znaleźć wolne fragmenty
#!/usr/bin/env bash
# Run with: bash -x ./myscript.sh 2>trace.log
# Or embed tracing inside the script:
export PS4='+ [${EPOCHREALTIME}] ${BASH_SOURCE}:${LINENO}: '
set -x
slow_function() {
local result
result=$(cat /etc/hostname) # fork — slow
echo "host: $result"
}
slow_function
set +x
# trace.log now contains timestamps so you can diff
# adjacent lines to find which step took longest.Czym jest bezużyteczna podpowłoka
Podpowłoka to potomna kopia bieżącego procesu powłoki. Jest tworzona przez:
- Podstawienie polecenia:
$(command) - Grupowanie za pomocą nawiasów:
( commands ) - Przekierowanie potokiem do konstrukcji powłoki:
cmd | while read ...
Podpowłoki są niezbędne, gdy rzeczywiście potrzebują Państwo izolacji lub potoku. Stają się bezużyteczne, gdy są używane wyłącznie do wywołania programu zewnętrznego, który powłoka mogłaby obsłużyć samodzielnie, albo gdy bez powodu opakowują konstrukcję wbudowaną w dodatkową warstwę rozwidlania.
Każde rozwidlenie podpowłoki kosztuje około 1–5 ms w nowoczesnym systemie Linux. W pętli wykonywanej 10 000 razy 1000 bezużytecznych podpowłok dodaje 1–5 sekund czystego narzutu.
Klasyczny antywzorzec: bezużyteczne użycie cat
cat file | grep pattern to najsłynniejszy antywzorzec powodujący liczne rozwidlenia. Uruchamia dwa procesy (cat + grep) połączone potokiem, mimo że samo grep może bezpośrednio odczytać plik.
Rozwiązanie jest proste: przekaż nazwę pliku bezpośrednio poleceniu, które obsługuje pliki. Gdy narzędzie nie przyjmuje nazw plików, nazywa się to przekierowaniem wejścia; w przeciwnym razie wystarczy pominąć cat.
- Wolno:
cat file | grep pattern— 2 procesy, 1 potok - Szybko:
grep pattern file— 1 proces, bez potoku - Również szybko:
grep pattern < file— 1 proces, przekierowanie stdin (bez bufora potoku)
#!/usr/bin/env bash
# Create a sample file
seq 1 10000 > /tmp/numbers.txt
# --- Slow: useless cat ---
time cat /tmp/numbers.txt | grep -c "^5"
# --- Fast: grep reads the file directly ---
time grep -c "^5" /tmp/numbers.txt
# Both print the same count; the second is measurably faster
# because it skips the cat process and the inter-process pipe.Zastępowanie poleceń zewnętrznych konstrukcjami wbudowanymi powłoki
Wiele przekształceń wykonywanych w jednej linii ma wbudowany odpowiednik, który całkowicie eliminuje rozwidlenie. Porównajmy te typowe zamienniki:
echo ${#var}zamiastecho "$var" | wc -c— długość ciągu znaków${var^^}i${var,,}zamiastecho "$var" | tr 'a-z' 'A-Z'— zmiana wielkości liter (bash 4+)${var//search/replace}zamiastecho "$var" | sed 's/search/replace/'— proste zastępowanie[[ "$var" =~ pattern ]]zamiastecho "$var" | grep -q pattern— dopasowanie wyrażenia regularnegoread -r line < filezamiastline=$(head -n1 file)— odczyt pierwszego wiersza
Żadna z tych konstrukcji wbudowanych nie rozwidla procesu potomnego. Oszczędność przy pojedynczym wywołaniu jest niewielka, ale w pętlach sumuje się bardzo wyraźnie.
#!/usr/bin/env bash
sentence="hello world from bash"
# --- Fork-heavy ---
upper_slow=$(echo "$sentence" | tr 'a-z' 'A-Z')
length_slow=$(echo "$sentence" | wc -c)
# --- Builtin equivalents (zero extra processes) ---
upper_fast=${sentence^^}
length_fast=${#sentence}
echo "Slow upper : $upper_slow"
echo "Fast upper : $upper_fast"
echo "Slow length: $length_slow"
echo "Fast length: $length_fast"Unikanie podpowłok wewnątrz pętli
Podstawienie polecenia wewnątrz pętli zwielokrotnia koszt rozwidlenia przez liczbę iteracji. Pętla wykonywana 500 razy, zawierająca jedno wywołanie $(date), uruchamia 500 procesów potomnych tylko po to, aby uzyskać znaczniki czasu.
Strategie zmniejszania narzutu pętli:
- Przenieś polecenia niezmienne poza pętlę (oblicz raz i używaj ponownie)
- Preferuj rozwinięcie arytmetyczne
$(( expr ))— jest to konstrukcja wbudowana, a nie rozwidlenie - Używaj
printfzamiast wywoływaniadate, gdy potrzebne jest tylko formatowanie - Grupuj wywołania zewnętrzne: najpierw zbierz dane, a następnie przetwórz je jednorazowo poza pętlą
#!/usr/bin/env bash
# Demonstrate: compute-once vs fork-per-iteration
# Bad: $(date) forks 1000 times
time (
for i in $(seq 1 1000); do
ts=$(date +%s) # fork each iteration
echo "$i $ts" > /dev/null
done
)
# Good: capture once, reuse
time (
ts=$(date +%s) # fork exactly once
for i in $(seq 1 1000); do
echo "$i $ts" > /dev/null
done
)Podpowłoki potoków i problem zakresu zmiennych
W bashu (w przeciwieństwie do ksh/zsh) każde polecenie w potoku jest wykonywane we własnej podpowłoce. Oznacza to, że zmienne ustawione wewnątrz potoku są tracone po jego zakończeniu.
Jest to zarówno błąd poprawności, jak i problem z wydajnością — można przekierować dane do while read w oczekiwaniu na ich zebranie, a następnie stwierdzić, że zmienna jest później pusta.
Dwa rozwiązania:
- Użyj podstawienia procesu
while read line; do ...; done < <(command)— pętla while działa w bieżącej powłoce, a nie w podpowłoce - Użyj opcji lastpipe (
shopt -s lastpipe) — powoduje ona wykonanie ostatniego segmentu potoku w bieżącej powłoce (bash 4.2+)
#!/usr/bin/env bash
count=0
# --- Bug: count is always 0 after pipe (subshell) ---
seq 1 5 | while read -r n; do
(( count++ ))
done
echo "After pipe : count=$count" # prints 0
# --- Fix 1: process substitution (no subshell for while) ---
count=0
while read -r n; do
(( count++ ))
done < <(seq 1 5)
echo "Process sub : count=$count" # prints 5
# --- Fix 2: lastpipe option ---
shopt -s lastpipe
count=0
seq 1 5 | while read -r n; do
(( count++ ))
done
echo "lastpipe : count=$count" # prints 5Pomiar kosztu podpowłok za pomocą mikrotestu wydajnościowego
Za pomocą niewielkiego testu łatwo wykazać narzut podpowłok. Porównaj operację arytmetyczną wykonaną za pomocą $(( )) (konstrukcja wbudowana) z tą samą operacją przekazaną potokiem do expr (proces zewnętrzny).
Wyniki uzyskane na typowym komputerze z systemem Linux pokazują, że 10 000 wywołań expr zajmuje około 5 sekund, podczas gdy taka sama liczba wywołań $(( )) zajmuje mniej niż 0,1 sekundy — 50-krotna różnica przy identycznym wyniku.
Ten wzorzec testu jest również przydatny do mierzenia dowolnej optymalizacji: uruchom obie wersje N razy w pętli i porównaj je za pomocą time.
#!/usr/bin/env bash
N=500
# External command (fork per call)
time (
x=0
for ((i=0; i<N; i++)); do
x=$(expr $x + 1) # forks expr each time
done
echo "expr result: $x"
)
# Arithmetic builtin (no fork)
time (
x=0
for ((i=0; i<N; i++)); do
(( x++ )) # pure builtin
done
echo "builtin result: $x"
)Używanie here-stringów w celu unikania potoków z echo
Typowym wzorcem jest echo "$var" | command, który przekazuje zmienną na standardowe wejście. Powoduje on uruchomienie dwóch procesów (echo + command) i tworzy potok. Here-string (<<<) pozwala osiągnąć ten sam rezultat przy użyciu tylko jednego procesu — polecenie zewnętrzne odczytuje dane z tymczasowego bufora zarządzanego przez jądro.
grep pattern <<< "$var"— jeden proces, bez potokuread -r field1 field2 <<< "$line"— podział zmiennej bez zewnętrznego narzędziawc -w <<< "$sentence"— zliczanie słów w zmiennej
Here-stringi są szczególnie przydatne wewnątrz intensywnie wykonywanych pętli, w których każde rozwidlenie ma znaczenie.
#!/usr/bin/env bash
data="The quick brown fox"
# --- Fork-heavy: echo spawns a child ---
word_count_slow=$(echo "$data" | wc -w)
echo "Slow word count: $word_count_slow"
# --- Fast: here-string, only wc spawns ---
word_count_fast=$(wc -w <<< "$data")
echo "Fast word count: $word_count_fast"
# --- Even better: use parameter expansion (zero forks) ---
# Split into array, count elements
read -ra words <<< "$data"
echo "Zero-fork count: ${#words[@]}"Praktyczna refaktoryzacja: przed i po
Przeanalizujmy realistyczny skrypt, który przetwarza plik dziennika, i zastosujmy wszystko, czego się nauczyliśmy. W oryginalnej wersji polecenia cat, grep, awk i tr są łączone potokami. Wersja po refaktoryzacji zmniejsza liczbę procesów z 8 do 2.
Najważniejsze wprowadzone zmiany:
- Usunięto
cat—grepodczytuje plik bezpośrednio - Zastąpiono
tr '[:lower:]' '[:upper:]'przez${var^^} - Zastąpiono
echo "$line" | grep -qprzez[[ $line =~ ]] - Użyto
read -rz podstawieniem procesu zamiast pętli while połączonej potokiem
Po refaktoryzacji ponownie uruchom time ./script.sh, aby potwierdzić poprawę. Zawsze mierz — nie zakładaj.
#!/usr/bin/env bash
# Create a sample log
printf 'ERROR: disk full\nINFO: started\nERROR: timeout\nINFO: done\n' \
> /tmp/sample.log
# === BEFORE (fork-heavy) ===
time (
cat /tmp/sample.log \
| grep 'ERROR' \
| while read -r line; do
label=$(echo "$line" | tr '[:lower:]' '[:upper:]')
echo "[ALERT] $label"
done
)
# === AFTER (builtin-first) ===
time (
while IFS= read -r line; do
echo "[ALERT] ${line^^}"
done < <(grep 'ERROR' /tmp/sample.log)
)Sprawdzenie wiedzy: zakres podpowłoki
Sprawdź swoją wiedzę na temat podpowłok potoków oraz sposobów unikania utraty zmian zmiennych wprowadzonych wewnątrz potoku.
Podsumowanie lekcji: najpierw profiluj, rzadziej twórz procesy
W tej lekcji dowiedziałeś się, jak identyfikować i eliminować najczęstsze źródła niepotrzebnego tworzenia procesów w skryptach bash.
Najważniejsze informacje:
- Przed optymalizacją używaj
timeorazbash -xwzbogaconego oPS4, aby wykonać pomiary - Zbędne cat to najczęściej spotykany antywzorzec — przekazuj nazwy plików bezpośrednio do poleceń, które je obsługują
- Zastępuj
echo "$var" | commandciągiem here-string (command <<< "$var") albo poleceniem wbudowanym - Rozszerzenia parametrów (
${var^^},${var//s/r},${#var}) zastępują wiele wywołańtr,sediwc - Podpowłoki potoków pochłaniają modyfikacje zmiennych — używaj podstawiania procesów albo
shopt -s lastpipe - Przenoś niezmienne wywołania poleceń poza pętle; zamiast
exprpreferuj operacje arytmetyczne$(( ))
Zasada praktyczna: najpierw wykonaj pomiar, w miarę możliwości zastąp polecenia zewnętrzne poleceniami wbudowanymi, a następnie zweryfikuj poprawę za pomocą drugiego pomiaru.
Często zadawane pytania
Czy lekcja „Profilowanie skryptów i unikanie zbędnych podpowłok” jest bezpłatna?
Tak — pełny tekst „Profilowanie skryptów i unikanie zbędnych podpowłok” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DevOps Bootcamp, przejdź na CoddyKit PRO. Kurs DevOps Bootcamp zawiera 4 lekcji w sumie.
Co nauczysz się w „Profilowanie skryptów i unikanie zbędnych podpowłok”?
Mierz czas działania skryptów i zastępuj wzorce wymagające wielu forków, takie jak potoki cat-grep, alternatywami wbudowanymi w powłokę. Ćwiczysz DevOps Bootcamp z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć DevOps Bootcamp?
Nie wymagamy żadnego doświadczenia. DevOps Bootcamp w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 1 z 4.
Ile czasu zajmuje lekcja „Profilowanie skryptów i unikanie zbędnych podpowłok”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji DevOps Bootcamp?
Tak. Każda lekcja DevOps Bootcamp zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Profilowanie skryptów i unikanie zbędnych podpowłok
- Równoległość z xargs -P i zadaniami w tle
- Orkiestracja obciążeń za pomocą GNU parallel
- Potoki strumieniowe i nazwane potoki zapewniające przepustowość