Einführung in Linux Troubleshooting
In diesem Blog zeigt euch unser Linux/Unix-Experte und langjähriger Trainer Renato Testa, wie Probleme unter Linux gefunden und behoben werden können – ausschliesslich mit Bordmitteln und Standard-Werkzeugen.
Die Transparenz unixoider Systeme ist zum Finden von Problemen ein wahrer Segen. Wir können sämtliche Aktionen Schritt für Schritt abarbeiten und sogar kompilierte C/C++-Programme tracen. Netzwerkverbindungen lassen sich bis ins kleinste Detail (Packet) analysieren. Mit der «-v»-Option lassen sich fast alle Programme dazu bewegen, ausführliche Ausgaben anzuzeigen.
Konfigurationsdateien lassen sich in vielen Fällen syntaktisch prüfen bevor ein Dienst/Service neu gestartet wird um dessen Ausfall zu verhindern.
Abgerundet wird das Ganze noch durch die Logdateien welche ausführliche Informationen enthalten. Diese Textdateien lassen sich beliebig Filtern und in Echtzeit anzeigen.
Systemlast
Ein wunderbares Werkzeug, um einen ersten Überblick über die Systemlast zu bekommen, ist das Kommando «top». Dieses zeigt die Informationen in Echtzeit an :
===CODE===
$ top
top - 08:42:10 up 368 days, 21:10, 2 users, load average: 17.42, 17.84, 17.33
Tasks: 1110 total, 22 running, 1077 sleeping, 0 stopped, 0 zombie
%Cpu(s): 6.1 us, 13.4 sy, 0.0 ni, 80.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 128501.4 total, 58225.3 free, 26473.8 used, 48532.2 buff/cache
MiB Swap: 6144.0 total, 6144.0 free, 0.0 used. 102027.6 avail Mem
…
===/CODE===
Im Header wird eine generelle Übersicht über den Verbrauch an Resourcen angezeigt. Ist die Performance des Systemes schlecht – beispielsweise hohe Antwortzeiten – prüfen wir als erstes die Werte des load average. Diese 3 Werte beschreiben den 1 Minuten, 5 Minuten und 15 Minuten Schnitt. Die Werte sollten die Anzahl Prozessoren/Cores + 1 nicht (dauerhaft/längerfristi) überschreiten. Liegen die Werte zu hoch, sinkt die Performance des Systemes – es können nicht alle Jobs in der wait queue abgearbeitet werden.
Stellen wir uns den Gotthard-Tunnel vor:
Load 1 | Load 0.5 | Load 1.7 |
Es passen 10 Autos in den Tunnel | Der Tunnel ist nur halb belegt | 7 Autos stehen im Stau |
In der 2. Zeile sehen wir einen Überblick über die vorhandenen Prozesse und in der 3. Zeile die Auslastung in Prozent der CPU(s).
Die 4. und 5. Zeile informiert uns über den RAM- und Swap-Verbrauch. Diese Informationen liefert aber auch das Kommando «free -h».
Memory-/Swap-Verbrauch
Wie oben erwähnt, «free -h» ist Ihr Freund:
===CODE===
$ free -h
total used free shared buff/cache available
Mem: 125Gi 11Gi 81Gi 1.9Gi 35Gi 113Gi
Swap: 6.0Gi 4.4Gi 1.6Gi
===/CODE===
Dieses System verfügt über mehr als genügend RAM. Demzufolge benützt es sehr grosszügig «buff/cache». Für uns ist immer der Wert unter «available» relevant. Beträgt dieser Wert 0 beginnt das System den Swap-Bereich zu benutzen (Auslagerung des RAM). Swapen bzw. eigentlich paging – es werden nur einzelne Pages von Programmen geladen. Dieses ständige paging macht das System sehr ineffizient.
Das System soll aber das RAM möglichst effizient nutzen. In der obigen Ausgabe steht unter free ja nur «11Gi» aber eben – «buff/cache» sowie «shared» werden sehr grosszügig genutzt.
Speicherverbrauch einzelner Prozesse oder ganzer Prozessgruppen
Stellen wir nun fest, dass das System zuviel Memory verbraucht, müssen wir eruieren wieviel Memory von einem Prozess oder einer Prozessgruppe konsumiert wird. Das Kommando »ps aux» stellt diese Information in der RSS-Spalte (ReSidentSize) dar. Ein einfaches Shell-Script leistet, vor allem für Prozessgruppen, gute Dienste:
===CODE===
#!/bin/bash
if !] ; then
echo "usage: ${0##*/} processname" >&2
exit 1
fi
ps aux | awk '$11 ~ /'$1'/{ sum += $6 } END {print sum}'
===/CODE===
Wollen wir nun herausfinden wieviel Memory unsere neun Apache2-Instanzen belegen, finden wir dies mittels
===CODE===
$ ./memproctotal apache2
297468
===/CODE===
heraus. Im Beispiel belegt unser Apache-Webservice 297468 Kilobytes (für Rappenspalter: der Kernel braucht dazu noch rund 20KB/Prozess).
Virtuelle Memory-Statistik
Mittels des Kommandos «vmstat» wird die virtual memory statistic angezeigt. Ein Aufruf à la:
===CODE===
$ vmstat 3 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 133804 100964 0 884032 0 1 17169 1183 0 0 3 1 97 0 0
0 0 133804 98624 0 884032 0 0 0 145 2672 3004 0 1 99 0 0
0 0 133804 95952 0 884032 0 0 427 531 2808 3237 0 0 100 0 0
0 0 133804 95700 0 884032 0 0 0 37 2436 2842 0 0 100 0 0
0 0 133804 95704 0 884032 0 0 0 64 2385 2828 0 0 100 0 0
===/CODE===
erzeugt 5 Ausgaben im Abstand von 3 Sekunden. Ein besonders Augenmerk sollten wir auf die zweitletzte Spalte «wa» (wait) legen. Haben wir einen Wert grösser 0 muss die CPU auf I/O warten und verlangsamt damit deutlich das System.
Netzwerkprobleme
Obwohl ich in diesem Artikel bemüht bin Troubleshooting mit «Bordmitteln», d.h. mit Standard-Kommandos des base packets zu betreiben, beim Networking kommen wir nicht darum herum das Packet «net-tools» zu installieren welches uns Kommandos wie:
- ifconfig – Zeigt die NW-Interface-Konfiguration und den Zustand an (Neu: ip)
- netstat – Zeigt Netzwerk-Verbindungen, Routing Informationen, Interface-Statisktiken und vieles mehr an (Neu: ss)
- route – Anzeigen und verwalten von Routing-Einträgen (Neu: ip)
- rarp – Manipulationen/Anzeigen des ARP-Caches (Neu: ip neigh)
Um nur die gängisten Kommandos zu erwähnen.
Status des NW-Interfaces
===CODE===
$ ifconfig eno1
# ifconfig enp6s0
enp6s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 123.123.123.123 netmask 255.255.255.255 broadcast 0.0.0.0
inet6 xxx::xxx:xxxx:xxxx:xxxx prefixlen 64 scopeid 0x20<link>
ether 0c:9d:92:c3:fb:9a txqueuelen 1000 (Ethernet)
RX packets 6606221912 bytes 692029905010 (644.5 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 16527848414 bytes 23543411183699 (21.4 TiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0x90200000-9027ffff
===/CODE===
Mit diesem einfachen Aufruf bekommen wir einen kompletten Überblick über die Konfiguration und den Zustand des NW-Interfaces (enp6s0).
Mittels «ethtool -S enp6s0» erhalten wir genauere Informationen zur Statistik des NW-Interfaces:
===CODE===
# ethtool -S enp6s0
NIC statistics:
rx_packets: 6606228429
tx_packets: 16527854861
rx_bytes: 718455472523
tx_bytes: 23610154023235
rx_broadcast: 1074222
tx_broadcast: 545
rx_multicast: 4
tx_multicast: 8622
multicast: 4
. . .
===/CODE===
Die Werte von von errors, dropped, overruns, collisions sollten dabei natürlich 0 sein. Bei Werten >0 besteht Handlungsbedarf. Der Grund für abweichende Werte ist sehr vielfältig und kann auf defekte Komponenten, fehlerhafte Konfigurationen oder Kernel-Module, falsches Routing, etc. hinweisen. Hier gilt es das Problem einzugrenzen oder mit Programmen wie «tcpdump» oder «WireShark» in der Tiefe zu analysieren.
Logging
Unixoide Systeme schreiben in Logdateien. Diese finden wir im «/var/log/»-Verzeichnis. Zum lesen der Dateien benötigen wir «root»-Rechte.
Über den «Loglevel» lassen sich viele Programme dazu bewegen mehr Output auszugeben. So veranlasst der Loglevel «debug» Dienste wie z.B. Postfix, Apache, nginx, etc. Jede Aktion sehr detailliert zu loggen.
===CODE===
Jun 29 00:03:41 mail2 postfix/submission/smtpd1026738: warning: hostname dynamic-ip-adsl.metfone.com.kh does not resolve to address 175.100.107.238
===/CODE===
Wir sehen in obiger Zeile aus einer Logdatei:
- Datum/Uhrzeit des Ereignisses
- Den Hostnamen (mail2)
- Welcher Dienst (postfix/submission/smtpd)
- Die Prozess-Identifikation (1026738) – für jeden Case wird ein separater Prozess gestartet!
- Die Meldung zu diesem Ereignis
Was uns die Meldung sagt: Die Prüfung des Hostnamens muss die korrekte IP-Adresse liefern und vice versa. Stimmt dies nicht überein wird die Verbindung abgebrochen.
Logfiles in Echtzeit anzeigen
Mit dem Kommando:
===CODE===
tail -f file | grep -E regexp
===/CODE===
lässt sich «file>» in Echtzeit anzeigen. Der optionale «grep» filtert dabei den Stream nach einem regulären Ausdruck.
Logmeldungen ausgeben mit «journalctl»
Mit «systemd» kam bei Linux auch «journald». Dieser führt ein Journal aller Systemmeldungen in binärer Form. Die gute Nachricht: Journald gibt die Meldungen weiter an den klassischen Log-Service unixoider Systeme.
Um Meldungen des Journals auszugeben gibt es das Kommando «journalctl». Damit können Meldungen zu einer bestimmten Unit und in einem definierten Zeitraum angezeigt werden:
===CODE===
# journalctl -S "2025-06-27 10:00:00" -U "2025-06-27 11:00:00" -u ssh
Jun 27 10:02:14 pmox sshd1633058: Invalid user odoo15 from 52.224.71.115 port 55872
Jun 27 10:02:14 pmox sshd1633058: Received disconnect from 52.224.71.115 port 55872:11: Bye Bye preauth
Jun 27 10:02:14 pmox sshd1633058: Disconnected from invalid user odoo15 52.224.71.115 port 55872 preauth
Jun 27 10:02:19 pmox sshd1633117: Connection closed by authenticating user root 185.156.73.233 port 35980 preauth
...
===/CODE===
Obiges Kommando liefert Meldungen der Unit «ssh» im Zeitraum von 10:00:00 bis 11:00:00 vom 27.06.2025. Leider sehen wir nur Login-Versuche irgendwelcher armseliger Bots …
Status von Units mittels systemctl
Das Kommando «systemctl status unit» liefert wenig überraschend den Status einer Unit. Kann eine Unit nicht gestartet werden, finden wir in der Ausgabe meist die Ursache des Problemes.
===CODE===
# systemctl status pve-container@101.service
-
pve-container@101.service - PVE LXC Container: 101
Loaded: loaded (/lib/systemd/system/pve-container@.service; static)
Active: active (running) since Tue 2025-06-24 12:26:01 CEST; 1 week 0 days ago
Docs: man:lxc-start
man:lxc
man:pct
Main PID: 2946569 (lxc-start)
Tasks: 0 (limit: 154123)
Memory: 3.2M
CPU: 784ms
CGroup: /system.slice/system-pve\x2dcontainer.slice/pve-container@101.service
‣ 2946569 /usr/bin/lxc-start -F -n 101
Jun 24 12:26:01 pmox systemd1: Started pve-container@101.service - PVE LXC Container: 101.
===/CODE===
Soweit so gut. Aber da lag ja auch kein Fehler vor. Stellen wir uns aber vor, wir hätten die SSH-Service Konfiguration verändert und dabei ist uns ein Tippfehler unterlaufen. Versuchen wir dabei nun den Service zu restarten, geht dies natürlich schief:
===CODE===
# systemctl restart sshd
Job for sshd.service failed because the control process exited with error code.
See "systemctl status sshd.service" and "journalctl -xeu sshd.service" for details.
# journalctl -xeu sshd.service
...
Jul 03 10:51:43 balboa2.home sshd1133192: /etc/ssh/sshd_config: line 40: Bad configuration option: PrmitRootLogin
Jul 03 10:51:43 balboa2.home sshd1133192: /etc/ssh/sshd_config: terminating, 1 bad configuration options
…
===/CODE===
Et voilà, die zwei wesentlichen Zeilen des Jornalctl-Outputs weisen uns klar auf den Fehler hin.
Probleme mit Zugriffsberechtigungen
Falsch gesetzte Zugriffsrechte können für Applikationen zu Problemen führen:
- Die Applikation kann die zum Start nötigen die Konfigurations-Dateien nicht lesen
- Das von der Applikation benutzte Verzeichnis ist nicht schreibbar
- Probleme beim Dateizugriff – typisch bei Webservices (DocumentRoot)
Ersichtlich sind diese Probleme wiederum in den Log-Dateien (nach «permission» suchen), mittels «systemctl status …» oder «journalctl» wie im obigen Beispiel.
Unter welchem Benutzer und Gruppe läuft der Prozess? Mittels des «ps»-Kommandos lässt sich dies herausfinden:
===CODE===
$ pgrep apache2
1651603
...
1959668
...
$ ps -o uid,gid -p 1959668
UID GID
33 33
$ ps -o uid,gid -p 1651603
UID GID
0 0
===/CODE===
Wie im obigen Beispiel ersichtlich, nehmen wir aus der mit «pgrep» erzeugten Liste von PIDs nicht die kleinste (diese läuft immer mit «root»-Rechten) sondern eine höhere, die eines Worker– Prozesses (UID und GID > 0).
Welcher User und welche Gruppe ist nun 33?
===CODE===
$ id 33
uid=33(www-data) gid=33(www-data) groups=33(www-data)
===/CODE===
Also prüfen wir ob die Dateien/Verzeichnisse des Webservices auch den richtigen Besitzern und Gruppen zugeordnet sind und die Zugriffsrechte dafür richtig gesetzt sind.
===CODE===
$ ls -l <DocumentRoot>/index.php
-rw-r----- 1 <someuser> <somegroup> 405 Feb 6 2025 <DocumentRoot>/index.php
===/CODE===
Somit kann Apache (apache2) die Datei nicht lesen. Korrekt müsste die Datei zur Gruppe «www-data» gehören:
===CODE===
$ ls -l <DocumentRoot>/index.php
-rw-r----- 1 <someuser> www-data 405 Feb 6 2025 <DocumentRoot>/index.php
===/CODE===
Natürlich muss auch das Verzeichnis, im Beispiel «DocumentRoot», gelesen werden können, d.h. in diesem Fall zur richtigen Gruppe gehören. Die Fehlermeldung erscheint aber bereits im Browser «You don’t have permission to access this resource.» oder natürlich im Error-Log-File des Webservices:
Bash-Scripte debuggen
Häufige Fehler in Shell-Scripten sind:
- Falsch oder nicht gesetzte Variabeln
- Falsche Pfade
- Falscher Aufruf von externen Programmen
Nicht gesetzte Variabeln
Stellen wir uns vor unser Script namens «fooscript» enthält folgenden Code:
===CODE===
#!/bin/bash
echo $NOTSET
===/CODE===
Wenig überraschend ist die Variable «NOTSET» nicht gesetzt. Führen wir das Script nun mittels der Option «nounset» aus weisst uns die Bash auf diesen Fehler hin: ===CODE===
$ bash -o nounset fooscript
fooscript: line 3: NOTSET: unbound variable
===/CODE===
Exit erzwingen
Shell-Scripte brechen bei einem Fehler nicht automatisch ab. Vom Programmierer wird sauberes Error-Handling erwartet, aber Programmierer sind i.A. Menschen. Häufig laufen Shell-Scripte einfach durch und generieren Folgefehler, d.h. erschlagen uns mit Fehlermeldungen welche es erschweren den echten Fehler klar zu erkennen.
Abhilfe schafft die Option «errexit» welche einen Exit erzwingt falls ein externes Programm einen Exit-Status != 0 zurückgibt. Unser Script namens «fooscript» enthält nun folgenden Code:
===CODE===
#!/bin/bash
cat /tmp/gibts-nicht.txt
echo "Das (bittere) Ende"
===/CODE===
Rufen wir nun das Script wie gewohnt auf:
===CODE===
$ bash fooscript
cat: /tmp/gibts-nicht.txt: No such file or directory
Das (bittere) Ende
===/CODE===
sehen wir, dass das «cat»-Kommando zwar nicht erfolgreich verlief, das Script führte das folgende «echo»-Kommando aber trotzdem aus. Erzwingen wir nun mittels «errexit» den Abbruch sieht das Ergebnis anders aus:
===CODE===
$ bash -o errexit fooscript
cat: /tmp/gibts-nicht.txt: No such file or directory
===/CODE===
Natürlich lernen wir unseren Shell-Scripting-Kursen korrektes Error-Handling einzubauen Nur so nebenbei:
===CODE===
#!/bin/bash
set -o errexit
cat /tmp/gibts-nicht.txt && stat=$? || stat=$?
if !] ; then
echo "Das ging voll daneben" >&2
exit 1
fi
echo "Das (bittere) Ende"
===/CODE===
Die Anwender danken es euch …
Shell-Scripte tracen
Mittels der Option «xtrace» zeigen Shell-Scripte an was sie wie tun:
===CODE===
bash -o xtrace fooscript
...
+ cat /tmp/gibts-nicht.txta
cat: /tmp/gibts-nicht.txta: No such file or directory
+ stat=1
+ !]
+ echo 'Das ging daneben'
Das ging daneben
+ exit 1
...
===/CODE===
Die Zeilen mit dem «+» am Anfang zeigen an wie die Shell die Zeile interpretiert,
C-Programme tracen
Auch kompilierte C-Programme lassen sich mittels «strace» dazu bewegen. Stellen wir uns folgendes C-Programm vor:
===CODE===
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int filedes;
char buf24;
void main() {
filedes = open("/tmp/foo", O_RDONLY);
read(filedes, buf, 24);
printf("%s\n", buf);
}
===/CODE===
Das Programm versucht, eine nicht existierende Datei «/tmp/foo» zum Lesen zu öffnen, daraus zu lesen und das Gelesene auszugeben. Der etwas faule/unerfahrene Programmierer (vibe coding?) verzichtete auf error handling.
Voller Freude darüber, dass das Programm fehlerfrei kompiliert:
===CODE===
$ cc -o buggy buggy.c
$
===/CODE===
wird dieses flugs ausgeführt:
===CODE===
$ ./buggy
$
===/CODE===
Die Enttäuschung ist grenzenlos. C-Programme welche error handling vermissen, in diesem Falle einfach davon ausgehen, dass «/tmp/foo» existiert, bringen keine Fehlermeldungen. Einzig «printf» bringt ein Newline (\n)!
Mittels strace werden aber die Aufrufe der Systemcalls von «buggy» angezeigt:
===CODE===
$ strace ./buggy
. . .
openat(AT_FDCWD, "/tmp/foo", O_RDONLY) = -1 ENOENT (No such file or directory)
. . .
===/CODE===
Reduziert auf die eine wesentliche Zeile ist klar ersichtlich was da genau schief gelaufen ist.
Fazit zum Schluss: Probleme gehören dazu
In der IT sind Probleme (leider) oft an der Tagesordnung. Auf unixoiden Systemen haben wir einfach das grosse Glück das wir dank der Transparenz und fabelhaften Werkzeugen diese Probleme schnell entdecken und beheben können. Unabdingbar ist dabei aber, dass wir die Zusammenhänge des Systeme kennen. Treten Probleme nur gelegentlich auf, beispielsweise ab einer gewissen Last, bei einer gewissen Verbindung, müssen wir in der Lage sein dies zu reproduzieren.
Auch bei Distributionen mit offiziellem Support ist nicht gewährt, das Probleme zeitnah behoben werden, diese tendieren gerne dazu den Ball weiterzuspielen. Firma X (Softwarelieferant) gibt das Problem gerne an Firma Y (Hardwarelieferant) weiter und vize versa. Das Problem sollte soweit eingegrenzt und benannt werden können, dass klar X oder Y tätig werden müssen. In diesem Beispiel wurde das System einfach mit einem Live-System einer anderen Distribution gebootet und das Problem trat nicht auf (womit Firma Y aus dem Schneider war).
Troubleshooting ist aber andererseits ein höchst spannendes Thema – der Lerneffekt ist phänomenal.