Modificări de API

Schimbări incompatibile în Protobuf: ce rezistă pe wire

6 min de citit

Un API REST se schimbă când se schimbă o formă JSON, și cea mai mare parte din acea formă e vizibilă în răspunsul pe care îl puteți citi într-un browser. Un API gRPC se schimbă când se schimbă un fișier .proto, iar formatul binar pe wire al Protocol Buffers are propriile reguli despre ce poate tolera un client, care n-au nimic de-a face cu ce spun numele câmpurilor. Două editări care arată la fel de mici într-un diff, renumerotarea unui câmp versus adăugarea unuia, cad pe părți opuse ale unei linii pe care schimbări care rup compatibilitatea o trasează în general: una e invizibilă pentru fiecare client existent, cealaltă le rupe pe toate deodată. Deosebirea schimbărilor incompatibile Protobuf de cele sigure înseamnă citirea regulilor proprii ale formatului pe wire, nu ghicirea din felul în care arată schimbarea într-un diff .proto.

De ce contează numerotarea câmpurilor mai mult decât numele câmpului în Protobuf?

Pentru că formatul pe wire codifică câmpurile după număr, nu după nume. Codul generat în fiecare limbaj citește și scrie acele numere; numele de câmp email din fișierul vostru .proto e o comoditate pentru oameni care nu atinge niciodată octeții binari trimiși prin rețea. Redenumirea unui câmp, email în email_address, e sigură pe wire-ul binar atât timp cât numărul rămâne același, ceea ce surprinde inginerele obișnuite cu REST, unde o cheie JSON redenumită e exact tipul de schimbare care rupe un client. Excepția e chiar cazul din REST: ProtoJSON și formatul text serializează numele, deci o redenumire rupe transcodarea JSON (un grpc-gateway, de exemplu), fișierele în format text și field masks. Renumerotarea aceluiași câmp, păstrând numele dar schimbând 1 în 7, e exact opusul: invizibilă într-un code review care arată doar nume, și corupe fiecare mesaj pe care un client îl trimite sau primește de la acel punct încolo.

SchimbareSigură pe wireDe ce
Redenumirea unui câmp, păstrarea număruluiBinar da, JSON și text nuCodificarea binară folosește numărul; ProtoJSON și formatul text folosesc numele
Schimbarea numărului unui câmpNuFiecare mesaj existent e acum citit ca și câmpul greșit
Adăugarea unui câmp nou cu un număr nouDaClienții vechi ignoră câmpurile pe care nu le recunosc
Eliminarea unui câmp, refolosirea numărului vechi pentru altcevaNuDatele vechi se decodifică în câmpul nou greșit
Schimbarea incompatibilă a tipului unui câmp (ex. int32 în string)NuCodificarea pe wire diferă în funcție de tip

Ce face eliminarea unui câmp diferită față de a o face într-un răspuns JSON REST?

Numărul devine radioactiv. Îndrumarea oficială a Protobuf recomandă marcarea numărului unui câmp eliminat ca reserved în loc să-l lăsați refolosit, pentru că refolosirea e unde se întâmplă adevărata pagubă: un client care încă rulează cod generat de luna trecută trimite un mesaj folosind numărul vechi al câmpului pentru înțelesul vechi, iar serverul, care acum se așteaptă ca acel număr să însemne altceva, interpretează greșit datele în tăcere în loc să le respingă direct. REST n-are o capcană echivalentă, pentru că o cheie JSON eliminată pur și simplu încetează să mai apară; nu există nicio cale ca cererea unui client vechi să fie reinterpretată tacit ca altceva. Un fișier .proto cu reserved 4, 9, 12; la începutul unui mesaj e o cicatrice permanentă, și acesta e scopul: oprește ca numărul să fie dat unui câmp nou de cineva care nu-i știa istoria.

message Invoice {
  reserved 4; // era `legacy_customer_id`, eliminat 2026-06-01
  reserved "legacy_customer_id"; // și numele, pentru JSON/text
  string customer_id = 5;
  string status = 6;
}

Adăugarea unui câmp ajunge vreodată să necesite o intrare de changelog?

De obicei nu o intrare de schimbare care rupe compatibilitatea, dar adesea una normală, pentru că “sigur pe wire” și “invizibil pentru o cititoare căreia îi pasă” sunt două afirmații diferite. Adăugarea unui câmp la un mesaj de răspuns nu costă nimic structural, clienții vechi decodifică mesajul și ignoră automat câmpul nou. Dar cineva care construiește o integrare nouă împotriva acelui serviciu n-are nicio cale să afle că câmpul există decât dacă îi spune cineva, pentru că nimic dintr-un build reușit sau un test care trece nu face vizibil un câmp opțional nou. Changelog de API acoperă în general ce datorează o intrare aditivă cititoarelor; motivul specific gRPC pentru a scrie una oricum e că nu există un echivalent al răsfoirii unui răspuns REST într-un debugger pentru a observa că a apărut o cheie nouă.

Prin ce diferă asta de ceea ce înfruntă apelanții GraphQL?

Regulile pentru adăugări sunt aceleași, dar expunerea diferă. Deprecierea schemei GraphQL acoperă un model în care un client primește doar câmpurile pe care le cere explicit, ceea ce face schimbările aditive esențial fără risc și eliminările singurul pericol real. Clienții gRPC, în schimb, primesc orice trimite serverul și decodifică totul împotriva propriei copii compilate a schemei; expunerea unui client nu e limitată de ce a cerut, doar de ce știe codul lui generat să citească. Această diferență contează pentru scrierea changelog-urilor: o intrare GraphQL poate presupune în mod rezonabil că clienții sunt protejați de câmpurile pe care nu le-au cerut, iar o intrare gRPC nu poate presupune asta deloc.

Versionarea unui serviciu gRPC funcționează la fel ca /v1/, /v2/ din REST?

Mecanismul e diferit chiar și când intenția e aceeași. Ce sunt v1 și v2 într-un API REST acoperă versionarea ca căi URL paralele care servesc contracte diferite; serviciile gRPC de obicei se versionează prin numele pachetului chiar în fișierul .proto, payments.v1.InvoiceService devenind payments.v2.InvoiceService, ceea ce schimbă numele de serviciu complet calificat pe care îl apelează un client, în loc de un segment URL pe care îl cere. Ambele abordări rezolvă aceeași problemă, lăsând un contract vechi să continue să funcționeze cât timp există unul nou, dar o echipă venită dintr-un background REST caută adesea un număr de versiune în locul greșit și pierde din vedere că declarația pachetului face acea treabă.

Ce ar trebui să numească de fapt o intrare de changelog gRPC?

Mesajul, numărul câmpului, și dacă e aditivă sau o eliminare care necesită migrare, în această ordine de importanță pentru o cititoare care decide dacă acționează. “Adăugat shipping_address (câmpul 8) la Order” îi spune unei integratoare tot ce e necesar pentru a actualiza codul generat și a începe să-l folosească. “Rezervat câmpul 4 pe Invoice, legacy_customer_id a dispărut” îi spune să verifice dacă ceva din baza ei de cod mai citește acel câmp, ceva ce o notă în stil REST “eliminat un câmp din răspuns” nu comunică cu aceeași urgență, pentru că eliminările REST pur și simplu returnează mai puține date în timp ce refolosirea câmpurilor Protobuf le corupe activ.

FAQ

Poate fi vreodată schimbat tipul unui câmp fără a rupe formatul pe wire? Doar în grupuri compatibile specifice pe care le documentează Protobuf, cum ar fi lărgirea int32 la int64 în unele cazuri. Tratați orice schimbare de tip ca rupătoare de compatibilitate decât dacă ați verificat-o împotriva propriei tabele de compatibilitate a Protobuf; presupunerea compatibilității prin analogie cu sistemul de tipuri al unui limbaj e cum merge asta prost.

Deprecierea unui câmp în Protobuf funcționează ca directiva @deprecated din GraphQL? Similar: Protobuf suportă o opțiune de câmp [deprecated = true] pe care uneltele o pot afișa. Niciuna nu e impusă: un server GraphQL tot răspunde la o interogare pentru un câmp depreciat, iar un client protobuf tot îl codifică. Ambele sunt consultative și au nevoie de același sprijin de changelog.

Renumerotarea e vreodată sigură dacă controlați fiecare client? Într-un sistem complet închis, în principiu, dar elimină întreaga proprietate de siguranță pentru care există numerele de câmp, iar “controlăm fiecare client” e o afirmație care încetează să fie adevărată în momentul în care un build ajunge în cache, un deploy e întârziat, sau e adăugat un client de care nimeni nu-și amintea. Rezervați numărul în loc să-l refolosiți, chiar și intern.

Serviciile gRPC au nevoie de o pagină de changelog ca un API REST public? Doar dacă echipe externe le consumă fără să citească diff-urile .proto direct, același test “cine e de partea cealaltă” pe care îl aplică în general changelog-urile de API intern. Un serviciu gRPC consumat doar de alte servicii ale aceleiași echipe poate adesea sări peste un changelog formal în favoarea istoricului de commit-uri, pentru că oricine îl citește are deja schema deschisă.


Afirmațiile tehnice din acest articol nu au fost verificate independent. Dacă ceva nu e corect, spune-ne și vom corecta.

Pe changeloop: Documentație pentru dezvoltatori, Comparație instrumente changelog

changeloop
Echipa care construiește un changelog care închide bucla. Utilizatorii cer ceva, echipa ta livrează, cel care a cerut află.