Bash Scripting Best Practices: En snabbguide till tillförlitliga och prestanda Bash-skript

Bash Scripting Best Practices: En snabbguide till tillförlitliga och prestanda Bash-skript
Skal tapet från manapi

Att felsöka bash-skript är som att leta efter en nål i en höstack, särskilt när nya tillägg dyker upp i den befintliga kodbasen utan att i tid överväga frågor om struktur, loggning och tillförlitlighet. Du kan hamna i sådana situationer antingen på grund av dina egna misstag eller när du hanterar komplexa högar av skript.

Team Mail.ru molnlösningar översatt en artikel med rekommendationer som hjälper dig att skriva, felsöka och underhålla dina skript bättre. Tro det eller ej, ingenting slår tillfredsställelsen av att skriva ren, färdig att använda bash-kod som fungerar varje gång.

I artikeln delar författaren med sig av vad han har lärt sig under de senaste åren, samt några vanliga misstag som har överraskat honom. Detta är viktigt eftersom varje mjukvaruutvecklare, någon gång i sin karriär, arbetar med skript för att automatisera rutinmässiga arbetsuppgifter.

Fällhanterare

De flesta bash-skript jag har stött på använder aldrig en effektiv rensningsmekanism när något oväntat händer under skriptkörning.

Överraskningar kan uppstå utifrån, som att ta emot en signal från kärnan. Att hantera sådana fall är extremt viktigt för att säkerställa att skripten är tillförlitliga nog att köras på produktionssystem. Jag använder ofta exit-hanterare för att svara på scenarier som detta:

function handle_exit() {
  // Add cleanup code here
  // for eg. rm -f "/tmp/${lock_file}.lock"
  // exit with an appropriate status code
}
  
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap är ett inbyggt skal-kommando som hjälper dig att registrera en rensningsfunktion som anropas vid eventuella signaler. Särskild försiktighet bör dock iakttas med hanterare som t.ex SIGINT, vilket gör att skriptet avbryts.

Dessutom ska man i de flesta fall bara fånga EXIT, men tanken är att du faktiskt kan anpassa skriptets beteende för varje enskild signal.

Inbyggda setfunktioner - snabb avslutning vid fel

Det är mycket viktigt att reagera på fel så snart de uppstår och stoppa exekveringen snabbt. Inget kan vara värre än att fortsätta köra ett kommando så här:

rm -rf ${directory_name}/*

Observera att variabeln directory_name inte bestämd.

Det är viktigt att använda inbyggda funktioner för att hantera sådana scenarier setsåsom set -o errexit, set -o pipefail eller set -o nounset i början av manuset. Dessa funktioner säkerställer att ditt skript kommer att avslutas så snart det stöter på någon exit-kod som inte är noll, användning av odefinierade variabler, ogiltiga kommandon som skickas över ett rör, och så vidare:

#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

function print_var() {
  echo "${var_value}"
}

print_var

$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable

Notera: inbyggda funktioner som t.ex set -o errexit, kommer att avsluta skriptet så snart det finns en "rå" returkod (annan än noll). Därför är det bättre att införa anpassad felhantering, till exempel:

#!/bin/bash
error_exit() {
  line=$1
  shift 1
  echo "ERROR: non zero return code from line: $line -- $@"
  exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code

Att skriva skript på det här sättet tvingar dig att vara mer försiktig med beteendet hos alla kommandon i skriptet och förutse risken för ett fel innan det överraskar dig.

ShellCheck för att upptäcka fel under utveckling

Det är värt att integrera något liknande Skalkontroll i dina utvecklings- och testpipelines för att kontrollera din bash-kod mot bästa praxis.

Jag använder den i mina lokala utvecklingsmiljöer för att få rapporter om syntax, semantik och några fel i koden som jag kan ha missat under utvecklingen. Detta är ett statiskt analysverktyg för dina bash-skript och jag rekommenderar starkt att du använder det.

Använda dina egna utgångskoder

Returkoder i POSIX är inte bara noll eller ett, utan noll eller ett värde som inte är noll. Använd dessa funktioner för att returnera anpassade felkoder (mellan 201-254) för olika felfall.

Denna information kan sedan användas av andra skript som omsluter ditt för att förstå exakt vilken typ av fel som uppstod och reagera därefter:

#!/usr/bin/env bash

SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241

function read_file() {
  if ${file_not_found}; then
    return ${FILE_NOT_FOUND}
  fi
}

Notera: Var särskilt försiktig med de variabelnamn du definierar för att undvika att miljövariabler av misstag åsidosätts.

Loggningsfunktioner

Vacker och strukturerad loggning är viktig för att enkelt förstå resultatet av ditt manus. Som med andra högnivåprogrammeringsspråk använder jag alltid inbyggda loggningsfunktioner i mina bash-skript, som t.ex. __msg_info, __msg_error och så vidare.

Detta hjälper till att tillhandahålla en standardiserad loggningsstruktur genom att göra ändringar på endast ett ställe:

#!/usr/bin/env bash

function __msg_error() {
    [[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}

function __msg_debug() {
    [[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}

function __msg_info() {
    [[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}

__msg_error "File could not be found. Cannot proceed"

__msg_debug "Starting script execution with 276MB of available RAM"

Jag brukar försöka ha någon form av mekanism i mina manus __init, där sådana loggervariabler och andra systemvariabler initieras eller ställs in på standardvärden. Dessa variabler kan också ställas in från kommandoradsalternativ under skriptanrop.

Till exempel något som:

$ ./run-script.sh --debug

När ett sådant skript exekveras, säkerställer det att systemomfattande inställningar ställs in på standardvärden om de krävs, eller åtminstone initieras till något lämpligt om det behövs.

Jag brukar basera valet av vad som ska initieras och vad man inte ska göra på en avvägning mellan användargränssnittet och detaljerna i de konfigurationer som användaren kan/bör fördjupa sig i.

Arkitektur för återanvändning och rent systemtillstånd

Modulär/återanvändbar kod

├── framework
│   ├── common
│   │   ├── loggers.sh
│   │   ├── mail_reports.sh
│   │   └── slack_reports.sh
│   └── daily_database_operation.sh

Jag har ett separat arkiv som jag kan använda för att initiera ett nytt projekt/bash-skript som jag vill utveckla. Allt som kan återanvändas kan lagras i ett arkiv och hämtas av andra projekt som vill använda den funktionen. Att organisera projekt på det här sättet minskar avsevärt storleken på andra skript och säkerställer också att kodbasen är liten och lätt att testa.

Som i exemplet ovan är alla loggningsfunktioner som t.ex __msg_info, __msg_error och andra, såsom Slack-rapporter, finns separat i common/* och anslut dynamiskt i andra scenarier som daily_database_operation.sh.

Lämna efter dig ett rent system

Om du laddar några resurser medan skriptet körs, rekommenderas det att lagra all sådan data i en delad katalog med ett slumpmässigt namn, t.ex. /tmp/AlRhYbD97/*. Du kan använda slumpmässiga textgeneratorer för att välja katalognamnet:

rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"

Efter avslutat arbete kan rensning av sådana kataloger tillhandahållas i de krokhanterare som diskuterats ovan. Om temporära kataloger inte tas om hand ackumuleras de och orsakar i något skede oväntade problem på värden, till exempel en full disk.

Använder låsfiler

Ofta måste du se till att endast en instans av ett skript körs på en värd vid varje given tidpunkt. Detta kan göras med hjälp av låsfiler.

Jag brukar skapa låsfiler /tmp/project_name/*.lock och kontrollera deras närvaro i början av manuset. Detta hjälper skriptet att avslutas elegant och undvika oväntade ändringar av systemtillståndet av ett annat skript som körs parallellt. Låsfiler behövs inte om du behöver samma skript som ska köras parallellt på en given värd.

Mät och förbättra

Vi behöver ofta arbeta med skript som körs över långa tidsperioder, till exempel daglig databasdrift. Sådana operationer involverar vanligtvis en sekvens av steg: laddar data, kontrollerar avvikelser, importerar data, skickar statusrapporter och så vidare.

I sådana fall försöker jag alltid dela upp skriptet i separata små skript och rapportera deras status och körningstid med hjälp av:

time source "${filepath}" "${args}">> "${LOG_DIR}/RUN_LOG" 2>&1

Senare kan jag se exekveringstiden med:

tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"

Detta hjälper mig att identifiera problem/långsamma områden i skript som behöver optimeras.

Lycka till!

Vad mer att läsa:

  1. Go och GPU-cacher.
  2. Ett exempel på en händelsedriven applikation baserad på webhooks i S3-objektlagringen av Mail.ru Cloud Solutions.
  3. Vår telegramkanal om digital transformation.

Källa: will.com

Lägg en kommentar