Stetig hereintröpfelnde Daten müssen nicht zwangsläufig die Festplatte
auffüllen. Wenn eine Round-Robin-Datenbank wie rrdtool selektiv
unwichtige Details vergisst, bleibt das System bis zum
Sankt-Nimmerleins-Tag wartungsfrei.
Das von Tobias Oetiker entwickelte rrdtool, das mit sogenannten
Round-Robin-Datenbanken (RRDs) als Storage-Medium
arbeitet, hat sich zum quasi-Standard
bei der Speicherung von Netzwerk-Überwachungsdaten gemausert. Applikationen wie
Cacti [3] machen von der vergesslichen Datenbank heftigen Gebrauch.
Ein Round-Robin-Archiv (RRA) stellt man sich am Besten wie in Abbildung
1 dargestellt vor: Dort liegen auf einer begrenzten Anzahl von
Speicherplätzen die
von einem Monitorskript auf einem Webserver gefundenen Lastwerte:
Angefangen vom Wert 6.1 um 01:00:00
Uhr (oben Mitte), dann, weiter rechts im Kreis, der Wert 2.0 um 01:05:00,
undsofort, bis schließlich um 01:20:00 der Wert 2.4 gespeichert wurde. Der
Zeiger deutet auf den zuletzt aktualisierten Eintrag. Das
Ergebnis der nächsten Messung passt aber nicht mehr ins Archiv --
und deshalb wird, wie Abbildung 2 zeigt, der Wert von 01:00:00 Uhr mit dem
neuen Wert 4.1 um 01:25:00 Uhr überschrieben.
|
| Abbildung 1: Das Round-Robin-Archiv hält eine feste Anzahl von Datenwerten vorrätig und überschreibt alte Werte, um Platz für neue zu schaffen. |
|
| Abbildung 2: Der alte, um 01:00:00 gemessene Wert wurde mit dem um 01:25:00 übermittelten ersetzt und der Zeiger weitergeführt. |
Nun ist der Admin aber nicht nur an Messwerten der letzten 25 Minuten interessiert, sondern möchte sicher auch einmal anzeigen, wie sich die Rechnerlast über die letzten dreißig Tage oder die zurückliegenden zwölf Monate entwickelt hat. Auch hierzu braucht man keine riesigen Datenmengen vorzuhalten, denn über größere Zeiträume hinweg akzeptiert man gerne eine geringere Granularität -- der Trick: Es werden einfach weitere RRAs angelegt, die die Durchschnittslast (oder die Höchstlast, ganz nach Geschmack) pro Stunde für den letzten Tag oder pro Tag für's laufende Jahr aufnehmen.
Sind diese Round-Robin-Archive einmal in der Datenbank angelegt, füttert
das als Kommandozeilen-Tool oder Perl-Schnittstelle erhältliche
rrdtool einfach transparent neue Messwerte hinein. Der darunterliegende
Datenbankmotor sorgt automatisch dafür, dass die Kreise mit den
verschiedenen Granularitäten die richtig aufpolierten Daten erhalten.
Spätere Abfragen liefern
dann die Werte über einen angegebenen Zeitraum in der höchsten verfügbaren
Genauigkeit und rrdtool zeichnet davon sogar formschöne Webgrafiken.
Die Definition einer Round-Robin-Datenbank besteht aus einer oder mehreren Datenquellen (DS (Data Sources)). Für jede einzelne gibt der RRD-Administrator beim Anlegen der Datenbank folgende Parameter an:
load
oder mem_usage), der die Eingabedatenquelle
eindeutig in der RRD identifiziert
einen Datenquellentyp (DST, Data Source Type), der zum
Beispiel festlegt, ob
die Eingabewerte einfach übernommen werden (GAUGE (sprich: ``Geydsch'')
oder von einem stetig
anwachsenden Zähler stammen, der Extra-Service beim Überlauf benötigt
(COUNTER).
die Breite des Eingabe-Zeitfensters (step). Purzeln während dieses
Zeitraums mehrere Messwerte herein, wird statt den Einzelwerten der
Mittelwert berechnet und abgespeichert. Kommt in diesem Intervall kein
einziger Wert daher, wird na (not available) abgelegt.
Minimale und maximale Eingangswerte. Liegen Messwerte daneben, werden
sie ignoriert.
Legen wir also mal eine Datenbank mit einer Eingabequelle names load an,
die alle 60 Sekunden Werte für die gerade anliegene Rechnerlast liefert:
use RRDs;
RRDs::create(
"/tmp/load.rrd", "--step=60",
"--start=" . time() - 10,
"DS:load:GAUGE:90:0:10.0",
"RRA:MAX:0.5:1:5",
"RRA:MAX:0.5:5:10");
Gaack! Wird Zeit, dass mal jemand ein intuitives OO-Interface für
RRDtool schreibt, bis es soweit ist, einige Erklärungen: In der
Datei /tmp/load.rrd wird rrdtool die Datenbank ablegen. Das vorgegebene
Eingabeintervall ist 60 Sekunden (--step=60), in diesen Zeitabständen
werden später die Daten eingefüttert. Die Startzeit der Datenbank
wird auf 10 Sekunden in der Vergangenheit gelegt. Das ist üblich (und
voreingestellt, wenn man --start weglässt), denn RRD wird alle
Eingaben zurückweisen, die einen Zeitstempel kleiner oder gleich der
Startzeit tragen. Die DS:-Zeile definiert die einzige Datenquelle
der Datenbank mit den oben beschriebenen Parametern: Quellenname
load, Eingabetyp GAUGE, Minimal- und Maximalwert
0 bzw. 10.0 und dem sogenannten Heartbeat von 90.
Diese mit 90 angegebene Pulsfrequenz legt fest, dass wir auch zufrieden
sind, falls die Daten nicht mit der in --step vorgeschriebenen Rate
(alle 60 Sekunden) ankommen, sondern mit bis zu 30 Sekunden Verzögerung.
RRDtool lügt dann und interpoliert einfach. Wäre -- als Extremfall --
der Heartbeat 24 Stunden und
die Schrittrate weiterhin 60, genügte ein einziger gefütterter Wert
pro Tag, auf den RRDtool dann alle Minuteneinträge setzen würde.
Andererseits kann ein schneller tickender Puls auch mehr Messdaten erfordern
als das Zeitfenster der Datenbank aufnehmen kann:
Dann erwartet rrdtool Dateneingaben
im Rhytmus des Herzens und speichert sofort streng
na für einen Schritt, falls der Herzschlag einmal aussetzt.
Liegen ordnungsgemäß mehrere Werte
pro Schrittfenster vor, mittelt RRD diese, bevor der sogenannte
Primary Data Point (PDP) abgespeichert wird.
Soweit zur Definition der (in diesem Fall einzigen) Datenquelle DS. Sie bestimmte die Transformation von Eingangsmesswerten zu Primary Data Points, dem Ausgangspunkt für die nun folgenden Round-Robin-Archive, die durch die Zeilen
"RRA:MAX:0.5:1:5",
"RRA:MAX:0.5:5:10"
definiert wurden.
Der Zahlenwert in der vorletzten Kolumne gibt jeweils an,
wieviele Primary Data Points das Archiv zu einem Archivpunkt zusammenfasst.
Das erste Archiv nimmt einen, entspricht also exakt dem in den Abbildungen
1 und 2 dargestellten Round-Robin-Archiven. Das zweite Archiv hingegen nimmt
fünf Messpunkte für einen Archiv-Punkt. Bei einem
einzigen PDP gibt's nichts zu entscheiden,
aber bei fünfen ist die zweite Kolumne der Definition oben
wichtig, welche die Consolidation Function (CF) angibt: AVERAGE nimmt
zum Beispiel den Mittelwert aus den PDPs, das oben verwendete MAX nimmt
den Höchstwert, MIN oder LAST wären andere Optionen. Die letzte Kolumne
bestimmt, wieviele Datenplätze das Archiv bereitstellt. Sind alle aufgefüllt,
beginnt es, die ältesten zu überschreiben. Und schließlich die magische
0.5: Dieser sogenannte xfiles factor bestimmt, welcher Bruchteil von den
PDPs undefiniert (na) sein darf, damit das Archiv einen interpolierten
Mittelwert als gültigen Eintrag einträgt. Wird der Wert unterschritten,
steht am Ende na im Archiv.
Abbildung 3 zeigt noch einmal, wie aus den Werten, die die Datenquelle liefert, PDPs werden, die anschließend in die verschiedenen Round-Robin-Archives wandern.
|
| Abbildung 3: Wie Sample-Werte zu Primary Data Points (PDPs) werden und daraus wiederum RRAs entstehen |
Listing rrdtest zeigt ein Testskript, das eine RRD definiert, fabrizierte
Daten hineinfüttert und dann die Archivdaten abfragt. Für reproduzierbare
Ergebnisse nutzt es statt der Systemzeit den Zeitstempel 1080460200.
(Geheimtipp: RRD fängt wild zu runden an, falls keine durch 60 (und für
das 5-Minuten-Archiv sogar 300) teilbare Zahl
verwendet wird. Das mittelt sich zwar auf Dauer, aber für
Demonstrationszwecke eignen sich glatte Werte besser).
Die Zeilen 17 bis 22 erzeugen die RRD, wie oben beschrieben. Die
for-Schleife ab Zeile 26 läuft von 0 bis 40 und schiebt mit
RRDs::update() die folgenden
Zeitstempel-Lastwert-Kombinationen als Strings in die RRD:
1080460200:2
1080460260:2.1
1080460320:2.2
...
Im Normalbetrieb kann man den Zeitstempel auch weglassen, dann nimmt
das RRDs-Modul
die aktuelle Systemzeit. Bei den übergebenen Werten handelt es sich
um künstlich erzeugte Beispielwerte für die Systemlast, das Testskript
startet einfach bei 2 und erhöht den Wert pro Schritt um 0.1.
Um ein Archiv abzufragen, nimmt
RRDs::fetch() das gewünschte Abfrageintervall mit der
beim Abspeichern verwendeten Consolidation Function (CF)
entgegen und ermittelt
daraus das Archiv mit der maximal verfügbaren Auflösung. Wurde
eine CF angegeben, für die kein Archiv existiert, hagelt's eine
Fehlermeldung. RRDs::fetch() gibt die
Datenpunkte des Archivs in $data zurück, einer Referenz auf einen
Array, der wiederum Referenzen enthält, die auf Arrays mit den
Floating-Point-Datenwerten zeigen.
Die außerdem von RRDs::fetch() zurückgelieferten Werte sind $dbstart
(Startzeitpunkt der RRD), $step (der Zeitabstand der Datenpunkte
im ausgewählten Archiv) und $names (eine Referenz auf einen Array
mit den Namen aller Datenquellen). $step ist übrigens nicht unbedingt
die mit --step eingestellten Datensammelabstand der Datenbank -- für
ein angegebenes Archiv, das eine Anzahl von PDPs zu einem Archivpunkt
zusammenfasst, ergibt sich $step aus der Multiplikation von Sammelabstand
und der Anzahl der pro Archivpunkt zusammengefassten Punkte.
Zeile 36 startet eine Abfrage im Zeitfenster der letzten fünf Minuten vor dem Ende und fördert folgendes zutage:
Last 5 minutes:
1080462300: N/A
1080462360: 5.6
1080462420: 5.7
1080462480: 5.8
1080462540: 5.9
1080462600: 6
Das Modul
RRDs hat hierzu das Kurzzeit-Archiv mit 60 Sekunden Datenabstand gewählt.
Da es immer nur fünf Werte vorhält, ist der älteste Wert na.
Fragt man, wie in Zeile 39,
die Funktion RRDs::fetch() hingegen nach Werten für ein
breiteres Fenster, wie zum Beispiel die
letzten 30 Minuten der Messreihe, enthält das Ergebnis Werte aus
dem zweiten Archiv, das die Daten im 300-Sekunden-Abstand speichert:
Last 30 minutes:
1080460800: 3
1080461100: 3.5
1080461400: 4
1080461700: 4.5
1080462000: 5
1080462300: 5.5
1080462600: 6
Bei den eingetragenen Werten handelt es sich um im jeweiligen
Intervall gemessenen Höchstwert, da das zweite Archiv mit der CF MAX
definiert wurde.
Das Modul
RRDs wird übrigens nicht versuchen, Werte aus einer Kombination
von Archiven darzustellen: Es wählt ein passendes Archiv aus
und nutzt dessen Granularität für eine Ergebnisreihe mit konstanten
Zeitabständen.
01 #!/usr/bin/perl
02 ###########################################
03 # Feed test data to RRD
04 # Mike Schilli, 2004 (m@perlmeister.com)
05 ###########################################
06 use warnings;
07 use strict;
08
09 use RRDs;
10
11 my $DB = "/tmp/mydemo.rrd";
12 my $start = 1080460200;
13 my $dst = "MAX";
14 my $nof_iterations = 40;
15 my $end = $start + $nof_iterations * 60;
16
17 RRDs::create(
18 $DB, "--step=60",
19 "--start=" . ($start-10),
20 "DS:load:GAUGE:90:0:10.0",
21 "RRA:$dst:0.5:1:5",
22 "RRA:$dst:0.5:5:10",
23 ) or
24 die "Cannot create rrd ($RRDs::error)";
25
26 for(0..$nof_iterations) {
27 my $time = $start + $_ * 60;
28 my $value = 2 + $_ * 0.1;
29
30 RRDs::update(
31 $DB, "$time:$value") or
32 die "Cannot update rrd ($!)";
33 }
34
35 print "Last 5 minutes:\n";
36 fetch($end - 5*60, $end, $dst);
37
38 print "Last 30 minutes:\n";
39 fetch($end - 30*60, $end, $dst);
40
41 ###########################################
42 sub fetch {
43 ###########################################
44 my($start, $end, $dst) = @_;
45
46 my ($dbstart, $step, $names, $data) =
47 RRDs::fetch($DB, "--start=$start",
48 "--end=$end", $dst);
49
50 foreach my $line (@$data) {
51 print "$start: ";
52 $start += $step;
53 foreach my $val (@$line) {
54 $val = "N/A" unless defined $val;
55 print "$val\n";
56 }
57 }
58 }
Nun aber, nachdem diese Ausführungen hoffentlich etwas Licht in den
dunklen Keller von RRDtool geworfen haben, zu einer praktischen
Anwendung: Das Skript in Listing rrdload läuft per cronjob einmal alle fünf
Minuten:
*/5 * * * * /home/mschilli/bin/rrdload -u
Mit der Option -u aufgerufen, frischt es ein Round-Robin-Archiv mit dem
Messwert der aktuellen Systemlast auf und verabschiedet sich dann wortlos.
Zur graphischen Auswertung wird es als
rrdload -g
aufgerufen. Es legt dann eine formschöne Grafik (wie in Abbildung 4 gezeigt) als PNG-Datei im Dokumentenpfad des Webservers ab. Lade ich dieses Bild im Browser, kann ich auf perlmeister.com kontrollieren, wie sich die Systemlast auf diesem Shared-System über die Zeit entwickelt. Abbildung 4 zeigt das Ergebnis.
C<rrdload> legt ab Zeile 21 drei Archive an: Das erste nimmt 12*24 = 288 Datenpunkte auf, stellt also genügend Plätze bereit, um die alle fünf Minuten ermittelten Werte einen Tag lang zu speichern. Das zweite Archiv sucht die Spitze aus zwölf Messpunkten, also eine Stunde (12*5min = 60min) lang einrieselnde Daten, und speichert 24*7 = 168 davon. Also steht später die jeweils letzte Woche im Stundentakt zur Abfrage bereit. Das dritte und letzte Archiv findet die Tagesspitzen und hält 365 davon für die Jahresbilanz vorrätig.
Die erzeugte Grafik im PNG-Format wird mit RRDs::graph() erzeugt
und erhält mit --vertical-label noch eine Beschriftung für
die Lastachse. Die beiden Argumente
"DEF:myload=$DB:load:MAX",
"LINE2:myload#FF0000")
bestimmen, dass das RRDs-Modul aus der angegebenen Datei (in $DB)
Ergebnisdaten bezieht und diese der Graphenvariable myload zuordnet.
Es werden Werte der Datenquelle load gesucht, in einem Archiv, das
zum vorher angegebenen Zeitraum (--start bis --end) Daten mit
der Consolidation Function MAX gewonnen hat. rrdtool hat die
zweifelhafte Angewohnheit, die Datenbank am Anfang zufällig zu füllen
und über die Startzeit eine falsche Auskunft zu geben -- deswegen
greift die ab Zeile 46 definierten Funktion rrd_start_time
hinein
und holt solange Daten heraus, bis etwas vernünftiges hervorkommt.
Das Datum diese Messwerts gibt sie dann zurück,
und die graph-Funktion nimmt es entgegen.
Das verwendete RRDs::fetch geht ohne Angabe einer
Startzeit genau einen Tag zurück,
wer mehr im Graphen sehen möchte, kann mit --start einen anderen
Zeitpunkt bestimmen. Negative Werte setzen
relative Zeitdifferenzen zur gegenwärtigen Uhrzeit, mit
"--start", -365*24*3600
werden immer alle verfügbaren Daten (allerdings in der gröbsten Auflösung) im Graphen angezeigt.
Den Graphen malt sie elegant ganz in Rot (#FF0000),
und wegen LINE2 genau zwei Pixel stark.
|
| Abbildung 4: Die Last auf perlmeister.com über eine Nacht. |
01 #!/usr/bin/perl
02 ###########################################
03 # rrdload -- Measure CPU load over time
04 # Mike Schilli, 2004 (m@perlmeister.com)
05 ###########################################
06 use warnings;
07 use strict;
08
09 use RRDs;
10 use Getopt::Std;
11
12 getopts("ug", \my %opts);
13
14 my $DB = "/tmp/load.rrd";
15 my $SERVER = "/www/htdocs";
16 my $UPTIME = "uptime";
17
18 if(! -f $DB) {
19 RRDs::create($DB, "--step=300",
20 "DS:load:GAUGE:330:U:U",
21 "RRA:MAX:0.5:1:288",
22 "RRA:MAX:0.5:12:168",
23 "RRA:MAX:0.5:288:365",
24 ) or die "Create error: ($RRDs::error)";
25 }
26
27 if(exists $opts{u}) {
28 my $uptime = `$UPTIME`;
29 my ($load) = ($uptime =~ /(\d\.\d+)/);
30
31 RRDs::update($DB, time() . ":$load") or
32 die "Update error: ($RRDs::error)";
33 }
34
35 if(exists $opts{g}) {
36 RRDs::graph("$SERVER/load.png",
37 "--vertical-label=Load perlmeister.com",
38 "--start=" . rrd_start_time(),
39 "--end=" . time(),
40 "DEF:myload=$DB:load:MAX",
41 "LINE2:myload#FF0000") or
42 die "graph failed ($RRDs::error)";
43 }
44
45 ###########################################
46 sub rrd_start_time {
47 ###########################################
48
49 my ($start,$step,$names,$data) =
50 RRDs::fetch($DB, "MAX");
51
52 foreach my $line (@$data) {
53 if(! defined $line->[0]) {
54 $start += $step;
55 next;
56 }
57 return $start;
58 }
59 }
Das Perl-Modul RRDs, das eine Shared Library von RRDtool nutzt,
gibt's nicht auf dem CPAN, sondern es liegt der
RRD-Distribution bei. Um es zu installieren, lädt und entpackt man
den neuesten Source-Tarball (rrdtool-1.0.46.tar.gz) von [2] und
compiliert ihn mittels
./configure
make
Im Unterverzeichnis perl-shared findet sich dann die Distribution von
RRDs.pm, die wie üblich mit
perl Makefile.PL
make install
installiert wird. Überwacht auch ihr euren Provider!
![]() |
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |