API-Änderungen

Protobuf-Breaking-Changes: was auf dem Wire überlebt

6 Min. Lesezeit

Eine REST-API ändert sich, wenn sich eine JSON-Form ändert, und das meiste davon ist sichtbar in der Antwort, die man im Browser lesen kann. Eine gRPC-API ändert sich, wenn sich eine .proto-Datei ändert, und das binäre Wire-Format von Protocol Buffers hat eigene Regeln dafür, was ein Client tolerieren kann, die nichts mit den Feldnamen zu tun haben. Zwei Änderungen, die im Diff gleich klein aussehen, ein Feld umnummerieren gegen eines hinzufügen, landen auf entgegengesetzten Seiten einer Linie, die Breaking Changes allgemein zieht: Die eine ist für jeden bestehenden Client unsichtbar, die andere bricht sie alle auf einmal. Protobuf-Breaking-Changes von sicheren Änderungen zu unterscheiden heißt, die eigenen Regeln des Wire-Formats zu lesen, nicht zu raten, wie sich die Änderung in einem .proto-Diff liest.

Warum zählt die Feldnummer in Protobuf mehr als der Feldname?

Weil das Wire-Format Felder nach Nummer kodiert, nicht nach Namen. Der generierte Code in jeder Sprache liest und schreibt diese Nummern; der Feldname email in eurer .proto-Datei ist eine Annehmlichkeit für Menschen, die die binären Bytes, die übers Netzwerk gehen, nie berührt. Ein Feld umzubenennen, email zu email_address, ist auf dem binären Wire sicher, solange die Nummer gleich bleibt, was Ingenieurinnen überrascht, die von REST kommen, wo ein umbenannter JSON-Key genau die Art Änderung ist, die einen Client bricht. Die Ausnahme ist derselbe REST-Fall: Die ProtoJSON- und Textformate serialisieren den Namen, also bricht eine Umbenennung JSON-Transcoding (etwa ein grpc-gateway), Textformat-Dateien und Field Masks. Dasselbe Feld umzunummerieren, den Namen zu behalten aber 1 in 7 zu ändern, ist genau umgekehrt: unsichtbar in einem Code-Review, das nur Namen zeigt, und es korrumpiert jede Nachricht, die ein Client ab diesem Punkt sendet oder empfängt.

ÄnderungSicher auf dem WireWarum
Feld umbenennen, Nummer behaltenBinär ja, JSON und Text neinDie Binärkodierung nutzt die Nummer; ProtoJSON und das Textformat nutzen den Namen
Nummer eines Felds ändernNeinJede bestehende Nachricht wird jetzt als falsches Feld gelesen
Neues Feld mit neuer Nummer hinzufügenJaAlte Clients ignorieren Felder, die sie nicht kennen
Feld entfernen, alte Nummer für etwas anderes wiederverwendenNeinAlte Daten dekodieren in das falsche neue Feld
Typ eines Felds inkompatibel ändern (z. B. int32 zu string)NeinDie Wire-Kodierung unterscheidet sich je Typ

Was macht das Entfernen eines Felds anders als in einer REST-JSON-Antwort?

Die Nummer wird radioaktiv. Protobufs eigene Anleitung empfiehlt, die Nummer eines entfernten Felds als reserved zu markieren, statt sie wiederverwenden zu lassen, weil die Wiederverwendung ist, wo der eigentliche Schaden passiert: Ein Client, der noch generierten Code vom letzten Monat läuft, sendet eine Nachricht mit der alten Nummer des Felds für die alte Bedeutung, und der Server, der jetzt erwartet, dass diese Nummer etwas anderes bedeutet, interpretiert die Daten lautlos falsch, statt sie rundweg abzulehnen. REST hat keine vergleichbare Falle, weil ein entfernter JSON-Key einfach aufhört zu erscheinen; es gibt keine Möglichkeit, dass die Anfrage eines alten Clients still als etwas anderes uminterpretiert wird. Eine .proto-Datei mit reserved 4, 9, 12; oben in einer Message ist eine dauerhafte Narbe, und das ist der Sinn: Sie verhindert, dass die Nummer an ein neues Feld vergeben wird von jemandem, der ihre Geschichte nicht kannte.

message Invoice {
  reserved 4; // war `legacy_customer_id`, entfernt 2026-06-01
  reserved "legacy_customer_id"; // auch den Namen, für JSON/Text
  string customer_id = 5;
  string status = 6;
}

Braucht das Hinzufügen eines Felds überhaupt einen Changelog-Eintrag?

Meist keinen Breaking-Change-Eintrag, aber oft einen normalen, weil „sicher auf dem Wire” und „unsichtbar für eine Leserin, der es wichtig ist” zwei verschiedene Aussagen sind. Ein Feld zu einer Antwort-Message hinzuzufügen kostet strukturell nichts, alte Clients dekodieren die Nachricht und ignorieren das neue Feld automatisch. Aber eine Aufruferin, die eine neue Integration gegen diesen Service baut, hat keine Möglichkeit zu wissen, dass das Feld existiert, außer jemand sagt es ihr, weil nichts an einem erfolgreichen Build oder einem bestandenen Test ein neues optionales Feld sichtbar macht. API-Changelog behandelt allgemein, was ein additiver Eintrag Leserinnen schuldet; der gRPC-spezifische Grund, trotzdem einen zu schreiben, ist, dass es kein Äquivalent zum Durchsuchen einer REST-Antwort in einem Debugger gibt, um zu bemerken, dass ein neuer Key aufgetaucht ist.

Wie unterscheidet sich das von dem, womit GraphQL-Aufruferinnen umgehen müssen?

Die Regeln für Ergänzungen sind dieselben, aber das Risiko ist ein anderes. GraphQL-Schema-Abkündigung behandelt ein Modell, bei dem ein Client nur Felder erhält, die er explizit anfragt, was additive Änderungen im Wesentlichen risikofrei macht und Entfernungen zur einzigen echten Gefahr. gRPC- Clients dagegen erhalten alles, was der Server sendet, und dekodieren alles gegen ihre eigene kompilierte Kopie des Schemas; die Exposition eines Clients ist nicht durch das begrenzt, was er angefragt hat, sondern nur durch das, was sein generierter Code lesen kann. Dieser Unterschied zählt beim Schreiben von Changelogs: Ein GraphQL-Eintrag kann vernünftigerweise annehmen, dass Clients vor Feldern geschützt sind, die sie nicht angefragt haben, und ein gRPC-Eintrag kann diese Annahme überhaupt nicht treffen.

Funktioniert das Versionieren eines gRPC-Service genauso wie RESTs /v1/, /v2/?

Der Mechanismus ist anders, selbst wenn die Absicht dieselbe ist. Was sind v1 und v2 in einer REST-API behandelt Versionierung als parallele URL-Pfade, die verschiedene Verträge bedienen; gRPC-Services versionieren typischerweise über den Paketnamen in der .proto-Datei selbst, payments.v1.InvoiceService wird zu payments.v2.InvoiceService, was den vollqualifizierten Servicenamen ändert, den eine Aufruferin anwählt, statt ein URL-Segment, das sie anfragt. Beide Ansätze lösen dasselbe Problem, einen alten Vertrag weiterlaufen zu lassen, während ein neuer existiert, aber ein Team aus einem REST- Hintergrund sucht oft an der falschen Stelle nach einer Versionsnummer und übersieht, dass die Paketdeklaration diese Aufgabe übernimmt.

Was sollte ein gRPC-Changelog-Eintrag tatsächlich benennen?

Die Message, die Feldnummer und ob es sich um eine Erweiterung oder eine Entfernung handelt, die Migration erfordert, in dieser Reihenfolge der Wichtigkeit für eine Leserin, die entscheidet, ob sie handeln muss. „shipping_address (Feld 8) zu Order hinzugefügt” sagt einer Integratorin alles Nötige, um generierten Code zu aktualisieren und es zu nutzen. „Feld 4 auf Invoice reserviert, legacy_customer_id ist weg” sagt ihr, zu prüfen, ob irgendetwas in ihrer Codebasis dieses Feld noch liest, was eine REST-artige Notiz „ein Feld aus der Antwort entfernt” nicht mit derselben Dringlichkeit vermittelt, weil REST-Entfernungen einfach weniger Daten zurückgeben, während Protobuf-Feldwiederverwendung sie aktiv korrumpiert.

FAQ

Kann der Typ eines Felds je geändert werden, ohne das Wire-Format zu brechen? Nur innerhalb bestimmter kompatibler Gruppen, die Protobuf dokumentiert, wie das Erweitern von int32 zu int64 in manchen Fällen. Behandelt jede Typänderung als brechend, sofern ihr sie nicht gegen Protobufs eigene Kompatibilitätstabelle geprüft habt; Kompatibilität durch Analogie zum Typsystem einer Sprache anzunehmen ist, wie es hier schiefgeht.

Funktioniert das Abkündigen eines Felds in Protobuf wie GraphQLs @deprecated-Direktive? Ähnlich: Protobuf unterstützt eine [deprecated = true]-Feldoption, die Tooling anzeigen kann. Keins von beiden wird durchgesetzt: Ein GraphQL-Server beantwortet eine Query auf ein abgekündigtes Feld weiterhin, und ein Protobuf-Client kodiert eines weiterhin. Beide sind rein beratend und brauchen dieselbe Changelog-Absicherung.

Ist Umnummerieren je sicher, wenn man jeden Client kontrolliert? In einem vollständig geschlossenen System im Prinzip, aber es entfernt die gesamte Sicherheitseigenschaft, für die Feldnummern existieren, und „wir kontrollieren jeden Client” ist eine Behauptung, die aufhört wahr zu sein, sobald ein Build gecacht wird, ein Deploy verzögert wird oder ein Client hinzugefügt wird, an den sich niemand erinnert. Reserviert die Nummer, anstatt sie wiederzuverwenden, auch intern.

Brauchen gRPC-Services eine Changelog-Seite wie eine öffentliche REST-API? Nur wenn externe Teams sie konsumieren, ohne .proto-Diffs direkt zu lesen, derselbe Test „wer sitzt am anderen Ende”, den interne API-Changelogs allgemein anwenden. Ein gRPC-Service, den nur andere Services des eigenen Teams konsumieren, kann einen formalen Changelog oft zugunsten der Commit-Historie überspringen, weil jeder, der sie liest, das Schema schon offen hat.


Die technischen Aussagen in diesem Artikel wurden nicht unabhängig geprüft. Wenn etwas nicht stimmt, sagen Sie es uns, und wir korrigieren es.

Mehr bei changeloop: Entwicklerdokumentation, Changelog-Tools im Vergleich

changeloop
Das Team hinter einem Changelog, das den Kreis schließt. Ihre Nutzer fragen, Ihr Team liefert, und wer gefragt hat, erfährt davon.