Ein einziger ChatGPT-Fehler kostete über 15 Millionen Won Umsatz
(asim.bearblog.dev)- Ein Startup, das gerade erst die Monetarisierung aktiviert hatte, erlebte eine Störung bei Abo-Zahlungen; intern ließ sie sich jedoch nicht reproduzieren, sodass sich die Ursachenanalyse um 5 Tage verzögerte
- Die Störung begann damit, dass beim Kopieren eines von ChatGPT erzeugten Prisma/TypeScript→Python/SQLAlchemy-Konvertierungsformats statt einer UUID-Erzeugungsfunktion ein hartcodierter ID-String wie ein Standardwert eingetragen wurde
- Wegen einer Architektur mit 8 AWS-ECS-Tasks und jeweils 5 Instanzen konnten Nutzer auf einen von bis zu 40 Pools eindeutiger IDs treffen; tagsüber wurde das Problem durch häufige Deployments verdeckt
- Nachts, als die Deployments stoppten, war die einzelne ID jedes Servers aufgebraucht; danach scheiterten neue Abo-Versuche wegen unique-ID-Kollisionen
- Bei 50 Beschwerden pro Tag, 5 Tagen und einem Monatspreis von 40 US-Dollar wurde der Verlust auf 10.000 US-Dollar monatlichen Umsatz geschätzt; fehlende Tests, Logging und Alerts sowie kopierter Code vergrößerten die Auswirkungen
Abo-Störung direkt nach der Monetarisierung
- Das Startup aktivierte im Mai erstmals die Monetarisierung und gewann innerhalb einer Stunde nach dem Launch den ersten Kunden
- Am nächsten Morgen hatten sich in Gmail mehr als 40 Nutzerbeschwerden angesammelt
- Nutzer konnten das Abo nicht abschließen
- Sie meldeten, dass beim Drücken des Abo-Buttons ein endloser Lade-Spinner erschien
- Das Team legte selbst ein neues Konto an und prüfte den Ablauf, konnte die Ursache intern aber nicht reproduzieren, da das Abo korrekt funktionierte
- Während der Arbeitszeit gab es kaum Beschwerden; die Störung häufte sich vor allem über Nacht
Monetarisierung unter Zeitdruck implementiert
- Im Mai begann der YC-S23-Batch, und das Team war sich nach dem Launch nicht sicher, welche Richtung die beste war
- YC-Group-Partner Dalton riet, zahlende Abonnenten als Richtungsindikator zu verwenden und den gedachten Monatspreis zu verdoppeln
- Der finale Preis wurde auf 40 US-Dollar pro Monat festgelegt
- Das Projekt war ursprünglich ein Full-Stack-NextJS-Projekt, wurde aber rund um die Monetarisierungsarbeiten auf Python/FastAPI migriert
- Für die Migration wurde ChatGPT genutzt
- Auch die Stripe-Integration wurde abgeschlossen
- In den folgenden 5 Tagen schlief das Team deutlich weniger und musste täglich 30 bis 50 Beschwerde-E-Mails bearbeiten
Von ChatGPT erzeugtes Modell-Konvertierungsformat
- Im Zuge der Backend-Migration wurden die Datenbankmodelle von Prisma/TypeScript nach Python/SQLAlchemy übertragen
- Die Modellkonvertierung war mühsam, und da ChatGPT sie nach Einschätzung des Teams gut erledigte, wurde es für fast die gesamte Migration eingesetzt
- Der generierte Code wurde kopiert, eingefügt und auf Funktion geprüft; auch in Produktion wirkte er normal, daher wurde so weitergemacht
- Zu diesem Zeitpunkt übernahm weiterhin die Next API das Einfügen in die Datenbank, während das Python-Backend die Datenbank nur las
- Mit der Implementierung der Abo-Funktion begann Python erstmals, DB-Records einzufügen
- Das neue SQLAlchemy-Modell wurde zwar direkt erstellt, übernahm aber das von ChatGPT erzeugte Format aus bestehenden Modellen unverändert
- In die ID-Erzeugung aller Modelle gelangte derselbe Fehler
Die tatsächliche Ursache und warum sie tagsüber nicht sichtbar war
- Der Kernfehler bestand darin, keinen Funktions- oder Lambda-Ausdruck zur UUID-Erzeugung zu übergeben, sondern einen einzelnen hartcodierten ID-String
- Sobald ein Nutzer auf einer bestimmten Backend-Instanz mit dieser ID ein Abo abschloss, lösten spätere Abo-Versuche auf derselben Instanz eine Kollision eindeutiger IDs aus
- Die Backend-Konfiguration verbarg dieses Problem noch länger
- Auf AWS liefen 8 ECS-Tasks
- Jeder Task führte 5 Backend-Instanzen aus
- Nutzer konnten potenziell eine von 40 verschiedenen IDs erreichen
- Tagsüber wurden täglich 10 bis 20 Commits direkt auf den main-Branch gepusht, und jedes Mal erfolgte ein neues Backend-Deployment
- Bei jedem Deployment entstanden 40 neue IDs, die Kunden verwenden konnten
- Nachts stoppten Commits und Deployments, wodurch die einzelne ID jedes Servers schnell aufgebraucht war
- Anfangs gab es noch fast 40 Server, über die ein Abo möglich war; mit der Zeit näherte sich die Zahl fast 0
Höhe des Verlusts und spätere Maßnahmen
- Der Verlust wurde mit
50 emails/day x 5 days x $40/monthberechnet und auf 10.000 US-Dollar monatlichen Umsatz geschätzt- Diese Berechnung berücksichtigt nur Nutzer, die eine Beschwerde geschickt hatten
- Um die Ursache zu finden, brauchte es 5 Tage, zahlreiche E-Mails, Hunderte Sentry-Logs, ein langes Discord-Gespräch mit einem Stripe-Engineer und die Prüfung von fünf zentralen Dateien
- Nachdem die Ursache entdeckt worden war, stellte Adam schnell einen Fix bereit
- Danach wurden robuste Unit- und Integrationstests, Alerts und Logging hinzugefügt
- Der Vorfall zeigt, dass eine kleine einzelne Zeile zu großen Umsatzverlusten führen kann, wenn menschliches Versagen, unzureichende Tests, kopierter Code und direkte Pushes auf main zusammenkommen
2 Kommentare
Äh, von KI automatisch generierten Code muss man unbedingt reviewen — warum verwendet man den einfach unverändert?
Meinungen auf Hacker News
Das fehlende Monitoring hat die 10.000 Dollar gekostet. Die App hat fortlaufend und in großer Zahl Datenbank-Exceptions geworfen, aber niemand wurde benachrichtigt.
Mit so einer Benachrichtigung wäre es keine fünftägige Untersuchung, sondern eine fünfminütige Untersuchung gewesen. Wenn sie ihr Alerting nicht behoben haben, haben sie tatsächlich gar nichts behoben.
Programmieren ist einfach, wenn alles gut läuft; schwierig ist der Teil, der Probleme behandelt.
Sobald zahlende Kunden da sind, braucht man jemanden mit Wissen und Erfahrung in Logging, Monitoring, Alerting, Security usw. Man darf DevOps nicht amateurhaft behandeln.
Aber dass es für die Datenbank kein Fehler-Logging und Alerting gibt, ist der wirklich verrückte Teil. Das ist kein 20 Jahre alter Legacy-Code, sondern ein neues Produkt, und auch kein Code aus Zeiten, in denen DB-Fehler als Datenvalidierung genutzt wurden.
Der Blogpost liefert einen 404, daher hier ein Web-Archive-Link:
https://web.archive.org/web/20240610032818/https://asim.bear...
Der Autor hat eine wichtige Ergänzung hinzugefügt: Die hier beschriebenen Praktiken seien sehr schlecht und peinlich, und man habe seitdem robuste Unit-/Integrationstests sowie Alerting/Logging ergänzt. Letztlich sei es ein menschlicher Fehler gewesen und im Rückblick eindeutig vermeidbar.
Außerdem fügte er hinzu, dass dies in den ersten Wochen des Unternehmens unter großem Zeitdruck passiert sei und man es als lustige Geschichte über einen Bug betrachten solle, der in der Produktion auf ungewöhnliche Weise reproduzierbar war.
Es war zwar ein dummer Fehler, aber Menschen machen nun einmal dumme Fehler, ob als Einzelne oder in Gruppen.
https://0912i390129ionkjan.bearblog.dev/how-a-single-chatgpt...
https://webcache.googleusercontent.com/search?q=cache%3Ahttp...
Der Fehler war sofort sichtbar. Bei allem Respekt vor dem Team: Das hat wenig mit ChatGPT zu tun, sondern eher damit, dass das Team ein Programmiermodell verwendet hat, mit dem es nicht ausreichend vertraut war.
Selbst wenn es durch ein Code-Review gekommen wäre, hätten ihn wahrscheinlich schon Monitoring-Tools gefunden, die man in fünf Minuten einrichten kann.
Ein Titel wie „Wir machten beim Einsatz eines LLM einen Programmierfehler und gaben wegen fehlender Qualitätssicherung 10.000 Dollar aus“ würde bei Führungskräften keine Reaktion auslösen wie: „Wenn ChatGPT etwas vermasselt, wie hoch ist dann unser Exposure?“ Es wird haufenweise mittlere und höhere Manager geben, die diesen Artikel auf LinkedIn posten.
Ein LLM kann keine „Fehler“ machen. Es ist nicht deterministisch, kann nicht schlussfolgern, denken oder Logik ausführen. Es ist ein sehr schicker Wortsalat-Generator, der statistische Wahrscheinlichkeiten nutzt, und es gibt keine Garantie, dass seine Ausgaben richtig oder korrekt sind; daher passt der Begriff Fehler definitionsgemäß nicht.
Update: Nachdem der Beitrag aus offensichtlichen Gründen stark downgevotet wurde, schoss sein Ranking plötzlich nach oben, was danach aussieht, als hätte ein Moderator ihn geboostet: https://hnrankings.info/40627558/
Amüsant, dass ein Clickbait-Beitrag, dessen Titel nach den Regeln hätte geändert werden müssen, von einem Moderator geboostet wurde. Und dass der Autor offenbar zu einer Y-Combinator-Firma gehört, ist sicher reiner Zufall: https://news.ycombinator.com/item?id=40629998
Für die UUID-Erzeugung des Primärschlüssels solle man nicht
str(uuid.uuid4())verwenden, sondern direkt das aufrufbareuuid.uuid4, und SQLAlchemy rufe die Funktion auf, wenn es den Wert erzeugt. Beim Datums-Default könneserver_default=text("(now())")möglicherweise nicht wie erwartet funktionieren, man sollefunc.now()verwenden; außerdem solle man die Imports vonuuidund SQLAlchemystextprüfen und für die Zeitzonenbehandlung auchDateTime(timezone=True)in Betracht ziehen.Anschließend schlug es als korrigierten Code
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), unique=True, nullable=False)vor; das hinzugefügtelambda:behebt hier das Problem.uuid.uuid4()vermutlich wie eine Schemadefinition in etwas wie Prisma verstanden. Daher überrascht mich dieser Bug an sich nicht, und ich hätte denselben Fehler machen können.Trotzdem hätte ein einziges
kubectl logsdas sofort ans Licht gebracht. Und außerdem: von Next.js und Prisma zu Python? Warum?Der Fehler an sich ist nachvollziehbar. Er sieht so aus, als könnte man ihn auch beim Schreiben von Code ohne ChatGPT relativ leicht übersehen.
Aber ich verstehe nicht, warum er nach dem ersten Fehlschlag nicht gefunden wurde. Hatte diese Firma kein Logging? Dass das Backend versucht, eine UUID wiederzuverwenden, hätte aus der Fehlermeldung sofort ersichtlich sein müssen.
Einer Funktion, die ein Callable erwartet, versehentlich einen String statt
Stringzu übergeben, ist ein häufiger Fehler. Ohne ORM wäre dieses konkrete Problem vermutlich vermieden worden, aber das ist vielleicht meine persönliche Voreingenommenheit gegenüber ORMs. Ähnliche Bugs können auch in Kontexten entstehen, die nichts mit Datenbanken zu tun haben.Leute, die großspurig behaupten, sie hätten diesen Bug mit Sicherheit gefunden, sind entweder viel bessere Engineers als ich oder, wahrscheinlicher, sie überschätzen ihre eigenen Fähigkeiten ein wenig.
Dass es aber keine Logs gab oder niemand in die Logs geschaut hat, ist wirklich schwer nachvollziehbar. Bei ECS hätte eine Duplicate-Key-Exception ohne zusätzliche Konfiguration an CloudWatch weitergereicht werden sollen; ich frage mich, ob das nicht passiert ist oder ob es passiert ist und trotzdem niemand über Nacht nachgesehen hat, welche Exceptions aufgetreten sind.
In so einer Situation ist es hilfreich zu fragen, warum die Erkennung verspätet war und warum die Diagnose so lange gedauert hat.
Ich habe denselben Fehler schon mehrfach in von Menschen geschriebenem Code gesehen. Besonders in React / TypeScript / JavaScript kommt es häufig vor, dass jemand die Lambda-Funktion vergisst.
Der Blogbeitrag erklärt die eigentliche Ursache des Problems nicht wirklich und springt gefühlt direkt dazu, ChatGPT die Schuld zu geben. Wenn man unter Zeitdruck arbeitet und große Änderungen oder Commits ohne Peer Review in
mainbringt, passiert so etwas.Das eigentliche Problem ist: Wenn man hetzt, Abkürzungen nimmt und nicht ausreichend testet oder Code Reviews durch Kolleg:innen macht, entstehen Fehler. Schon ein Test, der mehrere Anmeldeoptionen ausprobiert, hätte das vermutlich sofort gefunden.
Wenn man so jemanden in die Nähe von finanziell kritischem Code lässt, bekommt man ähnliche Probleme, und ich würde das Urteilsvermögen der Person infrage stellen, die entschieden hat, diesen Code mit kaum Tests zu deployen.
Es ist überraschend, dass es dafür keine Lint-Regel gab.
Ich hoffe, es war nicht so.
Die Stelle „Das ursprüngliche Projekt war Full-Stack NextJS, aber zuerst wollte ich alles auf Python/FastAPI migrieren“ hat mir die Augen geöffnet.
Ich weiß nicht, wie ein Startup ohne Kunden eine Neuschreibung rechtfertigt.
Unabhängig davon, ob es Kunden gibt oder nicht: Warum macht man in einem so frühen Stadium im Grunde eine horizontale Verschiebung von Node zu Python? Wenn es Hunderte Kunden gäbe und man auf etwas wie Go wechseln wollte, könnte ich es vielleicht eher verstehen, aber selbst das wäre weiterhin fragwürdig.
Zum Beispiel muss man massenhaft DTO-Objekte erstellen, aber AutoMapper funktioniert mit der von mir verwendeten Versionskombination und Projektkonfiguration nicht, und Entity Framework sowie JSON-Serialisierung/-Deserialisierung verursachen mehr Schmerz, als sie Nutzen bringen.
Natürlich kann man das schrittweise lösen: tief in die Dokumentation einsteigen, Hacks einbauen, Pakete upgraden und die Konfiguration neu schreiben. Aber als Mensch möchte man irgendwann metaphorisch den Benzinkanister holen, alles abfackeln und das zweite System besser machen. Natürlich wird es in Wirklichkeit nicht besser, sondern bekommt nur andere Schmerzpunkte und kann möglicherweise nicht einmal alles, was das erste System konnte, oder nicht richtig.
Auch bei der Arbeit habe ich jedes Mal denselben Impuls, wenn ich Legacy- oder umständliche Systeme sehe. Es braucht aktive, kontinuierliche Anstrengung, um das Gehirn zu überstimmen, das nach einer Neuschreibung schreit. Manchmal gehen Architekturänderungen wie eine Neuschreibung oder die Einführung von Containern gut aus, aber meistens führen sie direkt ins Feuer oder zu endloser Arbeit.
Wenn ich nicht sehr sicher bin, dass es den Betrieb des Systems oder die Developer Experience anderer Entwickler verbessert, bin ich froh, diesem Impuls nicht nachzugeben.
ChatGPT war eher der Teil, der das Geld für die App überhaupt eingebracht hat. Ohne ChatGPT hätten sie nicht die Fähigkeit gehabt, sie umzusetzen.
Der Mangel an Fähigkeit beim Coding, Debugging, Logging und Monitoring hat die 10.000 Dollar gekostet; in dieser Geschichte ist der Nettoeffekt von ChatGPT positiv.
In jeder Commit-Message steckt ein Emoji. Affen, Bananen, Raketen, Feuerwerk – es gibt nichts, was nicht dabei wäre.
https://grook.ai/share?id=e269e88a7b1a71eff4f176c864b30161&x...
Es heißt, das ursprüngliche Projekt sei Full-Stack NextJS gewesen, und beim Migrieren des Backends auf Python/FastAPI hätten sie das Datenbankmodell aus Prisma/Typescript nach Python/SQLAlchemy übersetzt. Diese Arbeit sei langweilig gewesen, und weil ChatGPT darin ziemlich gut zu sein schien, hätten sie es für fast die gesamte Migration genutzt.
Wenn es ChatGPT von Anfang an nicht gegeben hätte, hätten sie diese vorgelagerte Migration wohl nicht versucht; daher ist es schwer, den Nettoeffekt als positiv zu sehen. Der bestehende Stack hatte vielleicht besseres Error Logging, vielleicht auch nicht, und weil sie den Code selbst geschrieben hatten, kannten sie seine Struktur möglicherweise besser und brauchten es weniger.
Die Entscheidung, „den gesamten Code ein zweites Mal neu zu schreiben“, bevor man Monetarisierung einschaltet, ist an sich schon interessant.
Es gibt die Formulierung: „Ich möchte zuerst sagen, dass die Praktiken hier schlecht waren und vermeidbar gewesen wären. Das geschah in einer anderen Phase mit großem Zeitdruck. Bitte lest es mit diesem Kontext.“
Wegen solcher Rahmenbedingungen machen mir Software-Abos Angst.
Wegen Race Conditions haben wir Nutzer auch schon doppelt abgerechnet. Deshalb werde ich bei Timeouts oder Fehlern rund um Geld paranoid: Ich gehe erst einmal davon aus, dass die Zahlung erfolgt ist, und prüfe später noch einmal nach.
Code in TypeScript und Python, ein Framework wie Next.js, acht AWS-Tasks mit jeweils fünf laufenden Instanzen, und dabei 40 Dollar Umsatz und nur ein paar Wochen Entwicklungszeit?
Da fragt man sich wirklich, was da los war. Es ist schon schlimm genug, das mit Zeitdruck zu entschuldigen, weil der Code deshalb ein Chaos gewesen sei; noch schlimmer ist aber, dass dann Zeit in Refactoring über Sprachgrenzen hinweg und in den Aufbau eines grundlosen verteilten Systems gesteckt wurde.
Das ist selbstverletzende Komplexität: gleichzeitig Features und völlig absurde technische Komplexität jonglieren. Keine Ahnung, was man sich dabei gedacht hat.
Korrektur: Das ist ein Unternehmen aus YC Sommer 2023, aber offenbar steht das Produkt auch im Sommer 2024 noch hinter einer Warteliste. Vermutlich, weil es gerade in Rust neu geschrieben wird.
Ich habe wahrscheinlich insgesamt nicht einmal 1.000 Zeilen Python geschrieben, aber das Problem wurde treffend erkannt.
Python hat den Fehler, die Auswertungsstrategie von Common Lisp nicht richtig übernommen zu haben. Wenn es bei den Default-Ausdrücken optionaler Funktionsargumente ein Argument wie
foo=obj.whatever()gibt, wirdobj.whatever()nicht beim Funktionsaufruf ausgewertet, sondern zu dem Zeitpunkt, an dem die Funktionsdefinition verarbeitet wird.Ich vermute, dass das aus Effizienzgründen absichtlich so gemacht wurde. Python hat noch einen weiteren Fehler: Es gibt keine echte Literal-Syntax für gängige Objekte wie Listen.
[1, 2, 3]ist kein Literal, sondern eher ein Konstruktor; bei jeder Auswertung muss eine neue Liste erzeugt und mit Werten befüllt werden.Die Designer wollten wohl nicht, dass ein Parameter wie
list=[]jedes Mal eine neue leere Liste erzeugt, wenn das Argument weggelassen wird. In Lisp sind'(1 2 3)und'()echte Literale und verweisen bei jeder Referenz auf dasselbe Objekt. Programmierer können wählen, ob sie als Default-Ausdruck(list 1 2 3)oder'(1 2 3)verwenden.Ersteres erzeugt wie
[1, 2, 3]jedes Mal ein neues veränderbares Objekt, Letzteres gibt mit ziemlicher Sicherheit dasselbe Objekt zurück und lässt sich zuverlässig und portabel nicht verändern. Moderne populäre Sprachen haben die meisten Funktionen von Lisp übernommen; das wirkt wie ein Witz nach dem Motto, man habe dabei nichts zu verlieren.foo=obj.whatever()obj.whatever()nicht beim Funktionsaufruf, sondern bei der Verarbeitung der Funktionsdefinition ausgewertet wird, kann so kaum stimmen.Ich weiß nicht, was passieren soll, wenn
.whatever()von internem Zustand abhängt, der sich nach der Objektinitialisierung ändert.