246 lines
11 KiB
Markdown
246 lines
11 KiB
Markdown
# 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:
|
|
|
|
```sh
|
|
go test -tags last -run TestLast -v .
|
|
```
|
|
|
|
Mit eigenen Werten:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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/concurrency_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 `TestSlowBuildDoesNotBlockOtherProjects` und
|
|
`TestChangeDuringBuildIsNotLost`: 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.
|
|
|
|
---
|
|
|
|
## 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.
|