Zmiany w API

Zmiany łamiące w Protobuf: co przetrwa na wire

5 min czytania

API REST zmienia się, gdy zmienia się kształt JSON, a większość tego kształtu jest widoczna w odpowiedzi, którą można przeczytać w przeglądarce. API gRPC zmienia się, gdy zmienia się plik .proto, a binarny format wire Protocol Buffers ma własne zasady dotyczące tego, co może tolerować klient, które nie mają nic wspólnego z tym, co mówią nazwy pól. Dwie edycje, które wyglądają na jednakowo małe w diffie, zmiana numeru pola kontra dodanie nowego, lądują po przeciwnych stronach linii, którą zmiany łamiące kompatybilność rysuje ogólnie: jedna jest niewidoczna dla każdego istniejącego klienta, druga łamie ich wszystkich naraz. Odróżnienie zmian łamiących w Protobuf od bezpiecznych oznacza czytanie własnych zasad formatu wire, a nie zgadywanie na podstawie tego, jak zmiana wygląda w diffie .proto.

Dlaczego numeracja pól liczy się bardziej niż nazwa pola w Protobuf?

Ponieważ format wire koduje pola według numeru, nie nazwy. Wygenerowany kod w każdym języku czyta i zapisuje te numery; nazwa pola email w waszym pliku .proto to wygoda dla ludzi, która nigdy nie dotyka binarnych bajtów wysyłanych przez sieć. Zmiana nazwy pola, email na email_address, jest bezpieczna w binarnym formacie wire, dopóki numer pozostaje ten sam, co zaskakuje inżynierki przyzwyczajone do REST, gdzie zmieniony klucz JSON to dokładnie ten rodzaj zmiany, który łamie klienta. Wyjątkiem jest ten sam przypadek co w REST: ProtoJSON i format tekstowy serializują nazwę, więc zmiana nazwy psuje transkodowanie JSON (na przykład grpc-gateway), pliki w formacie tekstowym i field masks. Zmiana numeru tego samego pola, zachowanie nazwy ale zmiana 1 na 7, to dokładnie odwrotność: niewidoczna w code review, które pokazuje tylko nazwy, i psuje każdą wiadomość, którą klient wysyła lub odbiera od tego momentu.

ZmianaBezpieczna na wireDlaczego
Zmiana nazwy pola, zachowanie numeruBinarnie tak, JSON i tekst nieKodowanie binarne używa numeru; ProtoJSON i format tekstowy używają nazwy
Zmiana numeru polaNieKażda istniejąca wiadomość jest teraz czytana jako złe pole
Dodanie nowego pola z nowym numeremTakStarzy klienci ignorują pola, których nie rozpoznają
Usunięcie pola, ponowne użycie starego numeru dla czegoś innegoNieStare dane dekodują się do złego nowego pola
Niekompatybilna zmiana typu pola (np. int32 na string)NieKodowanie wire różni się w zależności od typu

Co czyni usunięcie pola innym niż w odpowiedzi JSON REST?

Numer staje się radioaktywny. Własne wytyczne Protobuf zalecają oznaczenie numeru usuniętego pola jako reserved zamiast pozwolić na jego ponowne użycie, ponieważ to właśnie w ponownym użyciu dzieje się prawdziwa szkoda: klient wciąż uruchamiający wygenerowany kod sprzed miesiąca wysyła wiadomość używając starego numeru pola dla starego znaczenia, a serwer, który teraz oczekuje, że ten numer oznacza coś innego, po cichu błędnie interpretuje dane zamiast je odrzucić wprost. REST nie ma równoważnej pułapki, ponieważ usunięty klucz JSON po prostu przestaje się pojawiać; nie ma sposobu, by żądanie starego klienta zostało po cichu przeinterpretowane jako coś innego. Plik .proto z reserved 4, 9, 12; na górze wiadomości to trwała blizna, i o to właśnie chodzi: powstrzymuje przypisanie numeru nowemu polu przez kogoś, kto nie znał jego historii.

message Invoice {
  reserved 4; // był `legacy_customer_id`, usunięty 2026-06-01
  reserved "legacy_customer_id"; // także nazwa, dla JSON/tekstu
  string customer_id = 5;
  string status = 6;
}

Czy dodanie pola kiedykolwiek wymaga wpisu changelogu?

Zwykle nie wpisu o zmianie łamiącej, ale często zwykłego, ponieważ “bezpieczne na wire” i “niewidoczne dla czytelniczki, której na tym zależy” to dwa różne twierdzenia. Dodanie pola do wiadomości odpowiedzi nic strukturalnie nie kosztuje, starzy klienci dekodują wiadomość i automatycznie ignorują nowe pole. Ale ktoś budujący nową integrację z tą usługą nie ma sposobu, by dowiedzieć się, że pole istnieje, chyba że ktoś mu powie, ponieważ nic w udanym buildzie czy zaliczonym teście nie ujawnia nowego opcjonalnego pola. Changelog API opisuje ogólnie, co addytywny wpis jest winien czytelniczkom; specyficzny dla gRPC powód, by mimo to go napisać, to fakt, że nie ma odpowiednika przeglądania odpowiedzi REST w debugerze, by zauważyć, że pojawił się nowy klucz.

Czym różni się to od tego, z czym mierzą się wywołujący GraphQL?

Zasady dla dodawania są te same, ale ekspozycja jest inna. Deprecacja schematu GraphQL opisuje model, w którym klient otrzymuje tylko pola, o które jawnie prosi, co czyni zmiany addytywne zasadniczo pozbawionymi ryzyka, a usunięcia jedynym prawdziwym zagrożeniem. Klienci gRPC, przeciwnie, otrzymują cokolwiek serwer wyśle i dekodują wszystko względem własnej skompilowanej kopii schematu; ekspozycja klienta nie jest ograniczona tym, o co prosił, tylko tym, co jego wygenerowany kod umie odczytać. Ta różnica ma znaczenie przy pisaniu changelogów: wpis GraphQL może rozsądnie zakładać, że klienci są chronieni przed polami, o które nie prosili, a wpis gRPC w ogóle nie może tego zakładać.

Czy wersjonowanie usługi gRPC działa tak samo jak /v1/, /v2/ REST?

Mechanizm jest inny, nawet gdy intencja jest ta sama. Czym są v1 i v2 w API REST opisuje wersjonowanie jako równoległe ścieżki URL obsługujące różne kontrakty; usługi gRPC zwykle wersjonują przez nazwę pakietu w samym pliku .proto, payments.v1.InvoiceService staje się payments.v2.InvoiceService, co zmienia w pełni kwalifikowaną nazwę usługi, którą wybiera klient, zamiast segmentu URL, o który prosi. Oba podejścia rozwiązują ten sam problem, pozwalając staremu kontraktowi nadal działać, gdy istnieje nowy, ale zespół pochodzący z tła REST często szuka numeru wersji w złym miejscu i przeoczy, że deklaracja pakietu wykonuje tę pracę.

Co powinien naprawdę nazywać wpis changelogu gRPC?

Wiadomość, numer pola, i czy to jest addytywne czy usunięcie wymagające migracji, w tej kolejności ważności dla czytelniczki decydującej, czy działać. “Dodano shipping_address (pole 8) do Order” mówi integratorce wszystko, co potrzebne, by zaktualizować wygenerowany kod i zacząć go używać. “Zarezerwowano pole 4 na Invoice, legacy_customer_id zniknęło” mówi jej, by sprawdziła, czy coś w jej bazie kodu wciąż czyta to pole, czego notatka w stylu REST “usunięto pole z odpowiedzi” nie komunikuje z tą samą pilnością, ponieważ usunięcia REST po prostu zwracają mniej danych, podczas gdy ponowne użycie pola Protobuf aktywnie je psuje.

FAQ

Czy typ pola można kiedykolwiek zmienić bez łamania formatu wire? Tylko w ramach konkretnych zgodnych grup, które dokumentuje Protobuf, jak rozszerzenie int32 do int64 w niektórych przypadkach. Traktujcie każdą zmianę typu jako łamiącą, chyba że sprawdziliście ją względem własnej tabeli kompatybilności Protobuf; zakładanie kompatybilności przez analogię do systemu typów jakiegoś języka to sposób, w jaki to idzie źle.

Czy deprecjonowanie pola w Protobuf działa jak dyrektywa @deprecated GraphQL? Podobnie: Protobuf wspiera opcję pola [deprecated = true], którą narzędzia mogą pokazywać. Żadna z nich nie jest wymuszana: serwer GraphQL nadal odpowiada na zapytanie o zdeprecjonowane pole, a klient protobuf nadal je koduje. Obie są doradcze i potrzebują tego samego wsparcia w changelogu.

Czy zmiana numeru pola jest kiedykolwiek bezpieczna, jeśli kontrolujecie każdego klienta? W całkowicie zamkniętym systemie, w zasadzie, ale to usuwa całą właściwość bezpieczeństwa, dla której istnieją numery pól, a “kontrolujemy każdego klienta” to twierdzenie, które przestaje być prawdziwe w momencie, gdy build trafia do cache’u, deploy jest opóźniony, lub dodawany jest klient, o którym nikt nie pamiętał. Zarezerwujcie numer zamiast go ponownie używać, nawet wewnętrznie.

Czy usługi gRPC potrzebują strony changelogu jak publiczne API REST? Tylko jeśli zewnętrzne zespoły je konsumują bez bezpośredniego czytania diffów .proto, ten sam test “kto jest po drugiej stronie”, który ogólnie stosują changelogi API wewnętrznego. Usługa gRPC konsumowana tylko przez inne usługi tego samego zespołu często może pominąć formalny changelog na rzecz historii commitów, ponieważ każdy, kto ją czyta, ma już otwarty schemat.


Twierdzenia techniczne w tym artykule nie zostały niezależnie zweryfikowane. Jeśli coś się nie zgadza, daj nam znać, a poprawimy to.

Powiązane w changeloop: Dokumentacja dla deweloperów, Porównanie narzędzi do changeloga

changeloop
Zespół, który tworzy changelog zamykający pętlę. Użytkownicy o coś proszą, Twój zespół to dostarcza, proszący się dowiaduje.