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.
| Änderung | Sicher auf dem Wire | Warum |
|---|---|---|
| Feld umbenennen, Nummer behalten | Binär ja, JSON und Text nein | Die Binärkodierung nutzt die Nummer; ProtoJSON und das Textformat nutzen den Namen |
| Nummer eines Felds ändern | Nein | Jede bestehende Nachricht wird jetzt als falsches Feld gelesen |
| Neues Feld mit neuer Nummer hinzufügen | Ja | Alte Clients ignorieren Felder, die sie nicht kennen |
| Feld entfernen, alte Nummer für etwas anderes wiederverwenden | Nein | Alte Daten dekodieren in das falsche neue Feld |
Typ eines Felds inkompatibel ändern (z. B. int32 zu string) | Nein | Die 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.