Serializacja jest jak kurczak (albo tofu, albo wołowina). Zależnie od tego, jakich przypraw narzędzi do niej użyjemy, możemy otrzymać danie zjadliwe, smaczne albo istne dzieło sztuki.
Czego możemy zatem użyć, aby uczynić z serializacji magnum opus sztuki transformacji struktur do zapisu binarnego/tekstowego? Jak możemy uzyskać trzy gwiazdki Michelin w kategorii serializacji danych? O tym w poniższym tekście.
Gorzko-gorzki XML
<?xml version="1.0"?> <nope> <niet id="-1"> <nein>I tak nie użyję.</nein> <pleaseno>Czy to lata 90.?</pleaseno> </niet> </nope>
Każdy, kto pamięta mikroserwisy w wersji 1.0, czyli Service Oriented Architecture i standardy WS*, a więc tzw. Gwiazdę Śmierci, zna dokładnie format XML i wszystkie jego zalety (albo ich brak). Format ten stał się na tyle popularny, że zagościł także w dużej liczbie narzędzi developerskich (pliki projektów, pliki konfiguracji) czy też różnego rodzaju standardów (np. Open Office).
XML sam w sobie jest po prostu standardem zapisu danych.
- Tak, jest redundantny (tag zamykający musi mieć tę samą nazwę co tag otwierający).
- Tak, jest opasły ze względu na swoje redundancję, typowanie oraz przestrzenie nazw (namespace’y).
- Tak, jest zapisywany w postaci odczytywalnej przez człowieka (ang. human readable).
Tę ostatnią cechę osobiście uważam za całkowicie zbędną i nie postrzegam jej jako zalety. Dane, także te serializowane, są przesyłane i przetwarzane przez maszyny. Odczyt przez człowieka odbywa się tylko w specjalnych przypadkach i może być wspomagany przez zewnętrzne narzędzie.
Czy powyższe cechy to wszystkie wykroczenia, jakie popełnił XML przeciwko zdrowiu psychicznemu tysięcy deweloperów? Z całą pewnością nie. Co zatem jest w moim mniemaniu jego największą wadą?
Największą wadą związaną z XML-em są według mnie zaniechania dotyczące prędkości przetwarzania. Weźmy pod lupę środowisko .NET Framework. Klasy związane z przetwarzaniem XML-a przez kilka dobrych lat były niezmieniane albo zmieniane w stopniu nieznacznym. Dodatkowo, biorąc pod uwagę, że część API jednej z chmur publicznych – Azure’a – używa właśnie tego formatu do przesyłania danych, nietrudno będzie znaleźć miejsca, w których da się przesyłać dane do chmury i z chmury w sposób sprawniejszy.
Jeśli ciekawi Cię, co można zrobić, aby przetwarzać dane w nieco sprawniejszy sposób, zachęcam do spojrzenia na mój projekt OSS o nazwie QueueBatch. Poza dostarczeniem obsługi batchy dla funkcji Azure’owych używających Azure Storage Queues ma też zaimplementowany niezwykle szybki parser XML-owych odpowiedzi Azure’a. Po stronie klienta jest do 20 razy szybszy od SDK dla usług Azure Storage. Oczywiście czasu odpowiedzi samych serwisów nie zmieni, ale i tak może pozytywnie wpłynąć na koszty związane z uruchamianiem funkcji.
Zerknij na mój projekt QueueBatch i zobacz jak sprawniej przetwarzać dane!
Mam cichą nadzieję, że XML i jego obsługa, ze względu na widoczny w .NET Core ruch zorientowania na wydajność, otrzyma odpowiednie wsparcie i znów jako developerzy będziemy mogli po prostu zaktualizować wersję Frameworka, na której uruchamiamy nasze aplikacje, i cieszyć się lepszą wydajnością za darmo.
Jeśli miałbym opisać smak naszej serializacji po przyprawieniu jej XML-em, powiedziałbym: gorzko-gorzki.
Kwaśny JSON
{ "isSour" : true }
JSON, czyli XML dwudziestego pierwszego wieku. Wraz z drugą falą mody na usługi nazwaną dumnie mikroserwisami zmienił się format przesyłania danych. JSON, będący znacznie lżejszą alternatywą, nadawał się do tego bardzo dobrze. Poza komunikacją pomiędzy usługami pojawiły się też bazy danych wspierające go w zintegrowany sposób.
Przykładem mogą być:
- MongoDB – dokumentowa baza przechowująca obiekty/dokumenty w postaci JSON-a,
- RavenDB – kolejna baza dokumentowa operująca na podobnej podstawie co MongoDB (dokumenty = JSON),
- PostgreSQL – relacyjna baza danych posiadająca niezwykle wydajne wsparcie JSON-a poprzez wbudowany typ JSONB,
- Cosmos DB – nowa baza oferowana przez Azure, dostarczająca niesamowity silnik indeksujący (zachęcam do przeczytania dokumentu: Schema Agnostic Indexing).
Poza bazami warto napomknąć o rynku bibliotek obsługujących ten format serializacji. Praktycznie dla każdej platformy/języka znajdziemy kilka, jeśli nie kilkanaście alternatyw obsługujących ten sam format. Różnice pomiędzy nimi występują na poziomie zarówno obsługiwanego wejścia/wyjścia, jak i dostarczanych funkcji (np. API do obsługi dowolnych obiektów JSON bez podania silnego typu, à la JavaScript).
JSON – XML XXI wieku
Przykładami bibliotek stworzonych dla .NET Framework mogą być:
- JSON.NET – nieśmiertelny serializator używany praktycznie w każdym projekcie. Poza dostarczeniem silnie typowanego API posiada też odpowiednik dynamicznego podejścia do JSON-a dostępny poprzez klasy JX, np. JToken, JObject itp.,
- Jil – niezwykle wydajny serializator stworzony przez jednego z pracowników StackOverflow,
- Utf8Json – jeszcze wydajniejszy serializator, zapisujący dane w postaci napisu enkodowanego do byte’ów UTF-8 za jednym razem, bez potrzeby wtórnej transformacji danych.
Jeżeli chodzi o wydajność, ze względu na używanie formatu czytelnego dla człowieka ponosimy pewien koszt. Zdecydowanie mniejszy niż w przypadku XML-a, ale cały czas niezaniedbywalny.
Ostatnią nieodłączną kwestią związaną z serializacją jest aspekt wersjonowania kontraktów czy kompatybilności wstecznej. Niestety, często odpowiedzią na wspomnienie tych kwestii będzie cisza, „u mnie działa” lub „to wersja beta”. Taki mamy (mikroserwisowy) klimat.
Podsumowując, mimo że JSON potrafi uszczypnąć w język, na pewno warto mieć go w palecie smaków.
Słodko-kwaśny – Message Pack
{"compact" : true, "schema" : 0} // JSON 82 A7 compact C3 A7 schema 00 // odpowiadający mu Message Pack
Twórcy Message Pack opisują go w następujący sposób: It’s like JSON, but fast and small. Faktycznie Message Pack może opisać i zapisać dowolne typy obiektów à la JSON. Dodatkowo jest to format binarny, który pozwala na gęstszy zapis danych. Ze względu na to, że nie posiada schematu, nazwy pól czy właściwości dalej są zapisywane do danych wyjściowych, podobnie jak w JSON-ie. Oznacza to też, że odbiorca nie potrzebuje schematu, aby odczytać dane (zrozumienie, co oznacza dane pole, to osobny aspekt).
Wsparcie – podobnie jak w przypadku JSON-a – znajdziemy na każdej platformie. Oznacza to, że – podobnie jak JSON – jest to dobre narzędzie do komunikacji pomiędzy platformami.
Z uwagi na wydajność odnajdujemy w nim zdecydowanie słodką nutę. Ze względu na brak schematu i zawieranie wszystkich informacji w każdym zapisanym obiekcie pozostawia kwaśny posmak.
Wytrawny – Google Protocol Buffers
message Person { string name = 1; int32 age = 2; string email_address = 3; }
Zdefiniowany przez twórców najpopularniejszej wyszukiwarki format Google Protocol Buffers to nieco inna (potencjalnie wyższa) liga od wcześniej prezentowanych smaków.
Pierwszą cechą, która odróżnia ten format od poprzedników, jest to, że bez znajomości schematu nie jesteśmy w stanie odczytać całkowicie zserializowanych danych. Owszem, da się odczytać pojedyncze pole, jeżeli nie znajduje się w zagnieżdżonym obiekcie, lecz do poprawnej pracy z tym protokołem potrzebujemy schematu.
Dodatkowo ze względu na zewnętrzny schemat i brak potrzeby każdorazowego zapisywania nazw pól poszczególne pola wiadomości zapisywane są razem z tagami. Tagi to całkowitoliczbowe znaczniki będące referencjami do wspomnianego wcześniej zewnętrznego schematu. To właśnie dzięki schematowi można przetłumaczyć np. tag „1” na nazwę pola „Name”, a tag „3” na nazwę pola „EmailAddress”. Protokoły Google’a doczekały się trzeciej wersji, która usunęła niezbyt często używane funkcje serializatora i dodała nowe, takie jak np. dyskryminowana unia pod postacią słowa „oneOf”.
Przez to, że Google Protocol Buffers przechowują schemat – w tym nazwy pól – poza serializowanymi obiektami, oraz to, że używają do zapisu formy binarnej, są bardziej wydajne niż wcześniej wspomniane protokoły. Po prostu mają do wykonania zdecydowanie mniej pracy, zapisując mniej danych.
Biorąc pod uwagę powyższe cechy, włączając w to potrzebę zarządzania schematami (przechowywanie i publikowanie w zewnętrznym medium) oraz bardzo dobrą wydajność, z całą pewnością można mówić o wytrawnym smaku google’owskich protokołów.
Umami – Twój własny format
Jeśli programujesz, to jest wielce prawdopodobne, że masz na swoim koncie własny format serializacji. Chociaż jeden. Za każdym razem, kiedy decydujemy się na zapisanie czegoś w jakiejś formie, jest to ostatecznie serializacja.
Na swojej drodze widziałem wiele:
- pipe-serialization – używanie „|” do oddzielenia pól,
- offset serialization – „od 15 znaku w imieniu umieszczamy imię ojca”,
- pisanie własnego serializatora, który ściga się z innymi – oops, akurat tego jestem winien sam, patrz: Enzyme.
O ile smak Twojej własnej serializacji jest ciekawy (wszak to umami), o tyle wychodzenie poza znane standardy może spowodować późniejsze problemy. Dlatego zawsze podczas decydowania się, jak zapisać dane, w pierwszym kroku zachęcam do przejrzenia tego, co jest już dostępne i zestandaryzowane.
Dopiero potem rozważanie różnego rodzaju eksperymentów. Pamiętajmy, że decyzja o wyborze serializacji zostaje z projektem na bardzo długi czas, a jego zmiana może oznaczać złamanie kompatybilności wstecznej.
Podsumowanie
Mam nadzieję, że powyższa paleta smaków zachęciła Cię do zapoznania się z nimi i zgłębienia różnych formatów serializacji. Wiedza o tym, co można i powinno się wybrać w danym projekcie oraz jak użyć wybranego formatu, może być kluczowa dla wydajnego przetwarzania danych.
Życzę Ci, aby Twoja praca z różnymi serializatorami dostarczała całej palety smaków, a przygotowane w ten sposób projekty zasługiwały na trzy gwiazdki Michelin.
Do przeczytania kolejnym razem!