11 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:
rememberauf 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.recallmit einem von acht Fachbegriffen, die in den vorab angelegten Einträgen vorkommengetauf 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:
- Kein Aufruf ist fehlgeschlagen.
- Kein Schreibvorgang ging verloren: Jedes erfolgreiche
remembererzeugt eine Fassung. Die Summe der Fassungen aller umkämpften Einträge muss genau der Zahl der erfolgreichen Aufrufe entsprechen. - 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.md1.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.md1.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 ininternal/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.md4.9 (mehrere Keys in einem MCP-Block) gebaut wird: Einrecallü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.
Nachmessung vom 2026-10-14: Suchprotokoll und Wortvarianten
32 Agenten, 10 Sekunden, ein Projekt. Seit diesem Stand schreibt jeder recall
eine Zeile ins Suchprotokoll, und die Suche prüft Wortvarianten.
| Stand | remember je s |
recall p50 / p99 |
|---|---|---|
| 2026-10-06 (5 s) | 1.374 | 4,7 / 11 ms |
| Wortvarianten, ohne Suchprotokoll | 1.278 | 5,5 / 12 ms |
| Wortvarianten und Suchprotokoll | 1.074 | 8,5 / 30 ms |
Fast der ganze Unterschied kommt vom Suchprotokoll: Jede Suche ist jetzt auch
ein Schreibvorgang und stellt sich an derselben Schreibverbindung an wie
remember. Für echte Nutzung bedeutungslos — dort sind es Dutzende Aufrufe
pro Stunde, nicht tausend pro Sekunde.
Falls es je stört: die Zeilen in einem Puffer sammeln und alle paar hundert Millisekunden gebündelt in einer Transaktion schreiben. Dann fällt das Protokoll aus dem Weg der Anfrage.