Die Einführung einer neuen Feature-Version steht oft unter Zeitdruck. Doch als die Entwickler von Autentico die Version 2.0 ihres OAuth-2.0- und OpenID-Connect-Identitätsproviders vorbereiteten, nahmen sie sich bewusst Zeit für Performance-Tests. Was sie erwartete, war eine Routineprüfung – doch stattdessen stürzten sie sich in eine mehrtägige Reise durch Profiling, Architektur-Experimente und überraschende Erkenntnisse über die wahren Grenzen von SQLite.
Autentico ist ein schlanker Identitätsprovider, der vollständig in Go implementiert ist und auf SQLite als einzige Datenbank setzt. Das Besondere: Eine einzelne Binärdatei, eine Datenbankdatei, keine externen Abhängigkeiten. Doch genau diese Einfachheit wurde zum Prüfstein, als die Entwickler die Performance unter Last testeten. Ihr Benchmark simulierte einen vollständigen OAuth-2.0-Autorisierungsablauf mit PKCE: Autorisierung, Passwortanmeldung, Token-Austausch, Token-Introspektion und Token-Aktualisierung. Fünf HTTP-Anfragen pro Iteration, vier bis fünf SQLite-Schreiboperationen und eine bcrypt-Passwortprüfung.
Die falsche Maschine, das falsche Problem
Die ersten Tests fanden auf einem älteren Laptop mit Intel i5-Prozessor statt. 100 virtuelle Nutzer, 30 Sekunden Last – ein überschaubares Szenario. Die Ergebnisse waren akzeptabel, aber nicht optimal. Doch statt direkt nach Optimierungen zu suchen, griffen die Entwickler zum Profiling-Tool. Was sie fanden, war eine Überraschung: 90 % der CPU-Zeit wurde von der Funktion bcrypt.CompareHashAndPassword verbraucht.
Diese Funktion ist bewusst langsam konstruiert – genau das macht bcrypt für Passwort-Hashes so sicher. Doch unter Last wurde sie zum Flaschenhals. SQLite-Schreiboperationen benötigten nur Mikrosekunden, JWT-Signierung war vernachlässigbar, HTTP-Routing unsichtbar. Der Engpass war eindeutig: bcrypt.
Die naheliegende Schlussfolgerung: bcrypt lässt sich nicht beschleunigen. Die einzige Option schien zu sein, die Verarbeitung zu parallelisieren. Doch SQLite ist ein Single-Writer-System – eine klassische horizontale Skalierung war hier nicht möglich.
Verifico: Ein experimenteller Lösungsansatz
Der Engpass lag nicht in der Datenbank, sondern in einer einzigen Funktion. Warum also nicht genau diese Funktion skalieren? Die Entwickler durchleuchteten verschiedene Optionen:
- CQRS mit SQLite-Replikation: Tools wie LiteFS ermöglichen die Replikation von SQLite über mehrere Knoten, wobei ein Primär-Knoten für Schreiboperationen und Replikate für Leseoperationen zuständig sind. Doch dies löste ein allgemeines Skalierungsproblem, nicht das spezifische bcrypt-Problem.
- PostgreSQL: Die Standardlösung für Datenbank-Skalierung. Doch PostgreSQL löst nicht das Problem der CPU-lastigen bcrypt-Operation. Selbst mit mehreren Anwendungsservern hinter einem Load Balancer würde die Last auf die bcrypt-Funktion zurückfallen.
- Kindprozesse: Separate Prozesse für bcrypt. Doch Go nutzt bereits die Goroutines und den Scheduler für die Parallelisierung auf einem einzelnen System. Separate Prozesse würden nur zusätzlichen IPC-Overhead verursachen.
- Sticky Sessions: Nutzer würden auf bestimmte Instanzen verteilt. Doch dies erfordert eine gemeinsame Lookup-Tabelle, die wiederum eine gemeinsame Datenbank benötigt – ein Teufelskreis.
Dann kam die zündende Idee: Autentico bleibt als einzelne Instanz erhalten, verwaltet die Datenbank und übernimmt alle anderen Aufgaben. Doch für die Passwortprüfung sendet es den Hash und das Passwort an einen externen Worker. Dieser Worker führt bcrypt aus und gibt das Ergebnis zurück. Die Worker sind zustandslos, einfach zu implementieren und können auf preiswerter Hardware laufen.
Das System erhielt den Namen Verifico – angelehnt an die italienische Bedeutung von "ich verifiziere", passend zum Namensschema von Autentico. Ein neuer Subbefehl autentico verifico start ermöglichte den Start der Worker. Ein HTTP-Endpunkt, eine Funktion, ein gemeinsames Geheimnis für die Authentifizierung und eine Round-Robin-Load-Balancing-Strategie mit automatischem Fallback auf lokale bcrypt-Verarbeitung bei Ausfall der Worker.
Die Sicherheitsarchitektur durchlief mehrere Iterationen: von mTLS (zu komplex für ein einfaches Boolean-Endpunkt) über AES-Verschlüsselung (eine schlechte TLS-Nachbildung) bis hin zu einem gemeinsamen Geheimnis innerhalb eines privaten Netzwerks. Schließlich wurde klar: Das Passwort durchläuft bereits das öffentliche Internet, um Autentico zu erreichen. Ein zusätzlicher Hop innerhalb eines VPC ist kein Sicherheitsrisiko.
Der erste Erfolg – und die Ernüchterung
Die ersten Tests auf dem i5-Prozessor zeigten deutliche Verbesserungen. Bei einer Beschränkung des Servers auf zwei Kerne und der Verteilung der bcrypt-Aufgaben auf Worker sanken die Latenzzeiten nicht-lognender Endpunkte von mehreren Sekunden auf wenige Millisekunden. Die Kerne des Servers waren nun frei für HTTP-Handling, SQLite-Abfragen und JWT-Signierung. Die Durchsatzrate skalierte linear mit der Anzahl der Worker, bis etwa sechs Kerne erreicht waren. Bei acht Kernen flachte die Kurve ab.
Die Entwickler waren zufrieden. Eine saubere Lösung, Benchmarks, die funktionierten. Zeit für den Rollout.
Doch dann kam der nächste Test – diesmal auf einem modernen Ryzen-7-Desktop mit 16 Kernen und höherer Single-Thread-Leistung.
Der Ryzen-Effekt: Warum mehr Kerne nicht immer helfen
Die Entwickler beschränkten Autentico auf zwei Kerne und starteten Worker mit jeweils zwei Kernen: 2+2, 2+4, 2+6, bis hin zu 2+14 Kernen. Die Ergebnisse waren ernüchternd:
- 2 Server-Kerne + 2 Worker-Kerne: 15,4 Anfragen/Sekunde, Login-P95 bei 3,61 Sekunden
- 2 Server-Kerne + 4 Worker-Kerne: 15,4 Anfragen/Sekunde, Login-P95 bei 3,68 Sekunden
- 2 Server-Kerne + 6 Worker-Kerne: 15,2 Anfragen/Sekunde, Login-P95 bei 3,58 Sekunden
- 2 Server-Kerne + 10 Worker-Kerne: 15,0 Anfragen/Sekunde, Login-P95 bei 3,60 Sekunden
- 2 Server-Kerne + 14 Worker-Kerne: 14,7 Anfragen/Sekunde, Login-P95 bei 3,76 Sekunden
Die Durchsatzrate blieb nahezu konstant. Die Hinzunahme weiterer Worker-Kerne brachte keine Verbesserung. Der Grund: Der Ryzen-7-Prozessor war einfach schneller in der bcrypt-Verarbeitung. Selbst bei der Standard-Kostenstufe von 10 konnte jeder Kern die Passwort-Hashes so schnell verarbeiten, dass bcrypt nicht mehr der Engpass war. Die wahre Blockade lag woanders.
Die Suche nach der echten Ursache
Die Entwickler griffen erneut zum Profiling-Tool, diesmal auf dem Ryzen-System. Ein Block-Profiling unter Last zeigte ein klares Bild: Jeder Engpasspunkt lag bei database/sql.(*DB).conn. Goroutines warteten auf eine Verbindung aus dem Pool. Nicht der Dateisperre von SQLite, nicht der I/O-Last – der Go-Verbindungspool.
- 65 % der Kontention entfielen auf Leseoperationen
- 35 % auf Schreiboperationen
Die Hauptverursacher waren Routineoperationen wie das Nachschlagen eines Clients anhand seiner ID, das Erstellen einer Session oder das Erstellen eines Tokens. Schnelle Abfragen, die in der Warteschlange feststeckten.
Die Lösung: WAL-Modus als Game-Changer
SQLite nutzt standardmäßig ein Rollback-Journal, das die gesamte Datenbank während Schreiboperationen sperrt und alle Leseoperationen blockiert. Die Lösung? WAL (Write-Ahead Logging), ein Modus, der Schreiboperationen von Leseoperationen entkoppelt.
Nach dem Wechsel zu WAL-Modus verschwanden die Engpässe im Verbindungspool. Die Goroutines mussten nicht mehr auf Datenbankverbindungen warten, da die Leseoperationen nicht mehr von Schreiboperationen blockiert wurden. Die Performance verbesserte sich spürbar – ohne komplexe Architekturänderungen, ohne zusätzliche Hardware.
Fazit: Die einfachen Lösungen sind oft die besten
Die Geschichte von Autentico zeigt, wie wichtig es ist, vor komplexen Architekturänderungen die Grundlagen zu prüfen. Ein vermeintlicher Engpass – bcrypt – entpuppte sich als hardwareabhängig. Die eigentliche Lösung lag in einer unscheinbaren SQLite-Einstellung: WAL-Modus.
Manchmal lohnt es sich, zuerst die einfachen Optimierungen auszuschöpfen, bevor man sich in experimentelle Lösungen stürzt. Die Performance-Story von Autentico ist ein Plädoyer für gründliches Profiling und die Bereitschaft, eigene Annahmen zu hinterfragen.
KI-Zusammenfassung
Autentico 2.0’nin Go ve SQLite ile geliştirilen OAuth 2.0 sisteminde performans sorunları nasıl çözüldü? Bcrypt darboğazından WAL moduna kadar detaylar burada.
Tags