Protocol Buffers - okładka

Czym jest Protocol Buffers?

Opublikowano Kategorie BackendCzas czytania 8min

Protocol Buffers lub też Protobuf to zaproponowana przez Google’a ciekawa alternatywa dla formatów takich jak JSON czy XML. W tym artykule przedstawię Ci koncepcję, jaka stoi za Protobufem oraz dowiesz się, dlaczego warto się nim zainteresować. W drugiej części artykułu pokażę Ci przykład aplikacji wykorzystującej Protobufa, którą możesz samodzielnie przetestować.

Po co powstał Protocol Buffers?

Protocol Buffers jest formatem serializacji danych i rozwiązuje dwa problemy, które uwidaczniają się przy pracy z innymi formatami danych takimi jak JSON czy XML.

Pierwszym z problemów jest czas serializacji i deserializacji danych. Chcąc przesłać dane po sieci, konieczna będzie ich serializacja przed wysłaniem, a następnie deserializacja przez klienta po ich otrzymaniu. Takie operacje w przypadku dużych zestawów danych to duży problem z punktu widzenia wydajności aplikacji. Wygenerowanie zestawu danych, dla którego serializacja i deserializacja w JavaScript metodami parse()stringify() z JSON API będzie liczona w minutach, nie stanowi większego wyzwania.

O problemach z formatem JSON i korzyściach, jakie dało przejście na Protocol Buffers, możesz przeczytać na LinkedIn Engineering Blog. Inżynierowie z LinkedIn-a przeprowadzili serię testów, które pokazały, że przejście na Protobufa w Rest.li (narzędzie rozwijane przez LinkedIn) dało wymierne korzyści:

Using Protobuf resulted in an average throughput per-host increase of 6.25% for response payloads, and 1.77% for request payloads across all services. For services with large payloads, we saw up to 60% improvement in latency. We didn’t notice any statistically significant degradations when compared to JSON in any service.

Warto jednak pamiętać, że mowa o dużych wiadomościach. Przy serializacji małych, prostych struktur różnice między Protobufem a serializacją innych formatów mogą być marginalne. Z drugiej jednak strony serializowanie zbyt dużych wiadomości w Protobuf również może okazać się nieefektywne. Idąc za dokumentacją:

Protocol buffers tend to assume that entire messages can be loaded into memory at once and are not larger than an object graph. For data that exceeds a few megabytes, consider a different solution; when working with larger data, you may effectively end up with several copies of the data due to serialized copies, which can cause surprising spikes in memory usage.

Podsumowując, w przypadku zbyt małych wiadomości korzyści ze wdrożenia Protobufa mogą być marginalne. Z kolei w przypadku ogromnych porcji danych samo wdrożenie Protobufa z punktu widzenia wydajności może być niewystarczające.

Rozmiar danych

Drugim problemem, jaki rozwiązuje Protocol Buffers, jest sam rozmiar zserializowanego zestawu danych. Truizmem jest stwierdzenie, że wysyłka mniejszych pakietów jest lepsza niż wysyłka większych. Protobuf serializuje dane do postaci binarnej, dzięki czemu przesyłane pakiety danych są mniejsze niż te z danych w postaci zserializowanych JSON-ów czy XML-i.

Walka o ograniczenie rozmiaru przesyłanych wiadomości da widoczne efekty szczególnie w przypadku systemów, gdzie przepustowość łącza stanowi problem. Korzyści z mniejszego rozmiaru przesyłanych danych będą też zauważalne w aplikacjach IoT i w aplikacjach w architekturze mikroserwisowej. Wiadomości w Protobuf mogą również służyć zmniejszeniu ostatecznego rozmiaru danych zapisanych na dysku lub w bazie danych. Może to mieć znaczenie, gdy liczy się każdy oszczędzony bajt np. przy urządzeniach z bardzo małą pamięcią wewnętrzną.

Struktura danych i czytelność

Google w dokumentacji opisuje Protobufa następująco:

Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler.

Idąc tropem porównywania Protocol Buffers do XML-a, wyróżniłbym jeszcze trzecią zaletę, czyli czytelność zdeserializowanych danych. XML w mojej opinii jest tragiczny w czytaniu i zrozumieniu go przez człowieka w porównaniu go z formatami JSON czy struktury plików .proto. Jednak jest to jedynie moja opinia i pewnie nie każdy się z nią zgodzi.

Łyżka dziegciu

Specyfika serializacji danych w Protobufie sprawia, że jego wykorzystanie np. w publicznych API może być problematyczne. Pod tym kątem klasyczne podejście z wykorzystaniem JSON-a będzie moim zdaniem bardziej przyjazne użytkownikom.

Również konieczność ręcznego definiowania struktury danych to dodatkowy wysiłek po stronie programistów. Metody stringify() struktura obiektu nie interesuje. Coś za coś 😉

Przykład wykorzystania

Protobuf jest narzędziem niezależnym od używanej technologii. Wsparcie dla Protocol Buffers mają praktycznie wszystkie popularne technologie, takie jak C++, C#, Go, Java, Kotlin, Python, Ruby, PHP czy JavaScript. W tym artykule pokażę Ci przykład prostej aplikacji w JavaScript, która zserializuje, a następnie zdeserializuje zaprojektowaną wiadomość.

Aby rozpocząć pracę z Protocol Buffers potrzebna będzie instalacja oficjalnej biblioteki protobufjs. Kolejnym krokiem będzie przygotowanie „koperty” na dane. Jedną z możliwości jest zdefiniowanie pliku .proto. Możesz również zdefiniować kształt wiadomości na inne sposoby opisane w dokumentacji, na przykład deskryptorem w formacie JSON lub za pomocą mechanizmu refleksji czy dekoratorów. W przykładzie postawiłem na stworzenie pliku .proto. Zaprojektowałem przykładowy event, który mógłby mieć praktyczne zastosowanie biznesowe.


package eventspackage;
syntax = "proto3";

message PaymentReceivedEvent {
    string transactionId = 1;
    uint32 priceInCents = 2;
    uint32 receivedAt = 3;
    optional string gateway = 4;
}

Każde pole w danej strukturze danych składają się z trzech elementów:

  • typu danych;
  • nazwy pola;
  • liczbę określającą, w jakiej kolejności ma występować. Przy serializacji, Protobuf ułoży dane zgodnie z podaną kolejnością. Zachęcam do samodzielnego pobawienia się kolejnością pól i porównania rezultatów. Nie powinno się zmieniać raz przypisanych wartości liczbowych. Może to powodować błędy serializacji i deserializacji danych. Jeśli aktualizacja wartości pola w jakieś miejsce jeszcze nie dotarła, istnieje ryzyko, że np. zserializujemy wiadomość z wykorzystaniem nowej definicji a zdeserializujemy nieaktualną wersją. Z tego samego powodu zalecałbym, by nie usuwać pól, nawet jeśli są nieużywane;
  • opcjonalnie można podać dodatkowe informacje o specyfice pola np. optional oznaczające, że pole jest opcjonalne czy repeated oznaczające, że pole jest powtarzalne (np. w JavaScript jest tablicą).

Następnym krokiem będzie wykorzystanie zaprojektowanej wiadomości.


import protobuf from "protobufjs";

( async function () {
  const paymentReceivedEventPayload = {
    transactionId: "fd311f124a",
    priceInCents: 10000,
    receivedAt: Date.now(),
    gateway: "stripe"
  }

  const root = await protobuf.load( "events/PaymentReceivedEvent.proto" );
  const PaymentReceivedEvent = root.lookupType( "eventspackage.PaymentReceivedEvent" );

  const encodedPayload = PaymentReceivedEvent.encode( paymentReceivedEventPayload ).finish();

  const paymentEvent = PaymentReceivedEvent.decode( encodedPayload );

  console.log( "ENCODED PAYLOAD:", encodedPayload );
  console.log( "DECODED PAYLOAD:", paymentEvent );
} )();

Rezultat uruchomienia programu wygląda następująco.


ENCODED PAYLOAD: <Buffer 0a 0a 66 64 33 31 31 66 31 32 34 61 10 90 4e 18 d5 91 a3 85 05 22 06 73 74 72 69 70 65>
DECODED PAYLOAD: PaymentReceivedEvent {
  transactionId: 'fd311f124a',
  priceInCents: 10000,
  receivedAt: 1353238741,
  gateway: 'stripe'
}

Po porównaniu rozmiarów danych zserializowanych w Protobuf i JSON uwidacznia się przewaga Protobufa:


console.log( "SERIALIZED PROTOBUF SIZE:", encodedPayload.toString().length ); // SERIALIZED PROTOBUF SIZE: 28
console.log( "SERIALIZED JSON SIZE:", JSON.stringify( paymentReceivedEventPayload ).length ); // SERIALIZED JSON SIZE: 97

Jak pokazuje przedstawiony przykład, wykorzystanie Protobufa nie jest dużo trudniejsze niż JSON-a czy XML-a. Na co dzień mam styczność z Protobufem w podejściu wykorzystującym dekoratory i nie sprawia ono większych problemów w zrozumieniu ani projektowaniu struktur danych.

Zachęcam do przetestowania kodu z tego artykułu. Dla wygody podrzucam link do repozytorium, gdzie znajdziesz pliki z tego artykułu oraz instrukcję jak samodzielnie przetestować kod.

Podsumowanie

Jestem ogromnie ciekaw czy miałeś/aś już styczność z Protobufem. Jeśli tak, to zachęcam Cię do opisania swoich doświadczeń w komentarzu. Zachęcam również do udostępnienia tego artykułu, by trafił do osób, które z Protocol Buffers nie miały jeszcze styczności.

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Notify of
guest

2 komentarzy
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
slsy
slsy
4 months ago

Brakuje najważniejszej cechy, która odróżnia proto od innych formatów binarnych a jest to maksymalny skręt w kierunku forward i backward compatibility kosztem UX. Proto pozwala na zmianę nazw, zmianę typów (możesz zmienić listę na skalar i odwrotnie, możesz dodać oneof lub usunąć dla istniejących pól ), brak required (był w 2jce), pusta wartość to zerowa wartość i pewnie wiele innych cech, o których nie pomyślałem