Erfahrene Sysadmins analysieren problematische Systemlasten unter Linux mit den immer gleichen Kommandos. Mike Schilli verpackt sie in ein praktisches Go-Tool, das alle Ergebnisse auf einen Blick anzeigt.
Als jüngst das von mir lang ersehnte Performance-Analyse-Buch von Brendan Gregg [1] erschien, verschlang ich es gleich gierig. Ich hatte sogar Zugriff auf eine Vorabversion, um den Lesern des Linux-Magazins im November einige Tipps zu den Berkeley Package Filters genannten Kernel-Sonden zur Durchsatzmessung mitzugeben [2]. Performance-Fuchs Gregg erläutert außerdem eingangs des Buchs, wie er oft mittels klassischer Kommandozeilenwerkzeuge herausfindet, woran ein schlapper Server nun krankt.
Zehn Performance-Gebote
Dabei klopft erst einmal eine Liste von zehn Kommandos (Listing 1), die jedes Unix-System versteht, ganz alltägliche Dinge ab – etwa, wie lange das System schon ohne Reboot läuft (»uptime«) oder ob irgendetwas Auffälliges im System-Log steht (»dmesg«). Das Kommando »vmstat« sucht nach wartenden Prozessen. Außerdem prüft es, ob das RAM komplett belegt ist und der Rechner wild Dateien ein- und auslagert. In ähnlicher Weise zeigt »free« noch verfügbares RAM an.
Die Eingabe von »pidstat« visualisiert die Verteilung der Prozesse auf die CPUs des Rechners; »iostat« ermittelt, ob der Datenaustausch mit der Festplatte einen Engpass darstellt. Die Auslastung einzelner CPUs illustriert »mpstat« und zeigt auch, ob ein einzelner Prozess eine ganze CPU dauerhaft blockiert. Die »sar«-Kommandos sammeln Statistiken über die Netzwerkaktivität des Rechners samt Durchsatzdaten. Das bekannteste der Tools, »top«, gibt einen Überblick über die RAM-Auslastung sowie laufende Prozesse, sollte aber laut Gregg erst zuletzt zum Einsatz kommen.
Listing 1
brendan-gregg-commands
# uptime # dmesg | tail # vmstat 1 # mpstat -P ALL 1 # pidstat 1 # iostat -xz 1 # free -m # sar -n DEV 1 # sar -n TCP,ETCP 1 # top
Da die Sprache Go sowohl eine exzellente Anbindung an extern gestartete Prozesse bietet als auch schön anzusehende Terminal-UIs zaubern kann, kam mir die Idee, alle Analysekommandos quasi gleichzeitig abzufeuern und die Ergebnisse dann ordentlich strukturiert in separaten Gucklöchern anzuzeigen (Abbildung 1).

Abbildung 1: Der Greggalizer feuert alle Analysekommandos ab und stellt die Ergebnisse in einem langen Rutsch dar.
Listing 2 zeigt die Funktion »runit()«, die ein Kommando in Form einer Reihe von Strings entgegennimmt, es ausführt und den Inhalt der Standardausgabe an den Aufrufer zurückreicht. Die Anbindung erfolgt über die Funktion »Command()« des Pakets »exec« aus dem Go-Standardfundus, die ein Kommando mit Parametern entgegennimmt. Auf das von »Command()« zurückgelieferte Objekt greift das nachfolgende »Output()« zu, führt das angeforderte Programm mit den angegebenen Argumenten aus und liefert dessen Ausgabe als Zeichenkette zurück.
Listing 2
runit.go
package main
import (
"fmt"
"os/exec"
"regexp"
)
func runit(argv []string) string {
out, err := exec.Command(
argv[0], argv[1:]...).Output()
if err != nil {
return fmt.Sprintf("%v\n", err)
}
r := regexp.MustCompile("\\t")
return r.ReplaceAllString(
string(out), " ")
}
Da verschiedene Kommandozeilenwerkzeuge ihre Ausgaben mit Tabulatoren strukturieren, mit denen wiederum die Widgets der Terminal-UI nicht zurechtkommen, definiert Zeile 17 einen regulären Ausdruck, der auf Tabs anspringt. Da Regexe das Tab-Zeichen als »\t« erwarten und der Ausdruck in doppelten Anführungszeichen steht, muss Listing 2 den Backslash aufdoppeln (»\\t«), damit er innerhalb der doppelten Anführungszeichen standhält.
Go muss reguläre Ausdrücke kompilieren; bei einem einfachen Tabulator kann dabei nichts schiefgehen. Daher nutzt Zeile 17 »MustCompile()«, das keinen Fehlercode liefert, sondern “explodiert”, falls sich ein Regex nicht kompilieren lässt. »ReplaceAllString()« ersetzt dann im Byte-Array »out« alle Tabs durch Leerzeichen, und »runit()« gibt das Ergebnis als String zurück an den Aufrufer.
Kommandieren mit System
Listing 3 zeigt das Hauptprogramm »main()«, das den Greggalizer startet – so habe ich das Analyseprogramm Brendan Gregg zu Ehren getauft. In den anfänglichen Importanweisungen zieht der Code das Termdash-Projekt vom Github-Server herein, das die Terminal-UI zeichnet und verwaltet. Das Slice von String-Slices »commands« ab Zeile 16 definiert die verschiedenen Kommandos, die das Programm ausführt, mitsamt ihren Parametern.
Listing 3
greggalizer.go
package main
import (
"context"
"fmt"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgets/text"
)
func main() {
commands := [][]string{
{"/usr/bin/uptime"},
{"/bin/bash", "-c",
"dmesg | tail -10"},
{"/usr/bin/vmstat", "1", "1"},
{"/usr/bin/mpstat", "-P", "ALL"},
{"/usr/bin/pidstat", "1", "1"},
{"/usr/bin/iostat", "-xz", "1", "1"},
{"/usr/bin/free", "-m"},
{"/usr/bin/sar",
"-n", "DEV", "1", "1"},
{"/usr/bin/sar",
"-n", "TCP,ETCP", "1", "1"},
}
t, err := termbox.New()
if err != nil {
panic(err)
}
defer t.Close()
ctx, cancel := context.WithCancel(
context.Background())
widgets := []container.Option{
container.ID("top"),
container.Border(linestyle.Light),
container.BorderTitle(
" Greggalizer ")}
panes := []*text.Text{}
for _, command := range commands {
pane, err := text.New(
text.RollContent(),
text.WrapAtWords())
if err != nil {
panic(err)
}
red := text.WriteCellOpts(
cell.FgColor(cell.ColorRed))
pane.Write(
fmt.Sprintf("%v\n", command), red)
pane.Write(runit(command))
panes = append(panes, pane)
}
rows := panesSplit(panes)
widgets = append(widgets, rows)
c, err := container.New(t, widgets...)
if err != nil {
panic(err)
}
quit := func(k *terminalapi.Keyboard) {
if k.Key == 'q' || k.Key == 'Q' {
cancel()
}
}
err = termdash.Run(ctx, t, c,
termdash.KeyboardSubscriber(quit))
if err != nil {
panic(err)
}
}
Manche Kommandos, wie zum Beispiel »pidstat«, nehmen sowohl ein Update-Intervall entgegen als auch – optional – die maximale Anzahl der Schleifendurchgänge. So druckt »pidstat 1« im Sekundentakt die Anzahl der aktuell im Kernel abgearbeiteten Tasks aus, und zwar in einer Endlosschleife. Ein zweiter Parameter gibt die Maximalzahl der Aufrufe an; also terminiert »pidstat 1 1« nach dem ersten Ergebnis, so wie sich das der Greggalizer wünscht.
Das Kommando »top« nutzt, genau wie der Greggalizer, Terminal-Escape-Sequenzen, um eine Terminal-UI zu zaubern. Deshalb eignet sich seine Ausgabe nicht direkt zur Darstellung im Greggalizer. Weil »exec« nur simpel ausführbare Programme mit Argumenten zur Ausführung bringen kann, vermag es das Kommando »dmesg | tail -10« nicht direkt zu verarbeiten. Zwei mit einer Pipe verknüpfte Kommandos versteht nur die Shell. Deswegen reicht Zeile 18 die ganze Chose einfach via »bash -c« als String an eine Bash zur Ausführung weiter.
In Zeile 31 definiert »termbox.New()« ein neues virtuelles Terminal, das der »defer«-Aufruf in Zeile 35 bei Programmende wieder sauber zusammenfaltet. Das Slice »widgets« in Zeile 40 definiert die Widgets im Fenster und belegt es mit dem Haupt-Widget »”top”« vor, das einen Rahmen zeichnet und den Titel des Programms darüber schreibt.
Das Slice »panes« definiert in Zeile 46 Pointer auf die verschiedenen übereinander aufgestapelten Text-Widgets im Top-Fenster. Die For-Schleife ab Zeile 48 erzeugt für jedes der Kommandos ein Text-Widget mit rollendem Inhalt. So verarbeiten die Widgets auch längere Ausgaben, ohne gleich auszuflippen oder Inhalte zu vergessen. Der User kann die Ausgaben jeweils mit dem Mausrad nach oben rollen.
Bauklötze stapeln
In jedes dieser Text-Widgets schreibt Zeile 58 zunächst in roter Farbe den Namen des ausgeführten Befehls und gibt das Kommando dann an »runit()« weiter, um es ausführen zu lassen, die Ausgabe abzufangen und dem Text-Widget zuzuführen. Alle Widgets landen im Slice »panes«, jeweils per »append«-Kommando hinten angehängt (Zeile 62).
Zeile 69 übergibt die Elemente im Slice dann einzeln mittels des Operators »…« an die Funktion »container.New()« der »termdash«-Bibliothek. Die »Run()«-Funktion ab Zeile 80 baut die UI auf und verwaltet sie, bis der User [Q] drückt, um den Reigen zu beenden. Dieses Event fängt der Keyboard-Handler ab Zeile 74 ab und ruft die »cancel()«-Funktion des vorher in Zeile 37 definierten Background-Kontexts ab, der der Event-Schleife den Teppich unter den Füßen wegzieht.
Aber wie stapelt die grafische Oberfläche die einzelnen Widgets übereinander und räumt jedem gleich viel Platz ein, egal wie viele Kommandos der User definiert? Dazu muss ein Trick her, denn als Layout-Verfahren zum vertikalen Stapeln zweier Widgets kennt »termdash« nur die Funktion »SplitHorizontal«. Sie nimmt zwei Widgets sowie einen Prozentwert entgegen, der festlegt, wie viel Platz das obere Widget im Verhältnis zum unteren bekommt.
Abbildung 2 zeigt, wie sich beliebig viele Widgets in Zweierschritten übereinander anordnen lassen: Oben steht in jedem Schritt das Konglomerat aller bislang gestapelten Widgets, unten das neue Widget, das der Algorithmus anhängt. Der Prozentwert, der das Verhältnis der oberen Widget-Höhe zur unteren festlegt, ändert sich dabei dynamisch, damit wirklich alle einzelnen Widgets gleich groß erscheinen.

Abbildung 2: Der Aufbau der Panels in Zweierschritten.
Steht oben nur ein Widget, bekommt es, genau wie das untere, exakt 50 Prozent des Platzes (grüner Kasten). Sind oben aber zum Beispiel schon drei Widgets verpackt, und unten kommt eines hinzu, erhält die Widget-Gruppe oben 75 Prozent des Platzes und das neue Widget 25 Prozent (blauer Kasten). Dementsprechend nimmt die Funktion »panesSplit()« aus Listing 4 ein Slice von Text-Widgets entgegen und initialisiert das resultierende Gruppen-Widget »rows« mit dem ersten Text-Widget vor.
Listing 4
pane-splitter.go
package main
import (
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/widgets/text"
"github.com/mum4k/termdash/linestyle"
)
func panesSplit(
panes []*text.Text) container.Option {
var rows container.Option
if len(panes) > 0 {
rows =
container.PlaceWidget(panes[0])
panes = panes[1:]
}
for idx, pane := range panes {
itemsPacked := idx + 2
rows = container.SplitHorizontal(
container.Top(rows),
container.Bottom(
container.PlaceWidget(pane),
container.Border(
linestyle.Light),
),
container.SplitPercent(
100*(itemsPacked-1)/itemsPacked),
)
}
return rows
}
Über die restlichen zu verpackenden Widgets iteriert die For-Schleife ab Zeile 19 und zählt in »itemsPacked« mit, wie viele Widgets im oberen Teil schon verpackt wurden. Jeder Aufruf von »SplitHorizontal()« in Zeile 22 erhält nun in »container.Top()« die oben liegende Widget-Gruppe (»rows«) und in »container.Bottom()« das neu hinzukommende Widget mit einem dünnen Rand zur Abgrenzung.
Die Platzverteilung bestimmt »SplitPercent()« in Zeile 29 nach der Formel »100*(n-1)/n«, wobei »n« für die Anzahl der bereits oben verpackten Widgets steht. Für »n=2« ergibt dies 50 Prozent, für »n=3« 66 Prozent und für »n=4« 75 Prozent – ganz wie vom Doktor verschrieben.
In einem Rutsch
Alle drei Listings lassen sich mit den Kommandos aus Listing 5 in einem Rutsch kompilieren. Der zweite Aufruf erzeugt ein Executable »greggalizer«, das unabhängig von irgendwelchen Shared Libraries läuft. So lässt es sich einfach auf verwandte Architekturen kopieren, um dort klaglos seinen Dienst zu tun.
Listing 5
Kompilieren der Listings
$ go mod init greggalizer $ go build greggalizer.go \ runit.go pane-splitter.go
Der Aufruf von »greggalizer« auf der Kommandozeile nimmt, wie in Abbildung 1 gezeigt, das Terminal in Beschlag, spaltet es in vertikal übereinanderliegende Kästen auf und druckt die ausgeführten Kommandos mit Parametern und dem Ausgabetext hinein. Die Performance-Analyse kann beginnen.
Ausblick
Als Verbesserung des vorgestellten Skripts böte sich eine Parallelisierung der Kommandos an, damit der User nicht ein paar Sekunden vor einem leeren Bildschirm warten muss, bis der sich mit Ergebnissen füllt. Stattdessen könnte das Programm zuerst die UI zeichnen, nebenher externe Prozesse quasi gleichzeitig über Go-Routinen abfeuern und über Channels die Text-Widgets stetig mit neuen Ausgaben füllen. Heraus käme ein Tool wie Top, das seine Widgets regelmäßig auffrischt. (uba)/
Online PLUS
Im Screencast unter http://www.linux-magazin.de/videos/ demonstriert Michael Schilli das vorgestellte Programmierbeispiel.
Der Autor
Michael Schilli arbeitet als Software Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mailto:mschilli@perlmeister.com beantwortet er gerne Fragen.
Infos
- “BPF Performance Tools”, Brendan Gregg, Addison-Wesley, 2019: https://www.amazon.com/BPF-Performance-Tools-Brendan-Gregg/dp/0136554822
- Snapshot: Mike Schilli, “Auf Herz und Nieren”, LM 11/2019, S. 90, https://www.linux-magazin.de/43348
- Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2020/04/snapshot/






