munin/LASTTEST.md

10 KiB

Lasttest

Wie viel hält Munin aus, wenn viele Agenten gleichzeitig lesen und schreiben, in einem Projekt, in mehreren Projekten eines Workspace oder über mehrere Workspaces verteilt? Der Lasttest in last_test.go beantwortet das mit echten Aufrufen über HTTP und MCP, also über denselben Weg wie im Betrieb: Key-Prüfung, Protokoll, Datenbank und Suchindex.


Ausführen

Der Test läuft nicht bei go test ./... mit. Er braucht Zeit und lastet den Rechner voll aus. Das Build-Tag last schaltet ihn erst ein, wenn man ausdrücklich danach fragt:

go test -tags last -run TestLast -v .

Mit eigenen Werten:

go test -tags last -run TestLast -v . -last.agenten=128 -last.dauer=30s

Mit dem Race-Detektor, der Datenwettläufe zwischen Goroutinen meldet. Er macht alles etwa zehnmal langsamer, deshalb mit kleineren Werten:

go test -race -tags last -run TestLast . -last.dauer=3s -last.agenten=16 -last.eintraege=100

Der Test legt alles in einem temporären Verzeichnis an und räumt danach auf. Echte Daten in data/ fasst er nicht an.

Schalter

Schalter Vorgabe Bedeutung
-last.agenten 32 Gleichzeitige Agenten je Aufstellung. Jeder hat einen eigenen Key und eine eigene MCP-Sitzung.
-last.dauer 10s Laufzeit je Aufstellung. Es gibt drei Aufstellungen, die Gesamtdauer ist also etwa das Dreifache.
-last.projekte 4 Zahl der Projekte bzw. Workspaces in den Aufstellungen mit mehreren
-last.eintraege 500 Einträge, die jedes Projekt vorab bekommt, damit die Suche einen realistischen Index hat
-last.slugs 20 Bezeichner, auf die die Agenten je Projekt schreiben. Weniger heißt mehr Gedränge auf denselben Einträgen.
-last.schreibanteil 30 Anteil von remember in Prozent. Der Rest teilt sich 2 : 1 auf recall und get auf.

Was der Test macht

Drei Aufstellungen

Alle drei laufen mit derselben Zahl Agenten, damit die Zahlen vergleichbar sind:

Aufstellung Was sie zeigt
ein Projekt Alle Agenten im selben Projekt. Der Grundwert.
ein Workspace, N Projekte Die Projekte liegen in einer Datei und teilen sich die eine Schreibverbindung (siehe openPool in internal/store/store.go). Bremsen sich die Projekte gegenseitig?
N Workspaces, je 1 Projekt Jeder Workspace hat seine eigene Datei. Hier kann echt parallel geschrieben werden.

Was ein Agent tut

Jeder Agent verbindet sich mit seinem Key über /mcp und ruft bis zum Ende der Laufzeit in einer Schleife zufällig auf:

  • remember auf einen der umkämpften Bezeichner (gemeinsam-00 …). Jeder Schreibvorgang enthält ein eindeutiges Wort, an dem sich später erkennen lässt, welche Fassung wo gelandet ist.
  • recall mit einem von acht Fachbegriffen, die in den vorab angelegten Einträgen vorkommen
  • get auf einen der vorab angelegten Einträge

Das Schreibbudget (Ausbaustufe 3.8) ist im Test abgeschaltet. Es soll eine Schleife stoppen und würde sonst die Messung abwürgen.

Was am Ende geprüft wird

Der Test schlägt fehl, wenn eines davon nicht stimmt:

  1. Kein Aufruf ist fehlgeschlagen.
  2. Kein Schreibvorgang ging verloren: Jedes erfolgreiche remember erzeugt eine Fassung. Die Summe der Fassungen aller umkämpften Einträge muss genau der Zahl der erfolgreichen Aufrufe entsprechen.
  3. Der Suchindex kennt den letzten Stand: Das eindeutige Wort der letzten gespeicherten Fassung muss den Eintrag über die Suche finden.

Die Ausgabe lesen

Für jede Aufstellung erscheint eine Tabelle:

Werkzeug    Aufrufe     je s       p50       p95       p99       max  Fehler
verbinden       128        0   18.03ms   33.46ms   35.04ms   35.54ms       0
recall        21496     2150    4.45ms    8.02ms   23.05ms   40.33ms       0
get           11381     1138    1.04ms     2.4ms    3.18ms    7.33ms       0
remember      14269     1427   67.84ms  190.04ms  269.16ms  439.31ms       0
  • je s: erfolgreiche Aufrufe pro Sekunde
  • p50: Die Hälfte der Aufrufe war schneller, also die typische Dauer.
  • p95 / p99: Die langsamsten 5 % bzw. 1 % der Aufrufe dauerten mindestens so lange. Das sind die Werte, die ein Nutzer als „hängt manchmal“ erlebt.
  • max: der langsamste Aufruf überhaupt
  • verbinden: der MCP-Verbindungsaufbau, einmal je Agent

Darunter steht Bestand geprüft: … Fassungen, Suchindex aktuell.


Ergebnis vom 2026-10-06

Testrechner: Mac mit 10 Kernen. Clients und Server liefen auf derselben Maschine, jeweils 500 Einträge vorab, 30 % Schreiben, 20 umkämpfte Bezeichner je Projekt, 4 Projekte bzw. Workspaces.

128 Agenten, 10 Sekunden

Aufstellung remember je s remember p50 / p95 / p99 recall p50 / p99 get p50 / p99 Fehler
ein Projekt 1.427 68 / 190 / 269 ms 4,5 / 23 ms 1,0 / 3,2 ms 0
ein Workspace, 4 Projekte 1.355 73 / 201 / 286 ms 4,5 / 24 ms 1,0 / 3,4 ms 0
4 Workspaces, je 1 Projekt 1.328 60 / 151 / 205 ms 15 / 36 ms 3,8 / 12 ms 0

32 Agenten, 5 Sekunden

Aufstellung remember je s remember p50 / p99 recall p50 / p99 Fehler
ein Projekt 1.374 13 / 42 ms 4,7 / 11 ms 0
ein Workspace, 4 Projekte 1.387 13 / 46 ms 4,4 / 10 ms 0
4 Workspaces, je 1 Projekt 1.141 8 / 18 ms 10 / 24 ms 0

In allen Läufen ging kein Schreibvorgang verloren, und der Suchindex war am Ende aktuell. Auch der Lauf mit Race-Detektor war ohne Befund.

Was die Zahlen sagen

  • Last ist bei echter Nutzung kein Thema. Der Server schafft rund 1.300 Schreibvorgänge pro Sekunde. Das Schreibbudget erlaubt 1.000 pro Stunde und Projekt, und echte Agenten liegen bei ein paar Dutzend pro Stunde.
  • Projekte im selben Workspace verhalten sich wie ein Projekt. Sie teilen sich die Schreibverbindung, Schreibvorgänge stellen sich also an. Mit mehr Agenten wachsen deshalb die Wartezeiten beim Schreiben (p50 von 13 ms bei 32 auf 68 ms bei 128 Agenten), aber nicht die Fehler.
  • Mehrere Workspaces schreiben etwas schneller, suchen aber langsamer. Getrennte Dateien heben die Warteschlange beim Schreiben auf. Dafür arbeiten dann mehr Schreiber gleichzeitig und konkurrieren mit der Suche um dieselben Kerne. Auf einem Rechner ist die CPU die eigentliche Grenze, nicht SQLite.
  • Der volle Weg kostet etwa das Sechsfache gegenüber dem reinen Schreiben in die Datenbank (8.656/s, siehe AUSBAUSTUFEN.md 1.2). Dazu kommen HTTP, MCP, Key-Prüfung, Protokolleintrag und Suchindex.

Grenzen der Messung

  • Clients und Server teilen sich den Rechner. Die Agenten verbrauchen selbst CPU. Ein Server allein schafft mehr, die Zahlen sind eher eine Untergrenze.
  • Kaltstart nicht gemessen. Der Suchindex jedes Projekts ist nach dem Anlegen der Einträge schon aufgebaut. Der erste Zugriff auf ein großes Projekt nach einem Neustart (etwa 170 ms je 10.000 Einträge, siehe AUSBAUSTUFEN.md 1.1) ist nicht enthalten. Während dieses Aufbaus wartet nur dieses eine Projekt. Bis 2026-10-06 warteten auch alle anderen Projekte desselben Workspace, das ist behoben (siehe unten).
  • Nur MCP. Die Weboberfläche ist nicht dabei.
  • Kein Rückkanal: Der zusätzliche SSE-Kanal, den manche MCP-Clients offen halten, ist abgeschaltet. Er kostet je Agent eine offene Verbindung, aber kaum Rechenzeit.
  • Gleichmäßige Dauerlast. Echte Agenten arbeiten in Schüben mit Pausen dazwischen. Der Test ist also deutlich härter als die Wirklichkeit.

Dabei gefunden

Suchindex hielt eine ältere Fassung (behoben). Speichern und Index aktualisieren waren zwei getrennte Schritte, und der Index übernahm den Stand, den der Aufrufer mitgab. Überholten sich zwei Schreiber auf demselben Eintrag zwischen diesen Schritten, stand Fassung 3 in der Datenbank, aber Fassung 2 im Index. recall fand den Eintrag dann bis zum Neustart über veraltete Wörter. Genauso konnte ein gerade gelöschter Eintrag durch eine verspätete Aktualisierung wieder in der Suche auftauchen.

Unter Last ließ sich das nicht zuverlässig auslösen, das Zeitfenster liegt bei Mikrosekunden. Mit gezielt vertauschter Reihenfolge war es sofort reproduzierbar.

Behebung: Registry.Refresh in internal/search/registry.go liest nach jedem Schreiben den aktuellen Stand aus der Datenbank nach. Der letzte Aufruf liest damit immer den neuesten Stand, egal in welcher Reihenfolge die Aufrufe ankommen. Abgesichert durch internal/search/registry_test.go und internal/mcpserver/gleichzeitig_test.go.

Aufbau des Suchindex blockierte den ganzen Workspace (behoben). Die Registry hatte nur eine Sperre für alle Projekte eines Workspace und hielt sie auch, während der Index eines Projekts aus der Datenbank aufgebaut wurde. Nach einem Neustart musste so jedes Projekt warten, bis das größte fertig eingelesen war. Seit 2026-10-06 hat jedes Projekt seine eigene Sperre. Abgesichert durch TestLangsamerAufbauBremstAndereProjekteNicht und TestAenderungWaehrendDesAufbausGehtNichtVerloren: Ein Schreibvorgang während des Aufbaus wartet, bis der Aufbau fertig ist, und trägt seinen Stand danach nach.

Stolperstein beim Messen: Mit den Vorgaben von Go hält ein HTTP-Client nur zwei Verbindungen je Ziel offen. Bei 128 Agenten öffnete der Test dadurch laufend neue Verbindungen, bis dem Mac die lokalen Ports ausgingen (connect: can't assign requested address). Das ist ein Fehler der Messung, nicht des Servers, denn echte Agenten sitzen auf verschiedenen Rechnern. Der Test nutzt deshalb einen gemeinsamen Transport mit einer offenen Verbindung je Agent. Taucht die Meldung wieder auf, liegt die Ursache dort.


Wann erneut messen

  • Nach Änderungen am Schreibweg: PutEntry, Protokoll, Suchindex, Verbindungspools in internal/store/store.go
  • Vor einem Umzug auf andere Hardware oder in einen Container mit begrenzter CPU. Dann am besten Server und Test auf getrennten Rechnern, oder den Test mit weniger Agenten.
  • Wenn AUSBAUSTUFEN.md 4.9 (mehrere Keys in einem MCP-Block) gebaut wird: Ein recall über mehrere Projekte belastet die Suche anders als heute.

Neue Ergebnisse bitte mit Datum unter Ergebnis ergänzen, die alten stehen lassen. So bleibt sichtbar, wie sich Änderungen ausgewirkt haben.