Best Practices for Bash-skript: En rask guide til pålitelige og ytelsesbaserte Bash-skript

Best Practices for Bash-skript: En rask guide til pålitelige og ytelsesbaserte Bash-skript
Shell tapet av manapi

Å feilsøke bash-skript er som å lete etter en nål i en høystakk, spesielt når nye tillegg vises i den eksisterende kodebasen uten rettidig vurdering av problemer med struktur, logging og pålitelighet. Du kan finne deg selv i slike situasjoner enten på grunn av dine egne feil eller når du håndterer komplekse hauger med skript.

Lag Mail.ru skyløsninger oversatt en artikkel med anbefalinger som vil hjelpe deg å skrive, feilsøke og vedlikeholde skriptene dine bedre. Tro det eller ei, ingenting slår tilfredsstillelsen av å skrive ren, klar til bruk bash-kode som fungerer hver gang.

I artikkelen deler forfatteren det han har lært de siste årene, samt noen vanlige feil som har tatt ham på vakt. Dette er viktig fordi enhver programvareutvikler, på et tidspunkt i karrieren, jobber med skript for å automatisere rutinemessige arbeidsoppgaver.

Fellebehandlere

De fleste bash-skript jeg har møtt bruker aldri en effektiv oppryddingsmekanisme når noe uventet skjer under kjøring av skript.

Overraskelser kan oppstå fra utsiden, for eksempel å motta et signal fra kjernen. Håndtering av slike saker er ekstremt viktig for å sikre at skriptene er pålitelige nok til å kjøre på produksjonssystemer. Jeg bruker ofte exit-behandlere for å svare på scenarier som dette:

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 er en innebygd kommando som hjelper deg med å registrere en oppryddingsfunksjon som kalles opp ved eventuelle signaler. Det bør imidlertid utvises spesiell forsiktighet med behandlere som f.eks SIGINT, som får skriptet til å avbryte.

I tillegg bør du i de fleste tilfeller bare fange EXIT, men tanken er at du faktisk kan tilpasse oppførselen til skriptet for hvert enkelt signal.

Innebygde settfunksjoner - rask avslutning ved feil

Det er svært viktig å reagere på feil så snart de oppstår og stoppe utførelse raskt. Ingenting kan være verre enn å fortsette å kjøre en kommando som dette:

rm -rf ${directory_name}/*

Vær oppmerksom på at variabelen directory_name ikke bestemt.

Det er viktig å bruke innebygde funksjoner for å håndtere slike scenarier setslik som set -o errexit, set -o pipefail eller set -o nounset i begynnelsen av manuset. Disse funksjonene sikrer at skriptet ditt avsluttes så snart det støter på en utgangskode som ikke er null, bruk av udefinerte variabler, ugyldige kommandoer som sendes over et rør, og så videre:

#!/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

Merk: innebygde funksjoner som f.eks set -o errexit, vil avslutte skriptet så snart det er en "rå" returkode (annet enn null). Derfor er det bedre å introdusere tilpasset feilhåndtering, for eksempel:

#!/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

Å skrive skript på denne måten tvinger deg til å være mer forsiktig med oppførselen til alle kommandoene i skriptet og forutse muligheten for en feil før den overrasker deg.

ShellCheck for å oppdage feil under utvikling

Det er verdt å integrere noe sånt som ShellCheck inn i utviklings- og testpipelines for å kontrollere bash-koden din mot beste praksis.

Jeg bruker den i mine lokale utviklingsmiljøer for å få rapporter om syntaks, semantikk og noen feil i koden som jeg kanskje har gått glipp av under utviklingen. Dette er et statisk analyseverktøy for bash-skriptene dine, og jeg anbefaler på det sterkeste å bruke det.

Bruke dine egne utgangskoder

Returkoder i POSIX er ikke bare null eller én, men null eller en verdi som ikke er null. Bruk disse funksjonene til å returnere egendefinerte feilkoder (mellom 201-254) for ulike feiltilfeller.

Denne informasjonen kan deretter brukes av andre skript som omslutter ditt for å forstå nøyaktig hvilken type feil som oppstod og reagere deretter:

#!/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
}

Merk: vær spesielt forsiktig med variabelnavnene du definerer for å unngå utilsiktet overstyring av miljøvariabler.

Loggfunksjoner

Vakker og strukturert logging er viktig for enkelt å forstå resultatene av skriptet ditt. Som med andre programmeringsspråk på høyt nivå, bruker jeg alltid native loggingsfunksjoner i bash-skriptene mine, som f.eks. __msg_info, __msg_error og så videre.

Dette bidrar til å gi en standardisert loggstruktur ved å gjøre endringer på bare ett sted:

#!/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"

Jeg prøver vanligvis å ha en slags mekanisme i skriptene mine __init, der slike loggervariabler og andre systemvariabler er initialisert eller satt til standardverdier. Disse variablene kan også settes fra kommandolinjealternativer under skriptpåkalling.

For eksempel noe som:

$ ./run-script.sh --debug

Når et slikt skript kjøres, sikrer det at systemomfattende innstillinger settes til standardverdier hvis de er nødvendige, eller i det minste initialisert til noe passende om nødvendig.

Jeg baserer vanligvis valget om hva som skal initialiseres og ikke gjøres på en avveining mellom brukergrensesnittet og detaljene i konfigurasjonene som brukeren kan/bør fordype seg i.

Arkitektur for gjenbruk og ren systemtilstand

Modulær/gjenbrukbar kode

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

Jeg har et eget depot som jeg kan bruke til å initialisere et nytt prosjekt/bash-skript som jeg vil utvikle. Alt som kan gjenbrukes kan lagres i et depot og hentes av andre prosjekter som ønsker å bruke den funksjonaliteten. Å organisere prosjekter på denne måten reduserer størrelsen på andre skript betydelig og sikrer også at kodebasen er liten og enkel å teste.

Som i eksempelet ovenfor er alle loggingsfunksjoner som f.eks __msg_info, __msg_error og andre, for eksempel Slack-rapporter, finnes separat i common/* og koble deg dynamisk til i andre scenarier som daily_database_operation.sh.

Legg igjen et rent system

Hvis du laster inn noen ressurser mens skriptet kjører, anbefales det å lagre alle slike data i en delt katalog med et tilfeldig navn, f.eks. /tmp/AlRhYbD97/*. Du kan bruke tilfeldige tekstgeneratorer for å velge katalognavnet:

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

Etter fullført arbeid kan opprydding av slike kataloger gis i krokbehandlerne diskutert ovenfor. Hvis midlertidige kataloger ikke blir tatt hånd om, akkumuleres de og forårsaker på et tidspunkt uventede problemer på verten, for eksempel en full disk.

Bruke låsefiler

Ofte må du sørge for at bare én forekomst av et skript kjører på en vert til enhver tid. Dette kan gjøres ved hjelp av låsefiler.

Jeg lager vanligvis låsefiler /tmp/project_name/*.lock og se etter deres tilstedeværelse i begynnelsen av manuset. Dette hjelper skriptet til å avsluttes elegant og unngå uventede endringer i systemtilstanden av et annet skript som kjører parallelt. Låsefiler er ikke nødvendig hvis du trenger at det samme skriptet skal kjøres parallelt på en gitt vert.

Mål og forbedre

Vi må ofte jobbe med skript som kjører over lange perioder, for eksempel daglige databaseoperasjoner. Slike operasjoner involverer vanligvis en sekvens av trinn: laste inn data, se etter uregelmessigheter, importere data, sende statusrapporter og så videre.

I slike tilfeller prøver jeg alltid å dele opp skriptet i separate små skript og rapportere status og utførelsestid ved å bruke:

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

Senere kan jeg se utførelsestiden med:

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

Dette hjelper meg med å identifisere problem/trege områder i skript som trenger optimalisering.

Lykke til!

Hva annet å lese:

  1. Gå og GPU-cacher.
  2. Et eksempel på en hendelsesdrevet applikasjon basert på webhooks i S3-objektlagringen til Mail.ru Cloud Solutions.
  3. Vår telegramkanal om digital transformasjon.

Kilde: www.habr.com

Legg til en kommentar