2020-09-30 19:38
Welche Sprache für Checkmk Local Checks?
Das Monitoring-System Checkmk erlaubt es, die eingebauten Standard-Checks durch eigene Erweiterungen zu ergänzen. Meistens werden dabei Local Checks verwendet.
(Nachtrag 2020-10-25: Messwerte für PHP hinzugefügt.)
Local Checks sind Skripte oder Programme, die man auf dem zu überwachenden System in einem speziellen Verzeichnis ablegt. Sie ermitteln einen Messwert für das Monitoring und geben ihn in einem definierten Format auf der Standardausgabe aus. Das Format ist sehr einfach: pro Check wird eine Zeile ausgegeben. Diese enthält vier Werte, durch Leerzeichen getrennt. Ein einfaches Beispiel sieht so aus:
0 Mein_Check - Beschreibung des Checks
Das erste Feld enthält den Status in numerischer Form; 0 steht für OK
(grün), 1 für WARN (gelb), 2 für CRIT (rot) und 3 für UNKNOWN (orange).
Das zweite Feld gibt den Namen des Checks an. Dieser sollte immer gleich
sein, da er vom Checkmk Server bei der Inventarisierung gelernt und dann
bei jedem Check-Durchlauf ein Eintrag dieses Namens erwartet wird.
Das dritte Feld kann optional quantitative Messwerte enthalten; wenn es
keine gibt, wird ein “-” angegeben.
Im vierten Feld steht eine Beschreibung des Checks und des Ergebnisses;
es wird so in der Monitoring-Konsole angezeigt und bei Benachrichtungen
verschickt, z.B. als E-Mail.
Welche Skriptsprache?
Einzelne Local Check Skripte sind normalerweise recht kurz. In realen Umgebungen werden aber oft sehr viele davon ausgerollt. Diese werden bei jedem Check-Durchlauf einzeln aufgerufen, so dass die Performance durchaus eine Rolle spielt.
Daher kommt bei jeder Checkmk Einführung früher oder später die Frage auf, welche Programmiersprache man am besten für Local Checks verwendet.
Viele Local Checks werden als Shellskript in Bash geschrieben. Das liegt nahe, denn Bash dürfte der einzige Skriptinterpreter sein, der auf wirklich jedem Linux-System vorhanden ist. Wenn aber die Komplexität etwas höher wird und zum Beispiel Arrays, Hashes oder Regular Expressions benötigt werden, stößt man schnell an die Grenzen von Bash und greift lieber zu einer “richtigen” Programmiersprache.
Von den vier großen stabilen Linux-Distributionen – CentOS, Debian stable, OpenSUSE Leap und Ubuntu LTS – bringen drei (alle außer CentOS) bereits in der Minimalinstallation einen Perl5 Interpreter mit. Weitere beliebte Skriptsprachen, die bei allen genannten Distributionen enthalten sind, umfassen PHP, Python und Ruby. Diese sind nicht Bestandteil der Minimalinstallation, lassen sich aber über die Standard-Paketquellen nachinstallieren; ebenso wie Perl auf CentOS.
Bei Python muss beachtet werden, dass derzeit zwei verschiedene Sprachversionen, Python 2 und 3, im Umlauf sind. Diese sind nicht kompatibel; abgesehen von Trivialfällen muss ein Skript für die jeweilige Sprachversion geschrieben werden. Python 2 wurde bereits abgekündigt. Die stabilen Distributionen unterstützen es noch im Rahmen ihrer Lebenszyklen, ich würde es jedoch für Neuentwicklungen nicht mehr in Betracht ziehen.
System | Bash | Perl | PHP | Python 3 | Ruby |
---|---|---|---|---|---|
CentOS 8 | 4.4.19 | 5.26.3 | 7.2.24 | 3.6.8 und 3.8.0 | 2.5.5 |
Debian 10 | 5.0.3 | 5.28.1 | 7.3.19 | 3.7.3 | 2.5.1 |
OpenSUSE Leap 15 | 4.4.23 | 5.26.1 | 7.4.6 | 3.6.10 | 2.5.8 |
Ubuntu 20.04 LTS | 5.0.17 | 5.30.0 | 7.4.3 | 3.8.2 | 2.7 |
Startup-Zeiten
Wenn viele kleine Skripte nacheinander ausgeführt werden, ist die Startup-Zeit des Interpreters ein entscheidender Faktor für die Gesamtperformance. Auf einem System mit dem Ryzen 7-1700 Prozessor unter Ubuntu 20.04 LTS dauert die Ausführung eines leeren Programmes wie folgt:
$ time bash -c ''
real 0m0,003s
user 0m0,002s
sys 0m0,000s
$ time perl -e ''
real 0m0,003s
user 0m0,003s
sys 0m0,001s
$ time php -r ''
real 0m0,017s
user 0m0,008s
sys 0m0,009s
$ time python3 -c ''
real 0m0,027s
user 0m0,023s
sys 0m0,004s
$ time ruby -e ''
real 0m0,057s
user 0m0,040s
sys 0m0,016s
Die Initialisierungszeit für den Bash und Perl liegt gleichauf im Bereich weniger Millisekunden. PHP benötigt knapp 20 und Python 3 (und 2 ebenfalls) bereits knapp 30 Millisekunden, Ruby sogar nochmal das doppelte.
Natürlich sind die Messungen mit time
nicht sehr präzise, zumal sich keine
höhere Genauigkeit als Millisekunden ausgeben lässt. Eine Tendenz ist aber
durchaus erkennbar. Ein Lauf mit strace -c
zeigt die Häufigkeit und Dauer
von Syscalls und liefert eine Begründung für die Unterschiede:
$ strace -c bash -c ''
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
32,12 0,000185 9 20 9 stat
17,36 0,000100 7 14 rt_sigaction
7,47 0,000043 7 6 rt_sigprocmask
6,42 0,000037 7 5 1 access
6,42 0,000037 7 5 geteuid
6,42 0,000037 7 5 getegid
4,86 0,000028 5 5 getgid
4,69 0,000027 5 5 getuid
3,12 0,000018 18 1 sysinfo
2,60 0,000015 7 2 1 ioctl
2,43 0,000014 7 2 getpid
1,22 0,000007 7 1 1 getpeername
1,22 0,000007 7 1 uname
1,22 0,000007 7 1 getppid
1,22 0,000007 7 1 getpgrp
1,22 0,000007 7 1 prlimit64
0,00 0,000000 0 3 read
0,00 0,000000 0 7 close
0,00 0,000000 0 6 fstat
0,00 0,000000 0 18 mmap
0,00 0,000000 0 6 mprotect
0,00 0,000000 0 1 munmap
0,00 0,000000 0 3 brk
0,00 0,000000 0 6 pread64
0,00 0,000000 0 1 execve
0,00 0,000000 0 2 1 arch_prctl
0,00 0,000000 0 7 openat
------ ----------- ----------- --------- --------- ----------------
100.00 0,000576 135 13 total
$ strace -c perl -e ''
harald@moonrise:~$ strace -c perl -e ''
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
30,29 0,000495 7 69 rt_sigaction
20,26 0,000331 12 27 mmap
6,61 0,000108 12 9 openat
6,12 0,000100 12 8 mprotect
4,22 0,000069 7 9 close
3,92 0,000064 8 8 pread64
3,30 0,000054 9 6 6 stat
3,18 0,000052 6 8 fstat
2,88 0,000047 7 6 read
2,26 0,000037 7 5 fcntl
2,02 0,000033 33 1 readlink
1,96 0,000032 8 4 1 ioctl
1,77 0,000029 9 3 geteuid
1,71 0,000028 7 4 3 lseek
1,59 0,000026 6 4 brk
1,53 0,000025 25 1 munmap
1,41 0,000023 7 3 getgid
1,35 0,000022 7 3 getuid
1,35 0,000022 7 3 getegid
0,49 0,000008 4 2 1 arch_prctl
0,49 0,000008 8 1 prlimit64
0,43 0,000007 7 1 rt_sigprocmask
0,43 0,000007 7 1 set_tid_address
0,43 0,000007 7 1 set_robust_list
0,00 0,000000 0 1 1 access
0,00 0,000000 0 1 execve
------ ----------- ----------- --------- --------- ----------------
100.00 0,001634 189 12 total
$ strace -c php -r ''
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
100,00 0,000790 11 69 munmap
0,00 0,000000 0 109 read
0,00 0,000000 0 116 close
0,00 0,000000 0 30 stat
0,00 0,000000 0 150 fstat
0,00 0,000000 0 12 7 lstat
0,00 0,000000 0 1 lseek
0,00 0,000000 0 314 mmap
0,00 0,000000 0 111 mprotect
0,00 0,000000 0 15 brk
0,00 0,000000 0 80 rt_sigaction
0,00 0,000000 0 2 rt_sigprocmask
0,00 0,000000 0 30 30 ioctl
0,00 0,000000 0 8 pread64
0,00 0,000000 0 2 1 access
0,00 0,000000 0 1 execve
0,00 0,000000 0 1 getcwd
0,00 0,000000 0 2 readlink
0,00 0,000000 0 1 sysinfo
0,00 0,000000 0 2 1 arch_prctl
0,00 0,000000 0 18 futex
0,00 0,000000 0 2 getdents64
0,00 0,000000 0 1 set_tid_address
0,00 0,000000 0 116 3 openat
0,00 0,000000 0 1 set_robust_list
0,00 0,000000 0 2 prlimit64
0,00 0,000000 0 2 getrandom
------ ----------- ----------- --------- --------- ----------------
100.00 0,000790 1198 42 total
$ strace -c python3 -c ''
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
25,63 0,000256 1 163 37 stat
19,32 0,000193 9 20 getdents64
12,91 0,000129 1 75 read
10,31 0,000103 1 56 2 openat
9,91 0,000099 1 89 fstat
8,61 0,000086 1 67 3 lseek
8,01 0,000080 1 57 close
5,01 0,000050 1 42 36 ioctl
0,30 0,000003 0 50 mmap
0,00 0,000000 0 12 mprotect
0,00 0,000000 0 6 munmap
0,00 0,000000 0 11 brk
0,00 0,000000 0 68 rt_sigaction
0,00 0,000000 0 1 rt_sigprocmask
0,00 0,000000 0 8 pread64
0,00 0,000000 0 1 1 access
0,00 0,000000 0 3 dup
0,00 0,000000 0 1 execve
0,00 0,000000 0 1 fcntl
0,00 0,000000 0 2 1 readlink
0,00 0,000000 0 1 sysinfo
0,00 0,000000 0 1 getuid
0,00 0,000000 0 1 getgid
0,00 0,000000 0 1 geteuid
0,00 0,000000 0 1 getegid
0,00 0,000000 0 3 sigaltstack
0,00 0,000000 0 2 1 arch_prctl
0,00 0,000000 0 1 futex
0,00 0,000000 0 1 set_tid_address
0,00 0,000000 0 1 set_robust_list
0,00 0,000000 0 1 prlimit64
0,00 0,000000 0 1 getrandom
------ ----------- ----------- --------- --------- ----------------
100.00 0,000999 748 81 total
$ strace -c ruby -e ''
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
18,61 0,000809 2 355 202 openat
18,08 0,000786 2 313 3 lstat
10,10 0,000439 2 216 read
9,89 0,000430 3 114 6 stat
7,54 0,000328 2 153 close
5,86 0,000255 1 156 fstat
4,90 0,000213 4 49 mprotect
4,16 0,000181 1 93 90 ioctl
2,60 0,000113 3 32 brk
2,25 0,000098 5 19 rt_sigaction
2,21 0,000096 1 51 fcntl
2,12 0,000092 1 50 lseek
2,00 0,000087 1 57 mmap
1,52 0,000066 1 42 geteuid
1,47 0,000064 6 10 getdents64
1,45 0,000063 1 41 getgid
1,43 0,000062 1 42 getegid
1,40 0,000061 1 41 getuid
0,44 0,000019 9 2 getrandom
0,39 0,000017 8 2 eventfd2
0,37 0,000016 3 5 getpid
0,32 0,000014 4 3 rt_sigprocmask
0,18 0,000008 8 1 prctl
0,16 0,000007 7 1 sigaltstack
0,16 0,000007 7 1 timer_create
0,16 0,000007 7 1 clock_gettime
0,14 0,000006 6 1 sysinfo
0,09 0,000004 4 1 futex
0,00 0,000000 0 2 munmap
0,00 0,000000 0 8 pread64
0,00 0,000000 0 1 1 access
0,00 0,000000 0 1 execve
0,00 0,000000 0 2 1 arch_prctl
0,00 0,000000 0 1 sched_getaffinity
0,00 0,000000 0 1 set_tid_address
0,00 0,000000 0 1 timer_delete
0,00 0,000000 0 1 set_robust_list
0,00 0,000000 0 3 prlimit64
------ ----------- ----------- --------- --------- ----------------
100.00 0,004348 1873 303 total
Die konkreten Zeitangaben sind hier gar nicht so interessant, da strace sich zwischen Programm und Syscall schiebt und damit die Laufzeiten beeinflusst. Entscheidend ist die Anzahl: Bash und Perl setzen bei der Initialisierung weniger als 200 Syscalls ab; Python etwa 750, PHP knapp 1.200 und Ruby sogar mehr als 1.800.
Fazit
Bezüglich der Startup-Zeiten für den Interpreter liegt es durch die Messungen nahe, bei ansonsten gleicher Eignung Perl5 den Vorzug gegenüber anderen Skriptsprachen wie PHP, Python 3 oder Ruby zu geben.
Sehr einfache Bash-Skripte sind sogar noch etwas performanter als Perl.
Allerdings kehrt sich der Vorteil zugunsten von Perl um, sobald die Skripte
größer werden. Denn Bash-Skripte rufen meistens viele andere externe
Kommandos wie sed
, awk
, grep
oder tr
auf, was zu weiteren Syscalls
führt. Perl hat entsprechende Funktionen oft bereits eingebaut, ggf. über
Bibliotheken.
Natürlich ist die Performance nicht das alleinige Kriterium für die Auswahl der Programmiersprache. Viele Faktoren können hier entscheidend sein, etwa die Standardisierung im gesamten Kontext des Systems, die Lesbarkeit, die eigenen Skills und die Verfügbarkeit von Libraries, die zur Erhebung der Monitoring-Ergebnisse ggf. benötigt werden.
Die hier getätigten Aussagen gelten nur für Umgebungen mit vielen kleinen Local Check Skripten; wenn man deren Funktionen zu wenigen größeren Skripten zusammenfassen kann, spielen die Startup-Zeiten eine geringere Rolle. Die Performance hängt dann eher am Parsen und Ausführen des jeweiligen Programmes, was hier gar nicht gemessen wurde. Auch kann es eine valide Option sein, anstelle einer Skriptsprache zu einer compilierten Sprache wie C, C++ oder Go zu greifen. Diese wurden hier ebenfalls nicht betrachtet.