In diesem Artikel stelle ich ein paar Tricks zur Behandlung von Fehlerbedingungen vor – einige fallen streng genommen nicht unter die Kategorie der Fehlerbehandlung (eine reaktive Art, mit dem Unerwarteten umzugehen), aber auch einige Techniken, um Fehler zu vermeiden, bevor sie passieren.
Fallstudie:Einfaches Skript, das einen Hardwarebericht von mehreren Hosts herunterlädt und in eine Datenbank einfügt.
Angenommen, Sie haben einen cron
job auf jedem Ihrer Linux-Systeme, und Sie haben ein Skript, um die Hardwareinformationen von jedem zu erfassen:
#!/bin/bash
# Script to collect the status of lshw output from home servers
# Dependencies:
# * LSHW: http://ezix.org/project/wiki/HardwareLiSter
# * JQ: http://stedolan.github.io/jq/
#
# On each machine you can run something like this from cron (Don't know CRON, no worries: https://crontab-generator.org/)
# 0 0 * * * /usr/sbin/lshw -json -quiet > /var/log/lshw-dump.json
# Author: Jose Vicente Nunez
#
declare -a servers=(
dmaf5
)
DATADIR="$HOME/Documents/lshw-dump"
/usr/bin/mkdir -p -v "$DATADIR"
for server in ${servers[*]}; do
echo "Visiting: $server"
/usr/bin/scp -o logLevel=Error ${server}:/var/log/lshw-dump.json ${DATADIR}/lshw-$server-dump.json &
done
wait
for lshw in $(/usr/bin/find $DATADIR -type f -name 'lshw-*-dump.json'); do
/usr/bin/jq '.["product","vendor", "configuration"]' $lshw
done
Wenn alles gut geht, sammeln Sie Ihre Dateien parallel, weil Sie nicht mehr als zehn Systeme haben. Sie können es sich leisten, alle gleichzeitig per ssh zu erreichen und dann die Hardwaredetails von jedem anzuzeigen.
Visiting: dmaf5
lshw-dump.json 100% 54KB 136.9MB/s 00:00
"DMAF5 (Default string)"
"BESSTAR TECH LIMITED"
{
"boot": "normal",
"chassis": "desktop",
"family": "Default string",
"sku": "Default string",
"uuid": "00020003-0004-0005-0006-000700080009"
}
Hier sind einige Möglichkeiten, warum etwas schief gelaufen ist:
- Ihr Bericht wurde nicht ausgeführt, weil der Server ausgefallen war
- Sie konnten das Verzeichnis, in dem die Dateien gespeichert werden müssen, nicht erstellen
- Die Tools, die Sie zum Ausführen des Skripts benötigen, fehlen
- Sie können den Bericht nicht abrufen, weil Ihr Remote-Computer abgestürzt ist
- Mindestens einer der Berichte ist beschädigt
Die aktuelle Version des Skripts hat ein Problem – es wird von Anfang bis Ende ausgeführt, Fehler hin oder her:
./collect_data_from_servers.sh
Visiting: macmini2
Visiting: mac-pro-1-1
Visiting: dmaf5
lshw-dump.json 100% 54KB 48.8MB/s 00:00
scp: /var/log/lshw-dump.json: No such file or directory
scp: /var/log/lshw-dump.json: No such file or directory
parse error: Expected separator between values at line 3, column 9
Als Nächstes demonstriere ich ein paar Dinge, um Ihr Skript robuster zu machen und in manchen Fällen nach einem Fehler wiederhergestellt zu werden.
Die nukleare Option:Hart scheitern, schnell scheitern
Der richtige Umgang mit Fehlern besteht darin, mithilfe von Rückgabecodes zu prüfen, ob das Programm erfolgreich beendet wurde oder nicht. Es klingt offensichtlich, aber Rückgabecodes, eine Ganzzahl, die in bash $?
gespeichert ist oder $!
Variable, haben manchmal eine breitere Bedeutung. Auf der Manpage von Bash erfahren Sie:
Für die Zwecke der Shell war ein Befehl erfolgreich, der mit einem Exit-
Status von Null endet. Ein Exit-Status von Null zeigt einen Erfolg an.
Ein Exit-Status ungleich Null zeigt einen Fehler an. Wenn ein Befehl
mit einem fatalen Signal N beendet wird, verwendet Bash den Wert 128+N als
Ausgangsstatus.
Wie üblich sollten Sie immer die Handbuchseiten der Skripte lesen, die Sie aufrufen, um zu sehen, welche Konventionen für jedes von ihnen gelten. Wenn Sie mit einer Sprache wie Java oder Python programmiert haben, sind Sie höchstwahrscheinlich mit ihren Ausnahmen, unterschiedlichen Bedeutungen und der Tatsache vertraut, dass nicht alle auf die gleiche Weise behandelt werden.
Wenn Sie set -o errexit
hinzufügen zu Ihrem Skript hinzufügen, wird es von diesem Punkt an die Ausführung abbrechen, wenn ein Befehl mit einem Code != 0
vorhanden ist . Aber errexit
wird nicht verwendet, wenn Funktionen innerhalb eines if
ausgeführt werden Bedingung. Anstatt mir also diese Ausnahme zu merken, führe ich lieber eine explizite Fehlerbehandlung durch.
Sehen Sie sich Version 2 des Skripts an. Es ist etwas besser:
1 #!/bin/bash
2 # Script to collect the status of lshw output from home servers
3 # Dependencies:
4 # * LSHW: http://ezix.org/project/wiki/HardwareLiSter
5 # * JQ: http://stedolan.github.io/jq/
6 #
7 # On each machine you can run something like this from cron (Don't know CRON, no worries: https://crontab-generator.org/ )
8 # 0 0 * * * /usr/sbin/lshw -json -quiet > /var/log/lshw-dump.json
9 Author: Jose Vicente Nunez
10 #
11 set -o errtrace # Enable the err trap, code will get called when an error is detected
12 trap "echo ERROR: There was an error in ${FUNCNAME-main context}, details to follow" ERR
13 declare -a servers=(
14 macmini2
15 mac-pro-1-1
16 dmaf5
17 )
18
19 DATADIR="$HOME/Documents/lshw-dump"
20 if [ ! -d "$DATADIR" ]; then
21 /usr/bin/mkdir -p -v "$DATADIR"|| "FATAL: Failed to create $DATADIR" && exit 100
22 fi
23 declare -A server_pid
24 for server in ${servers[*]}; do
25 echo "Visiting: $server"
26 /usr/bin/scp -o logLevel=Error ${server}:/var/log/lshw-dump.json ${DATADIR}/lshw-$server-dump.json &
27 server_pid[$server]=$! # Save the PID of the scp of a given server for later
28 done
29 # Iterate through all the servers and:
30 # Wait for the return code of each
31 # Check the exit code from each scp
32 for server in ${!server_pid[*]}; do
33 wait ${server_pid[$server]}
34 test $? -ne 0 && echo "ERROR: Copy from $server had problems, will not continue" && exit 100
35 done
36 for lshw in $(/usr/bin/find $DATADIR -type f -name 'lshw-*-dump.json'); do
37 /usr/bin/jq '.["product","vendor", "configuration"]' $lshw
38 done
Folgendes hat sich geändert:
- Zeile 11 und 12, ich aktiviere die Fehlerverfolgung und füge eine "Falle" hinzu, um dem Benutzer mitzuteilen, dass ein Fehler aufgetreten ist und Turbulenzen bevorstehen. Vielleicht möchten Sie Ihr Skript stattdessen hier beenden, ich zeige Ihnen, warum das vielleicht nicht das Beste ist.
- Zeile 20, wenn das Verzeichnis nicht existiert, versuchen Sie es in Zeile 21 zu erstellen. Wenn die Verzeichniserstellung fehlschlägt, beenden Sie es mit einem Fehler.
- In Zeile 27, nachdem ich jeden Hintergrundjob ausgeführt habe, erfasse ich die PID und verknüpfe sie mit der Maschine (1:1-Beziehung).
- In den Zeilen 33-35 warte ich auf den
scp
Aufgabe beenden, Rückgabecode abrufen und bei Fehler abbrechen. - In Zeile 37 überprüfe ich, ob die Datei geparst werden konnte, andernfalls breche ich mit einem Fehler ab.
Wie sieht nun die Fehlerbehandlung aus?
Visiting: macmini2
Visiting: mac-pro-1-1
Visiting: dmaf5
lshw-dump.json 100% 54KB 146.1MB/s 00:00
scp: /var/log/lshw-dump.json: No such file or directory
ERROR: There was an error in main context, details to follow
ERROR: Copy from mac-pro-1-1 had problems, will not continue
scp: /var/log/lshw-dump.json: No such file or directory
Wie Sie sehen können, ist diese Version besser darin, Fehler zu erkennen, aber sie ist sehr unversöhnlich. Außerdem werden nicht alle Fehler erkannt, oder?
Wenn Sie nicht weiterkommen und sich wünschen, Sie hätten einen Alarm
Der Code sieht besser aus, außer dass manchmal die scp
könnte auf einem Server hängen bleiben (beim Versuch, eine Datei zu kopieren), weil der Server zu beschäftigt ist, um zu antworten, oder einfach in einem schlechten Zustand ist.
Ein weiteres Beispiel ist der Versuch, über NFS auf ein Verzeichnis zuzugreifen, in dem $HOME
wird von einem NFS-Server gemountet:
/usr/bin/find $HOME -type f -name '*.csv' -print -fprint /tmp/report.txt
Und Sie stellen Stunden später fest, dass der NFS-Mount-Punkt veraltet ist und Ihr Skript hängen bleibt.
Ein Timeout ist die Lösung. Und GNU Timeout kommt zur Rettung:
/usr/bin/timeout --kill-after 20.0s 10.0s /usr/bin/find $HOME -type f -name '*.csv' -print -fprint /tmp/report.txt
Hier versuchen Sie regelmäßig den Prozess nach 10,0 Sekunden nach dem Start zu beenden (TERM-Signal). Wenn es nach 20,0 Sekunden immer noch läuft, senden Sie ein KILL-Signal (kill -9
). Prüfen Sie im Zweifelsfall, welche Signale in Ihrem System unterstützt werden (kill -l
, zum Beispiel).
Wenn dies aus meinem Dialog nicht klar hervorgeht, sehen Sie sich das Skript an, um mehr Klarheit zu erhalten.
/usr/bin/time /usr/bin/timeout --kill-after=10.0s 20.0s /usr/bin/sleep 60s
real 0m20.003s
user 0m0.000s
sys 0m0.003s
Zurück zum ursprünglichen Skript, um ein paar weitere Optionen hinzuzufügen, und Sie haben Version 3:
1 #!/bin/bash
2 # Script to collect the status of lshw output from home servers
3 # Dependencies:
4 # * Open SSH: http://www.openssh.com/portable.html
5 # * LSHW: http://ezix.org/project/wiki/HardwareLiSter
6 # * JQ: http://stedolan.github.io/jq/
7 # * timeout: https://www.gnu.org/software/coreutils/
8 #
9 # On each machine you can run something like this from cron (Don't know CRON, no worries: https://crontab-generator.org/)
10 # 0 0 * * * /usr/sbin/lshw -json -quiet > /var/log/lshw-dump.json
11 # Author: Jose Vicente Nunez
12 #
13 set -o errtrace # Enable the err trap, code will get called when an error is detected
14 trap "echo ERROR: There was an error in ${FUNCNAME-main context}, details to follow" ERR
15
16 declare -a dependencies=(/usr/bin/timeout /usr/bin/ssh /usr/bin/jq)
17 for dependency in ${dependencies[@]}; do
18 if [ ! -x $dependency ]; then
19 echo "ERROR: Missing $dependency"
20 exit 100
21 fi
22 done
23
24 declare -a servers=(
25 macmini2
26 mac-pro-1-1
27 dmaf5
28 )
29
30 function remote_copy {
31 local server=$1
32 echo "Visiting: $server"
33 /usr/bin/timeout --kill-after 25.0s 20.0s \
34 /usr/bin/scp \
35 -o BatchMode=yes \
36 -o logLevel=Error \
37 -o ConnectTimeout=5 \
38 -o ConnectionAttempts=3 \
39 ${server}:/var/log/lshw-dump.json ${DATADIR}/lshw-$server-dump.json
40 return $?
41 }
42
43 DATADIR="$HOME/Documents/lshw-dump"
44 if [ ! -d "$DATADIR" ]; then
45 /usr/bin/mkdir -p -v "$DATADIR"|| "FATAL: Failed to create $DATADIR" && exit 100
46 fi
47 declare -A server_pid
48 for server in ${servers[*]}; do
49 remote_copy $server &
50 server_pid[$server]=$! # Save the PID of the scp of a given server for later
51 done
52 # Iterate through all the servers and:
53 # Wait for the return code of each
54 # Check the exit code from each scp
55 for server in ${!server_pid[*]}; do
56 wait ${server_pid[$server]}
57 test $? -ne 0 && echo "ERROR: Copy from $server had problems, will not continue" && exit 100
58 done
59 for lshw in $(/usr/bin/find $DATADIR -type f -name 'lshw-*-dump.json'); do
60 /usr/bin/jq '.["product","vendor", "configuration"]' $lshw
61 done
Was sind die Änderungen?:
- Überprüfen Sie zwischen den Zeilen 16-22, ob alle erforderlichen Abhängigkeitstools vorhanden sind. Wenn es nicht ausgeführt werden kann, dann „Houston, wir haben ein Problem.“
- Eine
remote_copy
erstellt Funktion, die ein Timeout verwendet, um sicherzustellen, dassscp
endet spätestens bei 45.0s – Zeile 33. - Verbindungs-Timeout von 5 Sekunden anstelle des TCP-Standardwerts hinzugefügt – Zeile 37.
- Neuversuch zu
scp
hinzugefügt in Zeile 38 – 3 Versuche, die jeweils 1 Sekunde warten.
Es gibt andere Möglichkeiten, es erneut zu versuchen, wenn ein Fehler auftritt.
Warten auf das Ende der Welt – wie und wann es erneut versucht werden sollte
Sie haben bemerkt, dass dem scp
ein Wiederholungsversuch hinzugefügt wurde Befehl. Aber das wiederholt sich nur bei fehlgeschlagenen Verbindungen, was ist, wenn der Befehl mitten in der Kopie fehlschlägt?
Manchmal möchten Sie einfach scheitern, weil es nur sehr wenige Chancen gibt, sich von einem Problem zu erholen. Ein System, das beispielsweise Hardware-Fixes erfordert, oder Sie können einfach in einen eingeschränkten Modus zurückkehren – was bedeutet, dass Sie Ihre Systemarbeit ohne die aktualisierten Daten fortsetzen können. In diesen Fällen macht es keinen Sinn, ewig zu warten, sondern nur eine bestimmte Zeit.
Hier sind die Änderungen an remote_copy
, um es kurz zu halten (vierte Version):
#!/bin/bash
# Omitted code for clarity...
declare REMOTE_FILE="/var/log/lshw-dump.json"
declare MAX_RETRIES=3
# Blah blah blah...
function remote_copy {
local server=$1
local retries=$2
local now=1
status=0
while [ $now -le $retries ]; do
echo "INFO: Trying to copy file from: $server, attempt=$now"
/usr/bin/timeout --kill-after 25.0s 20.0s \
/usr/bin/scp \
-o BatchMode=yes \
-o logLevel=Error \
-o ConnectTimeout=5 \
-o ConnectionAttempts=3 \
${server}:$REMOTE_FILE ${DATADIR}/lshw-$server-dump.json
status=$?
if [ $status -ne 0 ]; then
sleep_time=$(((RANDOM % 60)+ 1))
echo "WARNING: Copy failed for $server:$REMOTE_FILE. Waiting '${sleep_time} seconds' before re-trying..."
/usr/bin/sleep ${sleep_time}s
else
break # All good, no point on waiting...
fi
((now=now+1))
done
return $status
}
DATADIR="$HOME/Documents/lshw-dump"
if [ ! -d "$DATADIR" ]; then
/usr/bin/mkdir -p -v "$DATADIR"|| "FATAL: Failed to create $DATADIR" && exit 100
fi
declare -A server_pid
for server in ${servers[*]}; do
remote_copy $server $MAX_RETRIES &
server_pid[$server]=$! # Save the PID of the scp of a given server for later
done
# Iterate through all the servers and:
# Wait for the return code of each
# Check the exit code from each scp
for server in ${!server_pid[*]}; do
wait ${server_pid[$server]}
test $? -ne 0 && echo "ERROR: Copy from $server had problems, will not continue" && exit 100
done
# Blah blah blah, process the files you just copied...
Wie sieht es jetzt aus? In diesem Durchlauf habe ich ein System ausgefallen (mac-pro-1-1) und ein System ohne die Datei (macmini2). Sie können sehen, dass die Kopie vom Server dmaf5 funktioniert sofort, aber für die anderen beiden gibt es einen Wiederholungsversuch für eine zufällige Zeit zwischen 1 und 60 Sekunden, bevor sie beendet werden:
INFO: Trying to copy file from: macmini2, attempt=1
INFO: Trying to copy file from: mac-pro-1-1, attempt=1
INFO: Trying to copy file from: dmaf5, attempt=1
scp: /var/log/lshw-dump.json: No such file or directory
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for macmini2:/var/log/lshw-dump.json. Waiting '60 seconds' before re-trying...
ssh: connect to host mac-pro-1-1 port 22: No route to host
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for mac-pro-1-1:/var/log/lshw-dump.json. Waiting '32 seconds' before re-trying...
INFO: Trying to copy file from: mac-pro-1-1, attempt=2
ssh: connect to host mac-pro-1-1 port 22: No route to host
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for mac-pro-1-1:/var/log/lshw-dump.json. Waiting '18 seconds' before re-trying...
INFO: Trying to copy file from: macmini2, attempt=2
scp: /var/log/lshw-dump.json: No such file or directory
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for macmini2:/var/log/lshw-dump.json. Waiting '3 seconds' before re-trying...
INFO: Trying to copy file from: macmini2, attempt=3
scp: /var/log/lshw-dump.json: No such file or directory
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for macmini2:/var/log/lshw-dump.json. Waiting '6 seconds' before re-trying...
INFO: Trying to copy file from: mac-pro-1-1, attempt=3
ssh: connect to host mac-pro-1-1 port 22: No route to host
ERROR: There was an error in main context, details to follow
WARNING: Copy failed for mac-pro-1-1:/var/log/lshw-dump.json. Waiting '47 seconds' before re-trying...
ERROR: There was an error in main context, details to follow
ERROR: Copy from mac-pro-1-1 had problems, will not continue
Wenn ich versage, muss ich das alles noch einmal machen? Verwenden eines Checkpoints
Angenommen, die Remote-Kopie ist der teuerste Vorgang dieses gesamten Skripts und Sie sind bereit oder in der Lage, dieses Skript erneut auszuführen, vielleicht mit cron
oder zweimal am Tag von Hand, um sicherzustellen, dass Sie die Dateien abholen, wenn ein oder mehrere Systeme ausfallen.
Sie könnten für den Tag einen kleinen „Status-Cache“ erstellen, in dem Sie nur die erfolgreichen Verarbeitungsvorgänge pro Maschine aufzeichnen. Wenn sich dort ein System befindet, suchen Sie für diesen Tag nicht erneut nach.
Einige Programme wie Ansible tun etwas Ähnliches und ermöglichen es Ihnen, ein Playbook nach einem Fehler auf einer begrenzten Anzahl von Computern erneut zu versuchen (--limit @/home/user/site.retry
).
Eine neue Version (Version fünf) des Skripts enthält Code zum Aufzeichnen des Status der Kopie (Zeile 15-33):
15 declare SCRIPT_NAME=$(/usr/bin/basename $BASH_SOURCE)|| exit 100
16 declare YYYYMMDD=$(/usr/bin/date +%Y%m%d)|| exit 100
17 declare CACHE_DIR="/tmp/$SCRIPT_NAME/$YYYYMMDD"
18 # Logic to clean up the cache dir on daily basis is not shown here
19 if [ ! -d "$CACHE_DIR" ]; then
20 /usr/bin/mkdir -p -v "$CACHE_DIR"|| exit 100
21 fi
22 trap "/bin/rm -rf $CACHE_DIR" INT KILL
23
24 function check_previous_run {
25 local machine=$1
26 test -f $CACHE_DIR/$machine && return 0|| return 1
27 }
28
29 function mark_previous_run {
30 machine=$1
31 /usr/bin/touch $CACHE_DIR/$machine
32 return $?
33 }
Haben Sie die Falle in Zeile 22 bemerkt? Wenn das Skript unterbrochen (beendet) wird, möchte ich sicherstellen, dass der gesamte Cache ungültig wird.
Fügen Sie dann diese neue Hilfslogik zu remote_copy
hinzu Funktion (Zeile 52-81):
52 function remote_copy {
53 local server=$1
54 check_previous_run $server
55 test $? -eq 0 && echo "INFO: $1 ran successfully before. Not doing again" && return 0
56 local retries=$2
57 local now=1
58 status=0
59 while [ $now -le $retries ]; do
60 echo "INFO: Trying to copy file from: $server, attempt=$now"
61 /usr/bin/timeout --kill-after 25.0s 20.0s \
62 /usr/bin/scp \
63 -o BatchMode=yes \
64 -o logLevel=Error \
65 -o ConnectTimeout=5 \
66 -o ConnectionAttempts=3 \
67 ${server}:$REMOTE_FILE ${DATADIR}/lshw-$server-dump.json
68 status=$?
69 if [ $status -ne 0 ]; then
70 sleep_time=$(((RANDOM % 60)+ 1))
71 echo "WARNING: Copy failed for $server:$REMOTE_FILE. Waiting '${sleep_time} seconds' before re-trying..."
72 /usr/bin/sleep ${sleep_time}s
73 else
74 break # All good, no point on waiting...
75 fi
76 ((now=now+1))
77 done
78 test $status -eq 0 && mark_previous_run $server
79 test $? -ne 0 && status=1
80 return $status
81 }
Beim ersten Start wird eine neue neue Nachricht für das Cache-Verzeichnis ausgegeben:
./collect_data_from_servers.v5.sh
/usr/bin/mkdir: created directory '/tmp/collect_data_from_servers.v5.sh'
/usr/bin/mkdir: created directory '/tmp/collect_data_from_servers.v5.sh/20210612'
ERROR: There was an error in main context, details to follow
INFO: Trying to copy file from: macmini2, attempt=1
ERROR: There was an error in main context, details to follow
Wenn Sie es erneut ausführen, weiß das Skript, dass dma5f ist einsatzbereit, Sie müssen den Kopiervorgang nicht wiederholen:
./collect_data_from_servers.v5.sh
INFO: dmaf5 ran successfully before. Not doing again
ERROR: There was an error in main context, details to follow
INFO: Trying to copy file from: macmini2, attempt=1
ERROR: There was an error in main context, details to follow
INFO: Trying to copy file from: mac-pro-1-1, attempt=1
Stellen Sie sich vor, wie sich dies beschleunigt, wenn Sie mehr Maschinen haben, die nicht erneut besucht werden sollten.
Krümel zurücklassen:Was protokollieren, wie protokollieren und ausführliche Ausgabe
Wenn Sie wie ich sind, mag ich ein bisschen Kontext, mit dem ich korrelieren kann, wenn etwas schief geht. Das echo
Anweisungen im Skript sind nett, aber was wäre, wenn Sie ihnen einen Zeitstempel hinzufügen könnten.
Wenn Sie logger
verwenden , können Sie die Ausgabe auf journalctl
speichern zur späteren Überprüfung (sogar Aggregation mit anderen Tools da draußen). Das Beste daran ist, dass Sie die Leistungsfähigkeit von journalctl
zeigen sofort.
Anstatt also einfach echo
zu machen , können Sie auch einen Aufruf zu logger
hinzufügen so mit einer neuen Bash-Funktion namens ‘message
’:
SCRIPT_NAME=$(/usr/bin/basename $BASH_SOURCE)|| exit 100
FULL_PATH=$(/usr/bin/realpath ${BASH_SOURCE[0]})|| exit 100
set -o errtrace # Enable the err trap, code will get called when an error is detected
trap "echo ERROR: There was an error in ${FUNCNAME[0]-main context}, details to follow" ERR
declare CACHE_DIR="/tmp/$SCRIPT_NAME/$YYYYMMDD"
function message {
message="$1"
func_name="${2-unknown}"
priority=6
if [ -z "$2" ]; then
echo "INFO:" $message
else
echo "ERROR:" $message
priority=0
fi
/usr/bin/logger --journald<<EOF
MESSAGE_ID=$SCRIPT_NAME
MESSAGE=$message
PRIORITY=$priority
CODE_FILE=$FULL_PATH
CODE_FUNC=$func_name
EOF
}
Sie können sehen, dass Sie separate Felder als Teil der Nachricht speichern können, z. B. die Priorität, das Skript, das die Nachricht erstellt hat usw.
Wie ist das nützlich? Nun, Sie könnten get
die Nachrichten zwischen 13:26 Uhr und 13:27 Uhr nur Fehler (priority=0
) und nur für unser Skript (collect_data_from_servers.v6.sh
) wie folgt, Ausgabe im JSON-Format:
journalctl --since 13:26 --until 13:27 --output json-pretty PRIORITY=0 MESSAGE_ID=collect_data_from_servers.v6.sh
{
"_BOOT_ID" : "dfcda9a1a1cd406ebd88a339bec96fb6",
"_AUDIT_LOGINUID" : "1000",
"SYSLOG_IDENTIFIER" : "logger",
"PRIORITY" : "0",
"_TRANSPORT" : "journal",
"_SELINUX_CONTEXT" : "unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023",
"__REALTIME_TIMESTAMP" : "1623518797641880",
"_AUDIT_SESSION" : "3",
"_GID" : "1000",
"MESSAGE_ID" : "collect_data_from_servers.v6.sh",
"MESSAGE" : "Copy failed for macmini2:/var/log/lshw-dump.json. Waiting '45 seconds' before re-trying...",
"_CAP_EFFECTIVE" : "0",
"CODE_FUNC" : "remote_copy",
"_MACHINE_ID" : "60d7a3f69b674aaebb600c0e82e01d05",
"_COMM" : "logger",
"CODE_FILE" : "/home/josevnz/BashError/collect_data_from_servers.v6.sh",
"_PID" : "41832",
"__MONOTONIC_TIMESTAMP" : "25928272252",
"_HOSTNAME" : "dmaf5",
"_SOURCE_REALTIME_TIMESTAMP" : "1623518797641843",
"__CURSOR" : "s=97bb6295795a4560ad6fdedd8143df97;i=1f826;b=dfcda9a1a1cd406ebd88a339bec96fb6;m=60972097c;t=5c494ed383898;x=921c71966b8943e3",
"_UID" : "1000"
}
Da es sich um strukturierte Daten handelt, können andere Protokollsammler alle Ihre Computer durchsuchen, Ihre Skriptprotokolle zusammenfassen und dann haben Sie nicht nur Daten, sondern auch die Informationen.
Sie können sich die gesamte sechste Version des Skripts ansehen.
Seien Sie nicht so eifrig, Ihre Daten zu ersetzen, bis Sie sie überprüft haben.
Wenn Sie von Anfang an bemerkt haben, dass ich immer wieder eine beschädigte JSON-Datei kopiert habe:
Parse error: Expected separator between values at line 4, column 11
ERROR parsing '/home/josevnz/Documents/lshw-dump/lshw-dmaf5-dump.json'
Das lässt sich leicht verhindern. Kopieren Sie die Datei an einen temporären Speicherort. Wenn die Datei beschädigt ist, versuchen Sie nicht, die vorherige Version zu ersetzen (und lassen Sie die fehlerhafte zur Überprüfung. Zeilen 99–107 der siebten Version des Skripts):
function remote_copy {
local server=$1
check_previous_run $server
test $? -eq 0 && message "$1 ran successfully before. Not doing again" && return 0
local retries=$2
local now=1
status=0
while [ $now -le $retries ]; do
message "Trying to copy file from: $server, attempt=$now"
/usr/bin/timeout --kill-after 25.0s 20.0s \
/usr/bin/scp \
-o BatchMode=yes \
-o logLevel=Error \
-o ConnectTimeout=5 \
-o ConnectionAttempts=3 \
${server}:$REMOTE_FILE ${DATADIR}/lshw-$server-dump.json.$$
status=$?
if [ $status -ne 0 ]; then
sleep_time=$(((RANDOM % 60)+ 1))
message "Copy failed for $server:$REMOTE_FILE. Waiting '${sleep_time} seconds' before re-trying..." ${FUNCNAME[0]}
/usr/bin/sleep ${sleep_time}s
else
break # All good, no point on waiting...
fi
((now=now+1))
done
if [ $status -eq 0 ]; then
/usr/bin/jq '.' ${DATADIR}/lshw-$server-dump.json.$$ > /dev/null 2>&1
status=$?
if [ $status -eq 0 ]; then
/usr/bin/mv -v -f ${DATADIR}/lshw-$server-dump.json.$$ ${DATADIR}/lshw-$server-dump.json && mark_previous_run $server
test $? -ne 0 && status=1
else
message "${DATADIR}/lshw-$server-dump.json.$$ Is corrupted. Leaving for inspection..." ${FUNCNAME[0]}
fi
fi
return $status
}
Wählen Sie die richtigen Tools für die Aufgabe aus und bereiten Sie Ihren Code ab der ersten Zeile vor
Ein sehr wichtiger Aspekt der Fehlerbehandlung ist die richtige Codierung. Wenn Sie eine schlechte Logik in Ihrem Code haben, wird es durch keine noch so große Fehlerbehandlung besser. Um dies kurz und bash-bezogen zu halten, gebe ich Ihnen im Folgenden ein paar Hinweise.
Sie sollten IMMER auf Fehlersyntax prüfen, bevor Sie Ihr Skript ausführen:
bash -n $my_bash_script.sh
Ernsthaft. Er sollte genauso automatisch ablaufen wie jeder andere Test.
Lesen Sie die Bash-Manpage und machen Sie sich mit den wichtigsten Optionen vertraut, wie:
set -xv
my_complicated_instruction1
my_complicated_instruction2
my_complicated_instruction3
set +xv
Verwenden Sie ShellCheck, um Ihre Bash-Skripte zu überprüfen
Es ist sehr leicht, einfache Probleme zu übersehen, wenn Ihre Skripts groß werden. ShellCheck ist eines dieser Tools, das Sie vor Fehlern bewahrt.
shellcheck collect_data_from_servers.v7.sh
In collect_data_from_servers.v7.sh line 15:
for dependency in ${dependencies[@]}; do
^----------------^ SC2068: Double quote array expansions to avoid re-splitting elements.
In collect_data_from_servers.v7.sh line 16:
if [ ! -x $dependency ]; then
^---------^ SC2086: Double quote to prevent globbing and word splitting.
Did you mean:
if [ ! -x "$dependency" ]; then
...
Wenn Sie sich fragen, die endgültige Version des Skripts, nachdem Sie ShellCheck bestanden haben, ist hier. Blitzsauber.
Sie haben etwas bei den scp-Prozessen im Hintergrund bemerkt
Sie haben wahrscheinlich bemerkt, dass, wenn Sie das Skript beenden, einige gegabelte Prozesse zurückbleiben. Das ist nicht gut und einer der Gründe, warum ich es vorziehe, Tools wie Ansible oder Parallel zu verwenden, um diese Art von Aufgabe auf mehreren Hosts zu erledigen, und die Frameworks die richtige Bereinigung für mich erledigen lassen. Sie können natürlich weiteren Code hinzufügen, um diese Situation zu handhaben.
Dieses Bash-Skript könnte möglicherweise eine Fork-Bombe erstellen. Es hat keine Kontrolle darüber, wie viele Prozesse gleichzeitig erzeugt werden, was in einer realen Produktionsumgebung ein großes Problem darstellt. Außerdem gibt es eine Grenze dafür, wie viele gleichzeitige SSH-Sitzungen Sie haben können (geschweige denn Bandbreite verbrauchen). Wiederum habe ich dieses fiktive Beispiel in Bash geschrieben, um Ihnen zu zeigen, wie Sie ein Programm immer verbessern können, um Fehler besser zu behandeln.
Fassen wir zusammen
[ Jetzt herunterladen:Eine Anleitung für Systemadministratoren zum Bash-Skripting. ]
1. Sie müssen den Rückgabecode Ihrer Befehle überprüfen. Das könnte bedeuten, dass Sie sich entscheiden, es erneut zu versuchen, bis sich ein vorübergehender Zustand verbessert, oder das gesamte Skript kurzschließen.
2. Apropos vorübergehende Bedingungen, Sie müssen nicht bei Null anfangen. Sie können den Status erfolgreicher Aufgaben speichern und es ab diesem Zeitpunkt erneut versuchen.
3. Bash 'Trap' ist dein Freund. Verwenden Sie es zur Bereinigung und Fehlerbehandlung.
4. Gehen Sie beim Herunterladen von Daten aus einer beliebigen Quelle davon aus, dass diese beschädigt sind. Überschreiben Sie niemals Ihren guten Datensatz mit neuen Daten, bis Sie einige Integritätsprüfungen durchgeführt haben.
5. Nutzen Sie die Vorteile von journalctl und benutzerdefinierten Feldern. Sie können anspruchsvolle Suchen nach Problemen durchführen und diese Daten sogar an Protokollaggregatoren senden.
6. Sie können den Status von Hintergrundaufgaben (einschließlich Sub-Shells) überprüfen. Denken Sie daran, die PID zu speichern und darauf zu warten.
7. Und schließlich:Verwenden Sie einen Bash-Flusenhelfer wie ShellCheck. Sie können es auf Ihrem bevorzugten Editor (wie VIM oder PyCharm) installieren. Sie werden überrascht sein, wie viele Fehler in Bash-Skripten unentdeckt bleiben...
Wenn Ihnen dieser Inhalt gefallen hat oder Sie ihn erweitern möchten, kontaktieren Sie das Team unter [email protected].